manage license app launch policies
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-10 13:14:29 +09:00
parent 6311f8a553
commit ada3309db4
18 changed files with 760 additions and 48 deletions
+137 -6
View File
@@ -2,26 +2,61 @@ import { ConflictException, Injectable, NotFoundException } from '@nestjs/common
import { Prisma } from '@prisma/auth-client';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { CreateOrganizationDto, UpdateOrganizationDto } from './dto';
import { CreateOrganizationDto, ListOrganizationsQuery, UpdateOrganizationDto, UpdateOrganizationLicenseDto } from './dto';
@Injectable()
export class OrganizationsService {
constructor(private readonly authPrisma: AuthPrismaService) {}
async list() {
const organizations = await this.authPrisma.organization.findMany({
where: { isActive: true },
async list(query: ListOrganizationsQuery = {}) {
const page = toPositiveInt(query.page, 1);
const limit = Math.min(toPositiveInt(query.limit, 20), 100);
const keyword = query.keyword?.trim();
const where = {
isActive: true,
...(query.type ? { type: query.type as 'COMPANY' | 'SCHOOL' | 'GOVERNMENT' | 'LAB' | 'OTHER' } : {}),
...(keyword
? {
OR: [
{ name: { contains: keyword, mode: 'insensitive' as const } },
{ domain: { contains: keyword, mode: 'insensitive' as const } },
],
}
: {}),
};
const [organizations, total, licenses] = await this.authPrisma.$transaction([
this.authPrisma.organization.findMany({
where,
orderBy: [{ type: 'asc' }, { name: 'asc' }],
skip: (page - 1) * limit,
take: limit,
select: {
id: true,
name: true,
type: true,
domain: true,
defaultLicenseCode: true,
createdAt: true,
},
});
}),
this.authPrisma.organization.count({ where }),
this.authPrisma.license.findMany({
select: { code: true, name: true },
}),
]);
const licenseNameByCode = new Map(licenses.map((license) => [String(license.code), license.name]));
return { organizations };
return {
organizations: organizations.map((organization, index) => ({
...organization,
defaultLicenseName: organization.defaultLicenseCode ? licenseNameByCode.get(organization.defaultLicenseCode) ?? organization.defaultLicenseCode : null,
no: total - ((page - 1) * limit + index),
})),
total,
page,
limit,
};
}
async create(body: CreateOrganizationDto) {
@@ -37,6 +72,7 @@ export class OrganizationsService {
name: true,
type: true,
domain: true,
defaultLicenseCode: true,
createdAt: true,
},
});
@@ -68,6 +104,7 @@ export class OrganizationsService {
name: true,
type: true,
domain: true,
defaultLicenseCode: true,
createdAt: true,
},
});
@@ -111,4 +148,98 @@ export class OrganizationsService {
throw error;
}
}
async updateLicense(id: string, body: UpdateOrganizationLicenseDto, assignedById?: string) {
const licenseCode = body.licenseCode ?? null;
const now = new Date();
const organization = await this.authPrisma.organization.findUnique({
where: { id },
select: { id: true, defaultLicenseCode: true },
});
if (!organization) {
throw new NotFoundException('조직을 찾을 수 없습니다.');
}
const license = licenseCode
? await this.authPrisma.license.findUnique({
where: { code: licenseCode },
select: { id: true, code: true, name: true, defaultDurationDays: true },
})
: null;
if (licenseCode && !license) {
throw new NotFoundException('라이센스 플랜을 찾을 수 없습니다.');
}
const userIds = await this.authPrisma.userAffiliation.findMany({
where: {
organizationId: id,
OR: [{ endsAt: null }, { endsAt: { gt: now } }],
},
distinct: ['userId'],
select: { userId: true },
});
const targetUserIds = userIds.map((item) => item.userId);
const updated = await this.authPrisma.$transaction(async (tx) => {
const nextOrganization = await tx.organization.update({
where: { id },
data: { defaultLicenseCode: licenseCode },
select: {
id: true,
name: true,
type: true,
domain: true,
defaultLicenseCode: true,
createdAt: true,
},
});
if (license && targetUserIds.length > 0) {
await tx.userLicense.updateMany({
where: {
userId: { in: targetUserIds },
startsAt: { lte: now },
OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
},
data: { expiresAt: now },
});
await tx.userLicense.createMany({
data: targetUserIds.map((userId) => ({
userId,
licenseId: license.id,
startsAt: now,
expiresAt: calculateExpiresAt(now, license.defaultDurationDays),
assignedById,
})),
});
}
return nextOrganization;
});
return {
organization: {
...updated,
defaultLicenseName: license?.name ?? null,
},
updatedUserCount: license ? targetUserIds.length : 0,
};
}
}
function toPositiveInt(value: string | number | undefined, fallback: number) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
return Math.floor(parsed);
}
function calculateExpiresAt(startsAt: Date, durationDays?: number | null) {
if (!durationDays) return null;
const expiresAt = new Date(startsAt);
expiresAt.setDate(expiresAt.getDate() + durationDays);
return expiresAt;
}