improve runtime environment variable modal usability
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -88,6 +88,28 @@ model ProvisionedDevice {
|
|||||||
@@map("provisioned_devices")
|
@@map("provisioned_devices")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DeviceAuthorization {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
deviceCodeHash String @unique @map("device_code_hash")
|
||||||
|
userCode String @unique @map("user_code")
|
||||||
|
status String @default("PENDING")
|
||||||
|
authUserId String? @map("auth_user_id") @db.Uuid
|
||||||
|
accountEmail String? @map("account_email")
|
||||||
|
authUserName String? @map("auth_user_name")
|
||||||
|
authUserRole String? @map("auth_user_role")
|
||||||
|
authUserRoles Json? @map("auth_user_roles")
|
||||||
|
device Json?
|
||||||
|
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
|
||||||
|
approvedAt DateTime? @map("approved_at") @db.Timestamptz(3)
|
||||||
|
consumedAt DateTime? @map("consumed_at") @db.Timestamptz(3)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||||
|
|
||||||
|
@@index([status, expiresAt])
|
||||||
|
@@index([authUserId, createdAt])
|
||||||
|
@@map("device_authorizations")
|
||||||
|
}
|
||||||
|
|
||||||
model LauncherSshKey {
|
model LauncherSshKey {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
accountEmail String @map("account_email")
|
accountEmail String @map("account_email")
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Controller, Delete, Get, Param, Patch, Query, Body } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ListLauncherSshKeysQuery,
|
ListLauncherSshKeysQuery,
|
||||||
|
CreateDeviceAuthorizationDto,
|
||||||
|
ListDeviceAuthorizationUsersQuery,
|
||||||
ListProvisionedDevicesQuery,
|
ListProvisionedDevicesQuery,
|
||||||
UpdateProvisionedDeviceDto,
|
UpdateProvisionedDeviceDto,
|
||||||
} from './dto';
|
} from './dto';
|
||||||
@@ -38,4 +40,24 @@ export class AgentsController {
|
|||||||
removeSshKey(@Param('id') id: string) {
|
removeSshKey(@Param('id') id: string) {
|
||||||
return this.agentsService.removeSshKey(id);
|
return this.agentsService.removeSshKey(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('registration-users')
|
||||||
|
listRegistrationUsers(@Query() query: ListDeviceAuthorizationUsersQuery) {
|
||||||
|
return this.agentsService.listRegistrationUsers(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('device-authorizations')
|
||||||
|
listDeviceAuthorizations() {
|
||||||
|
return this.agentsService.listDeviceAuthorizations();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('device-authorizations')
|
||||||
|
createDeviceAuthorization(@Body() body: CreateDeviceAuthorizationDto) {
|
||||||
|
return this.agentsService.createDeviceAuthorization(body.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('device-authorizations/:id/cancel')
|
||||||
|
cancelDeviceAuthorization(@Param('id') id: string) {
|
||||||
|
return this.agentsService.cancelDeviceAuthorization(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
|
||||||
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
|
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
|
||||||
import { AgentsController } from './agents.controller';
|
import { AgentsController } from './agents.controller';
|
||||||
import { AgentsService } from './agents.service';
|
import { AgentsService } from './agents.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [LauncherPrismaModule],
|
imports: [AuthPrismaModule, LauncherPrismaModule],
|
||||||
controllers: [AgentsController],
|
controllers: [AgentsController],
|
||||||
providers: [AgentsService],
|
providers: [AgentsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { createHash, randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
|
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
|
||||||
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
|
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
|
||||||
import {
|
import {
|
||||||
|
ListDeviceAuthorizationUsersQuery,
|
||||||
ListLauncherSshKeysQuery,
|
ListLauncherSshKeysQuery,
|
||||||
ListProvisionedDevicesQuery,
|
ListProvisionedDevicesQuery,
|
||||||
UpdateProvisionedDeviceDto,
|
UpdateProvisionedDeviceDto,
|
||||||
@@ -9,7 +12,10 @@ import {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AgentsService {
|
export class AgentsService {
|
||||||
constructor(private readonly launcherPrisma: LauncherPrismaService) {}
|
constructor(
|
||||||
|
private readonly authPrisma: AuthPrismaService,
|
||||||
|
private readonly launcherPrisma: LauncherPrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async listDevices(query: ListProvisionedDevicesQuery) {
|
async listDevices(query: ListProvisionedDevicesQuery) {
|
||||||
const keyword = query.keyword?.trim();
|
const keyword = query.keyword?.trim();
|
||||||
@@ -109,6 +115,114 @@ export class AgentsService {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listRegistrationUsers(query: ListDeviceAuthorizationUsersQuery) {
|
||||||
|
const keyword = query.keyword?.trim();
|
||||||
|
const users = await this.authPrisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
role: 'USER',
|
||||||
|
isActive: true,
|
||||||
|
...(keyword
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ email: { contains: keyword, mode: 'insensitive' } },
|
||||||
|
{ name: { contains: keyword, mode: 'insensitive' } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
orderBy: [{ name: 'asc' }, { email: 'asc' }],
|
||||||
|
take: 20,
|
||||||
|
select: { id: true, name: true, email: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { users };
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDeviceAuthorizations() {
|
||||||
|
const now = new Date();
|
||||||
|
const authorizations = await this.launcherPrisma.deviceAuthorization.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ status: { in: ['APPROVED', 'CONSUMING'] } },
|
||||||
|
{ createdAt: { gte: new Date(now.getTime() - 24 * 60 * 60 * 1000) } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 50,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userCode: true,
|
||||||
|
status: true,
|
||||||
|
authUserId: true,
|
||||||
|
accountEmail: true,
|
||||||
|
authUserName: true,
|
||||||
|
expiresAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
authorizations: authorizations.map((authorization) => ({
|
||||||
|
...authorization,
|
||||||
|
status: authorization.status === 'APPROVED' && authorization.expiresAt <= now ? 'EXPIRED' : authorization.status,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDeviceAuthorization(userId: string) {
|
||||||
|
const user = await this.authPrisma.user.findFirst({
|
||||||
|
where: { id: userId, role: 'USER', isActive: true },
|
||||||
|
select: { id: true, email: true, name: true, role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('플랫폼 등록 대상 사용자를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const expiresAt = new Date(now.getTime() + 5 * 60 * 1000);
|
||||||
|
await this.launcherPrisma.deviceAuthorization.updateMany({
|
||||||
|
where: {
|
||||||
|
authUserId: user.id,
|
||||||
|
accountEmail: user.email,
|
||||||
|
status: 'APPROVED',
|
||||||
|
expiresAt: { gt: now },
|
||||||
|
},
|
||||||
|
data: { status: 'CANCELLED' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorization = await this.launcherPrisma.deviceAuthorization.create({
|
||||||
|
data: {
|
||||||
|
deviceCodeHash: createHash('sha256').update(randomBytes(32).toString('base64url')).digest('hex'),
|
||||||
|
userCode: String(randomBytes(4).readUInt32BE() % 1_000_000).padStart(6, '0'),
|
||||||
|
status: 'APPROVED',
|
||||||
|
authUserId: user.id,
|
||||||
|
accountEmail: user.email,
|
||||||
|
authUserName: user.name,
|
||||||
|
authUserRole: user.role,
|
||||||
|
authUserRoles: [user.role],
|
||||||
|
expiresAt,
|
||||||
|
approvedAt: now,
|
||||||
|
},
|
||||||
|
select: { id: true, userCode: true, expiresAt: true, accountEmail: true, authUserName: true, status: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { authorization };
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelDeviceAuthorization(id: string) {
|
||||||
|
const result = await this.launcherPrisma.deviceAuthorization.updateMany({
|
||||||
|
where: { id, status: { in: ['APPROVED', 'CONSUMING'] } },
|
||||||
|
data: { status: 'CANCELLED' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.count) {
|
||||||
|
throw new NotFoundException('취소할 플랫폼 등록 키를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
private async ensureDeviceExists(id: string) {
|
private async ensureDeviceExists(id: string) {
|
||||||
const device = await this.launcherPrisma.provisionedDevice.findUnique({
|
const device = await this.launcherPrisma.provisionedDevice.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
+12
-1
@@ -1,4 +1,4 @@
|
|||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class ListProvisionedDevicesQuery {
|
export class ListProvisionedDevicesQuery {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -37,3 +37,14 @@ export class ListLauncherSshKeysQuery {
|
|||||||
@IsString()
|
@IsString()
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ListDeviceAuthorizationUsersQuery {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateDeviceAuthorizationDto {
|
||||||
|
@IsUUID()
|
||||||
|
userId!: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user