add launcher user app permission api

This commit is contained in:
2026-07-02 13:17:42 +09:00
parent 69ee632aa3
commit 8a610567b2
3 changed files with 129 additions and 3 deletions
+20 -1
View File
@@ -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()
+15 -2
View File
@@ -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);
+94
View File
@@ -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<LauncherUserAppPermissionRow[]>(
`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<Array<{ id: string; app_key: string }>>(
`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<LauncherAppRow[]>(`
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,