mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Split userMiddleware into tokenMiddleware and a new userMiddleware
This commit is contained in:
parent
5ad7f1d5d7
commit
438ab09216
7 changed files with 97 additions and 94 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { DittoDB } from '@ditto/db';
|
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 { DittoApp, type DittoEnv } from '@ditto/router';
|
||||||
import { type DittoTranslator } from '@ditto/translators';
|
import { type DittoTranslator } from '@ditto/translators';
|
||||||
import { type Context, Handler, Input as HonoInput, MiddlewareHandler } from '@hono/hono';
|
import { type Context, Handler, Input as HonoInput, MiddlewareHandler } from '@hono/hono';
|
||||||
|
|
@ -199,7 +199,7 @@ const ratelimit = every(
|
||||||
);
|
);
|
||||||
|
|
||||||
const factory = createFactory();
|
const factory = createFactory();
|
||||||
const requireSigner = userMiddleware({ privileged: false, required: true });
|
const requireSigner = userMiddleware();
|
||||||
const requireAdmin = factory.createHandlers(requireSigner, requireRole('admin'));
|
const requireAdmin = factory.createHandlers(requireSigner, requireRole('admin'));
|
||||||
const requireProof = factory.createHandlers(requireSigner, _requireProof());
|
const requireProof = factory.createHandlers(requireSigner, _requireProof());
|
||||||
|
|
||||||
|
|
@ -214,6 +214,7 @@ app.get('/relay', metricsMiddleware, ratelimit, relayController);
|
||||||
app.use(
|
app.use(
|
||||||
cspMiddleware(),
|
cspMiddleware(),
|
||||||
cors({ origin: '*', exposeHeaders: ['link'] }),
|
cors({ origin: '*', exposeHeaders: ['link'] }),
|
||||||
|
tokenMiddleware(),
|
||||||
uploaderMiddleware,
|
uploaderMiddleware,
|
||||||
auth98Middleware(),
|
auth98Middleware(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
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 { NSecSigner } from '@nostrify/nostrify';
|
||||||
import { genEvent } from '@nostrify/nostrify/test';
|
import { genEvent } from '@nostrify/nostrify/test';
|
||||||
import { bytesToString, stringToBytes } from '@scure/base';
|
import { bytesToString, stringToBytes } from '@scure/base';
|
||||||
|
|
@ -12,6 +13,13 @@ import { createTestDB } from '@/test.ts';
|
||||||
import cashuApp from '@/controllers/api/cashu.ts';
|
import cashuApp from '@/controllers/api/cashu.ts';
|
||||||
import { walletSchema } from '@/schema.ts';
|
import { walletSchema } from '@/schema.ts';
|
||||||
|
|
||||||
|
function testUserMiddleware(user: User<NSecSigner>): DittoMiddleware<{ user: User<NSecSigner> }> {
|
||||||
|
return async (c, next) => {
|
||||||
|
c.set('user', user);
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Deno.test('PUT /wallet must be successful', {
|
Deno.test('PUT /wallet must be successful', {
|
||||||
sanitizeOps: false,
|
sanitizeOps: false,
|
||||||
sanitizeResources: 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()) });
|
const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) });
|
||||||
|
|
||||||
|
app.use(testUserMiddleware({ signer, relay }));
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
const response = await app.request('/wallet', {
|
const response = await app.request('/wallet', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'authorization': `Bearer ${nip19.nsecEncode(sk)}`,
|
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
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();
|
await using db = await createTestDB();
|
||||||
const relay = db.store;
|
const relay = db.store;
|
||||||
const sk = generateSecretKey();
|
const sk = generateSecretKey();
|
||||||
|
const signer = new NSecSigner(sk);
|
||||||
|
|
||||||
const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) });
|
const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) });
|
||||||
|
|
||||||
|
app.use(testUserMiddleware({ signer, relay }));
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
const response = await app.request('/wallet', {
|
const response = await app.request('/wallet', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'authorization': `Bearer ${nip19.nsecEncode(sk)}`,
|
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -123,9 +132,11 @@ Deno.test('PUT /wallet must NOT be successful: wallet already exists', {
|
||||||
await using db = await createTestDB();
|
await using db = await createTestDB();
|
||||||
const relay = db.store;
|
const relay = db.store;
|
||||||
const sk = generateSecretKey();
|
const sk = generateSecretKey();
|
||||||
|
const signer = new NSecSigner(sk);
|
||||||
|
|
||||||
const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) });
|
const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) });
|
||||||
|
|
||||||
|
app.use(testUserMiddleware({ signer, relay }));
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
await db.store.event(genEvent({ kind: 17375 }, sk));
|
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()) });
|
const app = new DittoApp({ db, relay, conf: new DittoConf(new Map()) });
|
||||||
|
|
||||||
|
app.use(testUserMiddleware({ signer, relay }));
|
||||||
app.route('/', cashuApp);
|
app.route('/', cashuApp);
|
||||||
|
|
||||||
// Wallet
|
// Wallet
|
||||||
|
|
@ -243,9 +255,6 @@ Deno.test('GET /wallet must be successful', {
|
||||||
|
|
||||||
const response = await app.request('/wallet', {
|
const response = await app.request('/wallet', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
|
||||||
'authorization': `Bearer ${nip19.nsecEncode(sk)}`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Proof } from '@cashu/cashu-ts';
|
import { Proof } from '@cashu/cashu-ts';
|
||||||
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
import { DittoMiddleware, DittoRoute } from '@ditto/router';
|
import { DittoRoute } from '@ditto/router';
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
import { bytesToString, stringToBytes } from '@scure/base';
|
import { bytesToString, stringToBytes } from '@scure/base';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
@ -11,8 +11,6 @@ import { swapNutzapsMiddleware } from '@/middleware/swapNutzapsMiddleware.ts';
|
||||||
import { isNostrId } from '@/utils.ts';
|
import { isNostrId } from '@/utils.ts';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
import { errorJson } from '@/utils/log.ts';
|
import { errorJson } from '@/utils/log.ts';
|
||||||
import { SetRequired } from 'type-fest';
|
|
||||||
import { NostrSigner } from '@nostrify/nostrify';
|
|
||||||
|
|
||||||
type Wallet = z.infer<typeof walletSchema>;
|
type Wallet = z.infer<typeof walletSchema>;
|
||||||
|
|
||||||
|
|
@ -33,19 +31,6 @@ interface Nutzap {
|
||||||
recipient_pubkey: string;
|
recipient_pubkey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requireNip44Signer: DittoMiddleware<{ user: { signer: SetRequired<NostrSigner, 'nip44'> } }> = 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({
|
const createCashuWalletAndNutzapInfoSchema = z.object({
|
||||||
mints: z.array(z.string().url()).nonempty().transform((val) => {
|
mints: z.array(z.string().url()).nonempty().transform((val) => {
|
||||||
return [...new Set(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/60.md
|
||||||
* https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-informational-event
|
* 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 { conf, user, relay, signal } = c.var;
|
||||||
|
|
||||||
const pubkey = await user.signer.getPublicKey();
|
const pubkey = await user.signer.getPublicKey();
|
||||||
|
|
@ -119,12 +104,7 @@ app.put('/wallet', userMiddleware({ privileged: false, required: true }), requir
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Gets a wallet, if it exists. */
|
/** Gets a wallet, if it exists. */
|
||||||
app.get(
|
app.get('/wallet', userMiddleware('nip44'), swapNutzapsMiddleware, async (c) => {
|
||||||
'/wallet',
|
|
||||||
userMiddleware({ privileged: false, required: true }),
|
|
||||||
requireNip44Signer,
|
|
||||||
swapNutzapsMiddleware,
|
|
||||||
async (c) => {
|
|
||||||
const { conf, relay, user, signal } = c.var;
|
const { conf, relay, user, signal } = c.var;
|
||||||
|
|
||||||
const pubkey = await user.signer.getPublicKey();
|
const pubkey = await user.signer.getPublicKey();
|
||||||
|
|
@ -174,8 +154,7 @@ app.get(
|
||||||
};
|
};
|
||||||
|
|
||||||
return c.json(walletEntity, 200);
|
return c.json(walletEntity, 200);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
/** Get mints set by the CASHU_MINTS environment variable. */
|
/** Get mints set by the CASHU_MINTS environment variable. */
|
||||||
app.get('/mints', (c) => {
|
app.get('/mints', (c) => {
|
||||||
|
|
|
||||||
6
packages/mastoapi/middleware/User.ts
Normal file
6
packages/mastoapi/middleware/User.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import type { NostrSigner, NRelay } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
export interface User<S extends NostrSigner = NostrSigner, R extends NRelay = NRelay> {
|
||||||
|
signer: S;
|
||||||
|
relay: R;
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
export { paginationMiddleware } from './paginationMiddleware.ts';
|
export { paginationMiddleware } from './paginationMiddleware.ts';
|
||||||
export { tokenMiddleware } from './tokenMiddleware.ts';
|
export { tokenMiddleware } from './tokenMiddleware.ts';
|
||||||
|
export { userMiddleware } from './userMiddleware.ts';
|
||||||
|
|
||||||
|
export type { User } from './User.ts';
|
||||||
|
|
|
||||||
|
|
@ -11,27 +11,12 @@ import { UserStore } from '../storages/UserStore.ts';
|
||||||
import type { DittoConf } from '@ditto/conf';
|
import type { DittoConf } from '@ditto/conf';
|
||||||
import type { DittoDB } from '@ditto/db';
|
import type { DittoDB } from '@ditto/db';
|
||||||
import type { DittoMiddleware } from '@ditto/router';
|
import type { DittoMiddleware } from '@ditto/router';
|
||||||
|
import type { User } from './User.ts';
|
||||||
interface User {
|
|
||||||
signer: NostrSigner;
|
|
||||||
relay: NRelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** We only accept "Bearer" type. */
|
/** We only accept "Bearer" type. */
|
||||||
const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
|
const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
|
||||||
|
|
||||||
export function tokenMiddleware(opts: { privileged: true; required: false }): never;
|
export function tokenMiddleware(): DittoMiddleware<{ user?: User }> {
|
||||||
// @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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const header = c.req.header('authorization');
|
const header = c.req.header('authorization');
|
||||||
|
|
||||||
|
|
@ -48,13 +33,6 @@ export function tokenMiddleware(opts: { privileged: boolean; required?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
c.set('user', user);
|
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();
|
await next();
|
||||||
|
|
|
||||||
27
packages/mastoapi/middleware/userMiddleware.ts
Normal file
27
packages/mastoapi/middleware/userMiddleware.ts
Normal file
|
|
@ -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<NostrSigner, 'nip44'>;
|
||||||
|
|
||||||
|
export function userMiddleware(): DittoMiddleware<{ user: User }>;
|
||||||
|
// @ts-ignore Types are right.
|
||||||
|
export function userMiddleware(enc: 'nip44'): DittoMiddleware<{ user: User<Nip44Signer> }>;
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue