feat: auth flow

This commit is contained in:
talksik
2026-02-19 20:36:08 -08:00
parent 1f249c47a6
commit 144596fcaa
16 changed files with 559 additions and 24 deletions
+95
View File
@@ -0,0 +1,95 @@
import { useSessionStore } from "@/stores/session-store";
import type {
Human,
RequestCodeRequest,
SignInRequest,
SignInResponse,
StartupResponse,
} from "./types";
export class ApiError extends Error {
constructor(
public status: number,
message: string,
) {
super(message);
this.name = "ApiError";
}
}
interface ApiClientConfig {
baseUrl: string;
getToken: () => string | null;
onUnauthorized: () => void;
}
class ApiClient {
private config: ApiClientConfig;
constructor(config: ApiClientConfig) {
this.config = config;
}
private async request<T>(
method: string,
path: string,
body?: unknown,
): Promise<T> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
const token = this.config.getToken();
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch(`${this.config.baseUrl}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (response.status === 401) {
this.config.onUnauthorized();
throw new ApiError(401, "Unauthorized");
}
if (!response.ok) {
const text = await response.text().catch(() => "Unknown error");
throw new ApiError(response.status, text);
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
async requestCode(data: RequestCodeRequest): Promise<void> {
await this.request<void>("POST", "/auth/request-code", data);
}
async signIn(data: SignInRequest): Promise<SignInResponse> {
return this.request<SignInResponse>("POST", "/auth/sign-in", data);
}
async me(): Promise<Human> {
return this.request<Human>("GET", "/auth/me");
}
async signOut(): Promise<void> {
await this.request<void>("POST", "/auth/sign-out");
}
async startup(): Promise<StartupResponse> {
return this.request<StartupResponse>("GET", "/startup");
}
}
export const apiClient = new ApiClient({
baseUrl: "https://orion.dev.flowy.live",
getToken: () => useSessionStore.getState().token,
onUnauthorized: () => useSessionStore.getState().clearToken(),
});
+25
View File
@@ -0,0 +1,25 @@
export interface Human {
id: string;
email: string;
email_prefix: string;
created_at: string;
updated_at: string;
}
export interface RequestCodeRequest {
email: string;
}
export interface SignInRequest {
email: string;
code: string;
}
export interface SignInResponse {
human: Human;
token: string;
}
export interface StartupResponse {
networks: unknown[];
}