Files
redlight/src/pages/OAuthCallback.jsx
T
Michelle 8edcb7d3df
Build & Push Docker Image / build (push) Successful in 6m25s
feat(calendar): store only token hash in database to enhance security
feat(federation): escape LIKE special characters in originDomain to prevent wildcard injection

feat(oauth): redirect with token in hash fragment to avoid exposure in logs

feat(OAuthCallback): retrieve token from hash fragment for improved security
2026-03-04 13:41:40 +01:00

80 lines
2.8 KiB
React

import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext';
import { Loader2, AlertTriangle } from 'lucide-react';
import toast from 'react-hot-toast';
export default function OAuthCallback() {
const [searchParams] = useSearchParams();
const [error, setError] = useState(null);
const { loginWithOAuth } = useAuth();
const { t } = useLanguage();
const navigate = useNavigate();
useEffect(() => {
// Token is passed via hash fragment (never sent to server, not logged, not in Referer).
// Error is still a regular query param since it contains no sensitive data.
const hash = window.location.hash.slice(1); // strip leading '#'
const hashParams = new URLSearchParams(hash);
const token = hashParams.get('token');
const errorMsg = searchParams.get('error');
const returnTo = hashParams.get('return_to') || searchParams.get('return_to') || '/dashboard';
if (errorMsg) {
setError(errorMsg);
return;
}
if (!token) {
setError(t('auth.oauthNoToken'));
return;
}
// Store token and redirect
loginWithOAuth(token)
.then(() => {
toast.success(t('auth.loginSuccess'));
navigate(returnTo, { replace: true });
})
.catch(() => {
setError(t('auth.oauthLoginFailed'));
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (error) {
return (
<div className="min-h-screen flex items-center justify-center p-6">
<div className="absolute inset-0 bg-th-bg" />
<div className="relative w-full max-w-md">
<div className="card p-8 backdrop-blur-xl bg-th-card/80 border border-th-border shadow-2xl rounded-2xl text-center">
<div className="flex justify-center mb-4">
<div className="w-12 h-12 bg-red-500/20 rounded-full flex items-center justify-center">
<AlertTriangle size={24} className="text-red-400" />
</div>
</div>
<h2 className="text-xl font-bold text-th-text mb-2">{t('auth.oauthError')}</h2>
<p className="text-th-text-s mb-6">{error}</p>
<button
onClick={() => navigate('/login', { replace: true })}
className="btn-primary w-full py-3"
>
{t('auth.backToLogin')}
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center p-6">
<div className="absolute inset-0 bg-th-bg" />
<div className="relative flex flex-col items-center gap-4">
<Loader2 size={32} className="animate-spin text-th-accent" />
<p className="text-th-text-s">{t('auth.oauthRedirecting')}</p>
</div>
</div>
);
}