add instructor invite code..
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-08-07 11:56:00 +09:00
parent 339f858f89
commit f379bba89a
9 changed files with 155 additions and 9 deletions
+64 -1
View File
@@ -9,6 +9,7 @@ import {
} from '../storage/r2-public-files'
import { hashPassword, verifyPassword } from '../security/password'
import { redis } from '../redis'
import { randomBytes } from 'crypto'
import { createOpaqueToken, hashToken } from '../security/tokens'
import type { AuthUser } from '../types'
@@ -204,6 +205,52 @@ export async function createUser(input: { email: string; password: string; name?
return toAuthUser(user)
}
export async function createInstructorUser(input: {
email: string
password: string
name?: string | null
instructorInviteCode: string
}): Promise<AuthUser | null> {
return prisma.$transaction(async (tx) => {
const organization = await tx.organization.findFirst({
where: {
isActive: true,
instructorInviteCode: input.instructorInviteCode.trim().toUpperCase()
},
select: { id: true, defaultLicenseCode: true }
})
if (!organization) return null
const requestedLicenseCode = organization.defaultLicenseCode ?? 'FREE'
const license = await tx.license.findFirst({
where: { code: requestedLicenseCode as 'FREE' | 'EDU' | 'PRO', isActive: true },
select: { code: true }
})
const user = await tx.user.create({
data: {
email: input.email.toLowerCase(),
passwordHash: await hashPassword(input.password),
name: input.name?.trim() || null,
affiliations: {
create: { organizationId: organization.id, isPrimary: true }
},
appRoles: {
create: { appKey: 'LMS', roleCode: 'INSTRUCTOR' }
},
userLicenses: {
create: {
license: { connect: { code: license?.code ?? 'FREE' } }
}
}
},
select: userSelect()
})
return toAuthUser(user)
})
}
export async function findOrCreateOAuthUser(input: {
provider: string
providerUserId: string
@@ -487,11 +534,13 @@ export async function listOrganizations() {
}
export async function createOrganization(input: { name: string; type: 'COMPANY' | 'SCHOOL' | 'GOVERNMENT' | 'LAB' | 'OTHER'; domain?: string | null }) {
const instructorInviteCode = await createInstructorInviteCode()
return prisma.organization.create({
data: {
name: input.name.trim(),
type: input.type,
domain: input.domain?.trim() || null
domain: input.domain?.trim() || null,
instructorInviteCode
},
select: {
id: true,
@@ -503,6 +552,20 @@ export async function createOrganization(input: { name: string; type: 'COMPANY'
})
}
async function createInstructorInviteCode(): Promise<string> {
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
for (let attempt = 0; attempt < 10; attempt += 1) {
const code = Array.from(randomBytes(6), (byte) => alphabet[byte % alphabet.length]).join('')
const existing = await prisma.organization.findUnique({
where: { instructorInviteCode: code },
select: { id: true }
})
if (!existing) return code
}
throw new Error('failed_to_generate_instructor_invite_code')
}
export async function assignUserAffiliation(input: {
userId: string
organizationId: string
+30
View File
@@ -19,6 +19,7 @@ import {
createHandoffToken,
createRefreshToken,
createUser,
createInstructorUser,
deleteUserProfileImage,
exchangeOAuthLoginToken,
exchangeHandoffToken,
@@ -52,6 +53,10 @@ const signupSchema = z.object({
verificationCode: z.string().regex(/^\d{6}$/)
})
const instructorSignupSchema = signupSchema.extend({
instructorInviteCode: z.string().trim().regex(/^[A-Za-z0-9]{6}$/)
})
const emailVerificationSendSchema = z.object({
email: z.string().email()
})
@@ -606,6 +611,31 @@ authRoutes.post('/signup', async (c) => {
return c.json(await accessTokenResponse(user), 201)
})
authRoutes.post('/instructor-signup', async (c) => {
const body = instructorSignupSchema.safeParse(await c.req.json().catch(() => null))
if (!body.success) return c.json({ error: 'invalid_request', details: body.error.flatten() }, 400)
const email = body.data.email.toLowerCase()
const existingUser = await findUserByEmail(email)
if (existingUser) return c.json({ error: 'email_already_registered' }, 409)
const emailVerified = await verifyEmailVerificationCode(email, body.data.verificationCode)
if (!emailVerified) return c.json({ error: 'invalid_email_verification_code' }, 400)
const user = await createInstructorUser({
email,
password: body.data.password,
name: body.data.name,
instructorInviteCode: body.data.instructorInviteCode
})
if (!user) return c.json({ error: 'invalid_instructor_invite_code' }, 400)
const refreshToken = await createRefreshToken(user.id)
setRefreshCookie(c, refreshToken)
return c.json(await accessTokenResponse(user), 201)
})
authRoutes.post('/refresh', async (c) => {
const refreshToken = await readRefreshToken(c)
if (!refreshToken) return c.json({ error: 'missing_refresh_token' }, 401)