first
This commit is contained in:
commit
f4be03ceba
1826 changed files with 830924 additions and 0 deletions
190
frontend/src/lib/api/client.ts
Normal file
190
frontend/src/lib/api/client.ts
Normal 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;
|
||||
}
|
||||
38
frontend/src/lib/stores/auth.ts
Normal file
38
frontend/src/lib/stores/auth.ts
Normal 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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue