-
Notifications
You must be signed in to change notification settings - Fork 19
feat: admin and moderation features #105
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
Open
DragonSenseiGuy
wants to merge
21
commits into
hackclub:main
Choose a base branch
from
DragonSenseiGuy:admin
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
c134545
Add the admin dashboard
DragonSenseiGuy bafffb0
fix UI/UX issues
DragonSenseiGuy 01dd04b
Merge branch 'main' into admin
DragonSenseiGuy c72556c
feat: add isAdmin helper function for permission checks
DragonSenseiGuy be15212
refactor: use ban object instead of flat ban fields on User
DragonSenseiGuy 3487dd2
refactor: move administrative endpoints to admin.ts router
DragonSenseiGuy 5f8cfec
chore: exclude vitest.config.ts and test files from tsconfig
DragonSenseiGuy 8918671
feat: redirect non-banned users from banned page to /
DragonSenseiGuy 67eb6a0
test: update tests for admin router and ban object changes
DragonSenseiGuy 5b9497c
Use clsx
DragonSenseiGuy 2f7105c
add a new line before return statement
DragonSenseiGuy f57b3f1
add a new line before return statement
DragonSenseiGuy 215a3f5
refactor: extract user schemas to break circular dependency
DragonSenseiGuy d7e8a7b
refactor: use BANNED error code with tRPC link for ban handling
DragonSenseiGuy ab89cb6
style: improve admin dashboard UI
DragonSenseiGuy 08391be
add import
DragonSenseiGuy 697844b
Merge remote-tracking branch 'upstream/main' into admin
ascpixi 048d27a
fix: ensure video source is set before resuming (#125)
Efe-Cal dd5840e
feat: rest obo api w/ oauth apps (#133)
genr234 b57610b
fix: restore SelectInput
ascpixi 48a52f4
fix: use updated `adminProcedure` in `admin` API routes
ascpixi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // @ts-check | ||
| "use strict"; | ||
|
|
||
| import { parseArgs } from "node:util"; | ||
|
|
||
| import { confirm } from "@inquirer/prompts"; | ||
| import { PrismaPg } from "@prisma/adapter-pg"; | ||
| import { PrismaClient } from "../../src/generated/prisma/client.js"; | ||
|
|
||
| const MAX_HANDLE_LENGTH = 16; | ||
|
|
||
| async function main() { | ||
| const args = parseArgs({ | ||
| options: { | ||
| "database-url": { type: "string" }, | ||
| "dry-run": { type: "boolean", default: false } | ||
| } | ||
| }); | ||
|
|
||
| console.log(""); | ||
|
|
||
| const databaseUrl = args.values["database-url"]; | ||
| const dryRun = args.values["dry-run"] ?? false; | ||
|
|
||
| if (!databaseUrl) { | ||
| console.error("(error) Missing required parameter: --database-url"); | ||
| return; | ||
| } | ||
|
|
||
| const adapter = new PrismaPg({ connectionString: databaseUrl }); | ||
| const prisma = new PrismaClient({ adapter }); | ||
|
|
||
| try { | ||
| console.log("(info) Finding users with handles longer than 16 characters..."); | ||
|
|
||
| const usersWithLongHandles = await prisma.user.findMany({ | ||
| where: { | ||
| handle: { | ||
| not: { | ||
| // Prisma doesn't support length filters directly, so we fetch all and filter | ||
| } | ||
| } | ||
| }, | ||
| select: { id: true, handle: true, displayName: true } | ||
| }); | ||
|
|
||
| const affectedUsers = usersWithLongHandles.filter(user => user.handle.length > MAX_HANDLE_LENGTH); | ||
|
|
||
| if (affectedUsers.length === 0) { | ||
| console.log("(info) No users found with handles longer than 16 characters. Nothing to do."); | ||
| return; | ||
| } | ||
|
|
||
| console.log(`(info) Found ${affectedUsers.length} user(s) with handles longer than 16 characters:`); | ||
| console.log(""); | ||
|
|
||
| for (const user of affectedUsers) { | ||
| const trimmedHandle = user.handle.substring(0, MAX_HANDLE_LENGTH); | ||
| console.log(` - [${user.id}] "${user.handle}" (${user.handle.length} chars) -> "${trimmedHandle}"`); | ||
| } | ||
|
|
||
| console.log(""); | ||
|
|
||
| if (dryRun) { | ||
| console.log("(info) Dry run mode. No changes were made."); | ||
| return; | ||
| } | ||
|
|
||
| if (!await confirm({ message: `Do you wish to trim ${affectedUsers.length} handle(s)? (Y/N)` })) { | ||
| console.log("(info) Aborted. No changes were made."); | ||
| return; | ||
| } | ||
|
|
||
| let successCount = 0; | ||
| let failureCount = 0; | ||
|
|
||
| for (const user of affectedUsers) { | ||
| const trimmedHandle = user.handle.substring(0, MAX_HANDLE_LENGTH); | ||
|
|
||
| try { | ||
| await prisma.user.update({ | ||
| where: { id: user.id }, | ||
| data: { handle: trimmedHandle } | ||
| }); | ||
|
|
||
| console.log(`(info) [${user.id}] Handle updated: "${user.handle}" -> "${trimmedHandle}"`); | ||
| successCount++; | ||
| } | ||
| catch (error) { | ||
| console.error(`(error) [${user.id}] Failed to update handle:`, error); | ||
| failureCount++; | ||
| } | ||
| } | ||
|
|
||
| console.log(""); | ||
| console.log(`(info) Completed. ${successCount} handle(s) trimmed, ${failureCount} failure(s).`); | ||
| } | ||
| finally { | ||
| await prisma.$disconnect(); | ||
| } | ||
| } | ||
|
|
||
| main() | ||
| .catch(async (e) => { | ||
| console.error(e); | ||
| process.exit(1); | ||
| }); |
5 changes: 5 additions & 0 deletions
5
apps/web/prisma/migrations/20260113000000_add_user_ban_fields/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "User" ADD COLUMN "isBanned" BOOLEAN NOT NULL DEFAULT false; | ||
| ALTER TABLE "User" ADD COLUMN "bannedAt" TIMESTAMP(3); | ||
| ALTER TABLE "User" ADD COLUMN "bannedReason" TEXT NOT NULL DEFAULT ''; | ||
| ALTER TABLE "User" ADD COLUMN "bannedReasonInternal" TEXT NOT NULL DEFAULT ''; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should only use
BanRecordas our single source of truth. We should fetch the latest record for the user to get more information about a ban.isBannedcan remain denormalized.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so only 1 ban record? Instead of
bannedReasonandbannedReasonInternalThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have as many ban records as how many times the user's ban state changes. So, to get the latest ban reason for a user, you'd select the most recent
BanRecord, and display the reason stored in that row.I'd remove
bannedAt,bannedReason, andbannedReasonInternalfromUser, and instead get that data viaBanRecord, as we already store that there. The extra database hit is fine, as banned users are an exception, andisBannedshould remain because if it'sfalse, we can avoid fetching anyBanRecordsfor regular user actions.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay, thanks!