mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
fix: retain masterkey on non-logout error scenarios and refresh on db hit miss
This commit is contained in:
@@ -191,7 +191,7 @@ describe("initialize", () => {
|
|||||||
expect(useKeyStore.getState().masterKey).not.toBeNull();
|
expect(useKeyStore.getState().masterKey).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clear both stores if the refresh attempt fails", async () => {
|
it("should preserve the master key even if the refresh attempt fails", async () => {
|
||||||
server.use(
|
server.use(
|
||||||
http.post(
|
http.post(
|
||||||
`${API_URL}/api/auth/refresh/`,
|
`${API_URL}/api/auth/refresh/`,
|
||||||
@@ -206,6 +206,6 @@ describe("initialize", () => {
|
|||||||
|
|
||||||
expect(useAuthStore.getState().accessToken).toBeNull();
|
expect(useAuthStore.getState().accessToken).toBeNull();
|
||||||
expect(useAuthStore.getState().user).toBeNull();
|
expect(useAuthStore.getState().user).toBeNull();
|
||||||
expect(useKeyStore.getState().masterKey).toBeNull();
|
expect(useKeyStore.getState().masterKey).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { endpoints } from "../config/endpoints";
|
|||||||
import type { UserProfile } from "../store/useAuthStore";
|
import type { UserProfile } from "../store/useAuthStore";
|
||||||
import { useAuthStore } from "../store/useAuthStore";
|
import { useAuthStore } from "../store/useAuthStore";
|
||||||
import { useKeyStore } from "../store/useKeyStore";
|
import { useKeyStore } from "../store/useKeyStore";
|
||||||
|
import { CryptoUtils } from "../utils/crypto";
|
||||||
import {
|
import {
|
||||||
clearMasterKey,
|
clearMasterKey,
|
||||||
loadMasterKey,
|
loadMasterKey,
|
||||||
@@ -40,9 +41,17 @@ export const useAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initialize = useCallback(async () => {
|
const initialize = useCallback(async () => {
|
||||||
const { accessToken, user, setAuth, clearAuth, setInitializing } =
|
const { accessToken, user, setAuth, setInitializing } =
|
||||||
useAuthStore.getState();
|
useAuthStore.getState();
|
||||||
|
|
||||||
|
// Restore master key from IndexedDB
|
||||||
|
try {
|
||||||
|
const masterKey = await loadMasterKey();
|
||||||
|
if (masterKey) setMasterKey(masterKey);
|
||||||
|
} catch {
|
||||||
|
console.error("Master key restoration failed");
|
||||||
|
}
|
||||||
|
|
||||||
// If session in memory, don't trigger refresh/me again
|
// If session in memory, don't trigger refresh/me again
|
||||||
if (accessToken && user) {
|
if (accessToken && user) {
|
||||||
setInitializing(false);
|
setInitializing(false);
|
||||||
@@ -50,23 +59,34 @@ export const useAuth = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// try refresh
|
// try session refresh
|
||||||
const { data: refreshData } = await publicApi.post(endpoints.REFRESH);
|
const { data: refreshData } = await publicApi.post(endpoints.REFRESH);
|
||||||
const { data: userData } = await api.get(endpoints.ME, {
|
const { data: userData } = await api.get(endpoints.ME, {
|
||||||
headers: { Authorization: `Bearer ${refreshData.access}` },
|
headers: { Authorization: `Bearer ${refreshData.access}` },
|
||||||
});
|
});
|
||||||
setAuth(refreshData.access, userData);
|
setAuth(refreshData.access, userData);
|
||||||
|
|
||||||
// restore master key from IndexedDB
|
|
||||||
const masterKey = await loadMasterKey();
|
|
||||||
if (masterKey) setMasterKey(masterKey);
|
|
||||||
} catch {
|
} catch {
|
||||||
clearAuth();
|
// grace for temporary network errors
|
||||||
setMasterKey(null);
|
} finally {
|
||||||
await clearMasterKey();
|
setInitializing(false);
|
||||||
}
|
}
|
||||||
}, [setMasterKey]);
|
}, [setMasterKey]);
|
||||||
|
|
||||||
|
const unlock = async (password: string) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { masterKey } = await CryptoUtils.deriveKeyBundle(
|
||||||
|
password,
|
||||||
|
user.email,
|
||||||
|
);
|
||||||
|
await saveMasterKey(masterKey);
|
||||||
|
setMasterKey(masterKey);
|
||||||
|
} catch {
|
||||||
|
console.error("Master key restoration failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
user,
|
user,
|
||||||
@@ -74,5 +94,6 @@ export const useAuth = () => {
|
|||||||
setAuthStore,
|
setAuthStore,
|
||||||
logout,
|
logout,
|
||||||
initialize,
|
initialize,
|
||||||
|
unlock,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,11 +56,15 @@ async function decryptLetters(
|
|||||||
export function useLetters() {
|
export function useLetters() {
|
||||||
const [letters, setLetters] = useState<ProcessedLetter[]>([]);
|
const [letters, setLetters] = useState<ProcessedLetter[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isAuthRequired, setIsAuthRequired] = useState<boolean>(false);
|
||||||
const { masterKey } = useKeyStore();
|
const { masterKey } = useKeyStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!masterKey) return;
|
if (!masterKey) {
|
||||||
|
setIsAuthRequired(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsAuthRequired(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
api
|
api
|
||||||
.get(endpoints.LETTERS)
|
.get(endpoints.LETTERS)
|
||||||
@@ -83,5 +87,6 @@ export function useLetters() {
|
|||||||
...drawerItems,
|
...drawerItems,
|
||||||
loading,
|
loading,
|
||||||
refreshLetters: () => setLoading(true),
|
refreshLetters: () => setLoading(true),
|
||||||
|
isAuthRequired,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FeatherIcon } from "@phosphor-icons/react";
|
import { FeatherIcon, LockKeyIcon } from "@phosphor-icons/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Logo from "../components/Logo";
|
import Logo from "../components/Logo";
|
||||||
@@ -9,10 +9,11 @@ import { useAuth } from "../hooks/useAuth";
|
|||||||
import { useLetters } from "../hooks/useLetters";
|
import { useLetters } from "../hooks/useLetters";
|
||||||
|
|
||||||
export default function Drawer() {
|
export default function Drawer() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout, unlock } = useAuth();
|
||||||
|
|
||||||
const [openSection, setOpenSection] = useState<string | null>();
|
const [openSection, setOpenSection] = useState<string | null>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { drafts, kept, sent, vault, loading } = useLetters();
|
const { drafts, kept, sent, vault, loading, isAuthRequired } = useLetters();
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
@@ -23,6 +24,52 @@ export default function Drawer() {
|
|||||||
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
<div className="min-h-screen w-full bg-base-100 text-base-content flex flex-col items-center py-12 px-5 pb-32 font-serif transition-colors">
|
||||||
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
|
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)] pointer-events-none z-0" />
|
||||||
|
|
||||||
|
{isAuthRequired && (
|
||||||
|
<div className="modal modal-open bg-base-100/20 backdrop-blur-md">
|
||||||
|
<div className="modal-box p-12 flex flex-col items-center">
|
||||||
|
<LockKeyIcon
|
||||||
|
size={48}
|
||||||
|
className="text-primary mx-auto mb-8 animate-pulse"
|
||||||
|
/>
|
||||||
|
<h3 className="font-bold text-lg font-display text-primary">
|
||||||
|
Authentication Required
|
||||||
|
</h3>
|
||||||
|
<p className="py-4 font-sans">
|
||||||
|
We need your passkey to open your letters
|
||||||
|
</p>
|
||||||
|
<div className="divider w-1/2 mx-auto text-xs text-neutral-content/30 mt-0"></div>
|
||||||
|
<p className="text-xs text-neutral-content/30 font-mono italic">
|
||||||
|
P.S. We don't validate your input at the moment.
|
||||||
|
</p>
|
||||||
|
<div className="modal-action items-center gap-4">
|
||||||
|
<form
|
||||||
|
className="form-control w-full inline-flex"
|
||||||
|
onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const password = e.target.password.value;
|
||||||
|
if (!password) return;
|
||||||
|
unlock(password);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
className="font-sans validator input input-bordered rounded-r-none"
|
||||||
|
/>
|
||||||
|
<div className="validator-message text-xs text-error"></div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary rounded-l-none"
|
||||||
|
>
|
||||||
|
Unlock
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-1000">
|
<header className="text-center mb-12 z-10 animate-in fade-in slide-in-from-top-4 duration-1000">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2">
|
<div className="font-sans text-xs tracking-[0.3em] uppercase text-base-content/40 mt-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user