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")
|
||||
}
|
||||
|
||||
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 {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
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 {
|
||||
ListLauncherSshKeysQuery,
|
||||
CreateDeviceAuthorizationDto,
|
||||
ListDeviceAuthorizationUsersQuery,
|
||||
ListProvisionedDevicesQuery,
|
||||
UpdateProvisionedDeviceDto,
|
||||
} from './dto';
|
||||
@@ -38,4 +40,24 @@ export class AgentsController {
|
||||
removeSshKey(@Param('id') id: string) {
|
||||
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 { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
|
||||
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
|
||||
import { AgentsController } from './agents.controller';
|
||||
import { AgentsService } from './agents.service';
|
||||
|
||||
@Module({
|
||||
imports: [LauncherPrismaModule],
|
||||
imports: [AuthPrismaModule, LauncherPrismaModule],
|
||||
controllers: [AgentsController],
|
||||
providers: [AgentsService],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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 {
|
||||
ListDeviceAuthorizationUsersQuery,
|
||||
ListLauncherSshKeysQuery,
|
||||
ListProvisionedDevicesQuery,
|
||||
UpdateProvisionedDeviceDto,
|
||||
@@ -9,7 +12,10 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class AgentsService {
|
||||
constructor(private readonly launcherPrisma: LauncherPrismaService) {}
|
||||
constructor(
|
||||
private readonly authPrisma: AuthPrismaService,
|
||||
private readonly launcherPrisma: LauncherPrismaService,
|
||||
) {}
|
||||
|
||||
async listDevices(query: ListProvisionedDevicesQuery) {
|
||||
const keyword = query.keyword?.trim();
|
||||
@@ -109,6 +115,114 @@ export class AgentsService {
|
||||
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) {
|
||||
const device = await this.launcherPrisma.provisionedDevice.findUnique({
|
||||
where: { id },
|
||||
|
||||
+12
-1
@@ -1,4 +1,4 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class ListProvisionedDevicesQuery {
|
||||
@IsOptional()
|
||||
@@ -37,3 +37,14 @@ export class ListLauncherSshKeysQuery {
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export class ListDeviceAuthorizationUsersQuery {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export class CreateDeviceAuthorizationDto {
|
||||
@IsUUID()
|
||||
userId!: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user