feat: add authorization

This commit is contained in:
2026-08-13 15:03:59 +03:00
parent 1e13adabd3
commit 3448fea140
28 changed files with 4504 additions and 3707 deletions
+123
View File
@@ -0,0 +1,123 @@
import { useQueryClient } from "@tanstack/react-query";
import { createContext, useContext, useEffect, useRef, useState } from "react";
import { apiClient } from "../../../shared/api";
import type { paths } from "../../../shared/api/generated/schema";
type User =
paths["/api/v1/auth/me"]["get"]["responses"][200]["content"]["application/json"];
type Authentication =
paths["/api/v1/auth/login"]["post"]["responses"][200]["content"]["application/json"];
type Credentials =
paths["/api/v1/auth/login"]["post"]["requestBody"]["content"]["application/json"];
type AuthContextValue = {
user: User | null;
accessToken: string | null;
ready: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refresh: () => Promise<boolean>;
};
const AuthContext = createContext<AuthContextValue | null>(null);
async function request(
path: "/api/v1/auth/login" | "/api/v1/auth/register",
email: string,
password: string,
): Promise<Authentication> {
const { data, error } = await apiClient.POST(path, {
body: { email, password },
} satisfies { body: Credentials });
if (error) throw error;
return data as Authentication;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const queryClient = useQueryClient();
const [user, setUser] = useState<User | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const accessTokenRef = useRef<string | null>(null);
const [ready, setReady] = useState(false);
const refreshPromise = useRef<Promise<boolean> | null>(null);
async function refresh(): Promise<boolean> {
if (refreshPromise.current) return refreshPromise.current;
refreshPromise.current = (async () => {
const run = async () => {
const { data } = await apiClient.POST("/api/v1/auth/refresh");
if (!data) return false;
const authentication = data as Authentication;
setUser(authentication.user);
setAccessToken(authentication.accessToken);
accessTokenRef.current = authentication.accessToken;
return true;
};
return "locks" in navigator
? navigator.locks.request("wealth-ledger-refresh", run)
: run();
})().finally(() => {
refreshPromise.current = null;
});
return refreshPromise.current;
}
useEffect(() => {
void apiClient
.POST("/api/v1/auth/refresh")
.then(({ data }) => {
if (!data) return;
const authentication = data as Authentication;
setUser(authentication.user);
setAccessToken(authentication.accessToken);
})
.finally(() => setReady(true));
}, []);
async function authenticate(
path: "/api/v1/auth/login" | "/api/v1/auth/register",
email: string,
password: string,
) {
const authentication = await request(path, email, password);
setUser(authentication.user);
setAccessToken(authentication.accessToken);
accessTokenRef.current = authentication.accessToken;
}
async function logout() {
try {
await apiClient.POST("/api/v1/auth/logout");
} finally {
setUser(null);
setAccessToken(null);
accessTokenRef.current = null;
queryClient.clear();
}
}
return (
<AuthContext
value={{
user,
accessToken,
ready,
login: (email, password) =>
authenticate("/api/v1/auth/login", email, password),
register: (email, password) =>
authenticate("/api/v1/auth/register", email, password),
logout,
refresh,
}}
>
{children}
</AuthContext>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error("AuthProvider is required");
return context;
}