diff --git a/prisma/migrations/20250718032748_add_push_token/migration.sql b/prisma/migrations/20250718032748_add_push_token/migration.sql new file mode 100644 index 0000000..a0468ad --- /dev/null +++ b/prisma/migrations/20250718032748_add_push_token/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "AccountPushToken" ( + "id" TEXT NOT NULL, + "accountId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "AccountPushToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AccountPushToken_accountId_token_key" ON "AccountPushToken"("accountId", "token"); + +-- AddForeignKey +ALTER TABLE "AccountPushToken" ADD CONSTRAINT "AccountPushToken_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 07e8895..8a52aa6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,13 +19,25 @@ datasource db { // model Account { - id String @id @default(cuid()) - publicKey String @unique - seq Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - Session Session[] - Update Update[] + id String @id @default(cuid()) + publicKey String @unique + seq Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Session Session[] + Update Update[] + AccountPushToken AccountPushToken[] +} + +model AccountPushToken { + id String @id @default(cuid()) + accountId String + account Account @relation(fields: [accountId], references: [id]) + token String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([accountId, token]) } // @@ -61,6 +73,7 @@ model SessionMessage { content Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + @@unique([sessionId, localId]) } diff --git a/sources/app/api.ts b/sources/app/api.ts index a5e5eb1..e2efb16 100644 --- a/sources/app/api.ts +++ b/sources/app/api.ts @@ -342,6 +342,96 @@ export async function startApi() { } }); + // Push Token Registration API + typed.post('/v1/push-tokens', { + schema: { + body: z.object({ + token: z.string() + }) + }, + preHandler: app.authenticate + }, async (request, reply) => { + const userId = request.user.id; + const { token } = request.body; + + try { + await db.accountPushToken.upsert({ + where: { + accountId_token: { + accountId: userId, + token: token + } + }, + update: { + updatedAt: new Date() + }, + create: { + accountId: userId, + token: token + } + }); + + return reply.send({ success: true }); + } catch (error) { + return reply.code(500).send({ error: 'Failed to register push token' }); + } + }); + + // Delete Push Token API + typed.delete('/v1/push-tokens/:token', { + schema: { + params: z.object({ + token: z.string() + }) + }, + preHandler: app.authenticate + }, async (request, reply) => { + const userId = request.user.id; + const { token } = request.params; + + try { + await db.accountPushToken.deleteMany({ + where: { + accountId: userId, + token: token + } + }); + + return reply.send({ success: true }); + } catch (error) { + return reply.code(500).send({ error: 'Failed to delete push token' }); + } + }); + + // Get Push Tokens API + typed.get('/v1/push-tokens', { + preHandler: app.authenticate + }, async (request, reply) => { + const userId = request.user.id; + + try { + const tokens = await db.accountPushToken.findMany({ + where: { + accountId: userId + }, + orderBy: { + createdAt: 'desc' + } + }); + + return reply.send({ + tokens: tokens.map(t => ({ + id: t.id, + token: t.token, + createdAt: t.createdAt.getTime(), + updatedAt: t.updatedAt.getTime() + })) + }); + } catch (error) { + return reply.code(500).send({ error: 'Failed to get push tokens' }); + } + }); + // Messages API typed.get('/v1/sessions/:sessionId/messages', { schema: {