test
This commit is contained in:
parent
efd21a7297
commit
441676be27
97 changed files with 10764 additions and 223 deletions
71
frontend/src/stores/auth.ts
Normal file
71
frontend/src/stores/auth.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
import { ApiRequestError } from "@/lib/api/errors";
|
||||
import { fetchCsrfBootstrap } from "@/lib/api/csrf";
|
||||
import { fetchMe, loginSession, logoutSession, type SessionUser } from "@/lib/auth/session";
|
||||
import { useUiStore } from "@/stores/ui";
|
||||
|
||||
export type AuthState = "unknown" | "authenticated" | "anonymous";
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
state: "unknown" as AuthState,
|
||||
csrfToken: null as string | null,
|
||||
user: null as SessionUser | null,
|
||||
}),
|
||||
getters: {
|
||||
isAuthenticated: (state) => state.state === "authenticated",
|
||||
isAdmin: (state) => Boolean(state.user?.is_admin),
|
||||
},
|
||||
actions: {
|
||||
async bootstrapSession(): Promise<void> {
|
||||
const ui = useUiStore();
|
||||
ui.clearGlobalError();
|
||||
|
||||
try {
|
||||
const csrf = await fetchCsrfBootstrap();
|
||||
this.csrfToken = csrf.data.csrf_token;
|
||||
|
||||
if (!csrf.data.authenticated) {
|
||||
this.state = "anonymous";
|
||||
this.user = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const me = await fetchMe();
|
||||
this.state = "authenticated";
|
||||
this.user = me.data.user;
|
||||
this.csrfToken = me.data.csrf_token;
|
||||
} catch (error) {
|
||||
this.state = "anonymous";
|
||||
this.user = null;
|
||||
|
||||
if (error instanceof ApiRequestError) {
|
||||
ui.setGlobalError({
|
||||
message: error.message,
|
||||
requestId: error.requestId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
async login(email: string, password: string): Promise<void> {
|
||||
const response = await loginSession({ email, password });
|
||||
this.state = "authenticated";
|
||||
this.user = response.data.user;
|
||||
this.csrfToken = response.data.csrf_token;
|
||||
},
|
||||
async logout(): Promise<void> {
|
||||
await logoutSession();
|
||||
this.state = "anonymous";
|
||||
this.user = null;
|
||||
this.csrfToken = null;
|
||||
|
||||
try {
|
||||
const csrf = await fetchCsrfBootstrap();
|
||||
this.csrfToken = csrf.data.csrf_token;
|
||||
} catch (_err) {
|
||||
// No-op: app can still function by refreshing.
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
51
frontend/src/stores/theme.ts
Normal file
51
frontend/src/stores/theme.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export type ThemePreference = "system" | "light" | "dark";
|
||||
export type ThemeValue = "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "scholarr-theme-preference";
|
||||
|
||||
function systemTheme(): ThemeValue {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
function applyTheme(theme: ThemeValue, preference: ThemePreference): void {
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle("dark", theme === "dark");
|
||||
root.setAttribute("data-theme-preference", preference);
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore("theme", {
|
||||
state: () => ({
|
||||
preference: "system" as ThemePreference,
|
||||
active: "light" as ThemeValue,
|
||||
}),
|
||||
actions: {
|
||||
initialize(): void {
|
||||
let stored: string | null = null;
|
||||
try {
|
||||
stored = localStorage.getItem(STORAGE_KEY);
|
||||
} catch (_err) {
|
||||
stored = null;
|
||||
}
|
||||
|
||||
if (stored === "light" || stored === "dark" || stored === "system") {
|
||||
this.preference = stored;
|
||||
}
|
||||
|
||||
this.active = this.preference === "system" ? systemTheme() : this.preference;
|
||||
applyTheme(this.active, this.preference);
|
||||
},
|
||||
setPreference(preference: ThemePreference): void {
|
||||
this.preference = preference;
|
||||
this.active = preference === "system" ? systemTheme() : preference;
|
||||
applyTheme(this.active, this.preference);
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, preference);
|
||||
} catch (_err) {
|
||||
// Ignore storage failures in private contexts.
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
20
frontend/src/stores/ui.ts
Normal file
20
frontend/src/stores/ui.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export interface GlobalErrorState {
|
||||
message: string;
|
||||
requestId: string | null;
|
||||
}
|
||||
|
||||
export const useUiStore = defineStore("ui", {
|
||||
state: () => ({
|
||||
globalError: null as GlobalErrorState | null,
|
||||
}),
|
||||
actions: {
|
||||
setGlobalError(error: GlobalErrorState): void {
|
||||
this.globalError = error;
|
||||
},
|
||||
clearGlobalError(): void {
|
||||
this.globalError = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue