2026-03-04 18:07:18 +01:00
|
|
|
import { GraphQLError } from "graphql";
|
2026-03-04 18:42:58 +01:00
|
|
|
import { builder } from "../builder";
|
feat: role-based ACL + admin management UI
Backend:
- Add acl.ts with requireAuth/requireRole/requireOwnerOrAdmin helpers
- Gate premium videos from unauthenticated users in videos query/resolver
- Fix updateVideoPlay to verify ownership before updating
- Add admin mutations: adminListUsers, adminUpdateUser, adminDeleteUser
- Add admin mutations: createVideo, updateVideo, deleteVideo, setVideoModels, adminListVideos
- Add admin mutations: createArticle, updateArticle, deleteArticle, adminListArticles
- Add deleteComment mutation (owner or admin only)
- Add AdminUserListType to GraphQL types
- Fix featured filter on articles query
Frontend:
- Install marked for markdown rendering
- Add /admin/* section with sidebar layout and admin-only guard
- Admin users page: paginated table with search, role filter, inline role change, delete
- Admin videos pages: list, create form, edit form with file upload and model assignment
- Admin articles pages: list, create form, edit form with split-pane markdown editor
- Add admin nav link in header (desktop + mobile) for admin users
- Render article content through marked in magazine detail page
- Add all admin GraphQL service functions to services.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:31:33 +01:00
|
|
|
import { CurrentUserType, UserType, AdminUserListType } from "../types/index";
|
2026-03-04 18:42:58 +01:00
|
|
|
import { users } from "../../db/schema/index";
|
feat: role-based ACL + admin management UI
Backend:
- Add acl.ts with requireAuth/requireRole/requireOwnerOrAdmin helpers
- Gate premium videos from unauthenticated users in videos query/resolver
- Fix updateVideoPlay to verify ownership before updating
- Add admin mutations: adminListUsers, adminUpdateUser, adminDeleteUser
- Add admin mutations: createVideo, updateVideo, deleteVideo, setVideoModels, adminListVideos
- Add admin mutations: createArticle, updateArticle, deleteArticle, adminListArticles
- Add deleteComment mutation (owner or admin only)
- Add AdminUserListType to GraphQL types
- Fix featured filter on articles query
Frontend:
- Install marked for markdown rendering
- Add /admin/* section with sidebar layout and admin-only guard
- Admin users page: paginated table with search, role filter, inline role change, delete
- Admin videos pages: list, create form, edit form with file upload and model assignment
- Admin articles pages: list, create form, edit form with split-pane markdown editor
- Add admin nav link in header (desktop + mobile) for admin users
- Render article content through marked in magazine detail page
- Add all admin GraphQL service functions to services.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:31:33 +01:00
|
|
|
import { eq, ilike, or, count, and } from "drizzle-orm";
|
|
|
|
|
import { requireAuth, requireRole } from "../../lib/acl";
|
2026-03-04 18:07:18 +01:00
|
|
|
|
|
|
|
|
builder.queryField("me", (t) =>
|
|
|
|
|
t.field({
|
|
|
|
|
type: CurrentUserType,
|
|
|
|
|
nullable: true,
|
|
|
|
|
resolve: async (_root, _args, ctx) => {
|
|
|
|
|
if (!ctx.currentUser) return null;
|
|
|
|
|
const user = await ctx.db
|
|
|
|
|
.select()
|
|
|
|
|
.from(users)
|
|
|
|
|
.where(eq(users.id, ctx.currentUser.id))
|
|
|
|
|
.limit(1);
|
|
|
|
|
return user[0] || null;
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
builder.queryField("userProfile", (t) =>
|
|
|
|
|
t.field({
|
|
|
|
|
type: UserType,
|
|
|
|
|
nullable: true,
|
|
|
|
|
args: {
|
|
|
|
|
id: t.arg.string({ required: true }),
|
|
|
|
|
},
|
|
|
|
|
resolve: async (_root, args, ctx) => {
|
2026-03-04 22:27:54 +01:00
|
|
|
const user = await ctx.db.select().from(users).where(eq(users.id, args.id)).limit(1);
|
2026-03-04 18:07:18 +01:00
|
|
|
return user[0] || null;
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
builder.mutationField("updateProfile", (t) =>
|
|
|
|
|
t.field({
|
|
|
|
|
type: CurrentUserType,
|
|
|
|
|
nullable: true,
|
|
|
|
|
args: {
|
|
|
|
|
firstName: t.arg.string(),
|
|
|
|
|
lastName: t.arg.string(),
|
|
|
|
|
artistName: t.arg.string(),
|
|
|
|
|
description: t.arg.string(),
|
|
|
|
|
tags: t.arg.stringList(),
|
|
|
|
|
},
|
|
|
|
|
resolve: async (_root, args, ctx) => {
|
|
|
|
|
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
|
|
|
|
|
|
|
|
|
|
const updates: Record<string, unknown> = { date_updated: new Date() };
|
2026-03-04 22:27:54 +01:00
|
|
|
if (args.firstName !== undefined && args.firstName !== null)
|
|
|
|
|
updates.first_name = args.firstName;
|
2026-03-04 18:07:18 +01:00
|
|
|
if (args.lastName !== undefined && args.lastName !== null) updates.last_name = args.lastName;
|
2026-03-04 22:27:54 +01:00
|
|
|
if (args.artistName !== undefined && args.artistName !== null)
|
|
|
|
|
updates.artist_name = args.artistName;
|
|
|
|
|
if (args.description !== undefined && args.description !== null)
|
|
|
|
|
updates.description = args.description;
|
2026-03-04 18:07:18 +01:00
|
|
|
if (args.tags !== undefined && args.tags !== null) updates.tags = args.tags;
|
|
|
|
|
|
2026-03-04 22:27:54 +01:00
|
|
|
await ctx.db
|
|
|
|
|
.update(users)
|
|
|
|
|
.set(updates as any)
|
|
|
|
|
.where(eq(users.id, ctx.currentUser.id));
|
2026-03-04 18:07:18 +01:00
|
|
|
|
|
|
|
|
const updated = await ctx.db
|
|
|
|
|
.select()
|
|
|
|
|
.from(users)
|
|
|
|
|
.where(eq(users.id, ctx.currentUser.id))
|
|
|
|
|
.limit(1);
|
|
|
|
|
return updated[0] || null;
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
feat: role-based ACL + admin management UI
Backend:
- Add acl.ts with requireAuth/requireRole/requireOwnerOrAdmin helpers
- Gate premium videos from unauthenticated users in videos query/resolver
- Fix updateVideoPlay to verify ownership before updating
- Add admin mutations: adminListUsers, adminUpdateUser, adminDeleteUser
- Add admin mutations: createVideo, updateVideo, deleteVideo, setVideoModels, adminListVideos
- Add admin mutations: createArticle, updateArticle, deleteArticle, adminListArticles
- Add deleteComment mutation (owner or admin only)
- Add AdminUserListType to GraphQL types
- Fix featured filter on articles query
Frontend:
- Install marked for markdown rendering
- Add /admin/* section with sidebar layout and admin-only guard
- Admin users page: paginated table with search, role filter, inline role change, delete
- Admin videos pages: list, create form, edit form with file upload and model assignment
- Admin articles pages: list, create form, edit form with split-pane markdown editor
- Add admin nav link in header (desktop + mobile) for admin users
- Render article content through marked in magazine detail page
- Add all admin GraphQL service functions to services.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:31:33 +01:00
|
|
|
|
|
|
|
|
// ─── Admin queries & mutations ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
builder.queryField("adminListUsers", (t) =>
|
|
|
|
|
t.field({
|
|
|
|
|
type: AdminUserListType,
|
|
|
|
|
args: {
|
|
|
|
|
role: t.arg.string(),
|
|
|
|
|
search: t.arg.string(),
|
|
|
|
|
limit: t.arg.int(),
|
|
|
|
|
offset: t.arg.int(),
|
|
|
|
|
},
|
|
|
|
|
resolve: async (_root, args, ctx) => {
|
|
|
|
|
requireRole(ctx, "admin");
|
|
|
|
|
|
|
|
|
|
const limit = args.limit ?? 50;
|
|
|
|
|
const offset = args.offset ?? 0;
|
|
|
|
|
|
|
|
|
|
let query = ctx.db.select().from(users);
|
|
|
|
|
let countQuery = ctx.db.select({ total: count() }).from(users);
|
|
|
|
|
|
|
|
|
|
const conditions: any[] = [];
|
|
|
|
|
if (args.role) {
|
|
|
|
|
conditions.push(eq(users.role, args.role as any));
|
|
|
|
|
}
|
|
|
|
|
if (args.search) {
|
|
|
|
|
const pattern = `%${args.search}%`;
|
|
|
|
|
conditions.push(or(ilike(users.email, pattern), ilike(users.artist_name, pattern)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (conditions.length > 0) {
|
|
|
|
|
const where = conditions.length === 1 ? conditions[0] : and(...conditions);
|
|
|
|
|
query = (query as any).where(where);
|
|
|
|
|
countQuery = (countQuery as any).where(where);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [items, totalRows] = await Promise.all([
|
|
|
|
|
(query as any).limit(limit).offset(offset),
|
|
|
|
|
countQuery,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return { items, total: totalRows[0]?.total ?? 0 };
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
builder.mutationField("adminUpdateUser", (t) =>
|
|
|
|
|
t.field({
|
|
|
|
|
type: UserType,
|
|
|
|
|
nullable: true,
|
|
|
|
|
args: {
|
|
|
|
|
userId: t.arg.string({ required: true }),
|
|
|
|
|
role: t.arg.string(),
|
|
|
|
|
firstName: t.arg.string(),
|
|
|
|
|
lastName: t.arg.string(),
|
|
|
|
|
artistName: t.arg.string(),
|
|
|
|
|
},
|
|
|
|
|
resolve: async (_root, args, ctx) => {
|
|
|
|
|
requireRole(ctx, "admin");
|
|
|
|
|
|
|
|
|
|
const updates: Record<string, unknown> = { date_updated: new Date() };
|
|
|
|
|
if (args.role !== undefined && args.role !== null) updates.role = args.role as any;
|
|
|
|
|
if (args.firstName !== undefined && args.firstName !== null)
|
|
|
|
|
updates.first_name = args.firstName;
|
|
|
|
|
if (args.lastName !== undefined && args.lastName !== null) updates.last_name = args.lastName;
|
|
|
|
|
if (args.artistName !== undefined && args.artistName !== null)
|
|
|
|
|
updates.artist_name = args.artistName;
|
|
|
|
|
|
|
|
|
|
const updated = await ctx.db
|
|
|
|
|
.update(users)
|
|
|
|
|
.set(updates as any)
|
|
|
|
|
.where(eq(users.id, args.userId))
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
return updated[0] || null;
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
builder.mutationField("adminDeleteUser", (t) =>
|
|
|
|
|
t.field({
|
|
|
|
|
type: "Boolean",
|
|
|
|
|
args: {
|
|
|
|
|
userId: t.arg.string({ required: true }),
|
|
|
|
|
},
|
|
|
|
|
resolve: async (_root, args, ctx) => {
|
|
|
|
|
requireRole(ctx, "admin");
|
|
|
|
|
if (args.userId === ctx.currentUser!.id) throw new GraphQLError("Cannot delete yourself");
|
|
|
|
|
await ctx.db.delete(users).where(eq(users.id, args.userId));
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|