update api fields and add sso
This commit is contained in:
parent
b288cad771
commit
4d917a7e41
@ -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;
|
||||
|
@ -28,7 +28,7 @@ const SearchForm: React.FC<SearchFormProps> = (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<SearchFormProps> = (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 (<Card style={{marginBottom: 20}}>
|
||||
@ -81,9 +81,9 @@ const SearchForm: React.FC<SearchFormProps> = (props) => {
|
||||
<Col span={4}>
|
||||
<Form.Select showClear field="pay_method" label={t('bill.title_pay_method')}
|
||||
placeholder={t('base.please_select')} style={{width: '100%'}}>
|
||||
<Form.Select.Option value="operate">AsiaPay</Form.Select.Option>
|
||||
<Form.Select.Option value="rd">FlyWire</Form.Select.Option>
|
||||
<Form.Select.Option value="pm">PPS</Form.Select.Option>
|
||||
<Form.Select.Option value="ASIAPAY">AsiaPay</Form.Select.Option>
|
||||
<Form.Select.Option value="FLYWIRE">FlyWire</Form.Select.Option>
|
||||
<Form.Select.Option value="PPS">PPS</Form.Select.Option>
|
||||
</Form.Select>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
|
@ -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<AuthContextType | null>(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 (<AuthContext.Provider value={{
|
||||
...state,
|
||||
login, logout
|
||||
login, logout,
|
||||
mockLogin
|
||||
}}>{children}</AuthContext.Provider>)
|
||||
}
|
||||
export default AuthContext
|
@ -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;
|
||||
|
@ -9,7 +9,7 @@
|
||||
"student_number": "学号"
|
||||
},
|
||||
"bill": {
|
||||
"bill_date": "日期",
|
||||
"bill_date": "支付日期",
|
||||
"bill_number": "账单编号",
|
||||
"cancel": "作废",
|
||||
"cancel_confirm": "确定作废此账单",
|
||||
|
@ -9,7 +9,7 @@
|
||||
"student_number": "學號"
|
||||
},
|
||||
"bill": {
|
||||
"bill_date": "日期",
|
||||
"bill_date": "支付日期",
|
||||
"bill_number": "帳單編號",
|
||||
"cancel": "作廢",
|
||||
"cancel_confirm": "確定作廢此帳單",
|
||||
|
@ -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 = () => {
|
||||
</Space>
|
||||
</LogoContainer>
|
||||
<SubmitContainer>
|
||||
<Button theme='solid' type='primary' block onClick={showDashboard}>{t('login.submit')}</Button>
|
||||
<Button style={{marginTop:10}} theme='solid' type='primary' block onClick={()=>{
|
||||
navigate('/pay')
|
||||
}}>支付</Button>
|
||||
{state.loading && <Space>
|
||||
<Spin/><span>Authing ...</span>
|
||||
</Space>}
|
||||
{state.showLogin && <div style={{marginTop: 5}}>
|
||||
<a
|
||||
className="btn-auth-login semi-button semi-button-primary semi-button-block"
|
||||
href={`${AppConfig.SSO_AUTH_URL}/authorize?response_type=code&scope=openid%20profile%20email&client_id=${AppConfig.SSO_AUTH_CLIENT_KEY}&state=${Date.now()}&redirect_uri=${encodeURIComponent(LoginURL)}`}
|
||||
>
|
||||
<span className="semi-button-content" x-semi-prop="children">{t('login.submit')}</span>
|
||||
</a>
|
||||
<Button theme='solid' type='warning' block onClick={handleMockLogin} style={{marginTop: 5}}>MOCK
|
||||
LOGIN</Button>
|
||||
</div>}
|
||||
</SubmitContainer>
|
||||
</LoginContainer>)
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ const BillQuery = () => {
|
||||
<Button size={'small'} theme={'solid'} type={'primary'}>{t('bill.cancel')}</Button>
|
||||
</Popconfirm>
|
||||
<Button onClick={() => setShowBill(bill)} size={'small'} theme={'solid'} type={'primary'}>{t('base.qr-code')}</Button>
|
||||
{AppMode == 'development' && <a href={`/pay?bill=${bill.id}`} target={'_blank'}>Payment</a>}
|
||||
</>}
|
||||
{
|
||||
bill.status == BillStatus.PAID
|
||||
|
@ -40,8 +40,8 @@ const BillReconciliation = () => {
|
||||
|
||||
<SearchForm searchHeader={(
|
||||
<Tabs className={'no-border'} onChange={(apply_status) => setBillQueryParams({apply_status})}>
|
||||
<TabPane tab={<span>{t('bill.reconciliation_status_pending')}</span>} itemKey="pending"/>
|
||||
<TabPane tab={<span>{t('bill.reconciliation_status_submitted')}</span>} itemKey="confirmed"/>
|
||||
<TabPane tab={<span>{t('bill.reconciliation_status_pending')}</span>} itemKey="UNCHECKED"/>
|
||||
<TabPane tab={<span>{t('bill.reconciliation_status_submitted')}</span>} itemKey="CHECKED"/>
|
||||
</Tabs>
|
||||
)} loading={loading} onSearch={setBillQueryParams}/>
|
||||
<BillList
|
||||
|
@ -28,6 +28,7 @@ const HeaderUserProfile = styled.div({
|
||||
export const HeaderUserAvatar = () => {
|
||||
const {t} = useTranslation()
|
||||
const {user, logout} = useAuth()
|
||||
// const UserAvatar = () => (<Avatar color="orange" size="small">{user?.username.substring(0, 3)}</Avatar>)
|
||||
return (<Dropdown
|
||||
position={'bottomRight'}
|
||||
trigger={'click'}
|
||||
@ -35,11 +36,12 @@ export const HeaderUserAvatar = () => {
|
||||
<>
|
||||
<HeaderUserProfile>
|
||||
<Space>
|
||||
<Avatar color="orange" size="small">{user?.nickname}</Avatar>
|
||||
<Avatar color="orange" size="small">{user?.username.substring(0, 3)}</Avatar>
|
||||
<div>
|
||||
<Typography.Title heading={6}>{user?.nickname}</Typography.Title>
|
||||
<Typography.Text type="quaternary"
|
||||
size={'small'}>DEPT:{user?.department?.toUpperCase()}</Typography.Text>
|
||||
<Typography.Title heading={6}>{user?.username}</Typography.Title>
|
||||
<Typography.Text
|
||||
type="quaternary"
|
||||
size={'small'}>{user?.iss?.toUpperCase()}</Typography.Text>
|
||||
</div>
|
||||
</Space>
|
||||
</HeaderUserProfile>
|
||||
@ -51,7 +53,7 @@ export const HeaderUserAvatar = () => {
|
||||
}
|
||||
>
|
||||
|
||||
<Avatar color="orange" size="small">{user?.nickname}</Avatar>
|
||||
<Avatar color="orange" size="small">{user?.username.substring(0, 3)}</Avatar>
|
||||
</Dropdown>)
|
||||
}
|
||||
export const CommonHeader: React.FC<CommonHeaderProps> = ({children, title, rightExtra}) => {
|
||||
@ -97,7 +99,7 @@ type LayoutProps = {
|
||||
|
||||
const LayoutContentContainer = styled.div({
|
||||
borderRadius: 10,
|
||||
marginTop:20
|
||||
marginTop: 20
|
||||
})
|
||||
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
const {t} = useTranslation()
|
||||
|
@ -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})
|
||||
}
|
||||
|
14
src/service/api/user.ts
Normal file
14
src/service/api/user.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {get} from "@/service/request.ts";
|
||||
|
||||
export function getUserInfo() {
|
||||
return get<UserProfile>('/userinfo')
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 sso 返回的code、state换取登录凭证或用户信息
|
||||
* @param code
|
||||
* @param state
|
||||
*/
|
||||
export function auth(code:string,state:string){
|
||||
return get<UserProfile>('/auth', {code, state})
|
||||
}
|
@ -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}`;
|
||||
}
|
||||
|
@ -16,4 +16,4 @@ export enum BillStatus {
|
||||
PENDING= 'PENDING',
|
||||
PAID = 'PAID',
|
||||
CANCELED = 'CANCELED',
|
||||
}
|
||||
}
|
||||
|
13
src/types/auth.d.ts
vendored
13
src/types/auth.d.ts
vendored
@ -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<void>;
|
||||
login: () => Promise<void>;
|
||||
mockLogin: () => Promise<void>;
|
||||
login: (code:string,state:string) => Promise<void>;
|
||||
};
|
2
src/types/bill.d.ts
vendored
2
src/types/bill.d.ts
vendored
@ -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;
|
||||
|
0
src/types/user.d.ts
vendored
Normal file
0
src/types/user.d.ts
vendored
Normal file
7
src/vite-env.d.ts
vendored
7
src/vite-env.d.ts
vendored
@ -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;
|
||||
|
@ -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: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user