This commit is contained in:
Christoph Haas 2026-03-21 22:41:23 +01:00
commit f4be03ceba
1826 changed files with 830924 additions and 0 deletions

View file

@ -0,0 +1,190 @@
const API_BASE = '/api';
interface ApiResponse<T> {
data: T;
error: string | null;
}
function getToken(): string | null {
if (typeof localStorage === 'undefined') return null;
return localStorage.getItem('token');
}
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = getToken();
const res = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
},
...options,
});
if (res.status === 401) {
localStorage.removeItem('token');
window.location.href = '/auth/login';
throw new Error('Unauthorized');
}
if (!res.ok) {
const error = await res.json().catch(() => ({ error: 'Request failed' }));
throw new Error(error.error || `HTTP ${res.status}`);
}
if (res.status === 204) {
return {} as T;
}
const json: ApiResponse<T> = await res.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
export const api = {
auth: {
login: async (identifier: string, password: string) => {
const res = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: identifier, password }),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || 'Login failed');
localStorage.setItem('token', json.data.token);
return json.data;
},
logout: () => {
localStorage.removeItem('token');
},
me: () => request<UserInfo>('/auth/me'),
forgot: async (identifier: string) => {
const res = await fetch(`${API_BASE}/auth/forgot`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier }),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || 'Request failed');
return json.data;
},
changePassword: async (oldPassword: string, newPassword: string) => {
const res = await fetch(`${API_BASE}/auth/change-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({ oldPassword, newPassword }),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || 'Failed to change password');
return json.data;
},
isLoggedIn: () => {
return typeof localStorage !== 'undefined' && !!localStorage.getItem('token');
},
},
domains: {
list: () => request<Domain[]>('/domains'),
get: (id: number) => request<Domain>(`/domains/${id}`),
create: (data: { name: string }) => request<Domain>('/domains', { method: 'POST', body: JSON.stringify(data) }),
delete: (id: number) => request<void>(`/domains/${id}`, { method: 'DELETE' }),
},
users: {
listByDomain: (domainName: string) => request<User[]>(`/domains/${domainName}/users`),
get: (domainName: string, id: number) => request<User>(`/domains/${domainName}/users/${id}`),
create: (domainName: string, data: CreateUser) => request<User>(`/domains/${domainName}/users`, { method: 'POST', body: JSON.stringify(data) }),
update: (domainName: string, id: number, data: Partial<User>) => request<void>(`/domains/${domainName}/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (domainName: string, id: number) => request<void>(`/domains/${domainName}/users/${id}`, { method: 'DELETE' }),
},
aliases: {
listByDomain: (domainName: string) => request<Alias[]>(`/domains/${domainName}/aliases`),
create: (domainName: string, data: CreateAlias) => request<Alias>(`/domains/${domainName}/aliases`, { method: 'POST', body: JSON.stringify(data) }),
delete: (domainName: string, id: number) => request<void>(`/domains/${domainName}/aliases/${id}`, { method: 'DELETE' }),
},
queue: {
list: () => request<QueueEntry[]>('/queue'),
requeue: (id: string) => request<void>(`/queue/${id}/requeue`, { method: 'POST' }),
delete: (id: string) => request<void>(`/queue/${id}`, { method: 'DELETE' }),
},
logs: {
list: (hours = 1, filter = '') => request<LogEntry[]>(`/logs?hours=${hours}&filter=${encodeURIComponent(filter)}`),
},
};
export interface UserInfo {
id: number;
username?: string;
email?: string;
role: string;
domainId?: number;
quota?: number;
}
export interface AuthUser {
id: number;
username: string;
role: string;
domainId?: number;
}
export interface Domain {
id: number;
name: string;
userCount?: number;
aliasCount?: number;
}
export interface User {
id: number;
email: string;
domainId: number;
quota: number;
domain?: Domain;
}
export interface CreateUser {
email: string;
password: string;
quota?: number;
}
export interface Alias {
id: number;
source: string;
destination: string;
domainId: number;
domainName?: string;
}
export interface CreateAlias {
source: string;
destination: string;
}
export interface QueueEntry {
id: string;
sender: string;
recipients: string[];
size: number;
time: string;
reason: string;
}
export interface LogEntry {
timestamp: string;
priority: string;
message: string;
}

View file

@ -0,0 +1,38 @@
import { writable } from 'svelte/store';
import type { User } from '$lib/api/client';
interface AuthState {
user: User | null;
token: string | null;
loading: boolean;
}
function createAuthStore() {
const { subscribe, set, update } = writable<AuthState>({
user: null,
token: null,
loading: true,
});
return {
subscribe,
login: (user: User, token: string) => {
localStorage.setItem('token', token);
set({ user, token, loading: false });
},
logout: () => {
localStorage.removeItem('token');
set({ user: null, token: null, loading: false });
},
init: () => {
const token = localStorage.getItem('token');
if (token) {
set({ user: null, token, loading: false });
} else {
set({ user: null, token: null, loading: false });
}
},
};
}
export const auth = createAuthStore();