From 4c8a9998b126de1cf09a2305059ba0a0dc763f6b Mon Sep 17 00:00:00 2001 From: chyoo Date: Mon, 3 Aug 2026 13:47:40 +0900 Subject: [PATCH] patch.. --- .../migration.sql | 5 ++ prisma/schema.prisma | 2 + .../dto/device-authorization.dto.ts | 4 ++ src/provisioning/provisioning.controller.ts | 16 +++++- src/provisioning/provisioning.service.ts | 51 +++++++++++++++++-- 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/20260722090000_add_device_authorization_registration_reference/migration.sql diff --git a/prisma/migrations/20260722090000_add_device_authorization_registration_reference/migration.sql b/prisma/migrations/20260722090000_add_device_authorization_registration_reference/migration.sql new file mode 100644 index 0000000..e7db685 --- /dev/null +++ b/prisma/migrations/20260722090000_add_device_authorization_registration_reference/migration.sql @@ -0,0 +1,5 @@ +ALTER TABLE "device_authorizations" + ADD COLUMN "registration_reference" TEXT; + +CREATE INDEX "device_authorizations_registration_reference_idx" + ON "device_authorizations"("registration_reference"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 280f52e..0868c5d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -237,6 +237,7 @@ model DeviceAuthorization { authUserName String? @map("auth_user_name") authUserRole String? @map("auth_user_role") authUserRoles Json? @map("auth_user_roles") + registrationReference String? @map("registration_reference") device Json? expiresAt DateTime @map("expires_at") @db.Timestamptz(3) approvedAt DateTime? @map("approved_at") @db.Timestamptz(3) @@ -245,6 +246,7 @@ model DeviceAuthorization { updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) @@index([status, expiresAt]) + @@index([registrationReference]) @@map("device_authorizations") } diff --git a/src/provisioning/dto/device-authorization.dto.ts b/src/provisioning/dto/device-authorization.dto.ts index 5f7549c..23755cd 100644 --- a/src/provisioning/dto/device-authorization.dto.ts +++ b/src/provisioning/dto/device-authorization.dto.ts @@ -4,6 +4,10 @@ import { IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'clas import { ProvisioningDeviceDto } from './create-provisioning.dto'; export class CreateDeviceAuthorizationDto { + @IsString() + @IsOptional() + registrationReference?: string; + @IsString() @IsOptional() hostname?: string; diff --git a/src/provisioning/provisioning.controller.ts b/src/provisioning/provisioning.controller.ts index aa701e5..920e29c 100644 --- a/src/provisioning/provisioning.controller.ts +++ b/src/provisioning/provisioning.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Headers, Post, UnauthorizedException } from '@nestjs/common'; +import { Body, Controller, Get, Headers, Param, Post, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CreateProvisioningDto } from './dto/create-provisioning.dto'; import { CreateSessionProvisioningDto } from './dto/create-session-provisioning.dto'; import { ConsumeDeviceAuthorizationDto, + CreateDeviceAuthorizationDto, } from './dto/device-authorization.dto'; import { ProvisioningService } from './provisioning.service'; import type { ProvisioningResponse } from './provisioning.service'; @@ -37,9 +38,11 @@ export class ProvisioningController { @Post('device-authorizations') createDeviceAuthorization( @Headers('authorization') authorization: string | undefined, + @Body() body: CreateDeviceAuthorizationDto, ) { return this.provisioningService.createDeviceAuthorization( this.getBearerToken(authorization), + body, ); } @@ -66,6 +69,17 @@ export class ProvisioningController { return this.provisioningService.consumeDeviceAuthorization(body); } + @Get('device-authorizations/registration/:reference') + getDeviceAuthorizationByRegistrationReference( + @Headers('authorization') authorization: string | undefined, + @Param('reference') reference: string, + ) { + return this.provisioningService.getDeviceAuthorizationByRegistrationReference( + this.getBearerToken(authorization), + reference, + ); + } + @Post('ssh-keys/lookup') listSshKeys( @Headers('x-lms-gateway-token') token: string | undefined, diff --git a/src/provisioning/provisioning.service.ts b/src/provisioning/provisioning.service.ts index 8af75d3..ef94bd2 100644 --- a/src/provisioning/provisioning.service.ts +++ b/src/provisioning/provisioning.service.ts @@ -14,7 +14,10 @@ import { createHash, randomBytes } from 'crypto'; import { PrismaService } from '../prisma/prisma.service'; import { CreateProvisioningDto } from './dto/create-provisioning.dto'; import { CreateSessionProvisioningDto } from './dto/create-session-provisioning.dto'; -import { ConsumeDeviceAuthorizationDto } from './dto/device-authorization.dto'; +import { + ConsumeDeviceAuthorizationDto, + CreateDeviceAuthorizationDto, +} from './dto/device-authorization.dto'; export type ProvisioningResponse = { installTailscale: boolean; @@ -79,7 +82,10 @@ export class ProvisioningService { return this.createProvisioningForUser(provisioningDto, authUser); } - async createDeviceAuthorization(accessToken: string | undefined) { + async createDeviceAuthorization( + accessToken: string | undefined, + input: CreateDeviceAuthorizationDto = {}, + ) { const authUser = await this.authenticateAccessToken(accessToken); const expiresInSeconds = Math.max( 60, @@ -100,6 +106,7 @@ export class ProvisioningService { authUserName: authUser.name ?? null, authUserRole: authUser.role ?? null, authUserRoles: authUser.roles ? this.cleanJson(authUser.roles) : undefined, + registrationReference: this.cleanString(input.registrationReference), expiresAt, approvedAt: new Date(), }, @@ -155,6 +162,35 @@ export class ProvisioningService { return { cancelled: result.count > 0 }; } + async getDeviceAuthorizationByRegistrationReference( + accessToken: string | undefined, + reference: string, + ) { + const authUser = await this.authenticateAccessToken(accessToken); + const registrationReference = this.cleanString(reference); + if (!registrationReference) { + throw new NotFoundException('Device authorization request was not found'); + } + + const authorization = await this.prisma.deviceAuthorization.findFirst({ + where: { + registrationReference, + authUserId: authUser.id, + accountEmail: authUser.email, + }, + orderBy: { createdAt: 'desc' }, + select: { + status: true, + expiresAt: true, + consumedAt: true, + accountEmail: true, + device: true, + }, + }); + + return authorization ?? null; + } + async consumeDeviceAuthorization(body: ConsumeDeviceAuthorizationDto) { const userCode = this.normalizeUserCode(body.userCode); const authorization = await this.prisma.deviceAuthorization.findUnique({ @@ -202,7 +238,16 @@ export class ProvisioningService { const provisioning = await this.createProvisioningForUser(body, authUser); await this.prisma.deviceAuthorization.update({ where: { id: authorization.id }, - data: { status: 'CONSUMED', consumedAt: new Date() }, + data: { + status: 'CONSUMED', + consumedAt: new Date(), + device: this.cleanJson({ + ...(body.device ?? {}), + hostname: body.hostname ?? body.device?.hostname ?? null, + sshUser: body.sshUser ?? body.device?.sshUser ?? null, + workspaceRoot: body.workspaceRoot ?? body.device?.workspaceRoot ?? null, + }), + }, }); return provisioning; } catch (error) {