Security: - Clear the offline IndexedDB cache on sign-in (not only logout) so a previous user's persisted query cache and Yjs page bodies cannot leak to the next user on a shared device when the prior session ended without an explicit logout. Regressions: - Remove the double Yjs title write from the AI title-generation path: the title editor is bound to the Yjs `title` fragment and the server REST update reseeds it, so the local setContent raced that reseed and doubled/garbled the title. Conventions / i18n / docs: - Remove the unused showAiMenuAtom. - Register the 3 offline-fallback strings in en-US and ru-RU. - Fix the 5 broken links to the nonexistent docs/offline-sync-plan.md. Stability / simplification: - warmInfiniteAll now reports truncation (returns false) when it hits maxPages with a cursor still pending instead of silently succeeding. - space-tree make-offline catch logs the raw error and surfaces the real cause. - Move the Offline/Mobile/CORS CHANGELOG entries from the released 0.93.0 section into [Unreleased] (CORS is a documented breaking change). - Drop the pass-through sync-flag forwarders in use-page-collab-providers; set the atoms directly. - Collapse the three isSwaggerEnabled true-cases into it.each. Tests / architecture: - Extract collabTokenNeedsRefresh (pure) and cover all four token states. - Extract shouldPropagateTitleChange and cover the collab-origin skip; add a TitleEditor render test for the static-h1 vs collaborative-editor switch. - Add a use-auth test asserting the sign-in cache purge runs before login. - Add an OFFLINE_PERSIST_ROOTS guard test asserting every persisted root maps to an exported query-key factory; route make-offline's currentUser warm through a new userKeys factory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
198 lines
5.3 KiB
TypeScript
198 lines
5.3 KiB
TypeScript
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 { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
|
import {
|
|
IForgotPassword,
|
|
ILogin,
|
|
IPasswordReset,
|
|
ISetupWorkspace,
|
|
IVerifyUserToken,
|
|
} from "@/features/auth/types/auth.types";
|
|
import { notifications } from "@mantine/notifications";
|
|
import { IAcceptInvite } from "@/features/workspace/types/workspace.types.ts";
|
|
import { acceptInvitation } from "@/features/workspace/services/workspace-service.ts";
|
|
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route.ts";
|
|
import { RESET } from "jotai/utils";
|
|
import { useTranslation } from "react-i18next";
|
|
import { clearOfflineCache } from "@/features/offline/clear-offline-cache";
|
|
|
|
export default function useAuth() {
|
|
const { t } = useTranslation();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const navigate = useNavigate();
|
|
const [, setCurrentUser] = useAtom(currentUserAtom);
|
|
|
|
const handleSignIn = async (data: ILogin) => {
|
|
setIsLoading(true);
|
|
|
|
// Purge any previous user's offline data BEFORE signing in (mirrors logout).
|
|
// On a shared/kiosk device the prior session may have ended WITHOUT an
|
|
// explicit logout (cookie/JWT expiry, tab close, force-quit), leaving user
|
|
// A's persisted query cache (gitmost-rq-cache) and Yjs page bodies
|
|
// (page.<id>) in IndexedDB. Without this purge user B would briefly read A's
|
|
// cached currentUser/pages/comments on first render (UserProvider serves the
|
|
// cached user) and A's page bodies would stay readable offline. Best-effort:
|
|
// never block sign-in on cache cleanup.
|
|
try {
|
|
await clearOfflineCache();
|
|
} catch {
|
|
// best-effort: never block sign-in on cache cleanup
|
|
}
|
|
|
|
try {
|
|
await login(data);
|
|
setIsLoading(false);
|
|
|
|
navigate(getPostLoginRedirect());
|
|
} catch (err) {
|
|
setIsLoading(false);
|
|
|
|
const message = err.response?.data?.message;
|
|
notifications.show({
|
|
message,
|
|
color: "red",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleInvitationSignUp = async (data: IAcceptInvite) => {
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await acceptInvitation(data);
|
|
setIsLoading(false);
|
|
|
|
if (response?.requiresLogin) {
|
|
notifications.show({
|
|
message: t(
|
|
"Account created successfully. Please log in to set up two-factor authentication.",
|
|
),
|
|
});
|
|
navigate(APP_ROUTE.AUTH.LOGIN);
|
|
} else {
|
|
navigate(APP_ROUTE.HOME);
|
|
}
|
|
} catch (err) {
|
|
setIsLoading(false);
|
|
notifications.show({
|
|
message: err.response?.data.message,
|
|
color: "red",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSetupWorkspace = async (data: ISetupWorkspace) => {
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
await setupWorkspace(data);
|
|
setIsLoading(false);
|
|
navigate(APP_ROUTE.HOME);
|
|
} catch (err) {
|
|
setIsLoading(false);
|
|
notifications.show({
|
|
message: err.response?.data.message,
|
|
color: "red",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handlePasswordReset = async (data: IPasswordReset) => {
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await passwordReset(data);
|
|
setIsLoading(false);
|
|
|
|
if (response?.requiresLogin) {
|
|
notifications.show({
|
|
message: t(
|
|
"Password reset was successful. Please log in with your new password.",
|
|
),
|
|
});
|
|
navigate(APP_ROUTE.AUTH.LOGIN);
|
|
} else {
|
|
navigate(APP_ROUTE.HOME);
|
|
notifications.show({
|
|
message: t("Password reset was successful"),
|
|
});
|
|
}
|
|
} catch (err) {
|
|
setIsLoading(false);
|
|
notifications.show({
|
|
message: err.response?.data.message,
|
|
color: "red",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
setCurrentUser(RESET);
|
|
await logout();
|
|
// Purge the previous user's offline data while the page is still alive —
|
|
// window.location.replace below would otherwise interrupt async cleanup.
|
|
try {
|
|
await clearOfflineCache();
|
|
} catch {
|
|
// best-effort: never block logout on cache cleanup
|
|
}
|
|
window.location.replace(`${APP_ROUTE.AUTH.LOGIN}?logout=1`);
|
|
};
|
|
|
|
const handleForgotPassword = async (data: IForgotPassword) => {
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
await forgotPassword(data);
|
|
setIsLoading(false);
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.log(err);
|
|
setIsLoading(false);
|
|
notifications.show({
|
|
message: err.response?.data.message,
|
|
color: "red",
|
|
});
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleVerifyUserToken = async (data: IVerifyUserToken) => {
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
await verifyUserToken(data);
|
|
setIsLoading(false);
|
|
} catch (err) {
|
|
console.log(err);
|
|
setIsLoading(false);
|
|
notifications.show({
|
|
message: err.response?.data.message,
|
|
color: "red",
|
|
});
|
|
}
|
|
};
|
|
|
|
return {
|
|
signIn: handleSignIn,
|
|
invitationSignup: handleInvitationSignUp,
|
|
setupWorkspace: handleSetupWorkspace,
|
|
forgotPassword: handleForgotPassword,
|
|
passwordReset: handlePasswordReset,
|
|
verifyUserToken: handleVerifyUserToken,
|
|
logout: handleLogout,
|
|
isLoading,
|
|
};
|
|
}
|