init studio admin backend
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { IsEnum, IsISO8601, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
import { LicenseCode } from './license-enums';
|
||||
|
||||
export class ListUserLicensesQuery {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LicenseCode)
|
||||
licenseCode?: LicenseCode;
|
||||
}
|
||||
|
||||
export class AssignLicenseDto {
|
||||
@IsEnum(LicenseCode)
|
||||
licenseCode!: LicenseCode;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
startsAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
expiresAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assignedById?: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const LicenseCode = {
|
||||
FREE: 'FREE',
|
||||
EDU: 'EDU',
|
||||
PRO: 'PRO',
|
||||
} as const;
|
||||
|
||||
export type LicenseCode = (typeof LicenseCode)[keyof typeof LicenseCode];
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
|
||||
import { AssignLicenseDto, ListUserLicensesQuery } from './dto';
|
||||
import { LicensesService } from './licenses.service';
|
||||
|
||||
@Controller('licenses')
|
||||
export class LicensesController {
|
||||
constructor(private readonly licensesService: LicensesService) {}
|
||||
|
||||
@Get('plans')
|
||||
listPlans() {
|
||||
return this.licensesService.listPlans();
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
listUserLicenses(@Query() query: ListUserLicensesQuery) {
|
||||
return this.licensesService.listUserLicenses(query);
|
||||
}
|
||||
|
||||
@Post('users/:userId')
|
||||
assign(@Param('userId') userId: string, @Body() body: AssignLicenseDto) {
|
||||
return this.licensesService.assign(userId, body);
|
||||
}
|
||||
|
||||
@Patch('user-licenses/:id')
|
||||
update(@Param('id') id: string, @Body() body: AssignLicenseDto) {
|
||||
return this.licensesService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete('user-licenses/:id')
|
||||
revoke(@Param('id') id: string) {
|
||||
return this.licensesService.revoke(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
|
||||
import { LicensesController } from './licenses.controller';
|
||||
import { LicensesService } from './licenses.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthPrismaModule],
|
||||
controllers: [LicensesController],
|
||||
providers: [LicensesService],
|
||||
})
|
||||
export class LicensesModule {}
|
||||
@@ -0,0 +1,220 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
|
||||
import { AssignLicenseDto, ListUserLicensesQuery } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class LicensesService {
|
||||
constructor(private readonly authPrisma: AuthPrismaService) {}
|
||||
|
||||
async listPlans() {
|
||||
const now = new Date();
|
||||
|
||||
const plans = await this.authPrisma.license.findMany({
|
||||
orderBy: { code: 'asc' },
|
||||
include: {
|
||||
userLicenses: {
|
||||
select: {
|
||||
id: true,
|
||||
startsAt: true,
|
||||
expiresAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
plans: plans.map((plan) => ({
|
||||
id: plan.id,
|
||||
code: plan.code,
|
||||
name: plan.name,
|
||||
description: plan.description,
|
||||
defaultDurationDays: plan.defaultDurationDays,
|
||||
isActive: plan.isActive,
|
||||
assignedCount: plan.userLicenses.length,
|
||||
activeUserCount: plan.userLicenses.filter((license) =>
|
||||
isActiveLicense(license.startsAt, license.expiresAt, now),
|
||||
).length,
|
||||
expiringSoonCount: plan.userLicenses.filter((license) =>
|
||||
expiresSoon(license.expiresAt, now),
|
||||
).length,
|
||||
createdAt: plan.createdAt,
|
||||
updatedAt: plan.updatedAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async listUserLicenses(query: ListUserLicensesQuery) {
|
||||
const keyword = query.keyword?.trim();
|
||||
|
||||
const users = await this.authPrisma.user.findMany({
|
||||
where: {
|
||||
OR: keyword
|
||||
? [
|
||||
{ email: { contains: keyword, mode: 'insensitive' } },
|
||||
{ name: { contains: keyword, mode: 'insensitive' } },
|
||||
]
|
||||
: undefined,
|
||||
userLicenses: query.licenseCode
|
||||
? { some: { license: { code: query.licenseCode } } }
|
||||
: undefined,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
userLicenses: {
|
||||
orderBy: { startsAt: 'desc' },
|
||||
include: { license: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
users: users.map((user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
isActive: user.isActive,
|
||||
currentLicense:
|
||||
user.userLicenses.find((userLicense) =>
|
||||
isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now),
|
||||
) ?? null,
|
||||
licenses: user.userLicenses.map((userLicense) => ({
|
||||
...userLicense,
|
||||
status: isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now)
|
||||
? '활성'
|
||||
: '만료',
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async assign(userId: string, body: AssignLicenseDto) {
|
||||
const user = await this.authPrisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('사용자를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const activeLicense = await this.authPrisma.userLicense.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
startsAt: { lte: new Date() },
|
||||
OR: [{ expiresAt: null }, { expiresAt: { gte: new Date() } }],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (activeLicense) {
|
||||
throw new ConflictException('이미 활성 라이센스가 있습니다. 기존 이력을 수정하세요.');
|
||||
}
|
||||
|
||||
const license = await this.authPrisma.license.findUnique({
|
||||
where: { code: body.licenseCode },
|
||||
});
|
||||
|
||||
if (!license) {
|
||||
throw new NotFoundException('라이센스 플랜을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const startsAt = body.startsAt ? new Date(body.startsAt) : new Date();
|
||||
const expiresAt = body.expiresAt
|
||||
? new Date(body.expiresAt)
|
||||
: calculateExpiresAt(startsAt, license.defaultDurationDays);
|
||||
|
||||
const userLicense = await this.authPrisma.userLicense.create({
|
||||
data: {
|
||||
userId,
|
||||
licenseId: license.id,
|
||||
startsAt,
|
||||
expiresAt,
|
||||
assignedById: body.assignedById,
|
||||
},
|
||||
include: { license: true, user: true },
|
||||
});
|
||||
|
||||
return { userLicense };
|
||||
}
|
||||
|
||||
async update(id: string, body: AssignLicenseDto) {
|
||||
const current = await this.authPrisma.userLicense.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!current) {
|
||||
throw new NotFoundException('사용자 라이센스를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const license = await this.authPrisma.license.findUnique({
|
||||
where: { code: body.licenseCode },
|
||||
});
|
||||
|
||||
if (!license) {
|
||||
throw new NotFoundException('라이센스 플랜을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const startsAt = body.startsAt ? new Date(body.startsAt) : new Date();
|
||||
const expiresAt = body.expiresAt
|
||||
? new Date(body.expiresAt)
|
||||
: calculateExpiresAt(startsAt, license.defaultDurationDays);
|
||||
|
||||
const userLicense = await this.authPrisma.userLicense.update({
|
||||
where: { id },
|
||||
data: {
|
||||
licenseId: license.id,
|
||||
startsAt,
|
||||
expiresAt,
|
||||
assignedById: body.assignedById,
|
||||
},
|
||||
include: { license: true, user: true },
|
||||
});
|
||||
|
||||
return { userLicense };
|
||||
}
|
||||
|
||||
async revoke(id: string) {
|
||||
const userLicense = await this.authPrisma.userLicense.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!userLicense) {
|
||||
throw new NotFoundException('사용자 라이센스를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
await this.authPrisma.userLicense.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
function calculateExpiresAt(startsAt: Date, durationDays?: number | null) {
|
||||
if (!durationDays) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expiresAt = new Date(startsAt);
|
||||
expiresAt.setDate(expiresAt.getDate() + durationDays);
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
function isActiveLicense(startsAt: Date, expiresAt: Date | null, now: Date) {
|
||||
return startsAt <= now && (!expiresAt || expiresAt >= now);
|
||||
}
|
||||
|
||||
function expiresSoon(expiresAt: Date | null, now: Date) {
|
||||
if (!expiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const diffDays = (expiresAt.getTime() - now.getTime()) / 1000 / 60 / 60 / 24;
|
||||
return diffDays >= 0 && diffDays <= 30;
|
||||
}
|
||||
Reference in New Issue
Block a user