admin appPermissions
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 09:07:03 +09:00
parent beb9dda8c6
commit eb79bddcc0
6 changed files with 127 additions and 2 deletions
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS "admin_app_permissions" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"admin_id" UUID NOT NULL,
"app_id" UUID NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admin_app_permissions_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "admin_app_permissions_admin_id_app_id_key"
ON "admin_app_permissions"("admin_id", "app_id");
CREATE INDEX IF NOT EXISTS "admin_app_permissions_app_id_idx"
ON "admin_app_permissions"("app_id");
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'admin_app_permissions_admin_id_fkey'
) THEN
ALTER TABLE "admin_app_permissions"
ADD CONSTRAINT "admin_app_permissions_admin_id_fkey"
FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'admin_app_permissions_app_id_fkey'
) THEN
ALTER TABLE "admin_app_permissions"
ADD CONSTRAINT "admin_app_permissions_app_id_fkey"
FOREIGN KEY ("app_id") REFERENCES "apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
+15
View File
@@ -74,11 +74,25 @@ model Admin {
sessions AdminSession[] sessions AdminSession[]
auditLogs AdminAuditLog[] auditLogs AdminAuditLog[]
sshKeys AdminSshKey[] sshKeys AdminSshKey[]
appPermissions AdminAppPermission[]
@@index([isActive, role]) @@index([isActive, role])
@@map("admins") @@map("admins")
} }
model AdminAppPermission {
id String @id @default(uuid()) @db.Uuid
adminId String @map("admin_id") @db.Uuid
appId String @map("app_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@unique([adminId, appId])
@@index([appId])
@@map("admin_app_permissions")
}
model AdminSshKey { model AdminSshKey {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
adminId String @map("admin_id") @db.Uuid adminId String @map("admin_id") @db.Uuid
@@ -142,6 +156,7 @@ model App {
releases AppRelease[] releases AppRelease[]
deployments DeploymentJob[] deployments DeploymentJob[]
runtimeProfiles RuntimeEnvProfile[] runtimeProfiles RuntimeEnvProfile[]
adminPermissions AdminAppPermission[]
@@map("apps") @@map("apps")
} }
+4
View File
@@ -22,6 +22,9 @@ export class AdminAuthService {
role: true, role: true,
isActive: true, isActive: true,
passwordHash: true, passwordHash: true,
appPermissions: {
select: { appId: true },
},
}, },
}); });
@@ -57,6 +60,7 @@ export class AdminAuthService {
email: admin.email, email: admin.email,
name: admin.name, name: admin.name,
role: admin.role, role: admin.role,
appPermissions: admin.appPermissions.map((permission) => permission.appId),
}, },
}; };
} }
+12 -1
View File
@@ -1,9 +1,10 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { import {
CreateAdminSshKeyDto, CreateAdminSshKeyDto,
CreateAdminDto, CreateAdminDto,
ResetAdminPasswordDto, ResetAdminPasswordDto,
UpdateAdminAppPermissionsDto,
UpdateAdminRoleDto, UpdateAdminRoleDto,
UpdateAdminStatusDto, UpdateAdminStatusDto,
} from './dto'; } from './dto';
@@ -46,6 +47,16 @@ export class AdminsController {
return this.adminsService.resetPassword(id, body); return this.adminsService.resetPassword(id, body);
} }
@Get(':id/app-permissions')
listAppPermissions(@Param('id') id: string) {
return this.adminsService.listAppPermissions(id);
}
@Put(':id/app-permissions')
updateAppPermissions(@Param('id') id: string, @Body() body: UpdateAdminAppPermissionsDto) {
return this.adminsService.updateAppPermissions(id, body);
}
@Post(':id/ssh-keys') @Post(':id/ssh-keys')
createSshKey(@Param('id') id: string, @Body() body: CreateAdminSshKeyDto) { createSshKey(@Param('id') id: string, @Body() body: CreateAdminSshKeyDto) {
return this.adminsService.createSshKey(id, body); return this.adminsService.createSshKey(id, body);
+53
View File
@@ -9,6 +9,7 @@ import {
CreateAdminSshKeyDto, CreateAdminSshKeyDto,
CreateAdminDto, CreateAdminDto,
ResetAdminPasswordDto, ResetAdminPasswordDto,
UpdateAdminAppPermissionsDto,
UpdateAdminRoleDto, UpdateAdminRoleDto,
UpdateAdminStatusDto, UpdateAdminStatusDto,
} from './dto'; } from './dto';
@@ -63,6 +64,9 @@ export class AdminsService {
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
select: adminSshKeySelect, select: adminSshKeySelect,
}, },
appPermissions: {
select: { appId: true },
},
}, },
}); });
} }
@@ -132,6 +136,55 @@ export class AdminsService {
return { admin }; return { admin };
} }
async listAppPermissions(id: string) {
await this.ensureExists(id);
const permissions = await this.prisma.adminAppPermission.findMany({
where: { adminId: id },
orderBy: { createdAt: 'asc' },
select: { appId: true },
});
return { appIds: permissions.map((permission) => permission.appId) };
}
async updateAppPermissions(id: string, body: UpdateAdminAppPermissionsDto) {
const admin = await this.prisma.admin.findUnique({
where: { id },
select: { id: true, role: true },
});
if (!admin) {
throw new NotFoundException('관리자를 찾을 수 없습니다.');
}
if (admin.role === AdminRole.OWNER) {
throw new BadRequestException('오너 권한은 앱 권한을 개별 설정하지 않습니다.');
}
const appIds = [...new Set(body.appIds.filter(Boolean))];
const existingApps = await this.prisma.app.findMany({
where: { id: { in: appIds } },
select: { id: true },
});
const existingAppIds = new Set(existingApps.map((app) => app.id));
const validAppIds = appIds.filter((appId) => existingAppIds.has(appId));
await this.prisma.$transaction([
this.prisma.adminAppPermission.deleteMany({ where: { adminId: id } }),
...(validAppIds.length
? [
this.prisma.adminAppPermission.createMany({
data: validAppIds.map((appId) => ({ adminId: id, appId })),
skipDuplicates: true,
}),
]
: []),
]);
return { appIds: validAppIds };
}
async createSshKey(adminId: string, body: CreateAdminSshKeyDto) { async createSshKey(adminId: string, body: CreateAdminSshKeyDto) {
await this.ensureExists(adminId); await this.ensureExists(adminId);
+7 -1
View File
@@ -1,4 +1,4 @@
import { IsBoolean, IsEmail, IsEnum, IsOptional, IsString, MinLength } from 'class-validator'; import { IsArray, IsBoolean, IsEmail, IsEnum, IsOptional, IsString, IsUUID, MinLength } from 'class-validator';
import { AdminRole } from './admin-role'; import { AdminRole } from './admin-role';
export class CreateAdminDto { export class CreateAdminDto {
@@ -44,3 +44,9 @@ export class CreateAdminSshKeyDto {
@IsString() @IsString()
giteaUsername?: string; giteaUsername?: string;
} }
export class UpdateAdminAppPermissionsDto {
@IsArray()
@IsUUID('4', { each: true })
appIds!: string[];
}