This commit is contained in:
gili8420 2026-08-14 00:01:49 +09:00
commit b43955e0d9
162 changed files with 15425 additions and 0 deletions

22
src/lib/auth/guard.ts Normal file
View file

@ -0,0 +1,22 @@
import { goto } from '$app/navigation';
import { authStore } from './store';
/** Call in a page's onMount. Redirects anonymous users to `redirectTo`. */
export async function requireAuthenticated(redirectTo = '/login'): Promise<boolean> {
const state = await authStore.refresh();
if (state.status !== 'authenticated') {
await goto(redirectTo);
return false;
}
return true;
}
/** Like requireAuthenticated but also requires staff. */
export async function requireStaff(redirectTo = '/app'): Promise<boolean> {
const state = await authStore.refresh();
if (state.status !== 'authenticated' || !state.isStaff) {
await goto(redirectTo);
return false;
}
return true;
}

2
src/lib/auth/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from './store';
export * from './guard';

70
src/lib/auth/store.ts Normal file
View file

@ -0,0 +1,70 @@
import { writable, get } from 'svelte/store';
import { goto } from '$app/navigation';
import { setUnauthorizedHandler, authApi } from '$api';
export type AuthStatus = 'unknown' | 'anonymous' | 'authenticated';
export interface AuthState {
status: AuthStatus;
username: string | null;
isStaff: boolean;
}
const initial: AuthState = { status: 'unknown', username: null, isStaff: false };
function createAuthStore() {
const { subscribe, set } = writable<AuthState>(initial);
async function refresh(): Promise<AuthState> {
try {
const session = await authApi.session();
if (!session.isAuthenticated) {
const next: AuthState = { status: 'anonymous', username: null, isStaff: false };
set(next);
return next;
}
const me = await authApi.whoami();
const next: AuthState = {
status: 'authenticated',
username: me.username,
isStaff: me.is_staff
};
set(next);
return next;
} catch {
const next: AuthState = { status: 'anonymous', username: null, isStaff: false };
set(next);
return next;
}
}
async function login(username: string, password: string) {
await authApi.login(username, password);
await refresh();
}
async function register(username: string, email: string, password: string) {
await authApi.register(username, email, password);
await refresh();
}
async function logout() {
try {
await authApi.logout();
} finally {
set({ status: 'anonymous', username: null, isStaff: false });
}
}
return { subscribe, refresh, login, register, logout, snapshot: () => get({ subscribe }) };
}
export const authStore = createAuthStore();
// Route API 401s back through the store so an expired session redirects once.
let redirecting = false;
setUnauthorizedHandler(() => {
if (redirecting || typeof window === 'undefined') return;
if (window.location.pathname.startsWith('/login')) return;
redirecting = true;
goto('/login').finally(() => (redirecting = false));
});