Commit 5368db18 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

fix: login

parent 6455da6b
...@@ -92,13 +92,14 @@ instance.interceptors.response.use( ...@@ -92,13 +92,14 @@ instance.interceptors.response.use(
useUserStore.getState().clearUser(); useUserStore.getState().clearUser();
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const currentPath = `${window.location.pathname}${window.location.search}`; const currentPath = `${window.location.pathname}${window.location.search}`;
const redirect = const isLoginPage = window.location.pathname === "/admin/login";
currentPath.startsWith("/admin") && if (!isLoginPage) {
currentPath !== "/admin/login" const redirect = currentPath.startsWith("/admin")
? `?redirect=${encodeURIComponent(currentPath)}` ? `?redirect=${encodeURIComponent(currentPath)}`
: ""; : "";
window.location.replace(`/admin/login${redirect}`); window.location.replace(`/admin/login${redirect}`);
} }
}
return Promise.reject(error); return Promise.reject(error);
} }
......
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import useAuthStore from "@/store/useAuthStore";
import useUserStore from "@/store/useUserStore";
const DEFAULT_ADMIN_PATH = "/admin";
function normalizeRedirectPath(redirect: string | null) {
if (!redirect) return DEFAULT_ADMIN_PATH;
try {
const url = new URL(redirect, "http://localhost");
const pathname = url.pathname;
const isAdminPath = pathname === "/admin" || pathname.startsWith("/admin/");
const isAuthPath =
pathname === "/admin/login" ||
pathname.startsWith("/admin/login/") ||
pathname === "/admin/change-password";
if (!isAdminPath || isAuthPath) return DEFAULT_ADMIN_PATH;
return `${pathname}${url.search}${url.hash}`;
} catch {
return DEFAULT_ADMIN_PATH;
}
}
export default function LoginLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const userHasHydrated = useUserStore((state) => state._hasHydrated);
const appUser = useUserStore((state) => state.appUser);
const authHasHydrated = useAuthStore((state) => state._hasHydrated);
const isLoggedIn = useAuthStore((state) => state.appIsLoggedIn);
useEffect(() => {
if (!userHasHydrated || !authHasHydrated || !isLoggedIn || !appUser) return;
const redirect = normalizeRedirectPath(
new URLSearchParams(window.location.search).get("redirect"),
);
if (appUser.must_change_password) {
router.replace("/admin/change-password");
} else {
router.replace(redirect);
}
}, [appUser, authHasHydrated, isLoggedIn, router, userHasHydrated]);
return <>{children}</>;
}
"use client"; "use client";
import { FormEvent, Suspense, useEffect, useMemo, useState } from "react"; import { FormEvent, Suspense, useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { import {
ArrowLeft, ArrowLeft,
CheckCircle2, CheckCircle2,
...@@ -34,25 +33,11 @@ type ErrorResponse = { ...@@ -34,25 +33,11 @@ type ErrorResponse = {
}; };
}; };
const DEFAULT_REDIRECT = "/admin";
const authFieldClassName = const authFieldClassName =
"h-11 rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-400 shadow-sm focus-visible:ring-[#063e8e]/30"; "h-11 rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-400 shadow-sm focus-visible:ring-[#063e8e]/30";
const authButtonClassName = const authButtonClassName =
"h-11 rounded-xl bg-[#063e8e] text-white shadow-[0_12px_24px_rgba(6,62,142,0.16)] hover:bg-[#052f6c]"; "h-11 rounded-xl bg-[#063e8e] text-white shadow-[0_12px_24px_rgba(6,62,142,0.16)] hover:bg-[#052f6c]";
function normalizeRedirectPath(redirect: string | null) {
if (
!redirect ||
!redirect.startsWith("/admin") ||
redirect === "/admin/login" ||
redirect === "/admin/change-password"
) {
return DEFAULT_REDIRECT;
}
return redirect;
}
function getAuthErrorMessage(error: unknown, fallback: string) { function getAuthErrorMessage(error: unknown, fallback: string) {
const apiError = error as { const apiError = error as {
response?: { response?: {
...@@ -73,12 +58,12 @@ function normalizeUser( ...@@ -73,12 +58,12 @@ function normalizeUser(
user: any, user: any,
mustChangePassword: boolean, mustChangePassword: boolean,
): AuthenticatedAdminUser | null { ): AuthenticatedAdminUser | null {
if (!user?.id || !user.email || !user.username) return null; if (!user?.id) return null;
return { return {
id: user.id, id: String(user.id),
email: user.email, email: String(user.email ?? ""),
username: user.username, username: String(user.username ?? user.email ?? ""),
first_name: user.first_name ?? null, first_name: user.first_name ?? null,
last_name: user.last_name ?? null, last_name: user.last_name ?? null,
roles: Array.isArray(user.roles) roles: Array.isArray(user.roles)
...@@ -116,15 +101,7 @@ function InlineMessage({ type, message }: { ...@@ -116,15 +101,7 @@ function InlineMessage({ type, message }: {
} }
function AdminLoginPageContent() { function AdminLoginPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
const redirect = useMemo(
() => normalizeRedirectPath(searchParams.get("redirect")),
[searchParams],
);
const hasHydrated = useAuthStore((state) => state._hasHydrated); const hasHydrated = useAuthStore((state) => state._hasHydrated);
const isLoggedIn = useAuthStore((state) => state.appIsLoggedIn);
const rememberState = useAuthStore((state) => state.appUserRemember); const rememberState = useAuthStore((state) => state.appUserRemember);
const setAuthSession = useAuthStore((state) => state.setAuthSession); const setAuthSession = useAuthStore((state) => state.setAuthSession);
const setAppUser = useUserStore((state) => state.setAppUser); const setAppUser = useUserStore((state) => state.setAppUser);
...@@ -154,17 +131,6 @@ function AdminLoginPageContent() { ...@@ -154,17 +131,6 @@ function AdminLoginPageContent() {
setRemember(true); setRemember(true);
}, [rememberState]); }, [rememberState]);
useEffect(() => {
if (!hasHydrated || !isLoggedIn) return;
const currentUser = useUserStore.getState().appUser;
if (currentUser?.must_change_password) {
router.replace("/admin/change-password");
} else {
router.replace(redirect);
}
}, [hasHydrated, isLoggedIn, redirect, router]);
const { mutate: login, isPending: loginLoading } = usePostApiV10AuthLogin({ const { mutate: login, isPending: loginLoading } = usePostApiV10AuthLogin({
mutation: { mutation: {
onSuccess: async (res: any) => { onSuccess: async (res: any) => {
...@@ -174,9 +140,6 @@ function AdminLoginPageContent() { ...@@ -174,9 +140,6 @@ function AdminLoginPageContent() {
setLoginError("Thiếu dữ liệu phiên đăng nhập từ API."); setLoginError("Thiếu dữ liệu phiên đăng nhập từ API.");
return; return;
} }
const mustChangePassword = loginData.must_change_password === true;
setAuthSession({ setAuthSession({
accessToken: loginData.access_token, accessToken: loginData.access_token,
refreshToken: loginData.refresh_token, refreshToken: loginData.refresh_token,
...@@ -189,6 +152,8 @@ function AdminLoginPageContent() { ...@@ -189,6 +152,8 @@ function AdminLoginPageContent() {
remember, remember,
); );
const mustChangePassword = loginData.must_change_password === true;
try { try {
const profile = await getProfile(); const profile = await getProfile();
const profileData = const profileData =
...@@ -196,25 +161,36 @@ function AdminLoginPageContent() { ...@@ -196,25 +161,36 @@ function AdminLoginPageContent() {
(profile.data as any)?.data?.responseData; (profile.data as any)?.data?.responseData;
const normalizedUser = normalizeUser(profileData, mustChangePassword); const normalizedUser = normalizeUser(profileData, mustChangePassword);
if (normalizedUser) {
setAppUser(normalizedUser); if (normalizedUser) setAppUser(normalizedUser);
} else if (loginData.user) {
const fallbackUser = normalizeUser(loginData.user, mustChangePassword); else if (loginData.user) {
if (fallbackUser) setAppUser(fallbackUser);
}
} catch {
if (loginData.user) {
const fallbackUser = normalizeUser(loginData.user, mustChangePassword); const fallbackUser = normalizeUser(loginData.user, mustChangePassword);
if (fallbackUser) setAppUser(fallbackUser); if (fallbackUser) setAppUser(fallbackUser);
} }
}
if (mustChangePassword) { if (mustChangePassword) {
toast.success("Đăng nhập thành công. Vui lòng đổi mật khẩu để tiếp tục."); toast.success("Đăng nhập thành công. Vui lòng đổi mật khẩu để tiếp tục.");
router.replace("/admin/change-password");
} else { } else {
toast.success("Đăng nhập quản trị thành công"); toast.success("Đăng nhập quản trị thành công");
router.replace(redirect); }
} catch {
const stillLoggedIn = useAuthStore.getState().appIsLoggedIn;
let fallbackUser: AuthenticatedAdminUser | null = null;
if (stillLoggedIn && loginData.user) {
fallbackUser = normalizeUser(loginData.user, mustChangePassword);
if (fallbackUser) setAppUser(fallbackUser);
}
if (!fallbackUser) {
useAuthStore.getState().resetStore();
useUserStore.getState().clearUser();
setLoginError(
"Không tải được thông tin tài khoản. Vui lòng thử đăng nhập lại.",
);
return;
}
} }
}, },
onError: (err: any) => { onError: (err: any) => {
...@@ -228,6 +204,9 @@ function AdminLoginPageContent() { ...@@ -228,6 +204,9 @@ function AdminLoginPageContent() {
const handleLogin = async (event: FormEvent<HTMLFormElement>) => { const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
setLoginError(null); setLoginError(null);
// Do not let a stale profile from a previous session trigger the login
// layout redirect before the new profile request has completed.
useUserStore.getState().clearUser();
login({ login({
data: { data: {
email: email.trim(), email: email.trim(),
......
...@@ -79,10 +79,11 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) { ...@@ -79,10 +79,11 @@ export function AdminAuthGuard({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
if (hasHydrated && !isLoggedIn && pathname !== LOGIN_PATH) { if (hasHydrated && !isLoggedIn && pathname !== LOGIN_PATH) {
const currentPath = `${window.location.pathname}${window.location.search}`; const currentPath = `${window.location.pathname}${window.location.search}`;
const isLoginPage = window.location.pathname === LOGIN_PATH;
const isChangePasswordPage = currentPath.startsWith(CHANGE_PASSWORD_PATH); const isChangePasswordPage = currentPath.startsWith(CHANGE_PASSWORD_PATH);
const redirect = const redirect =
currentPath.startsWith("/admin") && currentPath.startsWith("/admin") &&
currentPath !== LOGIN_PATH && !isLoginPage &&
!isChangePasswordPage !isChangePasswordPage
? `?redirect=${encodeURIComponent(currentPath)}` ? `?redirect=${encodeURIComponent(currentPath)}`
: ""; : "";
...@@ -126,10 +127,11 @@ export function useAdminAuthStatus() { ...@@ -126,10 +127,11 @@ export function useAdminAuthStatus() {
useEffect(() => { useEffect(() => {
if (hasHydrated && !isLoggedIn && pathname !== LOGIN_PATH) { if (hasHydrated && !isLoggedIn && pathname !== LOGIN_PATH) {
const currentPath = `${window.location.pathname}${window.location.search}`; const currentPath = `${window.location.pathname}${window.location.search}`;
const isLoginPage = window.location.pathname === LOGIN_PATH;
const isChangePasswordPage = currentPath.startsWith(CHANGE_PASSWORD_PATH); const isChangePasswordPage = currentPath.startsWith(CHANGE_PASSWORD_PATH);
const redirect = const redirect =
currentPath.startsWith("/admin") && currentPath.startsWith("/admin") &&
currentPath !== LOGIN_PATH && !isLoginPage &&
!isChangePasswordPage !isChangePasswordPage
? `?redirect=${encodeURIComponent(currentPath)}` ? `?redirect=${encodeURIComponent(currentPath)}`
: ""; : "";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment