feat: refactor role system to is_admin flag, add Badge component, fix native dialogs

- Separate admin identity from role: viewer|model + is_admin boolean flag
- DB migration 0001_is_admin: adds column, migrates former admin role users
- Update ACL helpers, auth session, GraphQL types and all resolvers
- Admin layout guard and header links check is_admin instead of role
- Admin users table: show Admin badge next to name, remove admin from role select
- Admin user edit page: is_admin checkbox toggle
- Install shadcn Badge component; use in admin users table
- Fix duplicate photo keys in adminGetUser resolver
- Replace confirm() in /me recordings with Dialog component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 16:14:00 +01:00
parent 9ef490c1e5
commit 670c18bcb7
19 changed files with 162 additions and 43 deletions

View File

@@ -15,7 +15,7 @@ import {
files,
} from "../../db/schema/index";
import { eq, and, lte, desc, inArray, count } from "drizzle-orm";
import { requireRole } from "../../lib/acl";
import { requireAdmin } from "../../lib/acl";
async function enrichVideo(db: any, video: any) {
// Fetch models
@@ -432,7 +432,7 @@ builder.queryField("adminListVideos", (t) =>
t.field({
type: [VideoType],
resolve: async (_root, _args, ctx) => {
requireRole(ctx, "admin");
requireAdmin(ctx);
const rows = await ctx.db.select().from(videos).orderBy(desc(videos.upload_date));
return Promise.all(rows.map((v: any) => enrichVideo(ctx.db, v)));
},
@@ -454,7 +454,7 @@ builder.mutationField("createVideo", (t) =>
uploadDate: t.arg.string(),
},
resolve: async (_root, args, ctx) => {
requireRole(ctx, "admin");
requireAdmin(ctx);
const inserted = await ctx.db
.insert(videos)
.values({
@@ -491,7 +491,7 @@ builder.mutationField("updateVideo", (t) =>
uploadDate: t.arg.string(),
},
resolve: async (_root, args, ctx) => {
requireRole(ctx, "admin");
requireAdmin(ctx);
const updates: Record<string, unknown> = {};
if (args.title !== undefined && args.title !== null) updates.title = args.title;
if (args.slug !== undefined && args.slug !== null) updates.slug = args.slug;
@@ -522,7 +522,7 @@ builder.mutationField("deleteVideo", (t) =>
id: t.arg.string({ required: true }),
},
resolve: async (_root, args, ctx) => {
requireRole(ctx, "admin");
requireAdmin(ctx);
await ctx.db.delete(videos).where(eq(videos.id, args.id));
return true;
},
@@ -537,7 +537,7 @@ builder.mutationField("setVideoModels", (t) =>
userIds: t.arg.stringList({ required: true }),
},
resolve: async (_root, args, ctx) => {
requireRole(ctx, "admin");
requireAdmin(ctx);
await ctx.db.delete(video_models).where(eq(video_models.video_id, args.videoId));
if (args.userIds.length > 0) {
await ctx.db.insert(video_models).values(