diff --git a/packages/db/adapters/DummyDB.test.ts b/packages/db/adapters/DummyDB.test.ts new file mode 100644 index 00000000..c725ab51 --- /dev/null +++ b/packages/db/adapters/DummyDB.test.ts @@ -0,0 +1,9 @@ +import { assertEquals } from '@std/assert'; +import { DummyDB } from './DummyDB.ts'; + +Deno.test('DummyDB', async () => { + const db = DummyDB.create(); + const rows = await db.kysely.selectFrom('nostr_events').selectAll().execute(); + + assertEquals(rows, []); +}); diff --git a/packages/db/adapters/DummyDB.ts b/packages/db/adapters/DummyDB.ts new file mode 100644 index 00000000..51c29b10 --- /dev/null +++ b/packages/db/adapters/DummyDB.ts @@ -0,0 +1,29 @@ +import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely'; + +import type { DittoDB } from '../DittoDB.ts'; +import type { DittoTables } from '../DittoTables.ts'; + +export class DummyDB implements DittoDB { + readonly kysely: Kysely; + readonly poolSize = 0; + readonly availableConnections = 0; + + constructor() { + this.kysely = new Kysely({ + dialect: { + createAdapter: () => new PostgresAdapter(), + createDriver: () => new DummyDriver(), + createIntrospector: (db) => new PostgresIntrospector(db), + createQueryCompiler: () => new PostgresQueryCompiler(), + }, + }); + } + + listen(): void { + // noop + } + + [Symbol.asyncDispose](): Promise { + return Promise.resolve(); + } +} diff --git a/packages/db/mod.ts b/packages/db/mod.ts index 49100cd6..2766e524 100644 --- a/packages/db/mod.ts +++ b/packages/db/mod.ts @@ -1,4 +1,7 @@ +export { DittoPglite } from './adapters/DittoPglite.ts'; export { DittoPolyPg } from './adapters/DittoPolyPg.ts'; +export { DittoPostgres } from './adapters/DittoPostgres.ts'; +export { DummyDB } from './adapters/DummyDB.ts'; export type { DittoDB } from './DittoDB.ts'; export type { DittoTables } from './DittoTables.ts'; diff --git a/packages/mastoapi/middleware/userMiddleware.test.ts b/packages/mastoapi/middleware/userMiddleware.test.ts new file mode 100644 index 00000000..a72a5677 --- /dev/null +++ b/packages/mastoapi/middleware/userMiddleware.test.ts @@ -0,0 +1,99 @@ +import { DittoConf } from '@ditto/conf'; +import { DummyDB } from '@ditto/db'; +import { DittoApp, type DittoMiddleware } from '@ditto/router'; +import { type NostrSigner, NSecSigner } from '@nostrify/nostrify'; +import { MockRelay } from '@nostrify/nostrify/test'; +import { assertEquals } from '@std/assert'; +import { generateSecretKey, nip19 } from 'nostr-tools'; + +import { userMiddleware } from './userMiddleware.ts'; +import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts'; + +import type { User } from './User.ts'; + +Deno.test('no user 401', async () => { + const { app } = testApp(); + const response = await app.use(userMiddleware()).request('/'); + assertEquals(response.status, 401); +}); + +Deno.test('unsupported signer 400', async () => { + const { app, relay } = testApp(); + const signer = new ReadOnlySigner('0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd'); + + const response = await app + .use(setUser({ signer, relay })) + .use(userMiddleware({ enc: 'nip44' })) + .use((c, next) => { + c.var.user.signer.nip44.encrypt; // test that the type is set + return next(); + }) + .request('/'); + + assertEquals(response.status, 400); +}); + +Deno.test('with user 200', async () => { + const { app, user } = testApp(); + + const response = await app + .use(setUser(user)) + .use(userMiddleware()) + .get('/', (c) => c.text('ok')) + .request('/'); + + assertEquals(response.status, 200); +}); + +Deno.test('user and role 403', async () => { + const { app, user } = testApp(); + + const response = await app + .use(setUser(user)) + .use(userMiddleware({ role: 'admin' })) + .request('/'); + + assertEquals(response.status, 403); +}); + +Deno.test('admin role 200', async () => { + const { conf, app, user, relay } = testApp(); + + const event = await conf.signer.signEvent({ + kind: 30382, + tags: [ + ['d', await user.signer.getPublicKey()], + ['n', 'admin'], + ], + content: '', + created_at: Math.floor(Date.now() / 1000), + }); + + await relay.event(event); + + const response = await app + .use(setUser(user)) + .use(userMiddleware({ role: 'admin' })) + .get('/', (c) => c.text('ok')) + .request('/'); + + assertEquals(response.status, 200); +}); + +function testApp() { + const relay = new MockRelay(); + const signer = new NSecSigner(generateSecretKey()); + const conf = new DittoConf(new Map([['DITTO_NSEC', nip19.nsecEncode(generateSecretKey())]])); + const db = new DummyDB(); + const app = new DittoApp({ conf, relay, db }); + const user = { signer, relay }; + + return { app, relay, conf, db, user }; +} + +function setUser(user: User): DittoMiddleware<{ user: User }> { + return async (c, next) => { + c.set('user', user); + await next(); + }; +} diff --git a/packages/mastoapi/middleware/userMiddleware.ts b/packages/mastoapi/middleware/userMiddleware.ts index 1afef59a..8308172d 100644 --- a/packages/mastoapi/middleware/userMiddleware.ts +++ b/packages/mastoapi/middleware/userMiddleware.ts @@ -26,11 +26,11 @@ export function userMiddleware(opts: UserMiddlewareOpts = {}): DittoMiddleware<{ const { enc, role, verify } = opts; if (!user) { - throw new HTTPException(403, { message: 'Authorization required.' }); + throw new HTTPException(401, { message: 'Authorization required' }); } if (enc && !user.signer[enc]) { - throw new HTTPException(403, { message: `User does not have a ${enc} signer` }); + throw new HTTPException(400, { message: `User does not have a ${enc} signer` }); } if (role || verify) { @@ -40,7 +40,7 @@ export function userMiddleware(opts: UserMiddlewareOpts = {}): DittoMiddleware<{ const result = await validateAuthEvent(req, resEvent); if (!result.success) { - throw new HTTPException(403, { message: 'Verification failed.' }); + throw new HTTPException(401, { message: 'Verification failed' }); } // Prevent people from accidentally using the wrong account. This has no other security implications. @@ -57,7 +57,7 @@ export function userMiddleware(opts: UserMiddlewareOpts = {}): DittoMiddleware<{ }]); if (!user || !matchesRole(user, role)) { - throw new HTTPException(403, { message: `Must have ${role} role.` }); + throw new HTTPException(403, { message: `Must have ${role} role` }); } } }