quick commit
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 6m9s
CI/CD Pipeline / Security Scanning (push) Successful in 26s
CI/CD Pipeline / Tests (3.11) (push) Failing after 5m24s
CI/CD Pipeline / Tests (3.12) (push) Failing after 5m23s
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
CI/CD Pipeline / Notification (push) Successful in 1s

This commit is contained in:
2026-01-17 20:24:43 +01:00
parent 95cc3cdb8f
commit 831eed8dbc
82 changed files with 8860 additions and 167 deletions

View File

@@ -0,0 +1,120 @@
/**
* API client for GuardDen Dashboard
*/
import type {
AnalyticsSummary,
AutomodRuleConfig,
CreateUserNote,
Guild,
GuildSettings,
Me,
ModerationStats,
PaginatedLogs,
UserNote,
UserProfile,
} from '../types/api';
const BASE_URL = '';
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(BASE_URL + url, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Request failed: ${response.status} - ${error}`);
}
return response.json() as Promise<T>;
}
// Auth API
export const authApi = {
getMe: () => fetchJson<Me>('/api/me'),
};
// Guilds API
export const guildsApi = {
list: () => fetchJson<Guild[]>('/api/guilds'),
getSettings: (guildId: number) =>
fetchJson<GuildSettings>(`/api/guilds/${guildId}/settings`),
updateSettings: (guildId: number, settings: GuildSettings) =>
fetchJson<GuildSettings>(`/api/guilds/${guildId}/settings`, {
method: 'PUT',
body: JSON.stringify(settings),
}),
getAutomodConfig: (guildId: number) =>
fetchJson<AutomodRuleConfig>(`/api/guilds/${guildId}/automod`),
updateAutomodConfig: (guildId: number, config: AutomodRuleConfig) =>
fetchJson<AutomodRuleConfig>(`/api/guilds/${guildId}/automod`, {
method: 'PUT',
body: JSON.stringify(config),
}),
exportConfig: (guildId: number) =>
fetch(`${BASE_URL}/api/guilds/${guildId}/export`, {
credentials: 'include',
}).then((res) => res.blob()),
};
// Moderation API
export const moderationApi = {
getLogs: (guildId?: number, limit = 50, offset = 0) => {
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
if (guildId) {
params.set('guild_id', String(guildId));
}
return fetchJson<PaginatedLogs>(`/api/moderation/logs?${params}`);
},
};
// Analytics API
export const analyticsApi = {
getSummary: (guildId?: number, days = 7) => {
const params = new URLSearchParams({ days: String(days) });
if (guildId) {
params.set('guild_id', String(guildId));
}
return fetchJson<AnalyticsSummary>(`/api/analytics/summary?${params}`);
},
getModerationStats: (guildId?: number, days = 30) => {
const params = new URLSearchParams({ days: String(days) });
if (guildId) {
params.set('guild_id', String(guildId));
}
return fetchJson<ModerationStats>(`/api/analytics/moderation-stats?${params}`);
},
};
// Users API
export const usersApi = {
search: (guildId: number, username?: string, minStrikes?: number, limit = 50) => {
const params = new URLSearchParams({ guild_id: String(guildId), limit: String(limit) });
if (username) {
params.set('username', username);
}
if (minStrikes !== undefined) {
params.set('min_strikes', String(minStrikes));
}
return fetchJson<UserProfile[]>(`/api/users/search?${params}`);
},
getProfile: (userId: number, guildId: number) =>
fetchJson<UserProfile>(`/api/users/${userId}/profile?guild_id=${guildId}`),
getNotes: (userId: number, guildId: number) =>
fetchJson<UserNote[]>(`/api/users/${userId}/notes?guild_id=${guildId}`),
createNote: (userId: number, guildId: number, note: CreateUserNote) =>
fetchJson<UserNote>(`/api/users/${userId}/notes?guild_id=${guildId}`, {
method: 'POST',
body: JSON.stringify(note),
}),
deleteNote: (userId: number, noteId: number, guildId: number) =>
fetchJson<void>(`/api/users/${userId}/notes/${noteId}?guild_id=${guildId}`, {
method: 'DELETE',
}),
};

View File

@@ -0,0 +1,120 @@
/**
* WebSocket service for real-time updates
*/
import type { WebSocketEvent } from '../types/api';
type EventHandler = (event: WebSocketEvent) => void;
export class WebSocketService {
private ws: WebSocket | null = null;
private handlers: Map<string, Set<EventHandler>> = new Map();
private reconnectTimeout: number | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private guildId: number | null = null;
connect(guildId: number): void {
this.guildId = guildId;
this.reconnectAttempts = 0;
this.doConnect();
}
private doConnect(): void {
if (this.guildId === null) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/events?guild_id=${this.guildId}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as WebSocketEvent;
this.emit(data.type, data);
this.emit('*', data); // Emit to wildcard handlers
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = () => {
console.log('WebSocket closed');
this.scheduleReconnect();
};
}
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
return;
}
const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000);
this.reconnectTimeout = window.setTimeout(() => {
this.reconnectAttempts++;
console.log(`Reconnecting... (attempt ${this.reconnectAttempts})`);
this.doConnect();
}, delay);
}
disconnect(): void {
if (this.reconnectTimeout !== null) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.guildId = null;
}
on(eventType: string, handler: EventHandler): void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, new Set());
}
this.handlers.get(eventType)!.add(handler);
}
off(eventType: string, handler: EventHandler): void {
const handlers = this.handlers.get(eventType);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.handlers.delete(eventType);
}
}
}
private emit(eventType: string, event: WebSocketEvent): void {
const handlers = this.handlers.get(eventType);
if (handlers) {
handlers.forEach((handler) => handler(event));
}
}
send(data: unknown): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
ping(): void {
this.send('ping');
}
}
// Singleton instance
export const wsService = new WebSocketService();