diff --git a/prisma/migrations/20250911030241_add_version2_field/migration.sql b/prisma/migrations/20250911030241_add_version2_field/migration.sql new file mode 100644 index 0000000..e7c1bff --- /dev/null +++ b/prisma/migrations/20250911030241_add_version2_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "TerminalAuthRequest" ADD COLUMN "supportsV2" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 19c6d90..5a0553c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,6 +49,7 @@ model Account { model TerminalAuthRequest { id String @id @default(cuid()) publicKey String @unique + supportsV2 Boolean @default(false) response String? responseAccountId String? responseAccount Account? @relation(fields: [responseAccountId], references: [id]) @@ -227,16 +228,16 @@ model UploadedFile { } model ServiceAccountToken { - id String @id @default(cuid()) - accountId String - account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) - vendor String - token Bytes // Encrypted token - metadata Json? // Optional vendor metadata - lastUsedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + id String @id @default(cuid()) + accountId String + account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) + vendor String + token Bytes // Encrypted token + metadata Json? // Optional vendor metadata + lastUsedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([accountId, vendor]) @@index([accountId]) } diff --git a/sources/app/api/routes/authRoutes.ts b/sources/app/api/routes/authRoutes.ts index 2d95605..12d24cb 100644 --- a/sources/app/api/routes/authRoutes.ts +++ b/sources/app/api/routes/authRoutes.ts @@ -42,6 +42,7 @@ export function authRoutes(app: Fastify) { schema: { body: z.object({ publicKey: z.string(), + supportsV2: z.boolean().nullish() }), response: { 200: z.union([z.object({ @@ -70,7 +71,7 @@ export function authRoutes(app: Fastify) { const answer = await db.terminalAuthRequest.upsert({ where: { publicKey: publicKeyHex }, update: {}, - create: { publicKey: publicKeyHex } + create: { publicKey: publicKeyHex, supportsV2: request.body.supportsV2 ?? false } }); if (answer.response && answer.responseAccountId) { @@ -85,6 +86,43 @@ export function authRoutes(app: Fastify) { return reply.send({ state: 'requested' }); }); + // Get auth request status + app.get('/v1/auth/request/status', { + schema: { + querystring: z.object({ + publicKey: z.string(), + }), + response: { + 200: z.object({ + status: z.enum(['not_found', 'pending', 'authorized']), + supportsV2: z.boolean() + }) + } + } + }, async (request, reply) => { + const tweetnacl = (await import("tweetnacl")).default; + const publicKey = privacyKit.decodeBase64(request.query.publicKey); + const isValid = tweetnacl.box.publicKeyLength === publicKey.length; + if (!isValid) { + return reply.send({ status: 'not_found', supportsV2: false }); + } + + const publicKeyHex = privacyKit.encodeHex(publicKey); + const authRequest = await db.terminalAuthRequest.findUnique({ + where: { publicKey: publicKeyHex } + }); + + if (!authRequest) { + return reply.send({ status: 'not_found', supportsV2: false }); + } + + if (authRequest.response && authRequest.responseAccountId) { + return reply.send({ status: 'authorized', supportsV2: false }); + } + + return reply.send({ status: 'pending', supportsV2: authRequest.supportsV2 }); + }); + // Approve auth request app.post('/v1/auth/response', { preHandler: app.authenticate,