Enhance dashboard guild access with owner bypass functionality
- Introduced `resolveDashboardGuild` to handle guild resolution for both managed and owner-panel bypass scenarios. - Updated `GuildLayout` and `WelcomePage` components to utilize the new guild resolution logic, improving access handling. - Enhanced `DashboardShell` to display an informational banner for users accessing guilds via the owner-panel bypass. - Added localization support for owner bypass messages in English and German. - Updated authentication logic to allow owner-panel users to access guilds without Discord Manage Guild permission.
This commit is contained in:
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';
|
||||
import { ZodError } from 'zod';
|
||||
import { hasManageGuildPermission } from '@nexumi/shared';
|
||||
import { fetchDiscordUserGuildsCached } from './discord-oauth';
|
||||
import { hasOwnerGuildBypass } from './owner-auth';
|
||||
import { prisma } from './prisma';
|
||||
import { getSession, type SessionPayload } from './session';
|
||||
|
||||
@@ -46,19 +47,25 @@ export async function requireAuthOrRedirect(): Promise<SessionPayload> {
|
||||
/**
|
||||
* 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.
|
||||
* there (tracked via the `Guild` table). Owner-panel users bypass Discord
|
||||
* membership checks. Use in API routes.
|
||||
*/
|
||||
export async function requireGuildAccess(guildId: string): Promise<SessionPayload> {
|
||||
const session = await requireAuth();
|
||||
const dbGuild = await prisma.guild.findUnique({ where: { id: guildId } });
|
||||
if (!dbGuild) {
|
||||
throw new ForbiddenError('Nexumi is not installed on this server');
|
||||
}
|
||||
|
||||
if (await hasOwnerGuildBypass(session.user.id)) {
|
||||
return session;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -68,15 +75,20 @@ export async function requireGuildAccess(guildId: string): Promise<SessionPayloa
|
||||
*/
|
||||
export async function requireGuildAccessOrRedirect(guildId: string): Promise<SessionPayload> {
|
||||
const session = await requireAuthOrRedirect();
|
||||
const dbGuild = await prisma.guild.findUnique({ where: { id: guildId } });
|
||||
if (!dbGuild) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
if (await hasOwnerGuildBypass(session.user.id)) {
|
||||
return session;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { hasManageGuildPermission, type DashboardGuild } from '@nexumi/shared';
|
||||
import { fetchDiscordUserGuildsCached } from './discord-oauth';
|
||||
import { hasOwnerGuildBypass } from './owner-auth';
|
||||
import { getOwnerGuildDiscordMeta } from './owner-guilds';
|
||||
import { prisma } from './prisma';
|
||||
import type { SessionPayload } from './session';
|
||||
|
||||
/** Administrator bit — used for synthetic owner-bypass guild entries. */
|
||||
const ADMINISTRATOR_PERMISSION = '8';
|
||||
|
||||
/**
|
||||
* Returns the guilds the current session user can manage (Manage Guild
|
||||
* permission), enriched with whether Nexumi is already installed there.
|
||||
@@ -40,7 +45,7 @@ export async function getManageableGuilds(session: SessionPayload): Promise<Dash
|
||||
|
||||
/**
|
||||
* Looks up a single manageable guild by id, or null when the session user
|
||||
* does not manage it.
|
||||
* does not manage it via Discord OAuth.
|
||||
*/
|
||||
export async function getManageableGuild(
|
||||
session: SessionPayload,
|
||||
@@ -49,3 +54,39 @@ export async function getManageableGuild(
|
||||
const guilds = await getManageableGuilds(session);
|
||||
return guilds.find((guild) => guild.id === guildId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a guild for the dashboard shell: normal Manage Guild membership,
|
||||
* or owner-panel bypass via Discord bot guild metadata when the bot is
|
||||
* installed but the user is not a Discord manager of that server.
|
||||
*/
|
||||
export async function resolveDashboardGuild(
|
||||
session: SessionPayload,
|
||||
guildId: string
|
||||
): Promise<{ guild: DashboardGuild; ownerBypass: boolean } | null> {
|
||||
const managed = await getManageableGuild(session, guildId);
|
||||
if (managed?.botPresent) {
|
||||
return { guild: managed, ownerBypass: false };
|
||||
}
|
||||
|
||||
if (!(await hasOwnerGuildBypass(session.user.id))) {
|
||||
return managed ? { guild: managed, ownerBypass: false } : null;
|
||||
}
|
||||
|
||||
const dbGuild = await prisma.guild.findUnique({ where: { id: guildId } });
|
||||
if (!dbGuild) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const meta = await getOwnerGuildDiscordMeta(guildId);
|
||||
return {
|
||||
ownerBypass: true,
|
||||
guild: {
|
||||
id: guildId,
|
||||
name: meta?.name ?? `Guild ${guildId}`,
|
||||
icon: meta?.icon ?? null,
|
||||
botPresent: true,
|
||||
permissions: ADMINISTRATOR_PERMISSION
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ export async function isOwnerUser(userId: string): Promise<boolean> {
|
||||
return (await resolveOwnerRole(userId)) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner-panel team members (any role) may open every guild where the bot is
|
||||
* installed — for support without needing Manage Guild on Discord.
|
||||
*/
|
||||
export async function hasOwnerGuildBypass(userId: string): Promise<boolean> {
|
||||
return (await resolveOwnerRole(userId)) !== null;
|
||||
}
|
||||
|
||||
export async function requireOwner(minimum: OwnerRole = 'VIEWER'): Promise<OwnerSession> {
|
||||
const session = await requireAuth();
|
||||
const role = await resolveOwnerRole(session.user.id);
|
||||
|
||||
Reference in New Issue
Block a user