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:
parent
7b00fa5f68
commit
ae7db07ec3
3 changed files with 146 additions and 7 deletions
68
frontend/blog_portal/src/api/client.ts
Normal file
68
frontend/blog_portal/src/api/client.ts
Normal 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" });
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import { apiGet, apiPost } from "@/api/client";
|
||||||
|
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -8,18 +9,78 @@ export interface AuthUser {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MePayload {
|
||||||
|
user: AuthUser;
|
||||||
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const user = ref<AuthUser | null>(null);
|
const user = ref<AuthUser | null>(null);
|
||||||
const checked = ref(false);
|
const checked = ref(false);
|
||||||
const error = ref<string | null>(null);
|
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> {
|
async function fetchMe(): Promise<boolean> {
|
||||||
|
const result = await apiGet<MePayload>("/me");
|
||||||
checked.value = true;
|
checked.value = true;
|
||||||
|
if (result.ok && result.data) {
|
||||||
|
user.value = result.data.user;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.value = null;
|
||||||
return false;
|
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> {
|
async function logout(): Promise<void> {
|
||||||
|
await apiPost<null>("/logout", {});
|
||||||
user.value = null;
|
user.value = null;
|
||||||
|
signupCompleted.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearError(): void {
|
function clearError(): void {
|
||||||
|
|
@ -30,7 +91,13 @@ export const useAuthStore = defineStore("auth", () => {
|
||||||
user,
|
user,
|
||||||
checked,
|
checked,
|
||||||
error,
|
error,
|
||||||
|
signupCompleted,
|
||||||
|
isAuthenticated,
|
||||||
|
isAdmin,
|
||||||
fetchMe,
|
fetchMe,
|
||||||
|
signup,
|
||||||
|
confirmEmail,
|
||||||
|
login,
|
||||||
logout,
|
logout,
|
||||||
clearError,
|
clearError,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,18 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [vue(), vueJsx(), vueDevTools()],
|
||||||
vue(),
|
|
||||||
vueJsx(),
|
|
||||||
vueDevTools(),
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue