Enhance Prisma schema and documentation for dashboard features

- Added new models for DashboardAccessRule and DashboardAuditLog to the Prisma schema, improving access control and logging capabilities.
- Updated GuildSettings model to include timezone support.
- Revised PHASE-TRACKING.md to reflect the completion of Phase 5 and outline upcoming Phase 6 features, including dashboard layout and settings framework.
- Exported new phase 6 features in shared index for better modularity.
This commit is contained in:
smueller
2026-07-22 13:50:14 +02:00
parent 6f3da6bb15
commit 7bc1684566
22 changed files with 645 additions and 15 deletions

View File

@@ -7,6 +7,7 @@ export * from './phase2.js';
export * from './phase3.js';
export * from './phase4.js';
export * from './phase5.js';
export * from './phase6.js';
export const LocaleSchema = z.enum(['de', 'en']);
export type Locale = z.infer<typeof LocaleSchema>;
@@ -14,6 +15,7 @@ export type Locale = z.infer<typeof LocaleSchema>;
export const GuildSettingsSchema = z.object({
guildId: z.string(),
locale: LocaleSchema.default('de'),
timezone: z.string().default('Europe/Berlin'),
moderationEnabled: z.boolean().default(true)
});

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import {
GuildGeneralSettingsPatchSchema,
ModulesUpdateSchema,
hasManageGuildPermission,
DashboardAccessRulesUpdateSchema
} from './phase6.js';
describe('phase6 schemas', () => {
it('accepts general settings patches', () => {
const parsed = GuildGeneralSettingsPatchSchema.parse({
locale: 'en',
timezone: 'UTC'
});
expect(parsed.locale).toBe('en');
expect(parsed.timezone).toBe('UTC');
});
it('rejects empty general settings patches', () => {
expect(() => GuildGeneralSettingsPatchSchema.parse({})).toThrow();
});
it('validates module updates', () => {
const parsed = ModulesUpdateSchema.parse({
modules: { moderation: true, automod: false }
});
expect(parsed.modules.moderation).toBe(true);
expect(parsed.modules.automod).toBe(false);
});
it('detects manage guild permission', () => {
expect(hasManageGuildPermission('32')).toBe(true);
expect(hasManageGuildPermission('0')).toBe(false);
expect(hasManageGuildPermission(String(32n | 8n))).toBe(true);
});
it('validates access rules', () => {
const parsed = DashboardAccessRulesUpdateSchema.parse({
rules: [{ roleId: '123456789012345678', modules: 'all' }]
});
expect(parsed.rules).toHaveLength(1);
});
});

View File

