improve app registration and component service ports
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { randomBytes, scrypt as scryptCallback } from 'node:crypto';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
|
||||
import { ListUserSessionsQuery } from './dto';
|
||||
import { CreateManagedUserDto, ListUserSessionsQuery } from './dto';
|
||||
|
||||
const scrypt = promisify(scryptCallback);
|
||||
const passwordKeyLength = 64;
|
||||
|
||||
type UserSessionRow = {
|
||||
id: string;
|
||||
@@ -144,6 +149,66 @@ export class UsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async createManagedUser(body: CreateManagedUserDto) {
|
||||
const email = body.email.trim().toLowerCase();
|
||||
const existing = await this.authPrisma.user.findUnique({
|
||||
where: { email },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('이미 등록된 이메일입니다.');
|
||||
}
|
||||
|
||||
const license = await this.authPrisma.license.findUnique({
|
||||
where: { code: body.licenseCode ?? 'EDU' },
|
||||
select: { id: true, defaultDurationDays: true },
|
||||
});
|
||||
|
||||
if (!license) {
|
||||
throw new NotFoundException('라이센스를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const startsAt = new Date();
|
||||
const expiresAt = calculateLicenseExpiresAt(
|
||||
startsAt,
|
||||
license.defaultDurationDays,
|
||||
);
|
||||
|
||||
const user = await this.authPrisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
passwordHash: await hashPassword(body.password),
|
||||
name: body.name.trim(),
|
||||
role: 'USER',
|
||||
affiliations: body.organizationId
|
||||
? {
|
||||
create: {
|
||||
organizationId: body.organizationId,
|
||||
department: body.department?.trim() || null,
|
||||
position: body.position?.trim() || null,
|
||||
isPrimary: true,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
userLicenses: {
|
||||
create: {
|
||||
licenseId: license.id,
|
||||
startsAt,
|
||||
expiresAt,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { user };
|
||||
}
|
||||
|
||||
async lockUser(userId: string) {
|
||||
const user = await this.authPrisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
@@ -209,6 +274,23 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
async function hashPassword(password: string) {
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
const derivedKey = (await scrypt(password, salt, passwordKeyLength)) as Buffer;
|
||||
return `scrypt:${salt}:${derivedKey.toString('hex')}`;
|
||||
}
|
||||
|
||||
function calculateLicenseExpiresAt(
|
||||
startsAt: Date,
|
||||
durationDays: number | null | undefined,
|
||||
) {
|
||||
if (!durationDays) return null;
|
||||
|
||||
const expiresAt = new Date(startsAt);
|
||||
expiresAt.setDate(expiresAt.getDate() + durationDays);
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
function getUserAvatarUrl(
|
||||
userId: string,
|
||||
mimeType?: string | null,
|
||||
|
||||
Reference in New Issue
Block a user