-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Temporary role assignment (#128) #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1116e93
af26e4b
9f55047
0b3e154
cf20c84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /** | ||
| * Migration: Temporary Role Assignments | ||
| * | ||
| * Creates the temp_roles table to track roles assigned with an expiry. | ||
| * The scheduler polls this table and removes roles when they expire. | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ | ||
| exports.up = (pgm) => { | ||
| pgm.sql(` | ||
| CREATE TABLE IF NOT EXISTS temp_roles ( | ||
| id SERIAL PRIMARY KEY, | ||
| guild_id TEXT NOT NULL, | ||
| user_id TEXT NOT NULL, | ||
| user_tag TEXT NOT NULL, | ||
| role_id TEXT NOT NULL, | ||
| role_name TEXT NOT NULL, | ||
| moderator_id TEXT NOT NULL, | ||
| moderator_tag TEXT NOT NULL, | ||
| reason TEXT, | ||
| duration TEXT NOT NULL, | ||
| expires_at TIMESTAMPTZ NOT NULL, | ||
| removed BOOLEAN NOT NULL DEFAULT FALSE, | ||
| removed_at TIMESTAMPTZ, | ||
| created_at TIMESTAMPTZ DEFAULT NOW() | ||
| ) | ||
| `); | ||
|
|
||
| pgm.sql('CREATE INDEX IF NOT EXISTS idx_temp_roles_guild ON temp_roles(guild_id)'); | ||
| pgm.sql( | ||
| "CREATE INDEX IF NOT EXISTS idx_temp_roles_pending ON temp_roles(removed, expires_at) WHERE removed = FALSE", | ||
| ); | ||
| pgm.sql( | ||
| 'CREATE INDEX IF NOT EXISTS idx_temp_roles_guild_user ON temp_roles(guild_id, user_id, removed)', | ||
| ); | ||
| }; | ||
|
|
||
| /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ | ||
| exports.down = (pgm) => { | ||
| pgm.sql('DROP TABLE IF EXISTS temp_roles CASCADE'); | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,250 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Temp Roles API Routes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Exposes temporary role assignment data for the web dashboard. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @see https://github.com/VolvoxLLC/volvox-bot/issues/128 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Router } from 'express'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { info, error as logError } from '../../logger.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assignTempRole, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| listTempRoles, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revokeTempRoleById, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '../../modules/tempRoleHandler.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { formatDuration, parseDuration } from '../../utils/duration.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { rateLimit } from '../middleware/rateLimit.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { parsePagination, requireGuildModerator } from './guilds.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = Router(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Rate limiter β 120 req / 15 min per IP */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tempRoleRateLimit = rateLimit({ windowMs: 15 * 60 * 1000, max: 120 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Adapt ?guildId= query param to :id path param for requireGuildModerator. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Only use on routes that need guild id in params (GET list). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function adaptGuildIdParam(req, _res, next) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.query.guildId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.params.id = req.query.guildId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Adapt req.body.guildId to :id path param for requireGuildModerator. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Used for POST route where guildId is in the body, not query string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function adaptBodyGuildId(req, _res, next) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.body?.guildId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.params.id = req.body.guildId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.use(tempRoleRateLimit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // βββ GET /temp-roles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @openapi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * /temp-roles: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * get: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * tags: [TempRoles] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * summary: List active temp role assignments | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * parameters: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - in: query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * name: guildId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * required: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema: { type: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - in: query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * name: userId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema: { type: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - in: query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * name: page | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema: { type: integer, default: 1 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - in: query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * name: limit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema: { type: integer, default: 25, maximum: 100 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * responses: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * "200": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * description: Paginated list of active temp roles | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.get('/', adaptGuildIdParam, requireGuildModerator, async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BillChirico marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const guildId = req.query.guildId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate guildId is present and is a string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!guildId || typeof guildId !== 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: 'guildId is required and must be a string' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userId = req.query.userId || undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { page, limit, offset } = parsePagination(req.query); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { rows, total } = await listTempRoles(guildId, { userId, limit, offset }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: rows, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pagination: { page, limit, total, pages: Math.ceil(total / limit) }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('GET /temp-roles failed', { error: err.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(500).json({ error: 'Failed to fetch temp roles' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // βββ DELETE /temp-roles/:id βββββββββββββββββββββββββββββββββββββββββββββββββββ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @openapi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * /temp-roles/{id}: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * delete: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * tags: [TempRoles] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * summary: Revoke a temp role by record ID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * parameters: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - in: path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * name: id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * required: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema: { type: integer } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - in: query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * name: guildId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * required: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema: { type: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * responses: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * "200": { description: Revoked } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * "404": { description: Not found or already removed } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.delete('/:id', async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const guildId = req.query.guildId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+119
to
+121
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate guildId is present and is a string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!guildId || typeof guildId !== 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: 'guildId is required and must be a string' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const id = Number.parseInt(req.params.id, 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!Number.isInteger(id) || id <= 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: 'Invalid id' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Revoke by specific record id (not by user/role which can affect multiple rows) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updated = await revokeTempRoleById(id, guildId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!updated) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(404).json({ error: 'Temp role not found or already removed' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Best-effort Discord role removal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = res.app.locals.client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (client) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const guild = await client.guilds.fetch(guildId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const member = await guild.members.fetch(updated.user_id).catch(() => null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (member) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await member.roles.remove(updated.role_id, 'Temp role revoked via dashboard'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (discordErr) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('Dashboard revoke: Discord role removal failed', { error: discordErr.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info('Temp role revoked via dashboard', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guildId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: updated.user_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| roleId: updated.role_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| moderatorId: req.user?.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.json({ success: true, data: updated }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('DELETE /temp-roles/:id failed', { error: err.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(500).json({ error: 'Failed to revoke temp role' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // βββ POST /temp-roles βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @openapi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * /temp-roles: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * post: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * tags: [TempRoles] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * summary: Assign a temp role via the dashboard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * requestBody: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * required: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * content: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * application/json: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * schema: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * type: object | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * required: [guildId, userId, roleId, duration] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * properties: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * guildId: { type: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * userId: { type: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * roleId: { type: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * duration: { type: string, example: "7d" } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * reason: { type: string } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * responses: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * "201": { description: Assigned } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * "400": { description: Invalid input } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.post('/', adaptBodyGuildId, requireGuildModerator, async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BillChirico marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { guildId, userId, roleId, duration: durationStr, reason } = req.body || {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!guildId || !userId || !roleId || !durationStr) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: 'guildId, userId, roleId, and duration are required' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const durationMs = parseDuration(durationStr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!durationMs) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: 'Invalid duration. Use e.g. 1h, 7d, 2w.' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = res.app.locals.client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!client) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(503).json({ error: 'Discord client not available' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let guild, member, role; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guild = await client.guilds.fetch(guildId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| member = await guild.members.fetch(userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| role = await guild.roles.fetch(roleId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: 'Invalid guild, user, or role' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!role) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: 'Role not found' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Role hierarchy checks | |
| let botMember = guild.members.me; | |
| if (!botMember && client.user?.id) { | |
| try { | |
| botMember = await guild.members.fetch(client.user.id); | |
| } catch { | |
| // fall through and handle below | |
| } | |
| } | |
| if (!botMember) { | |
| return res.status(503).json({ error: 'Bot member not available in guild for role assignment' }); | |
| } | |
| if (role.position >= botMember.roles.highest.position) { | |
| return res.status(403).json({ error: 'Cannot assign a role higher than or equal to the botβs highest role' }); | |
| } | |
| if (req.user?.id) { | |
| try { | |
| const moderatorMember = await guild.members.fetch(req.user.id); | |
| if (moderatorMember && role.position >= moderatorMember.roles.highest.position) { | |
| return res.status(403).json({ error: 'Cannot assign a role higher than or equal to the moderatorβs highest role' }); | |
| } | |
| } catch { | |
| // If the moderator is not a guild member, skip moderator hierarchy check | |
| } | |
| } |
Uh oh!
There was an error while loading. Please reload this page.