diff --git a/packages/ditto/app.ts b/packages/ditto/app.ts index 8b291a66..459baf6b 100644 --- a/packages/ditto/app.ts +++ b/packages/ditto/app.ts @@ -1,6 +1,6 @@ import { DittoConf } from '@ditto/conf'; import { DittoDB } from '@ditto/db'; -import { paginationMiddleware, userMiddleware } from '@ditto/mastoapi/middleware'; +import { paginationMiddleware, tokenMiddleware, userMiddleware } from '@ditto/mastoapi/middleware'; import { DittoApp, type DittoEnv } from '@ditto/router'; import { type DittoTranslator } from '@ditto/translators'; import { type Context, Handler, Input as HonoInput, MiddlewareHandler } from '@hono/hono'; @@ -199,7 +199,7 @@ const ratelimit = every( ); const factory = createFactory(); -const requireSigner = userMiddleware({ privileged: false, required: true }); +const requireSigner = userMiddleware(); const requireAdmin = factory.createHandlers(requireSigner, requireRole('admin')); const requireProof = factory.createHandlers(requireSigner, _requireProof()); @@ -214,6 +214,7 @@ app.get('/relay', metricsMiddleware, ratelimit, relayController); app.use( cspMiddleware(), cors({ origin: '*', exposeHeaders: ['link'] }), + tokenMiddleware(), uploaderMiddleware, auth98Middleware(), ); diff --git a/packages/ditto/controllers/api/cashu.test.ts b/packages/ditto/controllers/api/cashu.test.ts index f8d37ca0..67530b6a 100644 --- a/packages/ditto/controllers/api/cashu.test.ts +++ b/packages/ditto/controllers/api/cashu.test.ts @@ -1,5 +1,6 @@ import { DittoConf } from '@ditto/conf'; -import { DittoApp } from '@ditto/router'; +import { type User } from '@ditto/mastoapi/middleware'; +import { DittoApp, DittoMiddleware } from '@ditto/router'; import { NSecSigner } from '@nostrify/nostrify'; import { genEvent } from '@nostrify/nostrify/test'; import { bytesToString, stringToBytes } from '@scure/base'; @@ -12,6 +13,13 @@ import { createTestDB } from '@/test.ts'; import cashuApp from '@/controllers/api/cashu.ts'; import { walletSchema } from '@/schema.ts'; +function testUserMiddleware(user: User): DittoMiddleware<{ user: User }> { + return async (c, next) => { + c.set('user', user); + await next(); + }; +} + Deno.test('PUT /wallet must be successful', { sanitizeOps: false, sanitizeResources: false, @@ -26,12 +34,12 @@ Deno.test('PUT /wallet must be successful', { const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) }); + app.use(testUserMiddleware({ signer, relay })); app.route('/', cashuApp); const response = await app.request('/wallet', { method: 'PUT', headers: { - 'authorization': `Bearer ${nip19.nsecEncode(sk)}`, 'content-type': 'application/json', }, body: JSON.stringify({ @@ -93,15 +101,16 @@ Deno.test('PUT /wallet must NOT be successful: wrong request body/schema', async await using db = await createTestDB(); const relay = db.store; const sk = generateSecretKey(); + const signer = new NSecSigner(sk); const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) }); + app.use(testUserMiddleware({ signer, relay })); app.route('/', cashuApp); const response = await app.request('/wallet', { method: 'PUT', headers: { - 'authorization': `Bearer ${nip19.nsecEncode(sk)}`, 'content-type': 'application/json', }, body: JSON.stringify({ @@ -123,9 +132,11 @@ Deno.test('PUT /wallet must NOT be successful: wallet already exists', { await using db = await createTestDB(); const relay = db.store; const sk = generateSecretKey(); + const signer = new NSecSigner(sk); const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) }); + app.use(testUserMiddleware({ signer, relay })); app.route('/', cashuApp); await db.store.event(genEvent({ kind: 17375 }, sk)); @@ -163,6 +174,7 @@ Deno.test('GET /wallet must be successful', { const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) }); + app.use(testUserMiddleware({ signer, relay })); app.route('/', cashuApp); // Wallet @@ -243,9 +255,6 @@ Deno.test('GET /wallet must be successful', { const response = await app.request('/wallet', { method: 'GET', - headers: { - 'authorization': `Bearer ${nip19.nsecEncode(sk)}`, - }, }); const body = await response.json(); diff --git a/packages/ditto/controllers/api/cashu.ts b/packages/ditto/controllers/api/cashu.ts index 16a28b09..9c7dcab0 100644 --- a/packages/ditto/controllers/api/cashu.ts +++ b/packages/ditto/controllers/api/cashu.ts @@ -1,6 +1,6 @@ import { Proof } from '@cashu/cashu-ts'; import { userMiddleware } from '@ditto/mastoapi/middleware'; -import { DittoMiddleware, DittoRoute } from '@ditto/router'; +import { DittoRoute } from '@ditto/router'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { bytesToString, stringToBytes } from '@scure/base'; import { z } from 'zod'; @@ -11,8 +11,6 @@ import { swapNutzapsMiddleware } from '@/middleware/swapNutzapsMiddleware.ts'; import { isNostrId } from '@/utils.ts'; import { logi } from '@soapbox/logi'; import { errorJson } from '@/utils/log.ts'; -import { SetRequired } from 'type-fest'; -import { NostrSigner } from '@nostrify/nostrify'; type Wallet = z.infer; @@ -33,19 +31,6 @@ interface Nutzap { recipient_pubkey: string; } -const requireNip44Signer: DittoMiddleware<{ user: { signer: SetRequired } }> = async ( - c, - next, -) => { - const { user } = c.var; - - if (!user?.signer.nip44) { - return c.json({ error: 'User does not have a NIP-44 signer' }, 400); - } - - await next(); -}; - const createCashuWalletAndNutzapInfoSchema = z.object({ mints: z.array(z.string().url()).nonempty().transform((val) => { return [...new Set(val)]; @@ -57,7 +42,7 @@ const createCashuWalletAndNutzapInfoSchema = z.object({ * https://github.com/nostr-protocol/nips/blob/master/60.md * https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-informational-event */ -app.put('/wallet', userMiddleware({ privileged: false, required: true }), requireNip44Signer, async (c) => { +app.put('/wallet', userMiddleware('nip44'), async (c) => { const { conf, user, relay, signal } = c.var; const pubkey = await user.signer.getPublicKey(); @@ -119,63 +104,57 @@ app.put('/wallet', userMiddleware({ privileged: false, required: true }), requir }); /** Gets a wallet, if it exists. */ -app.get( - '/wallet', - userMiddleware({ privileged: false, required: true }), - requireNip44Signer, - swapNutzapsMiddleware, - async (c) => { - const { conf, relay, user, signal } = c.var; +app.get('/wallet', userMiddleware('nip44'), swapNutzapsMiddleware, async (c) => { + const { conf, relay, user, signal } = c.var; - const pubkey = await user.signer.getPublicKey(); + const pubkey = await user.signer.getPublicKey(); - const [event] = await relay.query([{ authors: [pubkey], kinds: [17375] }], { signal }); - if (!event) { - return c.json({ error: 'Wallet not found' }, 404); - } + const [event] = await relay.query([{ authors: [pubkey], kinds: [17375] }], { signal }); + if (!event) { + return c.json({ error: 'Wallet not found' }, 404); + } - const decryptedContent: string[][] = JSON.parse(await user.signer.nip44.decrypt(pubkey, event.content)); + const decryptedContent: string[][] = JSON.parse(await user.signer.nip44.decrypt(pubkey, event.content)); - const privkey = decryptedContent.find(([value]) => value === 'privkey')?.[1]; - if (!privkey || !isNostrId(privkey)) { - return c.json({ error: 'Wallet does not contain privkey or privkey is not a valid nostr id.' }, 422); - } + const privkey = decryptedContent.find(([value]) => value === 'privkey')?.[1]; + if (!privkey || !isNostrId(privkey)) { + return c.json({ error: 'Wallet does not contain privkey or privkey is not a valid nostr id.' }, 422); + } - const p2pk = getPublicKey(stringToBytes('hex', privkey)); + const p2pk = getPublicKey(stringToBytes('hex', privkey)); - let balance = 0; - const mints: string[] = []; + let balance = 0; + const mints: string[] = []; - const tokens = await relay.query([{ authors: [pubkey], kinds: [7375] }], { signal }); - for (const token of tokens) { - try { - const decryptedContent: { mint: string; proofs: Proof[] } = JSON.parse( - await user.signer.nip44.decrypt(pubkey, token.content), - ); + const tokens = await relay.query([{ authors: [pubkey], kinds: [7375] }], { signal }); + for (const token of tokens) { + try { + const decryptedContent: { mint: string; proofs: Proof[] } = JSON.parse( + await user.signer.nip44.decrypt(pubkey, token.content), + ); - if (!mints.includes(decryptedContent.mint)) { - mints.push(decryptedContent.mint); - } - - balance += decryptedContent.proofs.reduce((accumulator, current) => { - return accumulator + current.amount; - }, 0); - } catch (e) { - logi({ level: 'error', ns: 'ditto.api.cashu.wallet.swap', error: errorJson(e) }); + if (!mints.includes(decryptedContent.mint)) { + mints.push(decryptedContent.mint); } + + balance += decryptedContent.proofs.reduce((accumulator, current) => { + return accumulator + current.amount; + }, 0); + } catch (e) { + logi({ level: 'error', ns: 'ditto.api.cashu.wallet.swap', error: errorJson(e) }); } + } - // TODO: maybe change the 'Wallet' type data structure so each mint is a key and the value are the tokens associated with a given mint - const walletEntity: Wallet = { - pubkey_p2pk: p2pk, - mints, - relays: [conf.relay], - balance, - }; + // TODO: maybe change the 'Wallet' type data structure so each mint is a key and the value are the tokens associated with a given mint + const walletEntity: Wallet = { + pubkey_p2pk: p2pk, + mints, + relays: [conf.relay], + balance, + }; - return c.json(walletEntity, 200); - }, -); + return c.json(walletEntity, 200); +}); /** Get mints set by the CASHU_MINTS environment variable. */ app.get('/mints', (c) => { diff --git a/packages/mastoapi/middleware/User.ts b/packages/mastoapi/middleware/User.ts new file mode 100644 index 00000000..ac38b8de --- /dev/null +++ b/packages/mastoapi/middleware/User.ts @@ -0,0 +1,6 @@ +import type { NostrSigner, NRelay } from '@nostrify/nostrify'; + +export interface User { + signer: S; + relay: R; +} diff --git a/packages/mastoapi/middleware/mod.ts b/packages/mastoapi/middleware/mod.ts index e4c346e1..fb6ffb59 100644 --- a/packages/mastoapi/middleware/mod.ts +++ b/packages/mastoapi/middleware/mod.ts @@ -1,2 +1,5 @@ export { paginationMiddleware } from './paginationMiddleware.ts'; export { tokenMiddleware } from './tokenMiddleware.ts'; +export { userMiddleware } from './userMiddleware.ts'; + +export type { User } from './User.ts'; diff --git a/packages/mastoapi/middleware/tokenMiddleware.ts b/packages/mastoapi/middleware/tokenMiddleware.ts index 9b666ed1..407548ed 100644 --- a/packages/mastoapi/middleware/tokenMiddleware.ts +++ b/packages/mastoapi/middleware/tokenMiddleware.ts @@ -11,27 +11,12 @@ import { UserStore } from '../storages/UserStore.ts'; import type { DittoConf } from '@ditto/conf'; import type { DittoDB } from '@ditto/db'; import type { DittoMiddleware } from '@ditto/router'; - -interface User { - signer: NostrSigner; - relay: NRelay; -} +import type { User } from './User.ts'; /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); -export function tokenMiddleware(opts: { privileged: true; required: false }): never; -// @ts-ignore The types are right. -export function tokenMiddleware(opts: { privileged: false; required: true }): DittoMiddleware<{ user: User }>; -export function tokenMiddleware(opts: { privileged: true; required?: boolean }): DittoMiddleware<{ user: User }>; -export function tokenMiddleware(opts: { privileged: false; required?: boolean }): DittoMiddleware<{ user?: User }>; -export function tokenMiddleware(opts: { privileged: boolean; required?: boolean }): DittoMiddleware<{ user?: User }> { - const { privileged, required = privileged } = opts; - - if (privileged && !required) { - throw new Error('Privileged middleware requires authorization.'); - } - +export function tokenMiddleware(): DittoMiddleware<{ user?: User }> { return async (c, next) => { const header = c.req.header('authorization'); @@ -48,13 +33,6 @@ export function tokenMiddleware(opts: { privileged: boolean; required?: boolean }; c.set('user', user); - } else if (required) { - throw new HTTPException(403, { message: 'Authorization required.' }); - } - - if (privileged) { - // TODO: add back nip98 auth - throw new HTTPException(500); } await next(); diff --git a/packages/mastoapi/middleware/userMiddleware.ts b/packages/mastoapi/middleware/userMiddleware.ts new file mode 100644 index 00000000..5b18e718 --- /dev/null +++ b/packages/mastoapi/middleware/userMiddleware.ts @@ -0,0 +1,27 @@ +import { HTTPException } from '@hono/hono/http-exception'; + +import type { DittoMiddleware } from '@ditto/router'; +import type { NostrSigner } from '@nostrify/nostrify'; +import type { SetRequired } from 'type-fest'; +import type { User } from './User.ts'; + +type Nip44Signer = SetRequired; + +export function userMiddleware(): DittoMiddleware<{ user: User }>; +// @ts-ignore Types are right. +export function userMiddleware(enc: 'nip44'): DittoMiddleware<{ user: User }>; +export function userMiddleware(enc?: 'nip04' | 'nip44'): DittoMiddleware<{ user: User }> { + return async (c, next) => { + const { user } = c.var; + + if (!user) { + throw new HTTPException(403, { message: 'Authorization required.' }); + } + + if (enc && !user.signer[enc]) { + throw new HTTPException(403, { message: `User does not have a ${enc} signer` }); + } + + await next(); + }; +}