diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a2b295d..aaa1dff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,8 +26,7 @@ model GuildSettings { birthdayConfig Json? reactionRolesEnabled Boolean? reactionRolesConfig Json? - eventsEnabled Boolean? - supportRoleId String? + eventsEnabled Boolean?\n registerEnabled Boolean?\n registerConfig Json?\n supportRoleId String? updatedAt DateTime @updatedAt createdAt DateTime @default(now()) } @@ -161,3 +160,55 @@ model EventSignup { @@unique([eventId, userId]) @@index([guildId, eventId]) } + +model RegisterForm { + id String @id @default(cuid()) + guildId String + name String + description String? + reviewChannelId String? + notifyRoleIds String[] + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + fields RegisterFormField[] + + @@index([guildId, isActive]) +} + +model RegisterFormField { + id String @id @default(cuid()) + formId String + label String + type String + required Boolean @default(false) + "order" Int @default(0) + + form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade) +} + +model RegisterApplication { + id String @id @default(cuid()) + guildId String + userId String + formId String + status String @default("pending") + reviewedBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + answers RegisterApplicationAnswer[] + + form RegisterForm @relation(fields: [formId], references: [id]) + + @@index([guildId, formId, status]) +} + +model RegisterApplicationAnswer { + id String @id @default(cuid()) + applicationId String + fieldId String + value String + + application RegisterApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade) +} + diff --git a/src/config/context.ts b/src/config/context.ts index 769e442..7c4ebcb 100644 --- a/src/config/context.ts +++ b/src/config/context.ts @@ -14,6 +14,7 @@ import { ReactionRoleService } from '../services/reactionRoleService'; import { EventService } from '../services/eventService'; import { TicketAutomationService } from '../services/ticketAutomationService'; import { KnowledgeBaseService } from '../services/knowledgeBaseService'; +import { RegisterService } from '../services/registerService'; export const context = { client: null as Client | null, @@ -31,7 +32,8 @@ export const context = { reactionRoles: new ReactionRoleService(), events: new EventService(), ticketAutomation: new TicketAutomationService(), - knowledgeBase: new KnowledgeBaseService() + knowledgeBase: new KnowledgeBaseService(), + register: new RegisterService() }; context.modules.setHooks({ diff --git a/src/config/state.ts b/src/config/state.ts index 2826de0..7270d20 100644 --- a/src/config/state.ts +++ b/src/config/state.ts @@ -60,6 +60,11 @@ export interface GuildSettings { }; reactionRolesEnabled?: boolean; reactionRolesConfig?: any; + registerEnabled?: boolean; + registerConfig?: { + reviewChannelId?: string; + notifyRoleIds?: string[]; + }; supportRoleId?: string; welcomeEnabled?: boolean; } @@ -80,7 +85,8 @@ class SettingsStore { 'statuspageEnabled', 'birthdayEnabled', 'reactionRolesEnabled', - 'eventsEnabled' + 'eventsEnabled', + 'registerEnabled' ] as const ).forEach((key) => { if (normalized[key] === undefined) normalized[key] = true; @@ -116,6 +122,8 @@ class SettingsStore { birthdayConfig: (row as any).birthdayConfig ?? undefined, reactionRolesEnabled: (row as any).reactionRolesEnabled ?? undefined, reactionRolesConfig: (row as any).reactionRolesConfig ?? undefined, + registerEnabled: (row as any).registerEnabled ?? undefined, + registerConfig: (row as any).registerConfig ?? undefined, supportRoleId: row.supportRoleId ?? undefined } satisfies GuildSettings; this.cache.set(row.guildId, this.applyModuleDefaults(cfg)); @@ -147,6 +155,9 @@ class SettingsStore { } else if (partial.reactionRolesConfig?.enabled !== undefined) { partial.reactionRolesEnabled = partial.reactionRolesConfig.enabled; } + if (!partial.registerConfig && partial.registerEnabled !== undefined) { + partial.registerConfig = { ...(partial.registerConfig ?? {}) }; + } const merged: GuildSettings = this.applyModuleDefaults({ ...(this.cache.get(guildId) ?? {}), ...partial }); const mergedAutomod = { ...(merged.automodConfig ?? {}), @@ -193,6 +204,8 @@ class SettingsStore { birthdayConfig: merged.birthdayConfig ?? null, reactionRolesEnabled: merged.reactionRolesEnabled ?? null, reactionRolesConfig: merged.reactionRolesConfig ?? null, + registerEnabled: merged.registerEnabled ?? null, + registerConfig: merged.registerConfig ?? null, supportRoleId: merged.supportRoleId ?? null }, create: { @@ -213,6 +226,8 @@ class SettingsStore { birthdayConfig: merged.birthdayConfig ?? null, reactionRolesEnabled: merged.reactionRolesEnabled ?? null, reactionRolesConfig: merged.reactionRolesConfig ?? null, + registerEnabled: merged.registerEnabled ?? null, + registerConfig: merged.registerConfig ?? null, supportRoleId: merged.supportRoleId ?? null } }); diff --git a/src/database/schema.prisma b/src/database/schema.prisma index d0216d0..42acc39 100644 --- a/src/database/schema.prisma +++ b/src/database/schema.prisma @@ -26,6 +26,8 @@ model GuildSettings { reactionRolesEnabled Boolean? reactionRolesConfig Json? eventsEnabled Boolean? + registerEnabled Boolean? + registerConfig Json? supportRoleId String? updatedAt DateTime @updatedAt createdAt DateTime @default(now()) @@ -160,3 +162,54 @@ model EventSignup { @@unique([eventId, userId]) @@index([guildId, eventId]) } + +model RegisterForm { + id String @id @default(cuid()) + guildId String + name String + description String? + reviewChannelId String? + notifyRoleIds String[] + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + fields RegisterFormField[] + + @@index([guildId, isActive]) +} + +model RegisterFormField { + id String @id @default(cuid()) + formId String + label String + type String + required Boolean @default(false) + "order" Int @default(0) + + form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade) +} + +model RegisterApplication { + id String @id @default(cuid()) + guildId String + userId String + formId String + status String @default("pending") + reviewedBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + answers RegisterApplicationAnswer[] + + form RegisterForm @relation(fields: [formId], references: [id]) + + @@index([guildId, formId, status]) +} + +model RegisterApplicationAnswer { + id String @id @default(cuid()) + applicationId String + fieldId String + value String + + application RegisterApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade) +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 8d4616a..9900e25 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -20,8 +20,19 @@ const event: EventHandler = { return; } } + if (interaction.customId.startsWith('register:')) { + await context.register.handleButton(interaction as any); + return; + } await context.tickets.handleButton(interaction); } + + if (interaction.isModalSubmit()) { + if (interaction.customId.startsWith('register:submit:')) { + await context.register.handleModal(interaction as any); + return; + } + } } }; diff --git a/src/index.ts b/src/index.ts index d72470b..f96dbe6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ async function bootstrap() { context.reactionRoles.setClient(client); context.events.setClient(client); context.events.startScheduler(); + context.register.setClient(client); await context.reactionRoles.loadCache(); logger.setSink((entry) => context.admin.pushLog(entry)); for (const gid of settingsStore.all().keys()) { diff --git a/src/services/moduleService.ts b/src/services/moduleService.ts index b9d552b..0ee722d 100644 --- a/src/services/moduleService.ts +++ b/src/services/moduleService.ts @@ -10,7 +10,8 @@ export type ModuleKey = | 'statuspageEnabled' | 'birthdayEnabled' | 'reactionRolesEnabled' - | 'eventsEnabled'; + | 'eventsEnabled' + | 'registerEnabled'; export interface GuildModuleState { key: ModuleKey; @@ -29,7 +30,8 @@ const MODULES: Record = { statuspageEnabled: { name: 'Statuspage', description: 'Service Checks, Uptime und Status-Embed.' }, birthdayEnabled: { name: 'Birthday', description: 'Geburtstage speichern und Glueckwuensche senden.' }, reactionRolesEnabled: { name: 'Reaction Roles', description: 'Reaktionen vergeben und entfernen Rollen.' }, - eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' } + eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' }, + registerEnabled: { name: 'Register', description: 'Registrierungsformulare und Bewerbungen.' } }; export class BotModuleService { diff --git a/src/services/registerService.ts b/src/services/registerService.ts new file mode 100644 index 0000000..c2f1cd0 --- /dev/null +++ b/src/services/registerService.ts @@ -0,0 +1,256 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + Client, + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ModalSubmitInteraction, + GuildMember +} from 'discord.js'; +import { prisma } from '../database'; +import { settingsStore } from '../config/state'; +import { env } from '../config/env'; + +export class RegisterService { + private client: Client | null = null; + + public setClient(client: Client) { + this.client = client; + } + + public async listForms(guildId: string) { + return prisma.registerForm.findMany({ where: { guildId }, include: { fields: { orderBy: { order: 'asc' } } }, orderBy: { createdAt: 'desc' } }); + } + + public async saveForm(form: { + id?: string; + guildId: string; + name: string; + description?: string; + reviewChannelId?: string; + notifyRoleIds?: string[]; + isActive?: boolean; + fields: { id?: string; label: string; type: string; required?: boolean; order?: number }[]; + }) { + const notify = (form.notifyRoleIds || []).filter(Boolean); + if (form.id) { + await prisma.registerForm.update({ + where: { id: form.id }, + data: { + name: form.name, + description: form.description, + reviewChannelId: form.reviewChannelId, + notifyRoleIds: notify, + isActive: form.isActive ?? true + } + }); + await prisma.registerFormField.deleteMany({ where: { formId: form.id } }); + await prisma.registerFormField.createMany({ + data: (form.fields || []).map((f, idx) => ({ + formId: form.id as string, + label: f.label, + type: f.type, + required: f.required ?? false, + order: f.order ?? idx + })) + }); + return prisma.registerForm.findUnique({ where: { id: form.id }, include: { fields: { orderBy: { order: 'asc' } } } }); + } + const created = await prisma.registerForm.create({ + data: { + guildId: form.guildId, + name: form.name, + description: form.description, + reviewChannelId: form.reviewChannelId, + notifyRoleIds: notify, + isActive: form.isActive ?? true, + fields: { + create: (form.fields || []).map((f, idx) => ({ + label: f.label, + type: f.type, + required: f.required ?? false, + order: f.order ?? idx + })) + } + }, + include: { fields: { orderBy: { order: 'asc' } } } + }); + return created; + } + + public async deleteForm(guildId: string, id: string) { + const form = await prisma.registerForm.findFirst({ where: { id, guildId } }); + if (!form) return false; + await prisma.registerFormField.deleteMany({ where: { formId: id } }); + await prisma.registerForm.delete({ where: { id } }); + return true; + } + + public async sendPanel(guildId: string, formId: string, channelId?: string, message?: string) { + if (!this.client) return null; + const form = await prisma.registerForm.findFirst({ where: { id: formId, guildId }, include: { fields: true } }); + if (!form) return null; + const targetChannelId = channelId || form.reviewChannelId || settingsStore.get(guildId)?.registerConfig?.reviewChannelId; + if (!targetChannelId) return null; + const guild = this.client.guilds.cache.get(guildId) ?? (await this.client.guilds.fetch(guildId).catch(() => null)); + if (!guild) return null; + const channel = await guild.channels.fetch(targetChannelId).catch(() => null); + if (!channel || !channel.isTextBased()) return null; + const embed = new EmbedBuilder() + .setTitle(form.name) + .setDescription(message || 'Klicke auf Registrieren, um das Formular auszufüllen.') + .setColor(0xf97316); + const btn = new ButtonBuilder().setCustomId(`register:form:${form.id}`).setLabel('Registrieren').setStyle(ButtonStyle.Primary); + const row = new ActionRowBuilder().addComponents(btn); + const sent = await (channel as any).send({ embeds: [embed], components: [row] }); + return sent.id; + } + + public async handleButton(interaction: ButtonInteraction) { + if (interaction.customId.startsWith('register:form:')) { + const formId = interaction.customId.split(':')[2]; + const form = await prisma.registerForm.findFirst({ where: { id: formId }, include: { fields: { orderBy: { order: 'asc' } } } }); + if (!form) return interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true }); + const modal = new ModalBuilder().setTitle(form.name).setCustomId(`register:submit:${form.id}`); + const components: any[] = []; + form.fields.slice(0, 5).forEach((f) => { + const input = new TextInputBuilder() + .setCustomId(f.id) + .setLabel(f.label.slice(0, 45) || 'Feld') + .setStyle(f.type === 'longText' ? TextInputStyle.Paragraph : TextInputStyle.Short) + .setRequired(f.required ?? false); + components.push(new ActionRowBuilder().addComponents(input)); + }); + modal.addComponents(components as any); + await interaction.showModal(modal); + return; + } + + if (interaction.customId.startsWith('register:review:')) { + const [, , action, appId] = interaction.customId.split(':'); + const app = await prisma.registerApplication.findUnique({ where: { id: appId }, include: { form: true } }); + if (!app) { + await interaction.reply({ content: 'Antrag nicht gefunden.', ephemeral: true }); + return; + } + const statusMap: any = { accept: 'accepted', invite: 'invited', reject: 'rejected' }; + const newStatus = statusMap[action] || 'pending'; + const updated = await prisma.registerApplication.update({ + where: { id: appId }, + data: { status: newStatus, reviewedBy: interaction.user.id } + }); + await this.updateReviewMessage(interaction, updated); + const user = await this.client?.users.fetch(app.userId).catch(() => null); + if (user) { + const msg = + newStatus === 'accepted' + ? 'Deine Registrierung wurde akzeptiert.' + : newStatus === 'invited' + ? 'Bitte komm für ein Gespräch vorbei.' + : 'Deine Registrierung wurde abgelehnt.'; + user.send(msg).catch(() => undefined); + } + await interaction.reply({ content: 'Status aktualisiert.', ephemeral: true }); + return; + } + } + + public async handleModal(interaction: ModalSubmitInteraction) { + if (!interaction.customId.startsWith('register:submit:')) return; + const formId = interaction.customId.split(':')[2]; + const form = await prisma.registerForm.findFirst({ + where: { id: formId }, + include: { fields: { orderBy: { order: 'asc' } } } + }); + if (!form) { + await interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true }); + return; + } + const answersPayload = form.fields.map((f) => ({ + fieldId: f.id, + value: interaction.fields.getTextInputValue(f.id) || '' + })); + const app = await prisma.registerApplication.create({ + data: { + guildId: interaction.guildId ?? '', + userId: interaction.user.id, + formId: form.id, + status: 'pending', + answers: { + create: answersPayload + } + }, + include: { answers: true } + }); + await interaction.reply({ content: 'Registrierung gesendet.', ephemeral: true }); + await this.postReviewEmbed(form, app, interaction.user.id, interaction.guildId || ''); + } + + private async postReviewEmbed(form: any, app: any, userId: string, guildId: string) { + if (!this.client) return; + const cfg = settingsStore.get(guildId); + const channelId = form.reviewChannelId || cfg?.registerConfig?.reviewChannelId; + if (!channelId) return; + const guild = this.client.guilds.cache.get(guildId) ?? (await this.client.guilds.fetch(guildId).catch(() => null)); + if (!guild) return; + const channel = await guild.channels.fetch(channelId).catch(() => null); + if (!channel || !channel.isTextBased()) return; + const member = await guild.members.fetch(userId).catch(() => null); + const fields = await prisma.registerFormField.findMany({ where: { formId: form.id }, orderBy: { order: 'asc' } }); + const answers = await prisma.registerApplicationAnswer.findMany({ where: { applicationId: app.id } }); + const embed = new EmbedBuilder() + .setTitle(`Registrierung: ${form.name}`) + .setDescription(form.description || '') + .setColor(0xf97316) + .addFields( + ...fields.map((f) => ({ + name: f.label, + value: answers.find((a) => a.fieldId === f.id)?.value || '-', + inline: false + })) + ) + .setFooter({ text: `Status: ${app.status}` }) + .setTimestamp(new Date(app.createdAt)); + if (member) embed.setAuthor({ name: member.user.tag, iconURL: member.user.displayAvatarURL() }); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`register:review:accept:${app.id}`).setLabel('Akzeptieren').setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`register:review:invite:${app.id}`).setLabel('Gespräch einladen').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId(`register:review:reject:${app.id}`).setLabel('Ablehnen').setStyle(ButtonStyle.Danger) + ); + const notifyRoles = form.notifyRoleIds || cfg?.registerConfig?.notifyRoleIds || []; + const content = notifyRoles.length ? notifyRoles.map((id: string) => `<@&${id}>`).join(' ') : null; + await (channel as any).send({ content: content || undefined, embeds: [embed], components: [row] }); + } + + private async updateReviewMessage(interaction: ButtonInteraction, app: any) { + if (!interaction.message || !interaction.message.editable) return; + const embed = (interaction.message.embeds?.[0] as any) ?? null; + if (embed) { + embed.data = { ...(embed.data || {}), footer: { text: `Status: ${app.status} | Reviewer: ${interaction.user.tag}` } }; + } + const components = interaction.message.components; + await interaction.message.edit({ embeds: embed ? [embed] : interaction.message.embeds, components }); + } + + public async listApplications(guildId: string, status?: string, formId?: string) { + const where: any = { guildId }; + if (status) where.status = status; + if (formId) where.formId = formId; + return prisma.registerApplication.findMany({ + where, + orderBy: { createdAt: 'desc' }, + include: { form: true } + }); + } + + public async getApplication(id: string) { + return prisma.registerApplication.findUnique({ + where: { id }, + include: { form: true, answers: true } + }); + } +} diff --git a/src/web/routes/api.ts b/src/web/routes/api.ts index a481451..2ac3a5f 100644 --- a/src/web/routes/api.ts +++ b/src/web/routes/api.ts @@ -543,6 +543,82 @@ router.delete('/reactionroles/:id', requireAuth, async (req, res) => { } }); +router.get('/register/forms', requireAuth, async (req, res) => { + const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const forms = await context.register.listForms(guildId); + res.json({ forms }); +}); + +router.post('/register/forms', requireAuth, async (req, res) => { + const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const data = req.body || {}; + const form = await context.register.saveForm({ + guildId, + name: data.name || 'Formular', + description: data.description || '', + reviewChannelId: data.reviewChannelId || undefined, + notifyRoleIds: Array.isArray(data.notifyRoleIds) ? data.notifyRoleIds : typeof data.notifyRoleIds === 'string' ? data.notifyRoleIds.split(',').map((s: string) => s.trim()).filter(Boolean) : [], + isActive: data.isActive !== false, + fields: Array.isArray(data.fields) ? data.fields : [] + }); + res.json({ form }); +}); + +router.put('/register/forms/:id', requireAuth, async (req, res) => { + const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined; + const id = req.params.id; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const data = req.body || {}; + const form = await context.register.saveForm({ + id, + guildId, + name: data.name || 'Formular', + description: data.description || '', + reviewChannelId: data.reviewChannelId || undefined, + notifyRoleIds: Array.isArray(data.notifyRoleIds) ? data.notifyRoleIds : typeof data.notifyRoleIds === 'string' ? data.notifyRoleIds.split(',').map((s: string) => s.trim()).filter(Boolean) : [], + isActive: data.isActive !== false, + fields: Array.isArray(data.fields) ? data.fields : [] + }); + res.json({ form }); +}); + +router.delete('/register/forms/:id', requireAuth, async (req, res) => { + const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined; + const id = req.params.id; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const ok = await context.register.deleteForm(guildId, id); + if (!ok) return res.status(404).json({ error: 'not found' }); + res.json({ ok: true }); +}); + +router.post('/register/forms/:id/panel', requireAuth, async (req, res) => { + const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined; + const id = req.params.id; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const channelId = typeof req.body.channelId === 'string' ? req.body.channelId : undefined; + const message = typeof req.body.message === 'string' ? req.body.message : undefined; + const msgId = await context.register.sendPanel(guildId, id, channelId, message); + if (!msgId) return res.status(400).json({ error: 'panel failed' }); + res.json({ ok: true, messageId: msgId }); +}); + +router.get('/register/apps', requireAuth, async (req, res) => { + const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined; + const status = typeof req.query.status === 'string' ? req.query.status : undefined; + const formId = typeof req.query.formId === 'string' ? req.query.formId : undefined; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const apps = await context.register.listApplications(guildId, status, formId); + res.json({ applications: apps }); +}); + +router.get('/register/apps/:id', requireAuth, async (req, res) => { + const app = await context.register.getApplication(req.params.id); + if (!app) return res.status(404).json({ error: 'not found' }); + res.json({ application: app }); +}); + router.get('/automations', requireAuth, async (req, res) => { const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined; if (!guildId) return res.status(400).json({ error: 'guildId required' }); @@ -700,9 +776,11 @@ router.post('/settings', requireAuth, async (req, res) => { eventsEnabled, birthdayEnabled, birthdayConfig, - reactionRolesEnabled, - reactionRolesConfig - } = req.body; + reactionRolesEnabled, + reactionRolesConfig, + registerEnabled, + registerConfig +} = req.body; if (!guildId) return res.status(400).json({ error: 'guildId required' }); const normalizeArray = (val: any) => Array.isArray(val) @@ -798,6 +876,22 @@ router.post('/settings', requireAuth, async (req, res) => { channelId: reactionRolesConfig?.channelId ?? existingReactionRoles.channelId }; + const parsedRegister = { + enabled: + registerConfig?.enabled ?? + (typeof registerEnabled === 'string' ? registerEnabled === 'true' : registerEnabled ?? (current as any).registerEnabled), + reviewChannelId: registerConfig?.reviewChannelId ?? (current as any).registerConfig?.reviewChannelId, + notifyRoleIds: + Array.isArray(registerConfig?.notifyRoleIds) && registerConfig?.notifyRoleIds.length + ? registerConfig.notifyRoleIds + : typeof registerConfig?.notifyRoleIds === 'string' + ? registerConfig.notifyRoleIds + .split(',') + .map((s: string) => s.trim()) + .filter(Boolean) + : (current as any).registerConfig?.notifyRoleIds || [] + }; + const updated = await settingsStore.set(guildId, { welcomeChannelId: welcomeChannelId ?? undefined, logChannelId: logChannelId ?? undefined, @@ -817,7 +911,9 @@ router.post('/settings', requireAuth, async (req, res) => { birthdayEnabled: parsedBirthday.enabled, birthdayConfig: parsedBirthday, reactionRolesEnabled: parsedReactionRoles.enabled, - reactionRolesConfig: parsedReactionRoles + reactionRolesConfig: parsedReactionRoles, + registerEnabled: parsedRegister.enabled, + registerConfig: parsedRegister }); // Live update logging target context.logging = new LoggingService(updated.logChannelId);