diff --git a/prisma/migrations/20250719042829_add_terminal_auth_request/migration.sql b/prisma/migrations/20250719042829_add_terminal_auth_request/migration.sql new file mode 100644 index 0000000..7963f1c --- /dev/null +++ b/prisma/migrations/20250719042829_add_terminal_auth_request/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "TerminalAuthRequest" ( + "id" TEXT NOT NULL, + "publicKey" TEXT NOT NULL, + "response" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TerminalAuthRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "TerminalAuthRequest_publicKey_key" ON "TerminalAuthRequest"("publicKey"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8a52aa6..170dc53 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,6 +29,14 @@ model Account { AccountPushToken AccountPushToken[] } +model TerminalAuthRequest { + id String @id @default(cuid()) + publicKey String @unique + response String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model AccountPushToken { id String @id @default(cuid()) accountId String diff --git a/sources/app/api.ts b/sources/app/api.ts index e2efb16..13811bd 100644 --- a/sources/app/api.ts +++ b/sources/app/api.ts @@ -130,16 +130,13 @@ export async function startApi() { } // Auth schema - const authSchema = z.object({ - publicKey: z.string(), - challenge: z.string(), - signature: z.string() - }); - - // Single auth endpoint typed.post('/v1/auth', { schema: { - body: authSchema + body: z.object({ + publicKey: z.string(), + challenge: z.string(), + signature: z.string() + }) } }, async (request, reply) => { const publicKey = privacyKit.decodeBase64(request.body.publicKey); @@ -164,6 +161,78 @@ export async function startApi() { }); }); + typed.post('/v1/auth/request', { + schema: { + body: z.object({ + publicKey: z.string(), + }), + response: { + 200: z.union([z.object({ + state: z.literal('requested'), + }), z.object({ + state: z.literal('authorized'), + token: z.string(), + response: z.string() + })]), + 401: z.object({ + error: z.literal('Invalid public key') + }) + } + } + }, async (request, reply) => { + const publicKey = privacyKit.decodeBase64(request.body.publicKey); + const isValid = tweetnacl.box.publicKeyLength === publicKey.length; + if (!isValid) { + return reply.code(401).send({ error: 'Invalid public key' }); + } + + const answer = await db.terminalAuthRequest.upsert({ + where: { publicKey: privacyKit.encodeHex(publicKey) }, + update: { updatedAt: new Date() }, + create: { publicKey: privacyKit.encodeHex(publicKey) } + }); + + if (answer.response) { + const token = await tokenGenerator.new({ user: answer.id, extras: { session: answer.id } }); + return reply.send({ + state: 'authorized', + token: token, + response: answer.response + }); + } + + return reply.send({ state: 'requested' }); + }); + + // Approve auth request + typed.post('/v1/auth/response', { + schema: { + body: z.object({ + response: z.string(), + publicKey: z.string() + }) + } + }, async (request, reply) => { + const publicKey = privacyKit.decodeBase64(request.body.publicKey); + const isValid = tweetnacl.box.publicKeyLength === publicKey.length; + if (!isValid) { + return reply.code(401).send({ error: 'Invalid public key' }); + } + const authRequest = await db.terminalAuthRequest.findUnique({ + where: { publicKey: privacyKit.encodeHex(publicKey) } + }); + if (!authRequest) { + return reply.code(404).send({ error: 'Request not found' }); + } + if (!authRequest.response) { + await db.terminalAuthRequest.update({ + where: { id: authRequest.id }, + data: { response: request.body.response } + }); + } + return reply.send({ success: true }); + }); + // Sessions API typed.get('/v1/sessions', { preHandler: app.authenticate