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
+4 -3
View File
@@ -1,8 +1,9 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import {
CreateAdminSshKeyDto,
CreateAdminDto,
ListAdminsQuery,
ResetAdminPasswordDto,
UpdateAdminAppPermissionsDto,
UpdateAdminRoleDto,
@@ -23,8 +24,8 @@ export class AdminsController {
}
@Get()
list() {
return this.adminsService.list();
list(@Query() query: ListAdminsQuery) {
return this.adminsService.list(query);
}
@Post()
+46 -6
View File
@@ -11,6 +11,7 @@ import { AdminRole } from './admin-role';
import {
CreateAdminSshKeyDto,
CreateAdminDto,
ListAdminsQuery,
ResetAdminPasswordDto,
UpdateAdminAppPermissionsDto,
UpdateAdminRoleDto,
@@ -43,9 +44,29 @@ export class AdminsService {
});
}
async list() {
const admins = await this.prisma.admin.findMany({
async list(query: ListAdminsQuery = {}) {
const page = toPositiveInt(query.page, 1);
const limit = Math.min(toPositiveInt(query.limit, 20), 100);
const keyword = query.keyword?.trim();
const where = {
...(query.role ? { role: query.role as AdminRole } : {}),
...(query.state === 'active' ? { isActive: true } : query.state === 'inactive' ? { isActive: false } : {}),
...(keyword
? {
OR: [
{ email: { contains: keyword, mode: 'insensitive' as const } },
{ name: { contains: keyword, mode: 'insensitive' as const } },
],
}
: {}),
};
const [admins, total] = await this.prisma.$transaction([
this.prisma.admin.findMany({
where,
orderBy: [{ role: 'asc' }, { createdAt: 'desc' }],
skip: (page - 1) * limit,
take: limit,
select: {
id: true,
email: true,
@@ -73,7 +94,9 @@ export class AdminsService {
select: { appId: true },
},
},
});
}),
this.prisma.admin.count({ where }),
]);
const users = await this.authPrisma.user.findMany({
where: { email: { in: admins.map((admin) => admin.email) } },
@@ -89,13 +112,19 @@ export class AdminsService {
?? this.configService.get<string>('AUTH_EXTERNAL_URL')
?? 'https://auth.autonomystudio.net';
return admins.map((admin) => {
return {
admins: admins.map((admin, index) => {
const user = userByEmail.get(admin.email);
return {
...admin,
no: total - ((page - 1) * limit + index),
avatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl),
};
});
}),
total,
page,
limit,
};
}
async create(body: CreateAdminDto) {
@@ -124,7 +153,12 @@ export class AdminsService {
}
async updateStatus(id: string, body: UpdateAdminStatusDto) {
await this.ensureExists(id);
const existing = await this.ensureExists(id);
await this.giteaService.updateUserLoginDisabled(
createGiteaUsername(existing.email),
!body.isActive,
);
const admin = await this.prisma.admin.update({
where: { id },
@@ -345,6 +379,12 @@ function hashPassword(password: string) {
return `scrypt:${salt}:${hash}`;
}
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 createGiteaUsername(email: string) {
const account = email.split('@')[0] || email;
const username = account
+22
View File
@@ -1,6 +1,28 @@
import { IsArray, IsBoolean, IsEmail, IsEnum, IsOptional, IsString, IsUUID, MinLength } from 'class-validator';
import { AdminRole } from './admin-role';
export class ListAdminsQuery {
@IsOptional()
@IsString()
page?: string;
@IsOptional()
@IsString()
limit?: string;
@IsOptional()
@IsString()
keyword?: string;
@IsOptional()
@IsString()
role?: string;
@IsOptional()
@IsString()
state?: string;
}
export class CreateAdminDto {
@IsEmail()
email!: string;