diff --git a/src/app.ts b/src/app.ts index edae5283..8d8b1139 100644 --- a/src/app.ts +++ b/src/app.ts @@ -263,7 +263,7 @@ app.get('/api/v1/mutes', requireSigner, mutesController); app.get('/api/v1/markers', requireProof(), markersController); app.post('/api/v1/markers', requireProof(), updateMarkersController); -app.post('/api/v1/push/subscription', requireSigner, pushSubscribeController); +app.post('/api/v1/push/subscription', requireProof(), pushSubscribeController); app.get('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions', reactionsController); app.get('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', reactionsController); diff --git a/src/controllers/api/push.ts b/src/controllers/api/push.ts index 987af708..f442064e 100644 --- a/src/controllers/api/push.ts +++ b/src/controllers/api/push.ts @@ -1,7 +1,10 @@ +import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; +import { Storages } from '@/storages.ts'; import { parseBody } from '@/utils/api.ts'; +import { getTokenHash } from '@/utils/auth.ts'; const pushSubscribeSchema = z.object({ subscription: z.object({ @@ -10,30 +13,62 @@ const pushSubscribeSchema = z.object({ p256dh: z.string(), auth: z.string(), }), - data: z.object({ - alerts: z.object({ - mention: z.boolean().optional(), - status: z.boolean().optional(), - reblog: z.boolean().optional(), - follow: z.boolean().optional(), - follow_request: z.boolean().optional(), - favourite: z.boolean().optional(), - poll: z.boolean().optional(), - update: z.boolean().optional(), - 'admin.sign_up': z.boolean().optional(), - 'admin.report': z.boolean().optional(), - }).optional(), - policy: z.enum(['all', 'followed', 'follower', 'none']).optional(), - }), }), + data: z.object({ + alerts: z.object({ + mention: z.boolean().optional(), + status: z.boolean().optional(), + reblog: z.boolean().optional(), + follow: z.boolean().optional(), + follow_request: z.boolean().optional(), + favourite: z.boolean().optional(), + poll: z.boolean().optional(), + update: z.boolean().optional(), + 'admin.sign_up': z.boolean().optional(), + 'admin.report': z.boolean().optional(), + }).optional(), + policy: z.enum(['all', 'followed', 'follower', 'none']).optional(), + }).optional(), }); export const pushSubscribeController: AppController = async (c) => { - const data = pushSubscribeSchema.safeParse(await parseBody(c.req.raw)); + const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); - if (!data.success) { - return c.json({ error: 'Invalid request', schema: data.error }, 400); + const header = c.req.header('authorization'); + const match = header?.match(BEARER_REGEX); + + if (!match) { + return c.json({ error: 'Unauthorized' }, 401); } + const [_, bech32] = match; + + if (!bech32.startsWith('token1')) { + return c.json({ error: 'Unauthorized' }, 401); + } + + const kysely = await Storages.kysely(); + const signer = c.get('signer')!; + + const result = pushSubscribeSchema.safeParse(await parseBody(c.req.raw)); + + if (!result.success) { + return c.json({ error: 'Invalid request', schema: result.error }, 400); + } + + const { subscription, data } = result.data; + + await kysely + .insertInto('push_subscriptions') + .values({ + pubkey: await signer.getPublicKey(), + token_hash: await getTokenHash(bech32 as `token1${string}`), + endpoint: subscription.endpoint, + p256dh: subscription.keys.p256dh, + auth: subscription.keys.auth, + data, + }) + .execute(); + return c.json({}); }; diff --git a/src/db/DittoTables.ts b/src/db/DittoTables.ts index 25bc26c1..85336452 100644 --- a/src/db/DittoTables.ts +++ b/src/db/DittoTables.ts @@ -59,6 +59,7 @@ interface EventZapRow { interface PushSubscriptionRow { id: Generated; pubkey: string; + token_hash: Uint8Array; endpoint: string; p256dh: string; auth: string; diff --git a/src/db/migrations/038_push_subscriptions.ts b/src/db/migrations/038_push_subscriptions.ts index 5e80ddc6..be0b9fcb 100644 --- a/src/db/migrations/038_push_subscriptions.ts +++ b/src/db/migrations/038_push_subscriptions.ts @@ -5,7 +5,7 @@ export async function up(db: Kysely): Promise { .createTable('push_subscriptions') .addColumn('id', 'bigint', (c) => c.primaryKey().autoIncrement()) .addColumn('pubkey', 'char(64)', (c) => c.notNull()) - .addColumn('token', 'char(64)', (c) => c.notNull()) + .addColumn('token_hash', 'bytea', (c) => c.references('auth_tokens.token_hash').onDelete('cascade').notNull()) .addColumn('endpoint', 'text', (c) => c.notNull()) .addColumn('p256dh', 'text', (c) => c.notNull()) .addColumn('auth', 'text', (c) => c.notNull())