fix: various fixes to sentry-reported errors and more

This commit is contained in:
Hampus Kraft
2026-02-18 15:38:51 +00:00
parent 302c0d2a0c
commit 0517a966a3
357 changed files with 25420 additions and 16281 deletions

View File

@@ -166,7 +166,7 @@ export function createAdminApp(options: CreateAdminAppOptions): AdminAppResult {
}
});
app.onError(createAdminErrorHandler(logger, config.env === 'development'));
app.onError(createAdminErrorHandler(logger, config.env === 'development', config.basePath));
app.get('/_health', (c) => c.json({status: 'ok'}));

View File

@@ -33,6 +33,7 @@ import type {
LookupUserResponse,
UserAdminResponse,
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
import type {WebAuthnCredentialListResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
export async function getCurrentAdmin(config: Config, session: Session): Promise<ApiResult<UserAdminResponse | null>> {
const client = new ApiClient(config, session);
@@ -417,3 +418,29 @@ export async function sendPasswordReset(
const client = new ApiClient(config, session);
return client.postVoid('/admin/users/send-password-reset', {user_id: userId}, auditLogReason);
}
export async function listWebAuthnCredentials(
config: Config,
session: Session,
userId: string,
): Promise<ApiResult<WebAuthnCredentialListResponse>> {
const client = new ApiClient(config, session);
return client.post<WebAuthnCredentialListResponse>('/admin/users/list-webauthn-credentials', {
user_id: userId,
});
}
export async function deleteWebAuthnCredential(
config: Config,
session: Session,
userId: string,
credentialId: string,
auditLogReason?: string,
): Promise<ApiResult<void>> {
const client = new ApiClient(config, session);
return client.postVoid(
'/admin/users/delete-webauthn-credential',
{user_id: userId, credential_id: credentialId},
auditLogReason,
);
}

View File

@@ -32,7 +32,13 @@ import type {Context, ErrorHandler} from 'hono';
const KNOWN_HTTP_STATUS_CODES: Array<HttpStatusCode> = Object.values(HttpStatus);
export function createAdminErrorHandler(logger: LoggerInterface, includeStack: boolean): ErrorHandler {
export function createAdminErrorHandler(
logger: LoggerInterface,
includeStack: boolean,
basePath: string,
): ErrorHandler {
const homeUrl = basePath || '/';
return createErrorHandler({
includeStack,
logError: (error, c) => {
@@ -55,9 +61,9 @@ export function createAdminErrorHandler(logger: LoggerInterface, includeStack: b
customHandler: (error, c) => {
const status = getStatus(error) ?? 500;
if (status === 404) {
return renderNotFound(c);
return renderNotFound(c, homeUrl);
}
return renderError(c, status);
return renderError(c, status, homeUrl);
},
});
}
@@ -67,7 +73,7 @@ function getStatus(error: Error): number | null {
return typeof statusValue === 'number' ? statusValue : null;
}
function renderNotFound(c: Context): Response | Promise<Response> {
function renderNotFound(c: Context, homeUrl: string): Response | Promise<Response> {
c.status(404);
return c.html(
<ErrorPage
@@ -75,13 +81,13 @@ function renderNotFound(c: Context): Response | Promise<Response> {
title="Page not found"
description="The page you are looking for does not exist or has been moved."
staticCdnEndpoint={CdnEndpoints.STATIC}
homeUrl="/admin"
homeUrl={homeUrl}
homeLabel="Go to admin"
/>,
);
}
function renderError(c: Context, status: number): Response | Promise<Response> {
function renderError(c: Context, status: number, homeUrl: string): Response | Promise<Response> {
const statusCode = isHttpStatusCode(status) ? status : HttpStatus.INTERNAL_SERVER_ERROR;
c.status(statusCode);
return c.html(
@@ -90,7 +96,7 @@ function renderError(c: Context, status: number): Response | Promise<Response> {
title="Something went wrong"
description="An unexpected error occurred. Please try again later."
staticCdnEndpoint={CdnEndpoints.STATIC}
homeUrl="/admin"
homeUrl={homeUrl}
homeLabel="Go to admin"
/>,
);

View File

@@ -105,6 +105,14 @@ const ReindexControls: FC<{config: Config; csrfToken: string}> = ({config, csrfT
<ReindexButton config={config} title="Guilds" indexType="guilds" csrfToken={csrfToken} />
<ReindexButton config={config} title="Reports" indexType="reports" csrfToken={csrfToken} />
<ReindexButton config={config} title="Audit Logs" indexType="audit_logs" csrfToken={csrfToken} />
<Heading level={3} class="subtitle mt-6 text-neutral-900">
Discovery Index
</Heading>
<Text color="muted" size="sm" class="mb-3">
Rebuilds the discovery search index for all approved discoverable communities. This syncs guild metadata,
descriptions, categories, and online counts.
</Text>
<ReindexButton config={config} title="Discovery Index" indexType="discovery" csrfToken={csrfToken} />
<Heading level={3} class="subtitle mt-6 text-neutral-900">
Guild-specific Search Indexes
</Heading>

View File

@@ -45,6 +45,7 @@ import type {
ListUserSessionsResponse,
UserAdminResponse,
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
import type {WebAuthnCredentialListResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
import {BackButton, NotFoundView} from '@fluxer/ui/src/components/Navigation';
import {formatDiscriminator, getUserAvatarUrl, getUserBannerUrl} from '@fluxer/ui/src/utils/FormatUser';
import type {FC} from 'hono/jsx';
@@ -170,6 +171,7 @@ export const UserDetailPage: FC<UserDetailPageProps> = async ({
let sessionsResult: {ok: true; data: ListUserSessionsResponse} | {ok: false; error: ApiError} | null = null;
let guildsResult: {ok: true; data: ListUserGuildsResponse} | {ok: false; error: ApiError} | null = null;
let dmChannelsResult: {ok: true; data: ListUserDmChannelsResponse} | {ok: false; error: ApiError} | null = null;
let webAuthnCredentials: WebAuthnCredentialListResponse | null = null;
let messageShredStatusResult:
| {ok: true; data: messagesApi.MessageShredStatusResponse}
| {ok: false; error: ApiError}
@@ -182,6 +184,13 @@ export const UserDetailPage: FC<UserDetailPageProps> = async ({
if (activeTab === 'account') {
sessionsResult = await usersApi.listUserSessions(config, session, userId);
const hasWebAuthn = user.authenticator_types.includes(2);
if (hasWebAuthn) {
const credResult = await usersApi.listWebAuthnCredentials(config, session, userId);
if (credResult.ok) {
webAuthnCredentials = credResult.data;
}
}
}
if (activeTab === 'guilds') {
@@ -304,6 +313,7 @@ export const UserDetailPage: FC<UserDetailPageProps> = async ({
user={user}
userId={userId}
sessionsResult={sessionsResult}
webAuthnCredentials={webAuthnCredentials}
csrfToken={csrfToken}
/>
)}

View File

@@ -32,6 +32,10 @@ import type {
UserAdminResponse,
UserSessionResponse,
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
import type {
WebAuthnCredentialListResponse,
WebAuthnCredentialResponse,
} from '@fluxer/schema/src/domains/auth/AuthSchemas';
import {Button} from '@fluxer/ui/src/components/Button';
import {Card} from '@fluxer/ui/src/components/Card';
import {CsrfInput} from '@fluxer/ui/src/components/CsrfInput';
@@ -43,10 +47,18 @@ interface AccountTabProps {
user: UserAdminResponse;
userId: string;
sessionsResult: {ok: true; data: ListUserSessionsResponse} | {ok: false; error: ApiError} | null;
webAuthnCredentials: WebAuthnCredentialListResponse | null;
csrfToken: string;
}
export function AccountTab({config: _config, user, userId: _userId, sessionsResult, csrfToken}: AccountTabProps) {
export function AccountTab({
config: _config,
user,
userId: _userId,
sessionsResult,
webAuthnCredentials,
csrfToken,
}: AccountTabProps) {
return (
<VStack gap={6}>
<Card padding="md">
@@ -266,10 +278,82 @@ export function AccountTab({config: _config, user, userId: _userId, sessionsResu
</div>
</VStack>
</Card>
{webAuthnCredentials && webAuthnCredentials.length > 0 && (
<Card padding="md">
<VStack gap={4}>
<Heading level={2} size="base">
WebAuthn Credentials
</Heading>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-neutral-200 border-b text-left">
<th class="pb-2 font-medium text-neutral-600">Name</th>
<th class="pb-2 font-medium text-neutral-600">Created</th>
<th class="pb-2 font-medium text-neutral-600">Last Used</th>
<th class="pb-2 font-medium text-neutral-600" />
</tr>
</thead>
<tbody>
{webAuthnCredentials.map((credential) => (
<WebAuthnCredentialRow credential={credential} csrfToken={csrfToken} />
))}
</tbody>
</table>
</div>
</VStack>
</Card>
)}
</VStack>
);
}
const WebAuthnCredentialRow: FC<{credential: WebAuthnCredentialResponse; csrfToken: string}> = ({
credential,
csrfToken,
}) => {
function formatTimestamp(value: string): string {
const [datePart, timePartRaw] = value.split('T');
if (!datePart || !timePartRaw) return value;
const timePart = timePartRaw.replace('Z', '').split('.')[0] ?? timePartRaw;
return `${datePart} ${timePart}`;
}
return (
<tr class="border-neutral-100 border-b">
<td class="py-2 pr-4">
<Text size="sm" class="text-neutral-900">
{credential.name}
</Text>
</td>
<td class="py-2 pr-4">
<Text size="sm" class="text-neutral-900">
{formatTimestamp(credential.created_at)}
</Text>
</td>
<td class="py-2 pr-4">
<Text size="sm" class="text-neutral-900">
{credential.last_used_at ? formatTimestamp(credential.last_used_at) : 'Never'}
</Text>
</td>
<td class="py-2">
<form
method="post"
action="?action=delete_webauthn_credential&tab=account"
onsubmit={`return confirm('Are you sure you want to delete the WebAuthn credential "${credential.name}"?')`}
>
<CsrfInput token={csrfToken} />
<input type="hidden" name="credential_id" value={credential.id} />
<Button type="submit" variant="primary" size="small">
Delete
</Button>
</form>
</td>
</tr>
);
};
const SessionCard: FC<{session: UserSessionResponse}> = ({session}) => {
function formatSessionTimestamp(value: string): string {
const [datePart, timePartRaw] = value.split('T');

View File

@@ -462,6 +462,23 @@ export function createUsersRoutes({config, assetVersion, requireAuth}: RouteFact
});
}
case 'delete_webauthn_credential': {
const credentialId = getRequiredString(formData, 'credential_id');
if (!credentialId) {
return redirectWithFlash(c, redirectUrl, {
message: 'Credential ID is required',
type: 'error',
});
}
const result = await usersApi.deleteWebAuthnCredential(config, session, userId, credentialId);
return redirectWithFlash(c, redirectUrl, {
message: result.ok ? 'WebAuthn credential deleted successfully' : 'Failed to delete WebAuthn credential',
type: result.ok ? 'success' : 'error',
});
}
case 'send_password_reset': {
const result = await usersApi.sendPasswordReset(config, session, userId);
return redirectWithFlash(c, redirectUrl, {