Update fun module configuration and enhance CSS styles
- Added 'enabled' property to FunConfigSchema with a default value of true for better configuration management. - Refactored getFunConfig to return a complete default configuration when no data is found. - Improved globals.css by expanding CSS variables for light and dark themes, enhancing styling consistency across the application.
This commit is contained in:
99
apps/webui/src/lib/auth.ts
Normal file
99
apps/webui/src/lib/auth.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { ZodError } from 'zod';
|
||||
import { hasManageGuildPermission } from '@nexumi/shared';
|
||||
import { fetchDiscordUserGuildsCached } from './discord-oauth';
|
||||
import { prisma } from './prisma';
|
||||
import { getSession, type SessionPayload } from './session';
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(message = 'Authentication required') {
|
||||
super(message);
|
||||
this.name = 'UnauthorizedError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends Error {
|
||||
constructor(message = 'You do not have access to this resource') {
|
||||
super(message);
|
||||
this.name = 'ForbiddenError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use in API routes. Throws {@link UnauthorizedError} when no session exists.
|
||||
*/
|
||||
export async function requireAuth(): Promise<SessionPayload> {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use in Server Components / layouts. Redirects to /login instead of throwing,
|
||||
* since there is no JSON response to return.
|
||||
*/
|
||||
export async function requireAuthOrRedirect(): Promise<SessionPayload> {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
redirect('/login');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the current session user has Manage Guild permission on the given
|
||||
* guild (via Discord OAuth guild list) and that the bot is actually installed
|
||||
* there (tracked via the `Guild` table). Use in API routes.
|
||||
*/
|
||||
export async function requireGuildAccess(guildId: string): Promise<SessionPayload> {
|
||||
const session = await requireAuth();
|
||||
const guilds = await fetchDiscordUserGuildsCached(session.accessToken);
|
||||
const guild = guilds.find((entry) => entry.id === guildId);
|
||||
if (!guild || !hasManageGuildPermission(guild.permissions)) {
|
||||
throw new ForbiddenError('Missing Manage Guild permission for this server');
|
||||
}
|
||||
const dbGuild = await prisma.guild.findUnique({ where: { id: guildId } });
|
||||
if (!dbGuild) {
|
||||
throw new ForbiddenError('Nexumi is not installed on this server');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link requireGuildAccess} but for Server Components: redirects
|
||||
* instead of throwing.
|
||||
*/
|
||||
export async function requireGuildAccessOrRedirect(guildId: string): Promise<SessionPayload> {
|
||||
const session = await requireAuthOrRedirect();
|
||||
const guilds = await fetchDiscordUserGuildsCached(session.accessToken);
|
||||
const guild = guilds.find((entry) => entry.id === guildId);
|
||||
if (!guild || !hasManageGuildPermission(guild.permissions)) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
const dbGuild = await prisma.guild.findUnique({ where: { id: guildId } });
|
||||
if (!dbGuild) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
export function toApiErrorResponse(error: unknown): NextResponse {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
return NextResponse.json({ error: error.message }, { status: 401 });
|
||||
}
|
||||
if (error instanceof ForbiddenError) {
|
||||
return NextResponse.json({ error: error.message }, { status: 403 });
|
||||
}
|
||||
if (error instanceof ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', issues: error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[api]', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
Reference in New Issue
Block a user