From 8a610567b20d1cbfb416fb03ae7f0e5892f2eef1 Mon Sep 17 00:00:00 2001 From: chyoo Date: Thu, 2 Jul 2026 13:17:42 +0900 Subject: [PATCH] add launcher user app permission api --- src/launcher/dto.ts | 21 ++++++- src/launcher/launcher.controller.ts | 17 +++++- src/launcher/launcher.service.ts | 94 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) diff --git a/src/launcher/dto.ts b/src/launcher/dto.ts index a89c534..9455592 100644 --- a/src/launcher/dto.ts +++ b/src/launcher/dto.ts @@ -1,4 +1,5 @@ -import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID } from 'class-validator'; +import { Type } from 'class-transformer'; +import { IsArray, IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator'; export class CreateLauncherVersionDto { @IsString() @@ -203,6 +204,24 @@ export class MoveLauncherAppLinkDto { direction!: 'up' | 'down'; } +export class UpdateLauncherUserAppPermissionDto { + @IsString() + appKey!: string; + + @IsBoolean() + canView!: boolean; + + @IsBoolean() + canLaunch!: boolean; +} + +export class UpdateLauncherUserAppPermissionsDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => UpdateLauncherUserAppPermissionDto) + permissions!: UpdateLauncherUserAppPermissionDto[]; +} + export class CreateLauncherCommonSettingDto { @IsString() diff --git a/src/launcher/launcher.controller.ts b/src/launcher/launcher.controller.ts index d521a26..c8b591a 100644 --- a/src/launcher/launcher.controller.ts +++ b/src/launcher/launcher.controller.ts @@ -1,6 +1,6 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; -import { CreateLauncherAppLinkDto, CreateLauncherBuildJobDto, CreateLauncherCommonSettingDto, CreateLauncherVersionDto, UpdateLauncherSourceDto, LinkLauncherVersionSourceDto, MoveLauncherAppLinkDto, UpdateLauncherAppLinkDto, UpdateLauncherCommonSettingDto } from './dto'; +import { CreateLauncherAppLinkDto, CreateLauncherBuildJobDto, CreateLauncherCommonSettingDto, CreateLauncherVersionDto, UpdateLauncherSourceDto, LinkLauncherVersionSourceDto, MoveLauncherAppLinkDto, UpdateLauncherAppLinkDto, UpdateLauncherCommonSettingDto, UpdateLauncherUserAppPermissionsDto } from './dto'; import { LauncherService } from './launcher.service'; @Controller('launcher') @@ -67,6 +67,19 @@ export class LauncherController { return this.launcherService.listAppLinks(); } + @Get('users/:userId/app-permissions') + listUserAppPermissions(@Param('userId') userId: string) { + return this.launcherService.listUserAppPermissions(userId); + } + + @Put('users/:userId/app-permissions') + updateUserAppPermissions( + @Param('userId') userId: string, + @Body() body: UpdateLauncherUserAppPermissionsDto, + ) { + return this.launcherService.updateUserAppPermissions(userId, body); + } + @Post('app-links') createAppLink(@Body() body: CreateLauncherAppLinkDto) { return this.launcherService.createAppLink(body); diff --git a/src/launcher/launcher.service.ts b/src/launcher/launcher.service.ts index 92efbd8..d571a32 100644 --- a/src/launcher/launcher.service.ts +++ b/src/launcher/launcher.service.ts @@ -18,6 +18,7 @@ import { LinkLauncherVersionSourceDto, UpdateLauncherAppLinkDto, UpdateLauncherCommonSettingDto, + UpdateLauncherUserAppPermissionsDto, } from './dto'; const execAsync = promisify(exec); @@ -233,6 +234,86 @@ export class LauncherService { }; } + async listUserAppPermissions(userId: string) { + const rows = await this.launcherPrisma.$queryRawUnsafe( + `SELECT app.id AS app_id, app.app_key, app.display_name, app.description, app.sort_order, app.is_enabled, + permission.id AS permission_id, permission.can_view, permission.can_launch, permission.assigned_at + FROM launcher_apps app + LEFT JOIN launcher_user_apps permission + ON permission.app_id = app.id + AND permission.auth_user_id = $1::uuid + WHERE app.is_enabled = true + ORDER BY app.sort_order ASC, app.display_name ASC`, + userId, + ); + + return { + permissions: rows.map((row) => ({ + appId: row.app_id, + appKey: row.app_key, + displayName: row.display_name, + description: row.description, + sortOrder: row.sort_order, + isEnabled: row.is_enabled, + permissionId: row.permission_id, + canView: row.can_view ?? false, + canLaunch: row.can_launch ?? false, + assignedAt: row.assigned_at, + })), + }; + } + + async updateUserAppPermissions(userId: string, body: UpdateLauncherUserAppPermissionsDto) { + const inputByAppKey = new Map( + body.permissions.map((permission) => [ + permission.appKey, + { + canView: permission.canView, + canLaunch: permission.canView && permission.canLaunch, + }, + ]), + ); + const apps = await this.launcherPrisma.$queryRawUnsafe>( + `SELECT id, app_key FROM launcher_apps WHERE is_enabled = true`, + ); + const appKeys = new Set(apps.map((app) => app.app_key)); + const invalidKeys = [...inputByAppKey.keys()].filter((appKey) => !appKeys.has(appKey)); + if (invalidKeys.length) { + throw new BadRequestException(`알 수 없는 앱 권한입니다: ${invalidKeys.join(', ')}`); + } + + for (const app of apps) { + const input = inputByAppKey.get(app.app_key); + if (!input || !input.canView) { + await this.launcherPrisma.$executeRawUnsafe( + `DELETE FROM launcher_user_apps WHERE auth_user_id = $1::uuid AND app_id = $2::uuid`, + userId, + app.id, + ); + continue; + } + + await this.launcherPrisma.$executeRawUnsafe( + `INSERT INTO launcher_user_apps ( + auth_user_id, app_id, can_view, can_launch, assigned_at, created_at, updated_at + ) VALUES ( + $1::uuid, $2::uuid, $3, $4, now(), now(), now() + ) + ON CONFLICT (auth_user_id, app_id) DO UPDATE SET + can_view = EXCLUDED.can_view, + can_launch = EXCLUDED.can_launch, + assigned_at = now(), + updated_at = now()`, + userId, + app.id, + input.canView, + input.canLaunch, + ); + } + + return this.listUserAppPermissions(userId); + } + async syncAdminAppsFromLauncher() { const launcherRows = await this.launcherPrisma.$queryRawUnsafe(` SELECT app.id, app.app_key, app.display_name, app.description, app.subtitle, app.detail_url, app.video_path, @@ -1037,6 +1118,19 @@ type LauncherAppRow = { download_url: string | null; }; +type LauncherUserAppPermissionRow = { + app_id: string; + app_key: string; + display_name: string; + description: string | null; + sort_order: number; + is_enabled: boolean; + permission_id: string | null; + can_view: boolean | null; + can_launch: boolean | null; + assigned_at: Date | null; +}; + function mapLauncherVersionRow(row: LauncherVersionRow) { return { id: row.id,