init studio admin backend

This commit is contained in:
2026-07-01 17:30:04 +09:00
commit 4437d9980d
144 changed files with 44964 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import { randomBytes, scryptSync, timingSafeEqual, createHash } from 'node:crypto';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AdminLoginDto } from './dto';
@Injectable()
export class AdminAuthService {
constructor(private readonly prisma: PrismaService) {}
async login(
body: AdminLoginDto,
context: { ipAddress?: string; userAgent?: string },
) {
const admin = await this.prisma.admin.findUnique({
where: { email: body.email.trim().toLowerCase() },
select: {
id: true,
email: true,
name: true,
role: true,
isActive: true,
passwordHash: true,
},
});
if (!admin || !admin.isActive || !verifyPassword(body.password, admin.passwordHash)) {
throw new UnauthorizedException('이메일 또는 비밀번호가 올바르지 않습니다.');
}
const token = randomBytes(32).toString('hex');
const tokenHash = hashToken(token);
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 12);
await this.prisma.$transaction([
this.prisma.admin.update({
where: { id: admin.id },
data: { lastLoginAt: new Date() },
}),
this.prisma.adminSession.create({
data: {
adminId: admin.id,
tokenHash,
ipAddress: context.ipAddress,
userAgent: context.userAgent,
expiresAt,
},
}),
]);
return {
token,
expiresAt,
admin: {
id: admin.id,
email: admin.email,
name: admin.name,
role: admin.role,
},
};
}
}
function verifyPassword(password: string, passwordHash: string) {
const [scheme, salt, storedHash] = passwordHash.split(':');
if (scheme !== 'scrypt' || !salt || !storedHash) {
return false;
}
const currentHash = scryptSync(password, salt, 64);
const storedBuffer = Buffer.from(storedHash, 'hex');
if (storedBuffer.length !== currentHash.length) {
return false;
}
return timingSafeEqual(storedBuffer, currentHash);
}
function hashToken(token: string) {
return createHash('sha256').update(token).digest('hex');
}