From 4d917a7e4153c6fc14fb64277951793937bc6d11 Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sun, 2 Jun 2024 01:18:01 +0800 Subject: [PATCH] update api fields and add sso --- src/assets/index.less | 5 +- src/components/bill/search-form.tsx | 18 +++---- src/contexts/auth/index.tsx | 71 ++++++++++++++++++-------- src/hooks/useAuth.ts | 34 ++++++++++-- src/i18n/translations/sc.json | 2 +- src/i18n/translations/tc.json | 2 +- src/pages/auth/login.tsx | 68 ++++++++++++++++++++---- src/pages/bill/query.tsx | 1 + src/pages/bill/reconciliation.tsx | 4 +- src/routes/layout/dashboard-layout.tsx | 14 ++--- src/service/api/bill.ts | 6 +-- src/service/api/user.ts | 14 +++++ src/service/request.ts | 3 +- src/service/types.ts | 2 +- src/types/auth.d.ts | 13 +++-- src/types/bill.d.ts | 2 +- src/types/user.d.ts | 0 src/vite-env.d.ts | 7 +++ vite.config.ts | 4 ++ 19 files changed, 205 insertions(+), 65 deletions(-) create mode 100644 src/service/api/user.ts create mode 100644 src/types/user.d.ts diff --git a/src/assets/index.less b/src/assets/index.less index ecd6336..94bf1db 100644 --- a/src/assets/index.less +++ b/src/assets/index.less @@ -50,7 +50,10 @@ .money-format{ white-space: nowrap; } - +.btn-auth-login{ + box-sizing: border-box; + text-decoration: none; +} .page-content-container { max-width: 90%; width: 1400px; diff --git a/src/components/bill/search-form.tsx b/src/components/bill/search-form.tsx index 579427a..4505a05 100644 --- a/src/components/bill/search-form.tsx +++ b/src/components/bill/search-form.tsx @@ -28,7 +28,7 @@ const SearchForm: React.FC = (props) => { params.end_date = dayjs(value.dateRange[1]).format('YYYY-MM-DD'); } if (value.student_number) { - params.student_name = value.student_number; + params.student_number = value.student_number; } // 对账状态 if (value.apply_status) { @@ -48,16 +48,16 @@ const SearchForm: React.FC = (props) => { // 根据语言变化更新订单状态options const billStatusOptions = useMemo(() => { return [ - {value: `${i18n.language}pending`, label: t('bill.pay_status_pending')}, - {value: 'paid', label: t('bill.pay_status_paid')}, - {value: 'canceled', label: t('bill.pay_status_canceled')} + {value: `PENDING`, label: t('bill.pay_status_pending')}, + {value: 'PAID', label: t('bill.pay_status_paid')}, + {value: 'CANCELLED', label: t('bill.pay_status_canceled')} ] }, [i18n.language]) // 根据语言变化更新对账状态options const applyStatusOptions = useMemo(() => { return [ - {value: 'pending', label: t('bill.reconciliation_status_pending')}, - {value: 'submitted', label: t('bill.reconciliation_status_submitted')} + {value: 'UNCHECKED', label: t('bill.reconciliation_status_pending')}, + {value: 'CHECKED', label: t('bill.reconciliation_status_submitted')} ] }, [i18n.language]) return ( @@ -81,9 +81,9 @@ const SearchForm: React.FC = (props) => { - AsiaPay - FlyWire - PPS + AsiaPay + FlyWire + PPS diff --git a/src/contexts/auth/index.tsx b/src/contexts/auth/index.tsx index 676c8fa..58fdf6d 100644 --- a/src/contexts/auth/index.tsx +++ b/src/contexts/auth/index.tsx @@ -1,5 +1,7 @@ import Loader from "@/components/loader"; import React, {createContext, useEffect, useReducer} from "react"; +import {auth, getUserInfo} from "@/service/api/user.ts"; +import {setAuthToken} from "@/hooks/useAuth.ts"; const AuthContext = createContext(null) @@ -43,45 +45,69 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => { // MOCK INIT DATA const init = async () => { - setTimeout(() => { + getUserInfo().then(user => { + dispatch({ + action: 'init', + payload: { + isInitialized: true, + isLoggedIn: !!user, + user + } + }) + }).finally(() => { dispatch({ payload: { isInitialized: true, - isLoggedIn: true, - user: { - id: 1, - nickname: 'TU', - department: 'root', - } } }) - }, 300) + }) return 'initialized' } // 登录 - const login = async () => { + const login = async (code: string, state: string) => { + const user = await auth(code, state) + // 保存token + setAuthToken(user.token, user.exp) + // + dispatch({ + action: 'login', + payload: { + isLoggedIn: true, + user + } + }) + } + // 登出 + const logout = async () => { + setAuthToken(null) + dispatch({ + action: 'logout', + payload: { + isLoggedIn: false, + user: null + } + }) + } + const mockLogin = async () => { + setAuthToken('test-123123', Date.now() + 36000 * 1000) dispatch({ action: 'login', payload: { isLoggedIn: true, user: { id: 1, - nickname: 'TU', - department: 'root', + token: 'test-123123', + email: 'test@qq.com', + exp: 1, + iat: 1, + iss: "Hong Kong Chu Hai College", + nbf: 1, + type: "id_token", + username: 'test-123123', } } }) } - // 登出 - const logout = async () => { - dispatch({ - action: 'logout', - payload: { - isLoggedIn: false, - user: null - } - }) - } useEffect(() => { init().then(console.log) }, []) @@ -92,7 +118,8 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => { } return ({children}) } export default AuthContext \ No newline at end of file diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 4e9b2e6..b3a7313 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import {useContext} from 'react'; // auth provider import AuthContext from '@/contexts/auth'; @@ -6,11 +6,37 @@ import AuthContext from '@/contexts/auth'; // ==============================|| AUTH HOOKS ||============================== // const useAuth = () => { - const context = useContext(AuthContext); + const context = useContext(AuthContext); - if (!context) throw new Error('context must be use inside provider'); + if (!context) throw new Error('context must be use inside provider'); - return context; + return context; }; +export const setAuthToken = (token: string | null, expiry_time = -1) => { + if (!token) { + localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY); + return; + } + localStorage.setItem(AppConfig.AUTH_TOKEN_KEY, JSON.stringify({ + token, expiry_time + })); +} + +export const getAuthToken = () => { + const data = localStorage.getItem(AppConfig.AUTH_TOKEN_KEY); + if (!data) return; + try { + const {token, expiry_time} = JSON.parse(data) as { token: string, expiry_time: number }; + if (expiry_time != -1 && expiry_time < Date.now()) { + localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY); + return; + } + return token; + } catch (_e) { + return; + } +} + + export default useAuth; diff --git a/src/i18n/translations/sc.json b/src/i18n/translations/sc.json index 73f1de1..ce5325d 100644 --- a/src/i18n/translations/sc.json +++ b/src/i18n/translations/sc.json @@ -9,7 +9,7 @@ "student_number": "学号" }, "bill": { - "bill_date": "日期", + "bill_date": "支付日期", "bill_number": "账单编号", "cancel": "作废", "cancel_confirm": "确定作废此账单", diff --git a/src/i18n/translations/tc.json b/src/i18n/translations/tc.json index 0db6555..6796773 100644 --- a/src/i18n/translations/tc.json +++ b/src/i18n/translations/tc.json @@ -9,7 +9,7 @@ "student_number": "學號" }, "bill": { - "bill_date": "日期", + "bill_date": "支付日期", "bill_number": "帳單編號", "cancel": "作廢", "cancel_confirm": "確定作廢此帳單", diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index dc6a3b4..0dde721 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -1,10 +1,14 @@ -import {Button, Space, Typography} from "@douyinfe/semi-ui"; +import {Button, Space, Spin, Typography} from "@douyinfe/semi-ui"; import {useTranslation} from 'react-i18next'; -import useConfig from "@/hooks/useConfig.ts"; +import {useEffect} from "react"; +import {useSetState} from "ahooks"; import styled from "@emotion/styled"; +import {useNavigate, useSearchParams} from "react-router-dom"; + +import useConfig from "@/hooks/useConfig.ts"; import AppLogo from "@/assets/AppLogo"; -import {useNavigate} from "react-router-dom"; import useAuth from "@/hooks/useAuth.ts"; +import {getAppUrl} from "@/hooks/useAppUrl"; const LoginContainer = styled.div({ @@ -22,14 +26,49 @@ const SubmitContainer = styled.div({ width: 300, margin: '70px auto 20px' }) + +const LoginURL = getAppUrl() + '/login?action=auth' const AuthLogin = () => { - const { login } = useAuth(); + const {login, mockLogin} = useAuth(); const navigate = useNavigate(); + const [state, setState] = useSetState({ + showLogin: false, + loading: false + }) + const {t} = useTranslation(); const {appName} = useConfig() + const [query] = useSearchParams() - const showDashboard = ()=> { - login().then(()=>{ + useEffect(() => { + const code = query.get('code'), + state = query.get('state'); + + if (query.get('action') == 'auth' && state && code) { + setState({loading: true}) + // 授权回调 + login(code, state).then(() => { + navigate('/dashboard') + }).catch(() => { + // 获取登录凭证失败 重新显示登录按钮 + setTimeout(() => { + setState({showLogin: true}) + }, 2000) + }).finally(() => { + setTimeout(() => { + setState({loading: false}) + }, 2000) + }) + } else { + // 正常登录 - 显示登录按钮 + setState({ + showLogin: true + }) + } + }, []) + + const handleMockLogin = () => { + mockLogin().then(() => { navigate('/dashboard') }) } @@ -42,10 +81,19 @@ const AuthLogin = () => { - - + {state.loading && + Authing ... + } + {state.showLogin &&
+ + {t('login.submit')} + + +
}
) } diff --git a/src/pages/bill/query.tsx b/src/pages/bill/query.tsx index f366e2e..6388661 100644 --- a/src/pages/bill/query.tsx +++ b/src/pages/bill/query.tsx @@ -36,6 +36,7 @@ const BillQuery = () => { + {AppMode == 'development' && Payment} } { bill.status == BillStatus.PAID diff --git a/src/pages/bill/reconciliation.tsx b/src/pages/bill/reconciliation.tsx index d18e8b3..d3ebb8b 100644 --- a/src/pages/bill/reconciliation.tsx +++ b/src/pages/bill/reconciliation.tsx @@ -40,8 +40,8 @@ const BillReconciliation = () => { setBillQueryParams({apply_status})}> - {t('bill.reconciliation_status_pending')}} itemKey="pending"/> - {t('bill.reconciliation_status_submitted')}} itemKey="confirmed"/> + {t('bill.reconciliation_status_pending')}} itemKey="UNCHECKED"/> + {t('bill.reconciliation_status_submitted')}} itemKey="CHECKED"/> )} loading={loading} onSearch={setBillQueryParams}/> { const {t} = useTranslation() const {user, logout} = useAuth() + // const UserAvatar = () => ({user?.username.substring(0, 3)}) return ( { <> - {user?.nickname} + {user?.username.substring(0, 3)}
- {user?.nickname} - DEPT:{user?.department?.toUpperCase()} + {user?.username} + {user?.iss?.toUpperCase()}
@@ -51,7 +53,7 @@ export const HeaderUserAvatar = () => { } > - {user?.nickname} + {user?.username.substring(0, 3)}
) } export const CommonHeader: React.FC = ({children, title, rightExtra}) => { @@ -97,7 +99,7 @@ type LayoutProps = { const LayoutContentContainer = styled.div({ borderRadius: 10, - marginTop:20 + marginTop: 20 }) export const BaseLayout: React.FC = ({children}) => { const {t} = useTranslation() diff --git a/src/service/api/bill.ts b/src/service/api/bill.ts index c7c2979..adec307 100644 --- a/src/service/api/bill.ts +++ b/src/service/api/bill.ts @@ -47,9 +47,9 @@ export function cancelBill(id: number) { } export function confirmBills(bill_ids: number[]) { - return post(`/api/bills/apply`, {bill_ids}) + return post(`/bills/apply`, {bill_ids}) } -export function updateBillPaymentSuccess(billId: number, merchant_ref: string,peyment_channel:string) { - return post(`api/bills/${billId}/finish`, {merchant_ref,peyment_channel}) +export function updateBillPaymentSuccess(billId: number, merchant_ref: string,payment_channel:string) { + return post(`/bills/${billId}/finish`, {merchant_ref,payment_channel}) } diff --git a/src/service/api/user.ts b/src/service/api/user.ts new file mode 100644 index 0000000..ce5caf1 --- /dev/null +++ b/src/service/api/user.ts @@ -0,0 +1,14 @@ +import {get} from "@/service/request.ts"; + +export function getUserInfo() { + return get('/userinfo') +} + +/** + * 使用 sso 返回的code、state换取登录凭证或用户信息 + * @param code + * @param state + */ +export function auth(code:string,state:string){ + return get('/auth', {code, state}) +} \ No newline at end of file diff --git a/src/service/request.ts b/src/service/request.ts index e62146e..981bdd1 100644 --- a/src/service/request.ts +++ b/src/service/request.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import {stringify} from 'qs' import {BizError} from './types'; +import {getAuthToken} from "@/hooks/useAuth.ts"; const JSON_FORMAT: string = 'application/json'; const REQUEST_TIMEOUT = 300000; // 超时时长5min @@ -14,7 +15,7 @@ const Axios = axios.create({ // 请求前拦截 Axios.interceptors.request.use(config => { - const token = localStorage.getItem('payment_front_token'); + const token = getAuthToken(); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } diff --git a/src/service/types.ts b/src/service/types.ts index 495eb3b..d72300f 100644 --- a/src/service/types.ts +++ b/src/service/types.ts @@ -16,4 +16,4 @@ export enum BillStatus { PENDING= 'PENDING', PAID = 'PAID', CANCELED = 'CANCELED', -} \ No newline at end of file +} diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts index 1b30d59..8518002 100644 --- a/src/types/auth.d.ts +++ b/src/types/auth.d.ts @@ -1,7 +1,13 @@ declare type UserProfile = { id: string | number; - nickname: string; - department: string; + token: string; + email: string; + exp: number; + iat: number; + iss: string; + nbf: number; + type: string; + username: string; } declare interface AuthProps { @@ -16,5 +22,6 @@ declare type AuthContextType = { isInitialized?: boolean; user?: UserProfile | null | undefined; logout: () => Promise; - login: () => Promise; + mockLogin: () => Promise; + login: (code:string,state:string) => Promise; }; \ No newline at end of file diff --git a/src/types/bill.d.ts b/src/types/bill.d.ts index 11be039..8a2b848 100644 --- a/src/types/bill.d.ts +++ b/src/types/bill.d.ts @@ -21,7 +21,7 @@ declare type BillQueryParam = { page_number:int; status:string; apply_status:string; - student_name:string; + student_number:string; payment_method:string; start_date:string; end_date:string; diff --git a/src/types/user.d.ts b/src/types/user.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index c715d4c..b4a9659 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -12,7 +12,14 @@ declare const AppConfig: { API_PREFIX: string; // flywire 支付网关 FIY_WIRE_GATEWAY: string; + // sso 认证地址 + SSO_AUTH_URL: string; + // sso 认证客户端 key + SSO_AUTH_CLIENT_KEY: string; + // 登录凭证 token key + AUTH_TOKEN_KEY: string; }; +declare const AppMode: 'test' | 'production' | 'development'; declare type BasicComponentProps = { children?: React.ReactNode; diff --git a/vite.config.ts b/vite.config.ts index f3c7a67..9286bf4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,7 +13,11 @@ export default defineConfig(({mode}) => { SITE_URL: process.env.APP_SITE_URL || null, API_PREFIX: process.env.APP_API_PREFIX || '/api', FIY_WIRE_GATEWAY: process.env.FIY_WIRE_GATEWAY || 'https://gateway.flywire.com/v1/transfers', + SSO_AUTH_URL: process.env.SSO_AUTH_URL || 'https://portal.chuhai.edu.hk', + SSO_AUTH_CLIENT_KEY: process.env.AUTH_CLIENT_KEY || '', + AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || 'payment-auth-token', }), + AppMode: JSON.stringify(mode) }, resolve: { alias: {