mirror of
https://github.com/docmost/docmost.git
synced 2025-11-21 08:01:09 +10:00
refactor: switch to HttpOnly cookie (#660)
* Switch to httpOnly cookie * create endpoint to retrieve temporary collaboration token * cleanups
This commit is contained in:
@ -1,8 +1,7 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { createJSONStorage, atomWithStorage } from "jotai/utils";
|
||||
import { ITokens } from "../types/auth.types";
|
||||
|
||||
const cookieStorage = createJSONStorage<ITokens>(() => {
|
||||
const cookieStorage = createJSONStorage<any>(() => {
|
||||
return {
|
||||
getItem: () => Cookies.get("authTokens"),
|
||||
setItem: (key, value) => Cookies.set(key, value, { expires: 30 }),
|
||||
@ -10,7 +9,7 @@ const cookieStorage = createJSONStorage<ITokens>(() => {
|
||||
};
|
||||
});
|
||||
|
||||
export const authTokensAtom = atomWithStorage<ITokens | null>(
|
||||
export const authTokensAtom = atomWithStorage<any | null>(
|
||||
"authTokens",
|
||||
null,
|
||||
cookieStorage,
|
||||
|
||||
@ -2,14 +2,7 @@ import * as z from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import { IPasswordReset } from "@/features/auth/types/auth.types";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
PasswordInput,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Box, Button, Container, PasswordInput, Title } from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -2,13 +2,13 @@ import { useState } from "react";
|
||||
import {
|
||||
forgotPassword,
|
||||
login,
|
||||
logout,
|
||||
passwordReset,
|
||||
setupWorkspace,
|
||||
verifyUserToken,
|
||||
} from "@/features/auth/services/auth-service";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import {
|
||||
IForgotPassword,
|
||||
@ -20,31 +20,26 @@ import {
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IAcceptInvite } from "@/features/workspace/types/workspace.types.ts";
|
||||
import { acceptInvitation } from "@/features/workspace/services/workspace-service.ts";
|
||||
import Cookies from "js-cookie";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { RESET } from "jotai/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function useAuth() {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [, setCurrentUser] = useAtom(currentUserAtom);
|
||||
const [authToken, setAuthToken] = useAtom(authTokensAtom);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSignIn = async (data: ILogin) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await login(data);
|
||||
await login(data);
|
||||
setIsLoading(false);
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate(APP_ROUTE.HOME);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setIsLoading(false);
|
||||
console.log(err);
|
||||
notifications.show({
|
||||
message: err.response?.data.message,
|
||||
color: "red",
|
||||
@ -56,11 +51,8 @@ export default function useAuth() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await acceptInvitation(data);
|
||||
await acceptInvitation(data);
|
||||
setIsLoading(false);
|
||||
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate(APP_ROUTE.HOME);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
@ -77,9 +69,6 @@ export default function useAuth() {
|
||||
try {
|
||||
const res = await setupWorkspace(data);
|
||||
setIsLoading(false);
|
||||
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate(APP_ROUTE.HOME);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
@ -94,14 +83,11 @@ export default function useAuth() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await passwordReset(data);
|
||||
await passwordReset(data);
|
||||
setIsLoading(false);
|
||||
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate(APP_ROUTE.HOME);
|
||||
notifications.show({
|
||||
message: "Password reset was successful",
|
||||
message: t("Password reset was successful"),
|
||||
});
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
@ -112,34 +98,10 @@ export default function useAuth() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleIsAuthenticated = async () => {
|
||||
if (!authToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const accessToken = authToken.accessToken;
|
||||
const payload = jwtDecode(accessToken);
|
||||
|
||||
// true if jwt is active
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
return payload.exp >= now;
|
||||
} catch (err) {
|
||||
console.log("invalid jwt token", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const hasTokens = (): boolean => {
|
||||
return !!authToken;
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
setAuthToken(null);
|
||||
setCurrentUser(null);
|
||||
Cookies.remove("authTokens");
|
||||
queryClient.clear();
|
||||
window.location.replace(APP_ROUTE.AUTH.LOGIN);;
|
||||
setCurrentUser(RESET);
|
||||
await logout();
|
||||
window.location.replace(APP_ROUTE.AUTH.LOGIN);
|
||||
};
|
||||
|
||||
const handleForgotPassword = async (data: IForgotPassword) => {
|
||||
@ -182,12 +144,10 @@ export default function useAuth() {
|
||||
signIn: handleSignIn,
|
||||
invitationSignup: handleInvitationSignUp,
|
||||
setupWorkspace: handleSetupWorkspace,
|
||||
isAuthenticated: handleIsAuthenticated,
|
||||
forgotPassword: handleForgotPassword,
|
||||
passwordReset: handlePasswordReset,
|
||||
verifyUserToken: handleVerifyUserToken,
|
||||
logout: handleLogout,
|
||||
hasTokens,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import useCurrentUser from "@/features/user/hooks/use-current-user.ts";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||
|
||||
export function useRedirectIfAuthenticated() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { data, isLoading } = useCurrentUser();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const validAuth = await isAuthenticated();
|
||||
if (validAuth) {
|
||||
navigate("/home");
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [isAuthenticated]);
|
||||
if (data && data?.user) {
|
||||
navigate(APP_ROUTE.HOME);
|
||||
}
|
||||
}, [isLoading, data]);
|
||||
}
|
||||
|
||||
@ -1,14 +1,28 @@
|
||||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { verifyUserToken } from "../services/auth-service";
|
||||
import { IVerifyUserToken } from "../types/auth.types";
|
||||
import { getCollabToken, verifyUserToken } from "../services/auth-service";
|
||||
import { ICollabToken, IVerifyUserToken } from "../types/auth.types";
|
||||
|
||||
export function useVerifyUserTokenQuery(
|
||||
verify: IVerifyUserToken,
|
||||
): UseQueryResult<any, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["verify-token", verify],
|
||||
queryFn: () => verifyUserToken(verify),
|
||||
enabled: !!verify.token,
|
||||
staleTime: 0,
|
||||
});
|
||||
}
|
||||
verify: IVerifyUserToken,
|
||||
): UseQueryResult<any, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["verify-token", verify],
|
||||
queryFn: () => verifyUserToken(verify),
|
||||
enabled: !!verify.token,
|
||||
staleTime: 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCollabToken(): UseQueryResult<ICollabToken, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["collab-token"],
|
||||
queryFn: () => getCollabToken(),
|
||||
staleTime: 24 * 60 * 60 * 1000, //24hrs
|
||||
refetchInterval: 20 * 60 * 60 * 1000, //20hrs
|
||||
retry: 10,
|
||||
retryDelay: (retryAttempt) => {
|
||||
// Exponential backoff: 5s, 10s, 20s, etc.
|
||||
return 5000 * Math.pow(2, retryAttempt - 1);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,51 +1,49 @@
|
||||
import api from "@/lib/api-client";
|
||||
import {
|
||||
IChangePassword,
|
||||
ICollabToken,
|
||||
IForgotPassword,
|
||||
ILogin,
|
||||
IPasswordReset,
|
||||
IRegister,
|
||||
ISetupWorkspace,
|
||||
ITokenResponse,
|
||||
IVerifyUserToken,
|
||||
} from "@/features/auth/types/auth.types";
|
||||
|
||||
export async function login(data: ILogin): Promise<ITokenResponse> {
|
||||
const req = await api.post<ITokenResponse>("/auth/login", data);
|
||||
return req.data;
|
||||
export async function login(data: ILogin): Promise<void> {
|
||||
await api.post<void>("/auth/login", data);
|
||||
}
|
||||
|
||||
/*
|
||||
export async function register(data: IRegister): Promise<ITokenResponse> {
|
||||
const req = await api.post<ITokenResponse>("/auth/register", data);
|
||||
return req.data;
|
||||
}*/
|
||||
export async function logout(): Promise<void> {
|
||||
await api.post<void>("/auth/logout");
|
||||
}
|
||||
|
||||
export async function changePassword(
|
||||
data: IChangePassword
|
||||
data: IChangePassword,
|
||||
): Promise<IChangePassword> {
|
||||
const req = await api.post<IChangePassword>("/auth/change-password", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function setupWorkspace(
|
||||
data: ISetupWorkspace
|
||||
): Promise<ITokenResponse> {
|
||||
const req = await api.post<ITokenResponse>("/auth/setup", data);
|
||||
data: ISetupWorkspace,
|
||||
): Promise<any> {
|
||||
const req = await api.post<any>("/auth/setup", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function forgotPassword(data: IForgotPassword): Promise<void> {
|
||||
await api.post<any>("/auth/forgot-password", data);
|
||||
await api.post<void>("/auth/forgot-password", data);
|
||||
}
|
||||
|
||||
export async function passwordReset(
|
||||
data: IPasswordReset
|
||||
): Promise<ITokenResponse> {
|
||||
const req = await api.post<any>("/auth/password-reset", data);
|
||||
return req.data;
|
||||
export async function passwordReset(data: IPasswordReset): Promise<void> {
|
||||
await api.post<void>("/auth/password-reset", data);
|
||||
}
|
||||
|
||||
export async function verifyUserToken(data: IVerifyUserToken): Promise<any> {
|
||||
return api.post<any>("/auth/verify-token", data);
|
||||
}
|
||||
|
||||
export async function getCollabToken(): Promise<ICollabToken> {
|
||||
const req = await api.post<ICollabToken>("/auth/collab-token");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
@ -16,15 +16,6 @@ export interface ISetupWorkspace {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ITokens {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface ITokenResponse {
|
||||
tokens: ITokens;
|
||||
}
|
||||
|
||||
export interface IChangePassword {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
@ -43,3 +34,7 @@ export interface IVerifyUserToken {
|
||||
token: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ICollabToken {
|
||||
token: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user