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