beta sign up sends an email with a magic token. then a separate email to login.

This commit is contained in:
2025-12-20 17:15:10 -05:00
parent c06696e66d
commit 8d9013d0dd
8 changed files with 459 additions and 119 deletions
+49 -29
View File
@@ -30,6 +30,19 @@ type AuthContextValue = {
}) => Promise<void>;
logout: () => void;
getAuthHeaders: () => HeadersInit;
/**
* Used for non-password auth flows (ex: magic link).
* Persists session exactly like login/register.
*/
setSession: (
token: string,
user: {
email: string;
displayName: string | null;
role: string;
}
) => void;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
@@ -42,6 +55,25 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser>(null);
const [loading, setLoading] = useState<boolean>(true);
const persistAuth = useCallback(
(nextToken: string | null, nextUser: AuthUser) => {
if (typeof window === "undefined") return;
if (nextToken) {
window.localStorage.setItem(TOKEN_KEY, nextToken);
} else {
window.localStorage.removeItem(TOKEN_KEY);
}
if (nextUser) {
window.localStorage.setItem(USER_KEY, JSON.stringify(nextUser));
} else {
window.localStorage.removeItem(USER_KEY);
}
},
[]
);
// Hydrate from localStorage on first load
useEffect(() => {
if (typeof window === "undefined") return;
@@ -49,15 +81,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const storedToken = window.localStorage.getItem(TOKEN_KEY);
const storedUser = window.localStorage.getItem(USER_KEY);
if (storedToken) {
setToken(storedToken);
}
if (storedToken) setToken(storedToken);
if (storedUser) {
try {
setUser(JSON.parse(storedUser));
} catch {
// bad JSON? wipe it
window.localStorage.removeItem(USER_KEY);
}
}
@@ -65,21 +94,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setLoading(false);
}, []);
const persistAuth = useCallback((nextToken: string | null, nextUser: AuthUser) => {
if (typeof window === "undefined") return;
if (nextToken) {
window.localStorage.setItem(TOKEN_KEY, nextToken);
} else {
window.localStorage.removeItem(TOKEN_KEY);
}
if (nextUser) {
window.localStorage.setItem(USER_KEY, JSON.stringify(nextUser));
} else {
window.localStorage.removeItem(USER_KEY);
}
}, []);
const setSession = useCallback(
(
nextToken: string,
nextUser: { email: string; displayName: string | null; role: string }
) => {
setToken(nextToken);
setUser(nextUser);
persistAuth(nextToken, nextUser);
},
[persistAuth]
);
const login = useCallback(
async ({ email, password }: { email: string; password: string }) => {
@@ -98,7 +123,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const data = await res.json();
// Adjust these to match your backend response shape
const nextToken: string = data.token ?? data.accessToken;
const nextUser: AuthUser =
data.user ??
@@ -108,14 +132,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: data.role ?? "USER",
} as AuthUser);
setToken(nextToken);
setUser(nextUser);
persistAuth(nextToken, nextUser);
setSession(nextToken, nextUser as NonNullable<AuthUser>);
} finally {
setLoading(false);
}
},
[persistAuth]
[setSession]
);
const register = useCallback(
@@ -152,14 +174,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: data.role ?? "USER",
} as AuthUser);
setToken(nextToken);
setUser(nextUser);
persistAuth(nextToken, nextUser);
setSession(nextToken, nextUser as NonNullable<AuthUser>);
} finally {
setLoading(false);
}
},
[persistAuth]
[setSession]
);
const logout = useCallback(() => {
@@ -180,12 +200,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
register,
logout,
getAuthHeaders,
setSession, // ✅ important
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// 🔑 This is what your useApi hook imports
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) {