Enhance environment configuration and expand Prisma schema for bot management

- Added OWNER_USER_IDS to .env.example for specifying global bot owners.
- Introduced new models in the Prisma schema: CommandOverride, OwnerTeamMember, GlobalUserBlacklist, GuildBlacklist, FeatureFlag, BotPresenceConfig, ChangelogEntry, and OwnerAuditLog to support advanced bot management features.
- Updated env.ts to preprocess OWNER_USER_IDS for better handling of global bot owner IDs.
- Modified ModulePlaceholderPage to ensure proper routing for remaining dashboard modules.
This commit is contained in:
smueller
2026-07-22 14:51:56 +02:00
parent 946283dfba
commit 1de28fa494
54 changed files with 3564 additions and 3 deletions

View File

@@ -0,0 +1,279 @@
import { z } from 'zod';
/**
* Discord snowflake ID: 17-20 digit numeric string.
*/
export const SnowflakeSchema = z.string().regex(/^\d{17,20}$/, 'Invalid Discord ID');
/**
* Optional snowflake field for forms: accepts an empty string (treated as
* "unset"/cleared) or a valid snowflake. Kept as a plain string (rather than
* transformed to `undefined`) so that `JSON.stringify` does not silently
* drop the field when a user clears it in the dashboard - the API layer
* maps `''` to `null` when persisting.
*/
export const OptionalSnowflakeSchema = z.union([z.literal(''), SnowflakeSchema]);
function nonEmptyPatch<T extends z.ZodRawShape>(schema: z.ZodObject<T>) {
return schema.partial().refine((value) => Object.keys(value).length > 0, {
message: 'At least one field is required'
});
}
// ---------------------------------------------------------------------------
// Moderation
// ---------------------------------------------------------------------------
export const EscalationActionSchema = z.enum(['TIMEOUT', 'KICK', 'BAN']);
export type EscalationAction = z.infer<typeof EscalationActionSchema>;
export const EscalationRuleDashboardSchema = z.object({
id: z.string().optional(),
warnCount: z.number().int().positive(),
action: EscalationActionSchema,
durationMs: z.number().int().positive().nullable().optional(),
reason: z.string().max(500).nullable().optional(),
enabled: z.boolean().default(true)
});
export type EscalationRuleDashboard = z.infer<typeof EscalationRuleDashboardSchema>;
export const ModerationDashboardSchema = z.object({
moderationEnabled: z.boolean(),
escalations: z.array(EscalationRuleDashboardSchema)
});
export type ModerationDashboard = z.infer<typeof ModerationDashboardSchema>;
export const ModerationDashboardPatchSchema = z
.object({
moderationEnabled: z.boolean().optional(),
escalations: z.array(EscalationRuleDashboardSchema).optional()
})
.refine((value) => Object.keys(value).length > 0, { message: 'At least one field is required' });
export type ModerationDashboardPatch = z.infer<typeof ModerationDashboardPatchSchema>;
export const CaseDashboardSchema = z.object({
id: z.string(),
caseNumber: z.number().int(),
targetUserId: z.string(),
moderatorId: z.string(),
action: z.string(),
reason: z.string().nullable(),
createdAt: z.string(),
deletedAt: z.string().nullable()
});
export type CaseDashboard = z.infer<typeof CaseDashboardSchema>;
export const CaseListQuerySchema = z.object({
search: z.string().trim().max(100).optional(),
action: z.string().trim().max(50).optional(),
limit: z.coerce.number().int().positive().max(100).default(25)
});
export type CaseListQuery = z.infer<typeof CaseListQuerySchema>;
export const WarningDashboardSchema = z.object({
id: z.string(),
userId: z.string(),
moderatorId: z.string(),
reason: z.string().nullable(),
caseId: z.string().nullable(),
createdAt: z.string()
});
export type WarningDashboard = z.infer<typeof WarningDashboardSchema>;
export const WarningListQuerySchema = z.object({
userId: SnowflakeSchema.optional()
});
export type WarningListQuery = z.infer<typeof WarningListQuerySchema>;
// ---------------------------------------------------------------------------
// AutoMod
// ---------------------------------------------------------------------------
export const AntiRaidActionSchema = z.enum(['LOCKDOWN']);
export const AutomodConfigDashboardSchema = z.object({
enabled: z.boolean(),
antiRaidEnabled: z.boolean(),
antiRaidJoinThreshold: z.number().int().positive(),
antiRaidWindowSeconds: z.number().int().positive(),
antiRaidAction: AntiRaidActionSchema,
antiNukeEnabled: z.boolean(),
antiNukeBanThreshold: z.number().int().positive(),
antiNukeChannelThreshold: z.number().int().positive(),
antiNukeWindowSeconds: z.number().int().positive(),
lockdownActive: z.boolean()
});
export type AutomodConfigDashboard = z.infer<typeof AutomodConfigDashboardSchema>;
export const AutomodConfigDashboardPatchSchema = nonEmptyPatch(AutomodConfigDashboardSchema);
export type AutomodConfigDashboardPatch = z.infer<typeof AutomodConfigDashboardPatchSchema>;
// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------
export const LogEventTypeDashboardSchema = z.enum([
'MESSAGE_EDIT',
'MESSAGE_DELETE',
'MESSAGE_BULK_DELETE',
'MEMBER_JOIN',
'MEMBER_LEAVE',
'MEMBER_BAN',
'MEMBER_UNBAN',
'MEMBER_UPDATE',
'CHANNEL_CREATE',
'CHANNEL_DELETE',
'CHANNEL_UPDATE',
'VOICE_JOIN',
'VOICE_LEAVE',
'VOICE_MOVE',
'INVITE_CREATE',
'EMOJI_CREATE',
'EMOJI_UPDATE',
'EMOJI_DELETE',
'STICKER_CREATE',
'STICKER_UPDATE',
'STICKER_DELETE',
'THREAD_CREATE',
'THREAD_UPDATE',
'THREAD_DELETE',
'GUILD_UPDATE',
'MOD_ACTION'
]);
export type LogEventTypeDashboard = z.infer<typeof LogEventTypeDashboardSchema>;
export const LogChannelMappingSchema = z.object({
eventType: LogEventTypeDashboardSchema,
channelId: SnowflakeSchema
});
export type LogChannelMapping = z.infer<typeof LogChannelMappingSchema>;
export const LoggingConfigDashboardSchema = z.object({
enabled: z.boolean(),
ignoreBots: z.boolean(),
ignoredChannelIds: z.array(z.string()),
ignoredRoleIds: z.array(z.string()),
logChannels: z.array(LogChannelMappingSchema)
});
export type LoggingConfigDashboard = z.infer<typeof LoggingConfigDashboardSchema>;
export const LoggingConfigDashboardPatchSchema = z
.object({
enabled: z.boolean().optional(),
ignoreBots: z.boolean().optional(),
ignoredChannelIds: z.array(z.string()).optional(),
ignoredRoleIds: z.array(z.string()).optional(),
logChannels: z.array(LogChannelMappingSchema).optional()
})
.refine((value) => Object.keys(value).length > 0, { message: 'At least one field is required' });
export type LoggingConfigDashboardPatch = z.infer<typeof LoggingConfigDashboardPatchSchema>;
// ---------------------------------------------------------------------------
// Welcome & Leave
// ---------------------------------------------------------------------------
export const WelcomeMessageTypeDashboardSchema = z.enum(['TEXT', 'EMBED', 'IMAGE']);
export const WelcomeConfigDashboardSchema = z.object({
welcomeEnabled: z.boolean(),
leaveEnabled: z.boolean(),
welcomeChannelId: OptionalSnowflakeSchema,
leaveChannelId: OptionalSnowflakeSchema,
welcomeType: WelcomeMessageTypeDashboardSchema,
welcomeContent: z.string().max(2000).nullable().optional(),
welcomeEmbed: z.record(z.string(), z.unknown()).nullable().optional(),
leaveContent: z.string().max(2000).nullable().optional(),
leaveEmbed: z.record(z.string(), z.unknown()).nullable().optional(),
welcomeDmEnabled: z.boolean(),
welcomeDmContent: z.string().max(2000).nullable().optional(),
userAutoroleIds: z.array(z.string()),
botAutoroleIds: z.array(z.string()),
imageTitle: z.string().max(200).nullable().optional(),
imageSubtitle: z.string().max(200).nullable().optional()
});
export type WelcomeConfigDashboard = z.infer<typeof WelcomeConfigDashboardSchema>;
export const WelcomeConfigDashboardPatchSchema = nonEmptyPatch(WelcomeConfigDashboardSchema);
export type WelcomeConfigDashboardPatch = z.infer<typeof WelcomeConfigDashboardPatchSchema>;
// ---------------------------------------------------------------------------
// Verification
// ---------------------------------------------------------------------------
export const VerificationModeDashboardSchema = z.enum(['BUTTON', 'CAPTCHA']);
export const VerificationFailActionDashboardSchema = z.enum(['KICK', 'BAN', 'NONE']);
export const VerificationConfigDashboardSchema = z.object({
enabled: z.boolean(),
channelId: OptionalSnowflakeSchema,
verifiedRoleId: OptionalSnowflakeSchema,
unverifiedRoleId: OptionalSnowflakeSchema,
mode: VerificationModeDashboardSchema,
minAccountAgeDays: z.number().int().nonnegative(),
failAction: VerificationFailActionDashboardSchema
});
export type VerificationConfigDashboard = z.infer<typeof VerificationConfigDashboardSchema>;
export const VerificationConfigDashboardPatchSchema = nonEmptyPatch(VerificationConfigDashboardSchema);
export type VerificationConfigDashboardPatch = z.infer<typeof VerificationConfigDashboardPatchSchema>;
// ---------------------------------------------------------------------------
// Leveling
// ---------------------------------------------------------------------------
export const LevelUpModeDashboardSchema = z.enum(['CHANNEL', 'DM', 'OFF']);
export const LevelingConfigDashboardSchema = z.object({
enabled: z.boolean(),
textXpMin: z.number().int().positive(),
textXpMax: z.number().int().positive(),
textCooldownSeconds: z.number().int().positive(),
voiceXpPerMinute: z.number().int().nonnegative(),
noXpChannelIds: z.array(z.string()),
noXpRoleIds: z.array(z.string()),
roleMultipliers: z.record(z.string(), z.number().positive()),
channelMultipliers: z.record(z.string(), z.number().positive()),
levelUpMode: LevelUpModeDashboardSchema,
levelUpChannelId: OptionalSnowflakeSchema,
levelUpMessage: z.string().max(500).nullable().optional(),
stackRewards: z.boolean(),
cardAccentColor: z.string().regex(/^#[0-9a-fA-F]{6}$/),
cardBackgroundColor: z.string().regex(/^#[0-9a-fA-F]{6}$/)
});
export type LevelingConfigDashboard = z.infer<typeof LevelingConfigDashboardSchema>;
export const LevelingConfigDashboardPatchSchema = nonEmptyPatch(LevelingConfigDashboardSchema);
export type LevelingConfigDashboardPatch = z.infer<typeof LevelingConfigDashboardPatchSchema>;
// ---------------------------------------------------------------------------
// Economy
// ---------------------------------------------------------------------------
export const EconomyConfigDashboardSchema = z.object({
enabled: z.boolean(),
currencyName: z.string().min(1).max(32),
currencySymbol: z.string().min(1).max(8),
dailyAmount: z.number().int().nonnegative(),
weeklyAmount: z.number().int().nonnegative(),
workMin: z.number().int().nonnegative(),
workMax: z.number().int().nonnegative(),
workCooldownSeconds: z.number().int().positive(),
startingBalance: z.number().int().nonnegative()
});
export type EconomyConfigDashboard = z.infer<typeof EconomyConfigDashboardSchema>;
export const EconomyConfigDashboardPatchSchema = nonEmptyPatch(EconomyConfigDashboardSchema);
export type EconomyConfigDashboardPatch = z.infer<typeof EconomyConfigDashboardPatchSchema>;
// ---------------------------------------------------------------------------
// Fun
// ---------------------------------------------------------------------------
export const FunConfigDashboardSchema = z.object({
enabled: z.boolean(),
mediaEnabled: z.boolean()
});
export type FunConfigDashboard = z.infer<typeof FunConfigDashboardSchema>;
export const FunConfigDashboardPatchSchema = nonEmptyPatch(FunConfigDashboardSchema);
export type FunConfigDashboardPatch = z.infer<typeof FunConfigDashboardPatchSchema>;