221 lines
6.0 KiB
TypeScript
221 lines
6.0 KiB
TypeScript
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;
|
|
}
|