Refactor layout and home page for improved localization and session handling
- Updated RootLayout to include locale support and integrated ThemeProvider and LocaleProvider for better internationalization. - Replaced static home page content with a redirect based on user session status, enhancing user experience by directing to the appropriate dashboard or login page. - Switched font from Geist to Inter for improved typography consistency.
This commit is contained in:
60
apps/webui/src/app/api/auth/callback/route.ts
Normal file
60
apps/webui/src/app/api/auth/callback/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { SessionUser } from '@nexumi/shared';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { exchangeCodeForToken, fetchDiscordUser } from '@/lib/discord-oauth';
|
||||
import { env } from '@/lib/env';
|
||||
import { redis } from '@/lib/redis';
|
||||
import { createSession } from '@/lib/session';
|
||||
|
||||
function oauthStateKey(state: string): string {
|
||||
return `webui:oauth:state:${state}`;
|
||||
}
|
||||
|
||||
function loginRedirect(error: string): NextResponse {
|
||||
const url = new URL('/login', env.WEBUI_URL);
|
||||
url.searchParams.set('error', error);
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const code = searchParams.get('code');
|
||||
const state = searchParams.get('state');
|
||||
const oauthError = searchParams.get('error');
|
||||
|
||||
if (oauthError) {
|
||||
return loginRedirect('access_denied');
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
return loginRedirect('invalid_request');
|
||||
}
|
||||
|
||||
const stateKey = oauthStateKey(state);
|
||||
const stateExists = await redis.get(stateKey);
|
||||
if (!stateExists) {
|
||||
return loginRedirect('invalid_state');
|
||||
}
|
||||
await redis.del(stateKey);
|
||||
|
||||
try {
|
||||
const token = await exchangeCodeForToken(code);
|
||||
const discordUser = await fetchDiscordUser(token.access_token);
|
||||
|
||||
const user: SessionUser = {
|
||||
id: discordUser.id,
|
||||
username: discordUser.username,
|
||||
globalName: discordUser.global_name ?? null,
|
||||
avatar: discordUser.avatar ?? null
|
||||
};
|
||||
|
||||
await createSession({
|
||||
user,
|
||||
accessToken: token.access_token,
|
||||
tokenExpiresAt: Date.now() + token.expires_in * 1000
|
||||
});
|
||||
} catch {
|
||||
return loginRedirect('oauth_failed');
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL('/dashboard', env.WEBUI_URL));
|
||||
}
|
||||
16
apps/webui/src/app/api/auth/login/route.ts
Normal file
16
apps/webui/src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { buildAuthorizeUrl } from '@/lib/discord-oauth';
|
||||
import { redis } from '@/lib/redis';
|
||||
|
||||
const OAUTH_STATE_TTL_SECONDS = 600;
|
||||
|
||||
function oauthStateKey(state: string): string {
|
||||
return `webui:oauth:state:${state}`;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const state = randomBytes(24).toString('hex');
|
||||
await redis.set(oauthStateKey(state), '1', 'EX', OAUTH_STATE_TTL_SECONDS);
|
||||
return NextResponse.redirect(buildAuthorizeUrl(state));
|
||||
}
|
||||
7
apps/webui/src/app/api/auth/logout/route.ts
Normal file
7
apps/webui/src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { destroySession } from '@/lib/session';
|
||||
|
||||
export async function POST() {
|
||||
await destroySession();
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
7
apps/webui/src/app/api/auth/me/route.ts
Normal file
7
apps/webui/src/app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getSession } from '@/lib/session';
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSession();
|
||||
return NextResponse.json({ user: session?.user ?? null });
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DashboardAccessRulesUpdateSchema } from '@nexumi/shared';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { listAccessRules, replaceAccessRules } from '@/lib/access-rules';
|
||||
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
|
||||
import { writeDashboardAudit } from '@/lib/audit';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ guildId: string }>;
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
||||
const { guildId } = await params;
|
||||
try {
|
||||
await requireGuildAccess(guildId);
|
||||
const rules = await listAccessRules(guildId);
|
||||
return NextResponse.json({ rules });
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
const { guildId } = await params;
|
||||
try {
|
||||
const session = await requireGuildAccess(guildId);
|
||||
const body = await request.json();
|
||||
const { rules: patch } = DashboardAccessRulesUpdateSchema.parse(body);
|
||||
|
||||
const before = await listAccessRules(guildId);
|
||||
const after = await replaceAccessRules(guildId, patch);
|
||||
|
||||
await writeDashboardAudit(prisma, {
|
||||
guildId,
|
||||
actorUserId: session.user.id,
|
||||
action: 'access-rules.update',
|
||||
path: `/dashboard/${guildId}/access`,
|
||||
before,
|
||||
after
|
||||
});
|
||||
|
||||
return NextResponse.json({ rules: after });
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
20
apps/webui/src/app/api/guilds/[guildId]/audit/route.ts
Normal file
20
apps/webui/src/app/api/guilds/[guildId]/audit/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
|
||||
import { listRecentDashboardAudit } from '@/lib/audit';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ guildId: string }>;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
const { guildId } = await params;
|
||||
try {
|
||||
await requireGuildAccess(guildId);
|
||||
const limitParam = request.nextUrl.searchParams.get('limit');
|
||||
const limit = limitParam ? Math.min(Math.max(Number.parseInt(limitParam, 10) || 10, 1), 50) : 10;
|
||||
const entries = await listRecentDashboardAudit(guildId, limit);
|
||||
return NextResponse.json({ entries });
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
46
apps/webui/src/app/api/guilds/[guildId]/modules/route.ts
Normal file
46
apps/webui/src/app/api/guilds/[guildId]/modules/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ModulesUpdateSchema } from '@nexumi/shared';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
|
||||
import { writeDashboardAudit } from '@/lib/audit';
|
||||
import { getModuleStatuses, updateModuleStatuses } from '@/lib/modules';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ guildId: string }>;
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
||||
const { guildId } = await params;
|
||||
try {
|
||||
await requireGuildAccess(guildId);
|
||||
const modules = await getModuleStatuses(guildId);
|
||||
return NextResponse.json({ modules });
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
const { guildId } = await params;
|
||||
try {
|
||||
const session = await requireGuildAccess(guildId);
|
||||
const body = await request.json();
|
||||
const { modules: patch } = ModulesUpdateSchema.parse(body);
|
||||
|
||||
const before = await getModuleStatuses(guildId);
|
||||
const after = await updateModuleStatuses(guildId, patch);
|
||||
|
||||
await writeDashboardAudit(prisma, {
|
||||
guildId,
|
||||
actorUserId: session.user.id,
|
||||
action: 'modules.update',
|
||||
path: `/dashboard/${guildId}/modules`,
|
||||
before,
|
||||
after
|
||||
});
|
||||
|
||||
return NextResponse.json({ modules: after });
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
46
apps/webui/src/app/api/guilds/[guildId]/settings/route.ts
Normal file
46
apps/webui/src/app/api/guilds/[guildId]/settings/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { GuildGeneralSettingsPatchSchema } from '@nexumi/shared';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
|
||||
import { writeDashboardAudit } from '@/lib/audit';
|
||||
import { getGuildGeneralSettings, updateGuildGeneralSettings } from '@/lib/guild-settings';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ guildId: string }>;
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
||||
const { guildId } = await params;
|
||||
try {
|
||||
await requireGuildAccess(guildId);
|
||||
const settings = await getGuildGeneralSettings(guildId);
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
const { guildId } = await params;
|
||||
try {
|
||||
const session = await requireGuildAccess(guildId);
|
||||
const body = await request.json();
|
||||
const patch = GuildGeneralSettingsPatchSchema.parse(body);
|
||||
|
||||
const before = await getGuildGeneralSettings(guildId);
|
||||
const after = await updateGuildGeneralSettings(guildId, patch);
|
||||
|
||||
await writeDashboardAudit(prisma, {
|
||||
guildId,
|
||||
actorUserId: session.user.id,
|
||||
action: 'settings.update',
|
||||
path: `/dashboard/${guildId}/settings`,
|
||||
before,
|
||||
after
|
||||
});
|
||||
|
||||
return NextResponse.json(after);
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
13
apps/webui/src/app/api/guilds/route.ts
Normal file
13
apps/webui/src/app/api/guilds/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { requireAuth, toApiErrorResponse } from '@/lib/auth';
|
||||
import { getManageableGuilds } from '@/lib/guilds';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const guilds = await getManageableGuilds(session);
|
||||
return NextResponse.json({ guilds });
|
||||
} catch (error) {
|
||||
return toApiErrorResponse(error);
|
||||
}
|
||||
}
|
||||
5
apps/webui/src/app/api/health/route.ts
Normal file
5
apps/webui/src/app/api/health/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
Reference in New Issue
Block a user