add api client and auth store

apiFetch wrapper sends JSON with credentials, parses error
shapes off the backend's {error: '...'} responses, and exposes
typed helpers (apiGet, apiPost, apiDelete). Auth store now
drives the real /signup -> /confirm-email -> /login -> /me ->
/logout flow. Vite dev proxy points /api at the backend on
:8000.
This commit is contained in:
Yisroel Baum 2026-05-06 22:48:17 +03:00
parent 7b00fa5f68
commit ae7db07ec3
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
3 changed files with 146 additions and 7 deletions

View file

@ -0,0 +1,68 @@
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? "/api";
export interface ApiResult<T> {
ok: boolean;
status: number;
data: T | null;
error: string | null;
}
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<ApiResult<T>> {
const response = await fetch(`${API_BASE}${path}`, {
credentials: "include",
headers: {
Accept: "application/json",
...(init?.body ? { "Content-Type": "application/json" } : {}),
...init?.headers,
},
...init,
});
let payload: unknown = null;
const text = await response.text();
if (text.length > 0) {
try {
payload = JSON.parse(text) as unknown;
} catch {
payload = null;
}
}
if (!response.ok) {
const errorMessage =
payload &&
typeof payload === "object" &&
"error" in payload &&
typeof (payload as { error: unknown }).error === "string"
? (payload as { error: string }).error
: `request failed: ${response.status}`;
return {
ok: false,
status: response.status,
data: null,
error: errorMessage,
};
}
return {
ok: true,
status: response.status,
data: payload as T,
error: null,
};
}
export function apiGet<T>(path: string): Promise<ApiResult<T>> {
return apiFetch<T>(path, { method: "GET" });
}
export function apiPost<T>(path: string, body: unknown): Promise<ApiResult<T>> {
return apiFetch<T>(path, {
method: "POST",
body: JSON.stringify(body ?? {}),
});
}
export function apiDelete<T>(path: string): Promise<ApiResult<T>> {
return apiFetch<T>(path, { method: "DELETE" });
}

View file

@ -1,5 +1,6 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { computed, ref } from "vue";
import { apiGet, apiPost } from "@/api/client";
export interface AuthUser {
id: number;
@ -8,18 +9,78 @@ export interface AuthUser {
isAdmin: boolean;
}
interface MePayload {
user: AuthUser;
}
export const useAuthStore = defineStore("auth", () => {
const user = ref<AuthUser | null>(null);
const checked = ref(false);
const error = ref<string | null>(null);
const signupCompleted = ref(false);
const isAuthenticated = computed(() => user.value !== null);
const isAdmin = computed(() => user.value?.isAdmin === true);
async function fetchMe(): Promise<boolean> {
const result = await apiGet<MePayload>("/me");
checked.value = true;
if (result.ok && result.data) {
user.value = result.data.user;
return true;
}
user.value = null;
return false;
}
async function signup(email: string, displayName: string): Promise<boolean> {
error.value = null;
const result = await apiPost<null>("/signup", {
email,
displayName,
});
if (!result.ok) {
error.value = result.error;
return false;
}
signupCompleted.value = true;
return true;
}
async function confirmEmail(token: string, password: string): Promise<boolean> {
error.value = null;
const result = await apiPost<null>("/confirm-email", {
token,
password,
});
if (!result.ok) {
error.value = result.error;
return false;
}
return true;
}
async function login(email: string, password: string): Promise<boolean> {
error.value = null;
const result = await apiPost<MePayload>("/login", {
email,
password,
});
if (!result.ok) {
error.value = result.error;
return false;
}
if (result.data) {
user.value = result.data.user;
checked.value = true;
}
return true;
}
async function logout(): Promise<void> {
await apiPost<null>("/logout", {});
user.value = null;
signupCompleted.value = false;
}
function clearError(): void {
@ -30,7 +91,13 @@ export const useAuthStore = defineStore("auth", () => {
user,
checked,
error,
signupCompleted,
isAuthenticated,
isAdmin,
fetchMe,
signup,
confirmEmail,
login,
logout,
clearError,
};

View file

@ -7,14 +7,18 @@ import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
],
plugins: [vue(), vueJsx(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
})