feat: expand admin and instructor tools
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import type { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { pool } from '../db/pool.js';
|
||||
import { createRawToken, hashToken } from '../auth/account-tokens.js';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const userParamsSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
@@ -12,6 +14,7 @@ const updateUserSchema = z.object({
|
||||
}).refine((input) => input.role !== undefined || input.isActive !== undefined, {
|
||||
message: 'Provide at least one field to update',
|
||||
});
|
||||
const invitationSchema = z.object({ email: z.string().email().transform((email) => email.toLowerCase()), role: z.enum(['student', 'instructor']).default('student') });
|
||||
|
||||
export const adminRoutes: FastifyPluginAsync = async (app) => {
|
||||
const adminAccess = { preHandler: app.requireRoles(['admin']) };
|
||||
@@ -26,6 +29,56 @@ export const adminRoutes: FastifyPluginAsync = async (app) => {
|
||||
return { data: result.rows };
|
||||
});
|
||||
|
||||
app.get('/dashboard', adminAccess, async () => {
|
||||
const result = await pool.query(
|
||||
`select
|
||||
(select count(*)::int from users) as "totalUsers",
|
||||
(select count(*)::int from users where is_active) as "activeUsers",
|
||||
(select count(*)::int from courses where status = 'published') as "publishedCourses",
|
||||
(select count(*)::int from lesson_progress where completed_at is not null) as "completedLessons",
|
||||
(select count(*)::int from comments) as "comments"`,
|
||||
);
|
||||
return { data: result.rows[0] };
|
||||
});
|
||||
|
||||
app.get('/users/:userId', adminAccess, async (request, reply) => {
|
||||
const { userId } = userParamsSchema.parse(request.params);
|
||||
const result = await pool.query(
|
||||
`select u.id, u.email, u.display_name as name, u.role, u.is_active as "isActive", u.created_at as "createdAt",
|
||||
(select count(*)::int from lesson_progress lp where lp.user_id = u.id and lp.completed_at is not null) as "completedLessons",
|
||||
(select max(lp.updated_at) from lesson_progress lp where lp.user_id = u.id) as "lastLearningAt"
|
||||
from users u where u.id = $1`, [userId],
|
||||
);
|
||||
if (!result.rows[0]) return reply.code(404).send({ error: 'User not found' });
|
||||
return { data: result.rows[0] };
|
||||
});
|
||||
|
||||
app.post('/invitations', adminAccess, async (request, reply) => {
|
||||
const input = invitationSchema.parse(request.body);
|
||||
const existing = await pool.query('select 1 from users where email = $1', [input.email]);
|
||||
if (existing.rowCount) return reply.code(409).send({ error: 'This email already has an account' });
|
||||
const rawToken = createRawToken();
|
||||
await pool.query(
|
||||
`insert into account_access_tokens (email, role, purpose, token_hash, expires_at, created_by)
|
||||
values ($1, $2::user_role, 'invitation', $3, now() + interval '7 days', $4)`,
|
||||
[input.email, input.role, hashToken(rawToken), request.user.id],
|
||||
);
|
||||
return reply.code(201).send({ data: { inviteUrl: `${config.FRONTEND_ORIGIN}/#/invite?token=${rawToken}` } });
|
||||
});
|
||||
|
||||
app.post('/users/:userId/password-reset', adminAccess, async (request, reply) => {
|
||||
const { userId } = userParamsSchema.parse(request.params);
|
||||
const account = await pool.query<{ email: string; role: 'student' | 'instructor' | 'admin' }>('select email, role from users where id = $1', [userId]);
|
||||
if (!account.rows[0]) return reply.code(404).send({ error: 'User not found' });
|
||||
const rawToken = createRawToken();
|
||||
await pool.query(
|
||||
`insert into account_access_tokens (email, role, purpose, token_hash, expires_at, created_by)
|
||||
values ($1, $2::user_role, 'password_reset', $3, now() + interval '1 day', $4)`,
|
||||
[account.rows[0].email, account.rows[0].role, hashToken(rawToken), request.user.id],
|
||||
);
|
||||
return { data: { resetUrl: `${config.FRONTEND_ORIGIN}/#/reset-password?token=${rawToken}` } };
|
||||
});
|
||||
|
||||
app.patch('/users/:userId', adminAccess, async (request, reply) => {
|
||||
const { userId } = userParamsSchema.parse(request.params);
|
||||
const input = updateUserSchema.parse(request.body);
|
||||
|
||||
Reference in New Issue
Block a user