feat: add scoped auth

This commit is contained in:
Steve Korshakov 2025-07-18 21:48:33 -07:00
parent f5118557d9
commit 4e14898e97
3 changed files with 98 additions and 8 deletions

View File

@ -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");

View File

@ -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

View File

@ -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