diff --git a/apps/server/src/modules/auth/controller.ts b/apps/server/src/modules/auth/controller.ts index 20a6148..b999f60 100644 --- a/apps/server/src/modules/auth/controller.ts +++ b/apps/server/src/modules/auth/controller.ts @@ -113,14 +113,21 @@ export class AuthController { async getCurrentUser(request: FastifyRequest, reply: FastifyReply) { try { - const userId = (request as any).user?.userId; + let userId: string | null = null; + try { + await request.jwtVerify(); + userId = (request as any).user?.userId; + } catch (err) { + return reply.send({ user: null }); + } + if (!userId) { - return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + return reply.send({ user: null }); } const user = await this.authService.getUserById(userId); if (!user) { - return reply.status(404).send({ error: "User not found" }); + return reply.send({ user: null }); } return reply.send({ user }); diff --git a/apps/server/src/modules/auth/routes.ts b/apps/server/src/modules/auth/routes.ts index af5902b..89b5858 100644 --- a/apps/server/src/modules/auth/routes.ts +++ b/apps/server/src/modules/auth/routes.ts @@ -153,33 +153,29 @@ export async function authRoutes(app: FastifyInstance) { tags: ["Authentication"], operationId: "getCurrentUser", summary: "Get Current User", - description: "Returns the current authenticated user's information", + description: "Returns the current authenticated user's information or null if not authenticated", response: { - 200: z.object({ - user: z.object({ - id: z.string().describe("User ID"), - firstName: z.string().describe("User first name"), - lastName: z.string().describe("User last name"), - username: z.string().describe("User username"), - email: z.string().email().describe("User email"), - image: z.string().nullable().describe("User profile image URL"), - isAdmin: z.boolean().describe("User is admin"), - isActive: z.boolean().describe("User is active"), - createdAt: z.date().describe("User creation date"), - updatedAt: z.date().describe("User last update date"), + 200: z.union([ + z.object({ + user: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), }), - }), - 401: z.object({ error: z.string().describe("Error message") }), + z.object({ + user: z.null().describe("No user when not authenticated"), + }), + ]), }, }, - preValidation: async (request: FastifyRequest, reply: FastifyReply) => { - try { - await request.jwtVerify(); - } catch (err) { - console.error(err); - reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); - } - }, }, authController.getCurrentUser.bind(authController) ); diff --git a/apps/web/messages/ar-SA.json b/apps/web/messages/ar-SA.json index ece8578..c088c3c 100644 --- a/apps/web/messages/ar-SA.json +++ b/apps/web/messages/ar-SA.json @@ -455,6 +455,11 @@ }, "pageTitle": "الملف الشخصي" }, + "qrCodeModal": { + "title": "مشاركة رمز QR", + "description": "امسح رمز QR هذا للوصول إلى الرابط.", + "download": "تحميل رمز QR" + }, "quickAccess": { "files": { "title": "ملفاتي", @@ -1746,10 +1751,5 @@ "passwordRequired": "كلمة المرور مطلوبة", "nameRequired": "الاسم مطلوب", "required": "هذا الحقل مطلوب" - }, - "qrCodeModal": { - "title": "مشاركة رمز QR", - "description": "امسح رمز QR هذا للوصول إلى الرابط.", - "download": "تحميل رمز QR" } -} \ No newline at end of file +} diff --git a/apps/web/messages/de-DE.json b/apps/web/messages/de-DE.json index eb3fb9d..6f3be86 100644 --- a/apps/web/messages/de-DE.json +++ b/apps/web/messages/de-DE.json @@ -455,6 +455,11 @@ }, "pageTitle": "Profil" }, + "qrCodeModal": { + "title": "QR-Code teilen", + "description": "Scannen Sie diesen QR-Code, um auf den Link zuzugreifen.", + "download": "QR-Code herunterladen" + }, "quickAccess": { "files": { "title": "Meine Dateien", @@ -1744,10 +1749,5 @@ "passwordRequired": "Passwort ist erforderlich", "nameRequired": "Name ist erforderlich", "required": "Dieses Feld ist erforderlich" - }, - "qrCodeModal": { - "title": "QR-Code teilen", - "description": "Scannen Sie diesen QR-Code, um auf den Link zuzugreifen.", - "download": "QR-Code herunterladen" } -} \ No newline at end of file +} diff --git a/apps/web/messages/es-ES.json b/apps/web/messages/es-ES.json index 1abb224..ca8b770 100644 --- a/apps/web/messages/es-ES.json +++ b/apps/web/messages/es-ES.json @@ -455,6 +455,11 @@ }, "pageTitle": "Perfil" }, + "qrCodeModal": { + "title": "Compartir Código QR", + "description": "Escanea este código QR para acceder al enlace.", + "download": "Descargar Código QR" + }, "quickAccess": { "files": { "title": "Mis archivos", @@ -1744,10 +1749,5 @@ "passwordRequired": "Se requiere la contraseña", "nameRequired": "El nombre es obligatorio", "required": "Este campo es obligatorio" - }, - "qrCodeModal": { - "title": "Compartir Código QR", - "description": "Escanea este código QR para acceder al enlace.", - "download": "Descargar Código QR" } -} \ No newline at end of file +} diff --git a/apps/web/messages/fr-FR.json b/apps/web/messages/fr-FR.json index d810c7d..18490d9 100644 --- a/apps/web/messages/fr-FR.json +++ b/apps/web/messages/fr-FR.json @@ -455,6 +455,11 @@ }, "pageTitle": "Profil" }, + "qrCodeModal": { + "title": "Code QR de Partage", + "description": "Scannez ce code QR pour accéder au lien.", + "download": "Télécharger le Code QR" + }, "quickAccess": { "files": { "title": "Mes Fichiers", @@ -1744,10 +1749,5 @@ "passwordRequired": "Le mot de passe est requis", "nameRequired": "Nome é obrigatório", "required": "Este campo é obrigatório" - }, - "qrCodeModal": { - "title": "Code QR de Partage", - "description": "Scannez ce code QR pour accéder au lien.", - "download": "Télécharger le Code QR" } -} \ No newline at end of file +} diff --git a/apps/web/messages/hi-IN.json b/apps/web/messages/hi-IN.json index 48da4ed..4d64a59 100644 --- a/apps/web/messages/hi-IN.json +++ b/apps/web/messages/hi-IN.json @@ -455,6 +455,11 @@ }, "pageTitle": "प्रोफ़ाइल" }, + "qrCodeModal": { + "title": "QR कोड साझा करें", + "description": "इस QR कोड को स्कैन करके लिंक तक पहुंच सकते हैं।", + "download": "QR कोड डाउनलोड करें" + }, "quickAccess": { "files": { "title": "मेरी फाइलें", @@ -1744,10 +1749,5 @@ "passwordRequired": "पासवर्ड आवश्यक है", "nameRequired": "नाम आवश्यक है", "required": "यह फ़ील्ड आवश्यक है" - }, - "qrCodeModal": { - "title": "QR कोड साझा करें", - "description": "इस QR कोड को स्कैन करके लिंक तक पहुंच सकते हैं।", - "download": "QR कोड डाउनलोड करें" } -} \ No newline at end of file +} diff --git a/apps/web/messages/it-IT.json b/apps/web/messages/it-IT.json index 45afc46..4038a46 100644 --- a/apps/web/messages/it-IT.json +++ b/apps/web/messages/it-IT.json @@ -455,6 +455,11 @@ }, "pageTitle": "Profilo" }, + "qrCodeModal": { + "title": "Condividi QR Code", + "description": "Scansiona questo codice QR per accedere al link.", + "download": "Scarica QR Code" + }, "quickAccess": { "files": { "title": "I Miei File", @@ -1744,10 +1749,5 @@ "passwordMinLength": "La password deve contenere almeno 6 caratteri", "nameRequired": "Il nome è obbligatorio", "required": "Questo campo è obbligatorio" - }, - "qrCodeModal": { - "title": "Condividi QR Code", - "description": "Scansiona questo codice QR per accedere al link.", - "download": "Scarica QR Code" } -} \ No newline at end of file +} diff --git a/apps/web/messages/ja-JP.json b/apps/web/messages/ja-JP.json index 66e49a3..c8d66a0 100644 --- a/apps/web/messages/ja-JP.json +++ b/apps/web/messages/ja-JP.json @@ -455,6 +455,11 @@ }, "pageTitle": "プロフィール" }, + "qrCodeModal": { + "title": "QRコードを共有", + "description": "このQRコードをスキャンしてリンクにアクセスしてください。", + "download": "QRコードをダウンロード" + }, "quickAccess": { "files": { "title": "マイファイル", @@ -1744,10 +1749,5 @@ "passwordRequired": "パスワードは必須です", "nameRequired": "名前は必須です", "required": "このフィールドは必須です" - }, - "qrCodeModal": { - "title": "QRコードを共有", - "description": "このQRコードをスキャンしてリンクにアクセスしてください。", - "download": "QRコードをダウンロード" } -} \ No newline at end of file +} diff --git a/apps/web/messages/ko-KR.json b/apps/web/messages/ko-KR.json index 80f05d8..c2ee8c3 100644 --- a/apps/web/messages/ko-KR.json +++ b/apps/web/messages/ko-KR.json @@ -455,6 +455,11 @@ }, "pageTitle": "프로필" }, + "qrCodeModal": { + "title": "QR 코드 공유", + "description": "이 QR 코드를 스캔하여 링크에 접근할 수 있습니다.", + "download": "QR 코드 다운로드" + }, "quickAccess": { "files": { "title": "내 파일", @@ -1744,10 +1749,5 @@ "passwordRequired": "비밀번호는 필수입니다", "nameRequired": "이름은 필수입니다", "required": "이 필드는 필수입니다" - }, - "qrCodeModal": { - "title": "QR 코드 공유", - "description": "이 QR 코드를 스캔하여 링크에 접근할 수 있습니다.", - "download": "QR 코드 다운로드" } -} \ No newline at end of file +} diff --git a/apps/web/messages/nl-NL.json b/apps/web/messages/nl-NL.json index 2edc02d..8f731ea 100644 --- a/apps/web/messages/nl-NL.json +++ b/apps/web/messages/nl-NL.json @@ -455,6 +455,11 @@ }, "pageTitle": "Profiel" }, + "qrCodeModal": { + "title": "QR Code Delen", + "description": "Scan deze QR-code om toegang te krijgen tot de link.", + "download": "QR Code Downloaden" + }, "quickAccess": { "files": { "title": "Mijn Bestanden", @@ -1744,10 +1749,5 @@ "passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten", "nameRequired": "Naam is verplicht", "required": "Dit veld is verplicht" - }, - "qrCodeModal": { - "title": "QR Code Delen", - "description": "Scan deze QR-code om toegang te krijgen tot de link.", - "download": "QR Code Downloaden" } -} \ No newline at end of file +} diff --git a/apps/web/messages/pl-PL.json b/apps/web/messages/pl-PL.json index 256e328..99d9bd2 100644 --- a/apps/web/messages/pl-PL.json +++ b/apps/web/messages/pl-PL.json @@ -455,6 +455,11 @@ }, "pageTitle": "Profil" }, + "qrCodeModal": { + "title": "Udostępnij kod QR", + "description": "Skanuj ten kod QR, aby uzyskać dostęp do linku.", + "download": "Pobierz kod QR" + }, "quickAccess": { "files": { "title": "Moje pliki", @@ -1744,10 +1749,5 @@ "passwordMinLength": "Hasło musi mieć co najmniej 6 znaków", "nameRequired": "Nazwa jest wymagana", "required": "To pole jest wymagane" - }, - "qrCodeModal": { - "title": "Udostępnij kod QR", - "description": "Skanuj ten kod QR, aby uzyskać dostęp do linku.", - "download": "Pobierz kod QR" } -} \ No newline at end of file +} diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index c02423c..74bfb30 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -455,6 +455,11 @@ }, "pageTitle": "Perfil" }, + "qrCodeModal": { + "title": "Compartilhar QR Code", + "description": "Escaneie este código QR para acessar o link.", + "download": "Baixar QR Code" + }, "quickAccess": { "files": { "title": "Meus Arquivos", @@ -1744,10 +1749,5 @@ "lastNameRequired": "O sobrenome é necessário", "usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres", "usernameSpaces": "O nome de usuário não pode conter espaços" - }, - "qrCodeModal": { - "title": "Compartilhar QR Code", - "description": "Escaneie este código QR para acessar o link.", - "download": "Baixar QR Code" } -} \ No newline at end of file +} diff --git a/apps/web/messages/ru-RU.json b/apps/web/messages/ru-RU.json index 6343062..da14aee4 100644 --- a/apps/web/messages/ru-RU.json +++ b/apps/web/messages/ru-RU.json @@ -455,6 +455,11 @@ }, "pageTitle": "Профиль" }, + "qrCodeModal": { + "title": "Поделиться QR-кодом", + "description": "Отсканируйте этот QR-код, чтобы получить доступ к ссылке.", + "download": "Скачать QR-код" + }, "quickAccess": { "files": { "title": "Мои файлы", @@ -1744,10 +1749,5 @@ "passwordRequired": "Требуется пароль", "nameRequired": "Требуется имя", "required": "Это поле обязательно" - }, - "qrCodeModal": { - "title": "Поделиться QR-кодом", - "description": "Отсканируйте этот QR-код, чтобы получить доступ к ссылке.", - "download": "Скачать QR-код" } -} \ No newline at end of file +} diff --git a/apps/web/messages/tr-TR.json b/apps/web/messages/tr-TR.json index eede50b..b96ada3 100644 --- a/apps/web/messages/tr-TR.json +++ b/apps/web/messages/tr-TR.json @@ -455,6 +455,11 @@ }, "pageTitle": "Profil" }, + "qrCodeModal": { + "title": "QR Kodu Paylaş", + "description": "Bu QR kodu tarayarak bağlantıya erişebilirsiniz.", + "download": "QR Kodu İndir" + }, "quickAccess": { "files": { "title": "Benim Dosyalarım", @@ -1744,10 +1749,5 @@ "passwordRequired": "Şifre gerekli", "nameRequired": "İsim gereklidir", "required": "Bu alan zorunludur" - }, - "qrCodeModal": { - "title": "QR Kodu Paylaş", - "description": "Bu QR kodu tarayarak bağlantıya erişebilirsiniz.", - "download": "QR Kodu İndir" } -} \ No newline at end of file +} diff --git a/apps/web/messages/zh-CN.json b/apps/web/messages/zh-CN.json index 05bcc55..cbdba8c 100644 --- a/apps/web/messages/zh-CN.json +++ b/apps/web/messages/zh-CN.json @@ -455,6 +455,11 @@ }, "pageTitle": "个人资料" }, + "qrCodeModal": { + "title": "分享QR Code", + "description": "扫描此QR Code以访问链接。", + "download": "下载QR Code" + }, "quickAccess": { "files": { "title": "我的文件", @@ -1507,11 +1512,7 @@ "copyToClipboard": "复制到剪贴板", "savedMessage": "我已保存备用码", "available": "可用备用码:{count}个", - "instructions": [ - "• 将这些代码保存在安全的位置", - "• 每个备用码只能使用一次", - "• 您可以随时生成新的备用码" - ] + "instructions": ["• 将这些代码保存在安全的位置", "• 每个备用码只能使用一次", "• 您可以随时生成新的备用码"] }, "verification": { "title": "双重认证", @@ -1744,10 +1745,5 @@ "passwordRequired": "密码为必填项", "nameRequired": "名称为必填项", "required": "此字段为必填项" - }, - "qrCodeModal": { - "title": "分享QR Code", - "description": "扫描此QR Code以访问链接。", - "download": "下载QR Code" } -} \ No newline at end of file +} diff --git a/apps/web/src/app/(home)/hooks/use-home.ts b/apps/web/src/app/(home)/hooks/use-home.ts index 046049a..5dcc95c 100644 --- a/apps/web/src/app/(home)/hooks/use-home.ts +++ b/apps/web/src/app/(home)/hooks/use-home.ts @@ -4,34 +4,51 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { create } from "zustand"; +import { useAuth } from "@/contexts/auth-context"; import { useSecureConfigValue } from "@/hooks/use-secure-configs"; interface HomeStore { isLoading: boolean; + shouldShowHomePage: boolean; setIsLoading: (loading: boolean) => void; + setShouldShowHomePage: (show: boolean) => void; } const useHomeStore = create((set) => ({ isLoading: true, + shouldShowHomePage: false, setIsLoading: (loading: boolean) => set({ isLoading: loading }), + setShouldShowHomePage: (show: boolean) => set({ shouldShowHomePage: show }), })); export function useHome() { const router = useRouter(); - const { isLoading, setIsLoading } = useHomeStore(); + const { isLoading, shouldShowHomePage, setIsLoading, setShouldShowHomePage } = useHomeStore(); + const { isAuthenticated } = useAuth(); const { value: showHomePage, isLoading: configLoading } = useSecureConfigValue("showHomePage"); useEffect(() => { - if (!configLoading) { + if (isAuthenticated === true) { + router.replace("/dashboard"); + return; + } + }, [isAuthenticated, router]); + + useEffect(() => { + if (!configLoading && isAuthenticated !== null) { setIsLoading(false); if (showHomePage !== "true") { router.push("/login"); + setShouldShowHomePage(false); + } else if (isAuthenticated === false) { + setShouldShowHomePage(true); } } - }, [router, showHomePage, configLoading, setIsLoading]); + }, [router, showHomePage, configLoading, isAuthenticated, setIsLoading, setShouldShowHomePage]); return { isLoading, + shouldShowHomePage, }; } diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index 32f0a1c..80ae3b5 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -7,9 +7,9 @@ import { Navbar } from "./components/navbar"; import { useHome } from "./hooks/use-home"; export default function HomePage() { - const { isLoading } = useHome(); + const { isLoading, shouldShowHomePage } = useHome(); - if (isLoading) { + if (isLoading || !shouldShowHomePage) { return ; } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 1eed3af..6c440f6 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -4,6 +4,7 @@ import { getLocale } from "next-intl/server"; import "./globals.css"; +import { RedirectHandler } from "@/components/auth/redirect-handler"; import { Favicon } from "@/components/layout/favicon"; import { DynamicToaster } from "@/components/ui/dynamic-toaster"; import { useAppInfo } from "@/contexts/app-info-context"; @@ -39,7 +40,9 @@ export default async function RootLayout({ - {children} + + {children} + diff --git a/apps/web/src/app/login/hooks/use-login.ts b/apps/web/src/app/login/hooks/use-login.ts index dc45c6b..8083e4d 100644 --- a/apps/web/src/app/login/hooks/use-login.ts +++ b/apps/web/src/app/login/hooks/use-login.ts @@ -34,6 +34,12 @@ export function useLogin() { const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(true); const [authConfigLoading, setAuthConfigLoading] = useState(true); + useEffect(() => { + if (isAuthenticated === true) { + router.replace("/dashboard"); + } + }, [isAuthenticated, router]); + useEffect(() => { const errorParam = searchParams.get("error"); const messageParam = searchParams.get("message"); diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx index 2db0e38..bc9181b 100644 --- a/apps/web/src/app/login/page.tsx +++ b/apps/web/src/app/login/page.tsx @@ -17,7 +17,7 @@ export default function LoginPage() { const login = useLogin(); const { firstAccess } = useAppInfo(); - if (login.isAuthenticated === null) { + if (login.isAuthenticated === null || login.isAuthenticated === true) { return ; } diff --git a/apps/web/src/components/auth/redirect-handler.tsx b/apps/web/src/components/auth/redirect-handler.tsx new file mode 100644 index 0000000..e589534 --- /dev/null +++ b/apps/web/src/components/auth/redirect-handler.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; + +import { LoadingScreen } from "@/components/layout/loading-screen"; +import { useAuth } from "@/contexts/auth-context"; + +interface RedirectHandlerProps { + children: React.ReactNode; +} + +const publicPaths = ["/login", "/forgot-password", "/reset-password", "/auth/callback", "/auth/oidc/callback"]; +const homePaths = ["/"]; + +export function RedirectHandler({ children }: RedirectHandlerProps) { + const router = useRouter(); + const pathname = usePathname(); + const { isAuthenticated } = useAuth(); + + useEffect(() => { + if (isAuthenticated === true) { + if (publicPaths.some((path) => pathname.startsWith(path)) || homePaths.includes(pathname)) { + router.replace("/dashboard"); + return; + } + } else if (isAuthenticated === false) { + if (!publicPaths.some((path) => pathname.startsWith(path)) && !homePaths.includes(pathname)) { + router.replace("/login"); + return; + } + } + }, [isAuthenticated, pathname, router]); + + if (isAuthenticated === null) { + return ; + } + + if ( + isAuthenticated === true && + (publicPaths.some((path) => pathname.startsWith(path)) || homePaths.includes(pathname)) + ) { + return ; + } + + if ( + isAuthenticated === false && + !publicPaths.some((path) => pathname.startsWith(path)) && + !homePaths.includes(pathname) + ) { + return ; + } + + return <>{children}; +} diff --git a/apps/web/src/contexts/auth-context.tsx b/apps/web/src/contexts/auth-context.tsx index 4a18fab..e53ff0f 100644 --- a/apps/web/src/contexts/auth-context.tsx +++ b/apps/web/src/contexts/auth-context.tsx @@ -39,11 +39,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; useEffect(() => { + let isMounted = true; + const checkAuth = async () => { try { const appInfoResponse = await getAppInfo(); const appInfo = appInfoResponse.data; + if (!isMounted) return; + if (appInfo.firstUserAccess) { setUser(null); setIsAdmin(false); @@ -52,8 +56,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } const response = await getCurrentUser(); + + if (!isMounted) return; + if (!response?.data?.user) { - throw new Error("No user data"); + setUser(null); + setIsAdmin(false); + setIsAuthenticated(false); + return; } const { isAdmin, ...userData } = response.data.user; @@ -62,6 +72,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setIsAdmin(isAdmin); setIsAuthenticated(true); } catch (err) { + if (!isMounted) return; + console.error(err); setUser(null); setIsAdmin(false); @@ -70,6 +82,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; checkAuth(); + + return () => { + isMounted = false; + }; }, []); return (