sync: phase 1 backend + frontend from server

This commit is contained in:
2026-06-30 09:26:03 -04:00
parent d4cdd89544
commit 30e159cbdb
31 changed files with 4758 additions and 114 deletions
+18 -3
View File
@@ -1,12 +1,17 @@
const API_BASE = '/api/v1';
export interface ApiError extends Error {
status?: number;
retryAfter?: number;
}
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const headers: Record<string, string> = {};
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(`${API_BASE}${path}`, {
const response = await fetch(API_BASE + path, {
method,
headers,
credentials: 'include',
@@ -14,7 +19,8 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
});
if (!response.ok) {
let errorMessage = `Request failed: ${response.status}`;
let errorMessage = 'Request failed: ' + response.status;
let retryAfter: number | undefined;
try {
const errorData = await response.json();
if (typeof errorData?.message === 'string') {
@@ -22,10 +28,19 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
} else if (typeof errorData?.error === 'string') {
errorMessage = errorData.error;
}
if (response.status === 429 && typeof errorData?.retry_after === 'number') {
retryAfter = errorData.retry_after;
}
} catch {
// ignore parse error
}
throw new Error(errorMessage);
const err = new Error(errorMessage) as ApiError;
err.status = response.status;
if (response.status === 429) {
const ra = response.headers.get('Retry-After');
err.retryAfter = retryAfter ?? (ra ? parseInt(ra, 10) : undefined);
}
throw err;
}
if (response.status === 204) {
+50
View File
@@ -0,0 +1,50 @@
import { useCallback } from 'react';
import { useServerStore } from '../stores/server.ts';
import { useAuthStore } from '../stores/auth.ts';
import { useRoleStore } from '../stores/role.ts';
const PERMS = {
VIEW_CHANNEL: 1,
SEND_MESSAGES: 2,
MANAGE_MESSAGES: 4,
KICK_MEMBERS: 8,
BAN_MEMBERS: 16,
MANAGE_SERVER: 32,
MANAGE_CHANNELS: 64,
ADMINISTRATOR: 128,
CONNECT_VOICE: 256,
SPEAK_VOICE: 512,
SHARE_SCREEN: 1024,
} as const;
export { PERMS };
export function usePermissions(serverId: string | null) {
const user = useAuthStore((s) => s.user);
const servers = useServerStore((s) => s.servers);
const roles = useRoleStore((s) => s.roles);
const server = serverId ? servers.find((s) => s.id === serverId) : null;
const isOwner = Boolean(server && user && server.ownerId === user.id);
const memberRoles = roles.filter((r) => r.server_id === serverId);
const has = useCallback(
(flag: number) => {
if (!serverId || !user) return false;
if (isOwner) return true;
const effective = memberRoles.reduce((acc, r) => acc | r.permissions, 0);
if ((effective & PERMS.ADMINISTRATOR) !== 0) return true;
return (effective & flag) === flag;
},
[serverId, user, isOwner, memberRoles],
);
return {
canKick: has(PERMS.KICK_MEMBERS),
canBan: has(PERMS.BAN_MEMBERS),
canMute: has(PERMS.MANAGE_MESSAGES),
canManageChannels: has(PERMS.MANAGE_CHANNELS),
isOwner,
};
}