feat(auth): refresh tokens + remember me

Backend: add refresh JWT (30d, token_type claim), POST /auth/refresh
endpoint (rotates token pair), remember_me on login, JWT_REFRESH_EXPIRY_DAYS
env var. Extractors now reject refresh tokens on protected routes.

Frontend: sessionStorage for non-remembered sessions, localStorage +
refresh token for remembered sessions. Transparent 401 recovery in
api.ts (retry once after refresh). Remember me checkbox on login page
with security note when checked.
This commit is contained in:
2026-03-19 22:24:26 +01:00
parent 8bdd5e2277
commit d2412da057
13 changed files with 307 additions and 35 deletions

View File

@@ -16,14 +16,21 @@ export function useCurrentUser() {
}
export function useLogin() {
const { setToken } = useAuthContext();
const { setTokens } = useAuthContext();
const router = useRouter();
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
api.auth.login(email, password),
onSuccess: (data) => {
setToken(data.access_token);
mutationFn: ({
email,
password,
rememberMe,
}: {
email: string;
password: string;
rememberMe: boolean;
}) => api.auth.login(email, password, rememberMe),
onSuccess: (data, { rememberMe }) => {
setTokens(data.access_token, data.refresh_token ?? null, rememberMe);
queryClient.invalidateQueries({ queryKey: ["me"] });
router.push("/dashboard");
},
@@ -31,14 +38,14 @@ export function useLogin() {
}
export function useRegister() {
const { setToken } = useAuthContext();
const { setTokens } = useAuthContext();
const router = useRouter();
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
api.auth.register(email, password),
onSuccess: (data) => {
setToken(data.access_token);
setTokens(data.access_token, null, false);
queryClient.invalidateQueries({ queryKey: ["me"] });
router.push("/dashboard");
},
@@ -46,13 +53,13 @@ export function useRegister() {
}
export function useLogout() {
const { token, setToken } = useAuthContext();
const { token, setTokens } = useAuthContext();
const router = useRouter();
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => (token ? api.auth.logout(token) : Promise.resolve()),
onSettled: () => {
setToken(null);
setTokens(null, null, false);
queryClient.clear();
router.push("/login");
},