91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
|
|
import { useEffect, useState } from 'react';
|
|||
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|||
|
|
import * as authApi from '../../api/auth';
|
|||
|
|
import { ApiError } from '../../api/http';
|
|||
|
|
import { DEFAULT_TENANT } from '../../config';
|
|||
|
|
import { useAuth } from '../../context/AuthContext';
|
|||
|
|
import * as permApi from '../../api/permission';
|
|||
|
|
|
|||
|
|
type OAuthMode = 'login' | 'register';
|
|||
|
|
|
|||
|
|
export function OAuthCallbackPage({ mode }: { mode: OAuthMode }) {
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
const [search] = useSearchParams();
|
|||
|
|
const { syncSession, refreshRoles } = useAuth();
|
|||
|
|
const [error, setError] = useState('');
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const code = search.get('code');
|
|||
|
|
const state = search.get('state');
|
|||
|
|
const oauthError = search.get('error');
|
|||
|
|
const tenant =
|
|||
|
|
localStorage.getItem('tenant_slug') ?? DEFAULT_TENANT;
|
|||
|
|
|
|||
|
|
if (oauthError) {
|
|||
|
|
setError(search.get('error_description') ?? oauthError);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!code || !state) {
|
|||
|
|
setError('缺少 OAuth 參數(code / state)');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let cancelled = false;
|
|||
|
|
|
|||
|
|
(async () => {
|
|||
|
|
try {
|
|||
|
|
if (mode === 'login') {
|
|||
|
|
const result = await authApi.loginSocialCallback(code, state);
|
|||
|
|
if (result.mfa_required && result.mfa_challenge_id) {
|
|||
|
|
navigate('/login', {
|
|||
|
|
replace: true,
|
|||
|
|
state: {
|
|||
|
|
message: '請完成雙因素驗證以完成登入',
|
|||
|
|
mfa_challenge_id: result.mfa_challenge_id,
|
|||
|
|
tenant,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
await authApi.registerSocialCallback(code, state);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (cancelled) return;
|
|||
|
|
syncSession();
|
|||
|
|
await refreshRoles();
|
|||
|
|
const me = await permApi.getMyPermissions();
|
|||
|
|
const admin = permApi.isAdminRole(me.roles ?? []);
|
|||
|
|
navigate(admin ? '/admin' : '/app', { replace: true });
|
|||
|
|
} catch (err) {
|
|||
|
|
if (cancelled) return;
|
|||
|
|
if (err instanceof ApiError && err.code === 29301000 && mode === 'login') {
|
|||
|
|
setError('尚無帳號,請先註冊或聯絡管理員開通 LDAP 帳號。');
|
|||
|
|
} else {
|
|||
|
|
setError(err instanceof ApiError ? err.message : '登入失敗');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
cancelled = true;
|
|||
|
|
};
|
|||
|
|
}, [mode, navigate, refreshRoles, search, syncSession]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="auth-card">
|
|||
|
|
<h1>{mode === 'login' ? '登入處理中' : '註冊處理中'}</h1>
|
|||
|
|
{error ? (
|
|||
|
|
<>
|
|||
|
|
<p className="form-error">{error}</p>
|
|||
|
|
<p className="auth-footer">
|
|||
|
|
<Link to={mode === 'login' ? '/login' : '/register'}>返回</Link>
|
|||
|
|
</p>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<p className="auth-hint">正在與身分提供者確認,請稍候…</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|