@@ -0,0 +1,149 @@
import { z } from 'zod';
const LocaleSchema = z.enum(['de', 'en']);
export const DashboardModuleIdSchema = z.enum([
'moderation',
'automod',
'logging',
'welcome',
'verification',
'leveling',
'economy',
'fun',
'giveaways',
'tickets',
'selfroles',
'tags',
'starboard',
'suggestions',
'birthdays',
'tempvoice',
'stats',
'feeds',
'scheduler',
'guildbackup'
]);
export type DashboardModuleId = z.infer<typeof DashboardModuleIdSchema>;
export const DashboardModuleGroupSchema = z.enum([
'moderation',
'engagement',
'utility',
'community',
'integrations'
]);
export type DashboardModuleGroup = z.infer<typeof DashboardModuleGroupSchema>;
export const DASHBOARD_MODULES = [
{ id: 'moderation', group: 'moderation', href: 'moderation' },
{ id: 'automod', group: 'moderation', href: 'automod' },
{ id: 'logging', group: 'moderation', href: 'logging' },
{ id: 'welcome', group: 'engagement', href: 'welcome' },
{ id: 'verification', group: 'engagement', href: 'verification' },
{ id: 'leveling', group: 'engagement', href: 'leveling' },
{ id: 'economy', group: 'engagement', href: 'economy' },
{ id: 'fun', group: 'engagement', href: 'fun' },
{ id: 'giveaways', group: 'community', href: 'giveaways' },
{ id: 'tickets', group: 'community', href: 'tickets' },
{ id: 'selfroles', group: 'community', href: 'selfroles' },
{ id: 'tags', group: 'utility', href: 'tags' },
{ id: 'starboard', group: 'community', href: 'starboard' },
{ id: 'suggestions', group: 'community', href: 'suggestions' },
{ id: 'birthdays', group: 'community', href: 'birthdays' },
{ id: 'tempvoice', group: 'utility', href: 'tempvoice' },
{ id: 'stats', group: 'integrations', href: 'stats' },
{ id: 'feeds', group: 'integrations', href: 'feeds' },
{ id: 'scheduler', group: 'integrations', href: 'scheduler' },
{ id: 'guildbackup', group: 'integrations', href: 'backup' }
] as const satisfies ReadonlyArray<{
id: DashboardModuleId;
group: DashboardModuleGroup;
href: string;
}>;
export const SessionUserSchema = z.object({
id: z.string().min(1),
username: z.string().min(1),
globalName: z.string().nullable(),
avatar: z.string().nullable(),
locale: LocaleSchema.optional()
});
export type SessionUser = z.infer<typeof SessionUserSchema>;
export const DashboardGuildSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
icon: z.string().nullable(),
botPresent: z.boolean(),
permissions: z.string()
});
export type DashboardGuild = z.infer<typeof DashboardGuildSchema>;
export const GuildGeneralSettingsSchema = z.object({
locale: LocaleSchema,
timezone: z.string().min(1).max(64),
moderationEnabled: z.boolean()
});
export type GuildGeneralSettings = z.infer<typeof GuildGeneralSettingsSchema>;
export const GuildGeneralSettingsPatchSchema = GuildGeneralSettingsSchema.partial().refine(
(value) => Object.keys(value).length > 0,
{ message: 'At least one field is required' }
);
export type GuildGeneralSettingsPatch = z.infer<typeof GuildGeneralSettingsPatchSchema>;
export const ModuleStatusSchema = z.object({
id: DashboardModuleIdSchema,
enabled: z.boolean(),
group: DashboardModuleGroupSchema,
href: z.string()
});
export type ModuleStatus = z.infer<typeof ModuleStatusSchema>;
export const ModulesUpdateSchema = z.object({
modules: z.record(DashboardModuleIdSchema, z.boolean()).refine((value) => Object.keys(value).length > 0, {
message: 'At least one module toggle is required'
})
});
export type ModulesUpdate = z.infer<typeof ModulesUpdateSchema>;
export const DashboardAccessRuleSchema = z.object({
id: z.string().optional(),
roleId: z.string().regex(/^\d{17,20}$/),
modules: z.union([z.literal('all'), z.array(DashboardModuleIdSchema).min(1)])
});
export type DashboardAccessRule = z.infer<typeof DashboardAccessRuleSchema>;
export const DashboardAccessRulesUpdateSchema = z.object({
rules: z.array(DashboardAccessRuleSchema)
});
export type DashboardAccessRulesUpdate = z.infer<typeof DashboardAccessRulesUpdateSchema>;
export const DashboardAuditEntrySchema = z.object({
id: z.string(),
guildId: z.string().nullable(),
actorUserId: z.string(),
action: z.string(),
path: z.string().nullable(),
createdAt: z.string()
});
export type DashboardAuditEntry = z.infer<typeof DashboardAuditEntrySchema>;
export const DISCORD_PERMISSION_MANAGE_GUILD = 1n << 5n;
export function hasManageGuildPermission(permissions: string | bigint): boolean {
const value = typeof permissions === 'bigint' ? permissions : BigInt(permissions);
return (value & DISCORD_PERMISSION_MANAGE_GUILD) === DISCORD_PERMISSION_MANAGE_GUILD;
}