diff --git a/src/auth/routes.ts b/src/auth/routes.ts index 4f2e5ad..89f9502 100644 --- a/src/auth/routes.ts +++ b/src/auth/routes.ts @@ -3,6 +3,7 @@ import { randomInt } from 'crypto' import { z } from 'zod' import { config } from '../config' import { sendEmailVerificationCode } from '../mail' +import { prisma } from '../prisma' import { verifyPassword } from '../security/password' import { signAccessToken, verifyAccessToken } from '../security/tokens' import type { AuthUser } from '../types' @@ -26,6 +27,7 @@ import { findOrCreateOAuthUser, findUserProfileImage, findUserByEmail, + findUserById, listOrganizations, isLoginBlocked, markLastLogin, @@ -153,6 +155,14 @@ const userResponse = (user: AuthUser) => ({ const requireAdmin = (user: AuthUser): boolean => user.role === 'ADMIN' +const canManageLmsStudents = (user: AuthUser): boolean => ( + user.role === 'ADMIN' + || (user.appRoles ?? []).some((appRole) => ( + appRole.appKey.toUpperCase() === 'LMS' + && ['INSTRUCTOR', 'LMS_ADMIN'].includes(appRole.roleCode.toUpperCase()) + )) +) + const accessTokenResponse = async (user: AuthUser) => ({ accessToken: await signAccessToken(user), accessTokenExpiresIn: config.accessTokenTtlSeconds, @@ -802,6 +812,49 @@ authRoutes.post('/admin/users', authRequired, async (c) => { } }) +authRoutes.post('/instructor/students', authRequired, async (c) => { + const instructor = c.get('user') + if (!canManageLmsStudents(instructor)) return c.json({ error: 'forbidden' }, 403) + + const body = adminCreateUserSchema.safeParse(await c.req.json().catch(() => null)) + if (!body.success) return c.json({ error: 'invalid_request', details: body.error.flatten() }, 400) + + const organizationId = instructor.primaryAffiliation?.organizationId + if (instructor.role !== 'ADMIN' && (!organizationId || body.data.organizationId !== organizationId)) { + return c.json({ error: 'forbidden' }, 403) + } + + const email = body.data.email.toLowerCase() + const existingUser = await findUserByEmail(email) + if (existingUser) return c.json({ error: 'email_already_registered' }, 409) + + try { + const user = await createManagedUser({ + email, + password: body.data.password, + name: body.data.name, + organizationId: body.data.organizationId, + department: body.data.department, + position: body.data.position, + licenseCode: body.data.licenseCode, + }) + + await prisma.userAppRole.create({ + data: { + userId: user.id, + appKey: 'LMS', + roleCode: 'STUDENT', + assignedById: instructor.id, + }, + }) + + const createdUser = await findUserById(user.id) + return c.json({ user: userResponse(createdUser ?? user) }, 201) + } catch { + return c.json({ error: 'user_create_failed' }, 400) + } +}) + authRoutes.post('/admin/users/:userId/affiliations', authRequired, async (c) => { if (!requireAdmin(c.get('user'))) return c.json({ error: 'forbidden' }, 403)