From ae7db07ec39b78446cc7d1c2d60e6293cec2cb6f Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Wed, 6 May 2026 22:48:17 +0300 Subject: [PATCH] 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. --- frontend/blog_portal/src/api/client.ts | 68 ++++++++++++++++++++++++ frontend/blog_portal/src/stores/auth.ts | 69 ++++++++++++++++++++++++- frontend/blog_portal/vite.config.ts | 16 +++--- 3 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 frontend/blog_portal/src/api/client.ts diff --git a/frontend/blog_portal/src/api/client.ts b/frontend/blog_portal/src/api/client.ts new file mode 100644 index 0000000..37f9d7a --- /dev/null +++ b/frontend/blog_portal/src/api/client.ts @@ -0,0 +1,68 @@ +const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? "/api"; + +export interface ApiResult { + ok: boolean; + status: number; + data: T | null; + error: string | null; +} + +export async function apiFetch(path: string, init?: RequestInit): Promise> { + 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(path: string): Promise> { + return apiFetch(path, { method: "GET" }); +} + +export function apiPost(path: string, body: unknown): Promise> { + return apiFetch(path, { + method: "POST", + body: JSON.stringify(body ?? {}), + }); +} + +export function apiDelete(path: string): Promise> { + return apiFetch(path, { method: "DELETE" }); +} diff --git a/frontend/blog_portal/src/stores/auth.ts b/frontend/blog_portal/src/stores/auth.ts index 4435f36..aca181b 100644 --- a/frontend/blog_portal/src/stores/auth.ts +++ b/frontend/blog_portal/src/stores/auth.ts @@ -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(null); const checked = ref(false); const error = ref(null); + const signupCompleted = ref(false); + + const isAuthenticated = computed(() => user.value !== null); + const isAdmin = computed(() => user.value?.isAdmin === true); async function fetchMe(): Promise { + const result = await apiGet("/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 { + error.value = null; + const result = await apiPost("/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 { + error.value = null; + const result = await apiPost("/confirm-email", { + token, + password, + }); + if (!result.ok) { + error.value = result.error; + return false; + } + return true; + } + + async function login(email: string, password: string): Promise { + error.value = null; + const result = await apiPost("/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 { + await apiPost("/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, }; diff --git a/frontend/blog_portal/vite.config.ts b/frontend/blog_portal/vite.config.ts index d49d708..6acd587 100644 --- a/frontend/blog_portal/vite.config.ts +++ b/frontend/blog_portal/vite.config.ts @@ -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, + }, }, }, })