diff --git a/packages/ditto/app.ts b/packages/ditto/app.ts index 9944426c..8b291a66 100644 --- a/packages/ditto/app.ts +++ b/packages/ditto/app.ts @@ -7,6 +7,7 @@ import { type Context, Handler, Input as HonoInput, MiddlewareHandler } from '@h import { every } from '@hono/hono/combine'; import { cors } from '@hono/hono/cors'; import { serveStatic } from '@hono/hono/deno'; +import { createFactory } from '@hono/hono/factory'; import { NostrEvent, NostrSigner, NRelay, NUploader } from '@nostrify/nostrify'; import '@/startup.ts'; @@ -137,7 +138,7 @@ import { metricsController } from '@/controllers/metrics.ts'; import { manifestController } from '@/controllers/manifest.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; -import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; +import { auth98Middleware, requireProof as _requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cacheControlMiddleware } from '@/middleware/cacheControlMiddleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts'; @@ -197,7 +198,10 @@ const ratelimit = every( rateLimitMiddleware(300, Time.minutes(5), false), ); +const factory = createFactory(); const requireSigner = userMiddleware({ privileged: false, required: true }); +const requireAdmin = factory.createHandlers(requireSigner, requireRole('admin')); +const requireProof = factory.createHandlers(requireSigner, _requireProof()); app.use('/api/*', metricsMiddleware, ratelimit, paginationMiddleware(), logiMiddleware); app.use('/.well-known/*', metricsMiddleware, ratelimit, logiMiddleware); @@ -258,7 +262,7 @@ app.post('/oauth/revoke', revokeTokenController); app.post('/oauth/authorize', oauthAuthorizeController); app.get('/oauth/authorize', oauthController); -app.post('/api/v1/accounts', requireProof(), createAccountController); +app.post('/api/v1/accounts', ...requireProof, createAccountController); app.get('/api/v1/accounts/verify_credentials', requireSigner, verifyCredentialsController); app.patch('/api/v1/accounts/update_credentials', requireSigner, updateCredentialsController); app.get('/api/v1/accounts/search', accountSearchController); @@ -372,25 +376,25 @@ app.get('/api/v1/bookmarks', requireSigner, bookmarksController); app.get('/api/v1/blocks', requireSigner, blocksController); app.get('/api/v1/mutes', requireSigner, mutesController); -app.get('/api/v1/markers', requireProof(), markersController); -app.post('/api/v1/markers', requireProof(), updateMarkersController); +app.get('/api/v1/markers', ...requireProof, markersController); +app.post('/api/v1/markers', ...requireProof, updateMarkersController); app.get('/api/v1/push/subscription', requireSigner, getSubscriptionController); -app.post('/api/v1/push/subscription', requireProof(), 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); app.put('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', requireSigner, reactionController); app.delete('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', requireSigner, deleteReactionController); -app.get('/api/v1/pleroma/admin/config', requireRole('admin'), configController); -app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController); -app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAdminDeleteStatusController); +app.get('/api/v1/pleroma/admin/config', ...requireAdmin, configController); +app.post('/api/v1/pleroma/admin/config', ...requireAdmin, updateConfigController); +app.delete('/api/v1/pleroma/admin/statuses/:id', ...requireAdmin, pleromaAdminDeleteStatusController); -app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); -app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); +app.get('/api/v1/admin/ditto/relays', ...requireAdmin, adminRelaysController); +app.put('/api/v1/admin/ditto/relays', ...requireAdmin, adminSetRelaysController); -app.put('/api/v1/admin/ditto/instance', requireRole('admin'), updateInstanceController); +app.put('/api/v1/admin/ditto/instance', ...requireAdmin, updateInstanceController); app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); @@ -399,7 +403,7 @@ app.get('/api/v1/ditto/captcha', rateLimitMiddleware(3, Time.minutes(1)), captch app.post( '/api/v1/ditto/captcha/:id/verify', rateLimitMiddleware(8, Time.minutes(1)), - requireProof(), + ...requireProof, captchaVerifyController, ); @@ -410,8 +414,8 @@ app.get( ); app.get('/api/v1/ditto/:id{[0-9a-f]{64}}/zap_splits', statusZapSplitsController); -app.put('/api/v1/admin/ditto/zap_splits', requireRole('admin'), updateZapSplitsController); -app.delete('/api/v1/admin/ditto/zap_splits', requireRole('admin'), deleteZapSplitsController); +app.put('/api/v1/admin/ditto/zap_splits', ...requireAdmin, updateZapSplitsController); +app.delete('/api/v1/admin/ditto/zap_splits', ...requireAdmin, deleteZapSplitsController); app.post('/api/v1/ditto/zap', requireSigner, zapController); app.get('/api/v1/ditto/statuses/:id{[0-9a-f]{64}}/zapped_by', zappedByController); @@ -419,35 +423,35 @@ app.get('/api/v1/ditto/statuses/:id{[0-9a-f]{64}}/zapped_by', zappedByController app.route('/api/v1/ditto/cashu', cashuApp); app.post('/api/v1/reports', requireSigner, reportController); -app.get('/api/v1/admin/reports', requireSigner, requireRole('admin'), adminReportsController); -app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requireSigner, requireRole('admin'), adminReportController); +app.get('/api/v1/admin/reports', requireSigner, ...requireAdmin, adminReportsController); +app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requireSigner, ...requireAdmin, adminReportController); app.post( '/api/v1/admin/reports/:id{[0-9a-f]{64}}/resolve', requireSigner, - requireRole('admin'), + ...requireAdmin, adminReportResolveController, ); app.post( '/api/v1/admin/reports/:id{[0-9a-f]{64}}/reopen', requireSigner, - requireRole('admin'), + ...requireAdmin, adminReportReopenController, ); -app.get('/api/v1/admin/accounts', requireRole('admin'), adminAccountsController); -app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminActionController); +app.get('/api/v1/admin/accounts', ...requireAdmin, adminAccountsController); +app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, ...requireAdmin, adminActionController); app.post( '/api/v1/admin/accounts/:id{[0-9a-f]{64}}/approve', requireSigner, - requireRole('admin'), + ...requireAdmin, adminApproveController, ); -app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/reject', requireSigner, requireRole('admin'), adminRejectController); +app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/reject', requireSigner, ...requireAdmin, adminRejectController); -app.put('/api/v1/pleroma/admin/users/tag', requireRole('admin'), pleromaAdminTagController); -app.delete('/api/v1/pleroma/admin/users/tag', requireRole('admin'), pleromaAdminUntagController); -app.patch('/api/v1/pleroma/admin/users/suggest', requireRole('admin'), pleromaAdminSuggestController); -app.patch('/api/v1/pleroma/admin/users/unsuggest', requireRole('admin'), pleromaAdminUnsuggestController); +app.put('/api/v1/pleroma/admin/users/tag', ...requireAdmin, pleromaAdminTagController); +app.delete('/api/v1/pleroma/admin/users/tag', ...requireAdmin, pleromaAdminUntagController); +app.patch('/api/v1/pleroma/admin/users/suggest', ...requireAdmin, pleromaAdminSuggestController); +app.patch('/api/v1/pleroma/admin/users/unsuggest', ...requireAdmin, pleromaAdminUnsuggestController); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/packages/ditto/controllers/api/cashu.ts b/packages/ditto/controllers/api/cashu.ts index 60832ac4..16a28b09 100644 --- a/packages/ditto/controllers/api/cashu.ts +++ b/packages/ditto/controllers/api/cashu.ts @@ -93,6 +93,7 @@ app.put('/wallet', userMiddleware({ privileged: false, required: true }), requir await createEvent({ kind: 17375, content: encryptedWalletContentTags, + // @ts-ignore kill me }, c); // Nutzap information @@ -103,6 +104,7 @@ app.put('/wallet', userMiddleware({ privileged: false, required: true }), requir ['relay', conf.relay], // TODO: add more relays once things get more stable ['pubkey', p2pk], ], + // @ts-ignore kill me }, c); // TODO: hydrate wallet and add a 'balance' field when a 'renderWallet' view function is created diff --git a/packages/ditto/middleware/auth98Middleware.ts b/packages/ditto/middleware/auth98Middleware.ts index 18fce5fd..48ac5875 100644 --- a/packages/ditto/middleware/auth98Middleware.ts +++ b/packages/ditto/middleware/auth98Middleware.ts @@ -3,7 +3,6 @@ import { NostrEvent } from '@nostrify/nostrify'; import { type AppContext, type AppMiddleware } from '@/app.ts'; import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts'; -import { Storages } from '@/storages.ts'; import { localRequest } from '@/utils/api.ts'; import { buildAuthEventTemplate, @@ -40,10 +39,9 @@ type UserRole = 'user' | 'admin'; /** Require the user to prove their role before invoking the controller. */ function requireRole(role: UserRole, opts?: ParseAuthRequestOpts): AppMiddleware { return withProof(async (c, proof, next) => { - const { conf } = c.var; - const store = await Storages.db(); + const { conf, relay } = c.var; - const [user] = await store.query([{ + const [user] = await relay.query([{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [proof.pubkey], @@ -108,6 +106,7 @@ function withProof( /** Get the proof over Nostr Connect. */ async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) { const signer = c.var.user?.signer; + if (!signer) { throw new HTTPException(401, { res: c.json({ error: 'No way to sign Nostr event' }, 401), diff --git a/packages/ditto/middleware/swapNutzapsMiddleware.ts b/packages/ditto/middleware/swapNutzapsMiddleware.ts index aa68c1c1..79bdf01e 100644 --- a/packages/ditto/middleware/swapNutzapsMiddleware.ts +++ b/packages/ditto/middleware/swapNutzapsMiddleware.ts @@ -1,13 +1,12 @@ import { CashuMint, CashuWallet, getEncodedToken, type Proof } from '@cashu/cashu-ts'; -import { type DittoConf } from '@ditto/conf'; import { MiddlewareHandler } from '@hono/hono'; import { HTTPException } from '@hono/hono/http-exception'; import { getPublicKey } from 'nostr-tools'; -import { NostrFilter, NostrSigner, NSchema as n, NStore } from '@nostrify/nostrify'; -import { SetRequired } from 'type-fest'; +import { NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { stringToBytes } from '@scure/base'; import { logi } from '@soapbox/logi'; +import { AppEnv } from '@/app.ts'; import { isNostrId } from '@/utils.ts'; import { errorJson } from '@/utils/log.ts'; import { createEvent } from '@/utils/api.ts'; @@ -17,33 +16,28 @@ import { z } from 'zod'; * Swap nutzaps into wallet (create new events) if the user has a wallet, otheriwse, just fallthrough. * Errors are only thrown if 'signer' and 'store' middlewares are not set. */ -export const swapNutzapsMiddleware: MiddlewareHandler< - { Variables: { signer: SetRequired; store: NStore; conf: DittoConf } } -> = async (c, next) => { - const { conf } = c.var; - const signer = c.get('signer'); - const store = c.get('store'); +export const swapNutzapsMiddleware: MiddlewareHandler = async (c, next) => { + const { conf, relay, user, signal } = c.var; - if (!signer) { + if (!user) { throw new HTTPException(401, { message: 'No pubkey provided' }); } - if (!signer.nip44) { + if (!user.signer.nip44) { throw new HTTPException(401, { message: 'No NIP-44 signer provided' }); } - if (!store) { + if (!relay) { throw new HTTPException(401, { message: 'No store provided' }); } - const { signal } = c.req.raw; - const pubkey = await signer.getPublicKey(); - const [wallet] = await store.query([{ authors: [pubkey], kinds: [17375] }], { signal }); + const pubkey = await user.signer.getPublicKey(); + const [wallet] = await relay.query([{ authors: [pubkey], kinds: [17375] }], { signal }); if (wallet) { let decryptedContent: string; try { - decryptedContent = await signer.nip44.decrypt(pubkey, wallet.content); + decryptedContent = await user.signer.nip44.decrypt(pubkey, wallet.content); } catch (e) { logi({ level: 'error', @@ -68,7 +62,7 @@ export const swapNutzapsMiddleware: MiddlewareHandler< } const p2pk = getPublicKey(stringToBytes('hex', privkey)); - const [nutzapInformation] = await store.query([{ authors: [pubkey], kinds: [10019] }], { signal }); + const [nutzapInformation] = await relay.query([{ authors: [pubkey], kinds: [10019] }], { signal }); if (!nutzapInformation) { return c.json({ error: 'You need to have a nutzap information event so we can get the mints.' }, 400); } @@ -88,14 +82,14 @@ export const swapNutzapsMiddleware: MiddlewareHandler< const nutzapsFilter: NostrFilter = { kinds: [9321], '#p': [pubkey], '#u': mints }; - const [nutzapHistory] = await store.query([{ kinds: [7376], authors: [pubkey] }], { signal }); + const [nutzapHistory] = await relay.query([{ kinds: [7376], authors: [pubkey] }], { signal }); if (nutzapHistory) { nutzapsFilter.since = nutzapHistory.created_at; } const mintsToProofs: { [key: string]: { proofs: Proof[]; redeemed: string[][] } } = {}; - const nutzaps = await store.query([nutzapsFilter], { signal }); + const nutzaps = await relay.query([nutzapsFilter], { signal }); for (const event of nutzaps) { try { @@ -154,7 +148,7 @@ export const swapNutzapsMiddleware: MiddlewareHandler< const unspentProofs = await createEvent({ kind: 7375, - content: await signer.nip44.encrypt( + content: await user.signer.nip44.encrypt( pubkey, JSON.stringify({ mint, @@ -169,7 +163,7 @@ export const swapNutzapsMiddleware: MiddlewareHandler< await createEvent({ kind: 7376, - content: await signer.nip44.encrypt( + content: await user.signer.nip44.encrypt( pubkey, JSON.stringify([ ['direction', 'in'], diff --git a/packages/ditto/utils/api.ts b/packages/ditto/utils/api.ts index 0ac80a73..37a38d6a 100644 --- a/packages/ditto/utils/api.ts +++ b/packages/ditto/utils/api.ts @@ -18,16 +18,16 @@ import { purifyEvent } from '@/utils/purify.ts'; type EventStub = TypeFest.SetOptional; /** Publish an event through the pipeline. */ -async function createEvent(t: EventStub, c: Context): Promise { - const signer = c.get('signer'); +async function createEvent(t: EventStub, c: AppContext): Promise { + const { user } = c.var; - if (!signer) { + if (!user) { throw new HTTPException(401, { res: c.json({ error: 'No way to sign Nostr event' }, 401), }); } - const event = await signer.signEvent({ + const event = await user.signer.signEvent({ content: '', created_at: nostrNow(), tags: [],