Skip to content

feat: Temporary role assignment (#128)#208

Merged
BillChirico merged 5 commits intomainfrom
feat/issue-128
Mar 2, 2026
Merged

feat: Temporary role assignment (#128)#208
BillChirico merged 5 commits intomainfrom
feat/issue-128

Conversation

@BillChirico
Copy link
Collaborator

Summary

Implements issue #128 — allow assigning roles that expire after a set time.

Features

🤖 Discord Command: /temprole

  • /temprole assign @user @role <duration> [reason] — Assigns a role that expires after the given duration (supports 1h, 7d, 2w, etc.)
  • /temprole revoke @user @role — Removes a temp role before it expires
  • /temprole list [@user] — Lists active temp role assignments (server-wide or per user)
  • Admin-only command with hierarchy checks (bot role + moderator role position enforcement)

⏱ Automatic Expiry Scheduler

  • Polls every 60s for expired assignments
  • Immediate pass on startup to catch missed removals
  • Optimistic locking prevents double-processing across restarts

🗄️ Database

  • New temp_roles table (migration 004_temp_roles.cjs)
  • Indexes for efficient pending-poll and per-user queries

🌐 REST API

  • GET /api/v1/temp-roles?guildId=... — List active assignments (paginated)
  • POST /api/v1/temp-roles — Assign via dashboard
  • DELETE /api/v1/temp-roles/:id?guildId=... — Revoke via dashboard

📊 Web Dashboard

  • New /dashboard/temp-roles page
  • Table: user, role badge, duration, relative expiry, moderator, reason
  • One-click revoke with confirmation
  • Pagination support
  • Added to sidebar nav under Moderation

Tests

  • 9 module tests (tempRoleHandler)
  • 11 command tests (temprole)
  • 20/20 passing, lint clean

Files Changed

migrations/004_temp_roles.cjs            — DB table
src/modules/tempRoleHandler.js           — Core logic + scheduler
src/commands/temprole.js                 — Slash command
src/api/routes/tempRoles.js              — Bot REST API
src/api/index.js                         — Mount tempRolesRouter
src/index.js                             — Wire scheduler lifecycle
tests/modules/tempRoleHandler.test.js    — Module tests
tests/commands/temprole.test.js          — Command tests
web/src/app/api/temp-roles/route.ts      — Next.js proxy (GET, POST)
web/src/app/api/temp-roles/[id]/route.ts — Next.js proxy (DELETE)
web/src/app/dashboard/temp-roles/page.tsx — Dashboard page
web/src/components/layout/sidebar.tsx    — Sidebar nav entry

Closes #128

- Add temp_roles DB table (migration 004)
- Add tempRoleHandler module with assign/revoke/list/scheduler
- Add /temprole slash command (assign | revoke | list subcommands)
- Add /temp-roles REST API routes (GET list, POST assign, DELETE revoke)
- Wire tempRoleScheduler into bot startup/shutdown lifecycle
- Add /api/temp-roles Next.js proxy route (GET list, POST assign)
- Add /api/temp-roles/[id] proxy route (DELETE revoke)
- Add /dashboard/temp-roles page with active role table + revoke UI
- Add Clock icon + 'Temp Roles' nav entry to sidebar
- Add unit tests: 9 module tests + 11 command tests (20 total, all passing)
- Fix lint: biome format + import sort across all new files
Copilot AI review requested due to automatic review settings March 2, 2026 04:23
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 2, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 44 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b1bf421 and cf20c84.

📒 Files selected for processing (12)
  • migrations/004_temp_roles.cjs
  • src/api/index.js
  • src/api/routes/tempRoles.js
  • src/commands/temprole.js
  • src/index.js
  • src/modules/tempRoleHandler.js
  • tests/commands/temprole.test.js
  • tests/modules/tempRoleHandler.test.js
  • web/src/app/api/temp-roles/[id]/route.ts
  • web/src/app/api/temp-roles/route.ts
  • web/src/app/dashboard/temp-roles/page.tsx
  • web/src/components/layout/sidebar.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/issue-128

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class “temporary role” support across the bot, REST API, and web dashboard, enabling moderators/admins to assign roles that automatically expire and can be managed from both Discord and the dashboard.

Changes:

  • Introduces temp_roles persistence + core handler logic with a 60s expiry polling scheduler.
  • Adds /temprole slash command (assign/revoke/list) and new REST endpoints for listing/assigning/revoking temp roles.
  • Adds a dashboard page + sidebar navigation and Next.js API proxy routes for managing temp roles via the web UI.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
migrations/004_temp_roles.cjs Creates temp_roles table + indexes for expiry polling and queries.
src/modules/tempRoleHandler.js Core DB operations + expiry poller/scheduler lifecycle.
src/commands/temprole.js Discord slash command interface for managing temp roles.
src/api/routes/tempRoles.js Express REST API for listing/assigning/revoking temp roles.
src/api/index.js Mounts the temp roles router under /api/v1/temp-roles.
src/index.js Starts/stops the temp role scheduler with the bot lifecycle.
tests/modules/tempRoleHandler.test.js Unit tests for handler DB logic and scheduler start/stop.
tests/commands/temprole.test.js Unit tests for /temprole command behavior.
web/src/app/api/temp-roles/route.ts Next.js proxy for list/assign temp roles to bot API.
web/src/app/api/temp-roles/[id]/route.ts Next.js proxy for revoking temp roles by id.
web/src/app/dashboard/temp-roles/page.tsx Dashboard UI for viewing/revoking active temp roles.
web/src/components/layout/sidebar.tsx Adds “Temp Roles” entry in dashboard navigation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Greptile Summary

Implements temporary role assignment feature with automatic expiry scheduler, Discord command, REST API, and web dashboard.

Key Changes:

  • New temp_roles table with optimistic locking for safe concurrent expiry processing
  • /temprole slash command with assign/revoke/list subcommands and proper hierarchy checks
  • 60-second scheduler polls for expired roles and removes them automatically
  • REST API endpoints for dashboard integration with rate limiting and validation
  • React dashboard page with pagination, real-time expiry display, and one-click revoke

Critical Issue Found:

  • src/api/index.js uses ticketsRouter on line 52 but the import was accidentally removed when adding tempRolesRouter — this will cause a ReferenceError crash on startup

Previously Acknowledged:

  • Missing requireGuildModerator middleware on DELETE endpoint (acknowledged by senior dev)
  • Database operations need try/catch wrappers (acknowledged as nitpicks)

Strengths:

  • Comprehensive test coverage (20/20 passing tests)
  • Proper error handling with Winston logging throughout
  • Clean separation of concerns (module, command, API, UI)
  • Parameterized SQL queries prevent injection
  • Optimistic locking prevents duplicate processing across restarts

Confidence Score: 1/5

  • Critical issue will cause immediate crash on startup
  • The missing ticketsRouter import in src/api/index.js will throw a ReferenceError when the API server initializes, crashing the entire application. While the temp role feature itself is well-implemented with good tests and error handling, this blocking bug must be fixed before merge.
  • Fix src/api/index.js immediately — add back the missing ticketsRouter import that was accidentally removed

Important Files Changed

Filename Overview
src/api/index.js Missing import causes ReferenceError - ticketsRouter used on line 52 but import was removed
migrations/004_temp_roles.cjs Clean migration with proper indexes for pending roles, guild queries, and compound lookups
src/modules/tempRoleHandler.js Solid implementation with proper error handling, optimistic locking, and Winston logging
src/commands/temprole.js Well-structured command with hierarchy checks, comprehensive error handling, and user-friendly responses
src/api/routes/tempRoles.js Good validation and error handling; missing middleware already flagged in previous review
web/src/app/dashboard/temp-roles/page.tsx Clean React component with proper state management, pagination, and user-friendly interface

Sequence Diagram

sequenceDiagram
    participant Mod as Moderator
    participant Bot as Discord Bot
    participant DB as PostgreSQL
    participant Sched as Scheduler
    participant User as Target User

    Note over Mod,User: Assign Temp Role Flow
    Mod->>Bot: /temprole assign @user @role 7d
    Bot->>Bot: Validate hierarchy<br/>(bot & mod roles)
    Bot->>User: Add role via Discord API
    Bot->>DB: INSERT temp_roles record<br/>(expires_at = now + 7d)
    Bot->>Mod: ✅ Role assigned for 7 days

    Note over Sched,User: Automatic Expiry Flow
    loop Every 60s
        Sched->>DB: SELECT expired roles<br/>(removed=FALSE, expires_at <= NOW)
        DB-->>Sched: Return expired records
        Sched->>DB: UPDATE removed=TRUE<br/>(optimistic lock)
        Sched->>User: Remove role via Discord API
    end

    Note over Mod,DB: Dashboard Revoke Flow
    Mod->>Bot: DELETE /api/temp-roles/:id
    Bot->>DB: UPDATE removed=TRUE<br/>WHERE id=:id
    Bot->>User: Remove role via Discord API
    Bot->>Mod: ✅ Role revoked
Loading

Last reviewed commit: cf20c84

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 2, 2026
- Fix adaptGuildIdParam middleware collision: scope to GET only, not DELETE
- Fix POST auth: add adaptBodyGuildId middleware to read guildId from req.body
- Fix DELETE revoke: use revokeTempRoleById to revoke by specific record ID
- Add guildId validation: return 400 if guildId missing/not a string in GET/DELETE
- Wrap DB operations in try/catch: add error handling in tempRoleHandler.js
- Remove empty test: removed redundant scheduler test with no assertions
- Use proxyToBotApi helper: refactor web routes to use shared helper instead of raw fetch
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 2, 2026
Copilot AI review requested due to automatic review settings March 2, 2026 11:54
@BillChirico BillChirico merged commit 785bf53 into main Mar 2, 2026
8 of 12 checks passed
@BillChirico BillChirico deleted the feat/issue-128 branch March 2, 2026 11:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +119 to +121
router.delete('/:id', async (req, res) => {
try {
const guildId = req.query.guildId;
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DELETE endpoint is not protected by requireGuildModerator, unlike the GET/POST routes. This enables any authenticated user (with access to the API) to revoke temp roles by guessing IDs + guildId. Add adaptGuildIdParam (or equivalent) and requireGuildModerator middleware to the DELETE route to enforce guild-level authorization.

Copilot uses AI. Check for mistakes.
guildId: row.guild_id,
userId: row.user_id,
roleId: row.role_id,
});
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheduler marks records as removed = TRUE before attempting Discord role removal. If Discord removal fails (permissions outage, transient API errors, rate limits), the record will never be retried and the user may keep the role indefinitely. Consider a separate claim/processing state (e.g., claimed_at, claimed_by, processing = true) and only set removed = TRUE after successful removal (or after confirming the member is gone); alternatively reset removed back to FALSE on failure so it can be retried.

Suggested change
});
});
// Reset removal flags so this record can be retried on a future poll
try {
await pool.query(
`UPDATE temp_roles
SET removed = FALSE,
removed_at = NULL
WHERE id = $1`,
[row.id],
);
} catch (resetErr) {
logError('Failed to reset temp role removal state after error', {
error: resetErr.message,
id: row.id,
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +29
{ params }: { params: Promise<{ id: string }> },
): Promise<NextResponse> {
const { id } = await params;
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The route handler context typing is off: params is typically a plain object ({ params: { id: string } }) rather than a Promise, and await params is unnecessary. Using the correct type here prevents masking real type errors and keeps the handler aligned with Next.js route handler conventions.

Suggested change
{ params }: { params: Promise<{ id: string }> },
): Promise<NextResponse> {
const { id } = await params;
{ params }: { params: { id: string } },
): Promise<NextResponse> {
const { id } = params;

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +46
const upstream = buildUpstreamUrl(
config.baseUrl,
`/temp-roles/${encodeURIComponent(id)}`,
LOG_PREFIX,
);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The id path segment is forwarded upstream without validation. Since the upstream endpoint expects a numeric record ID, validate id (e.g., positive integer) and return a 400 with a clear error when invalid, instead of proxying malformed requests.

Copilot uses AI. Check for mistakes.
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [revoking, setRevoking] = useState<number | null>(null);
const abortRef = useRef<AbortController | null>(null);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requests are aborted when a new fetch starts, but there is no unmount cleanup to abort an in-flight request. Add a useEffect cleanup (or equivalent) to call abortRef.current?.abort() on unmount to avoid updates after navigation and reduce unnecessary work.

Copilot uses AI. Check for mistakes.

useEffect(() => {
if (!guildId) return;
void fetchTempRoles(guildId, page);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requests are aborted when a new fetch starts, but there is no unmount cleanup to abort an in-flight request. Add a useEffect cleanup (or equivalent) to call abortRef.current?.abort() on unmount to avoid updates after navigation and reduce unnecessary work.

Suggested change
void fetchTempRoles(guildId, page);
void fetchTempRoles(guildId, page);
return () => {
abortRef.current?.abort();
};

Copilot uses AI. Check for mistakes.
if (!role) {
return res.status(400).json({ error: 'Role not found' });
}

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dashboard assignment path does not perform role hierarchy checks before attempting member.roles.add(...). When the role is above the bot/moderator, this predictably fails and returns a generic 500. Mirror the command’s hierarchy validation and return a 400/403 with a specific error (e.g., role is higher than bot/mod) to make failures actionable.

Suggested change
// 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
}
}

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Additional Comments (1)

src/api/index.js, line 52
ticketsRouter is used but not imported — the import on line 22 was replaced with tempRolesRouter instead of added alongside it

import ticketsRouter from './routes/tickets.js';

Add this import back on line 22 after the tempRolesRouter import

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: end-to-end tests for web dashboard with Playwright

2 participants