From 0a5682d1e4e20220a8eb5b0644cc5ca875dd6b5f Mon Sep 17 00:00:00 2001 From: Pablo Fernandez Date: Wed, 20 Dec 2023 10:21:24 +0000 Subject: [PATCH] create_account work --- CONFIGURATION.md | 36 +++++++++ package.json | 2 +- .../migration.sql | 2 + .../20231218135715_key/migration.sql | 33 ++++++++ .../migrations/20231218140114_/migration.sql | 8 ++ prisma/schema.prisma | 9 +++ src/client.ts | 13 +--- src/commands/start.ts | 1 - src/config/index.ts | 3 +- src/daemon/admin/commands/create_account.ts | 10 ++- src/daemon/admin/index.ts | 4 +- src/daemon/authorize.ts | 11 +-- src/daemon/backend/index.ts | 2 +- src/daemon/backend/publish-event.ts | 7 +- src/daemon/lib/profile.ts | 3 +- src/daemon/run.ts | 2 +- src/daemon/web/authorize.ts | 52 +++++++++++-- src/utils/dm-user.ts | 2 +- src/utils/prompts/boolean.ts | 72 ----------------- templates/authorizeRequest.handlebar | 8 -- templates/createAccount.handlebar | 72 ++++++++--------- templates/redirect.handlebar | 77 +++++++++++++++++++ 22 files changed, 270 insertions(+), 159 deletions(-) create mode 100644 CONFIGURATION.md create mode 100644 prisma/migrations/20231218135408_request_result/migration.sql create mode 100644 prisma/migrations/20231218135715_key/migration.sql create mode 100644 prisma/migrations/20231218140114_/migration.sql delete mode 100644 src/utils/prompts/boolean.ts create mode 100644 templates/redirect.handlebar diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..a4b564e --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,36 @@ +# Configuration + +nsecbunker.json is a JSON that stores the configuration of the bunker. + +## Properties + +All properties are optional unless otherwise specified. + +`admin.adminRelays`: Relays where the bunker will listen to for admin commands, including for the ability to create new users. + +`admin.key`: Private key of the bunker. This is used only for communicating with bunker. It's automatically generated. + +`admin.npubs`: Npubs that are allowed to administrate the bunker. + +`database`: URI of the database. + +`logs`: Path where the logs will be stored. + +`verbose`: If true, the bunker will log all messages. + +`version`: Version of the bunker. This is automatically generated. + +`nostr.relays`: Relays where the bunker will listen to for NIP-46 requests. + +### OAuth-like flow properties + +`baseUrl`: URL where the bunker can be accessed for OAuth-like authentication. This should be a URL where the bunker can be widely reached. + +`authPort`: The port where the bunker will listen for OAuth-like authentication. You should setup a reverse proxy from your main server to this port. + +`domains`: Domains that are allowed to create new users from. When a `create_account` is issued the NIP-05 (nostr address) issued should use one of these domains. + +`domains.$domain.nip05`: The file pointing to the domain's NIP-05 file. + +`keys`: Keys are stored in this object. Encrypted keys are stored as `keys.$keyId.iv` + `keys.$keyId.data`. Unecrypted (recoverable) keys are stored as `keys.$keyId.key`. + diff --git a/package.json b/package.json index 6b3445e..63e4af2 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@fastify/view": "^8.2.0", "@inquirer/password": "^1.1.2", "@inquirer/prompts": "^1.2.3", - "@nostr-dev-kit/ndk": "^2.2.0", + "@nostr-dev-kit/ndk": "^2.3.0", "@prisma/client": "^5.4.1", "@scure/base": "^1.1.1", "@types/yargs": "^17.0.24", diff --git a/prisma/migrations/20231218135408_request_result/migration.sql b/prisma/migrations/20231218135408_request_result/migration.sql new file mode 100644 index 0000000..25b666a --- /dev/null +++ b/prisma/migrations/20231218135408_request_result/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Request" ADD COLUMN "result" TEXT; diff --git a/prisma/migrations/20231218135715_key/migration.sql b/prisma/migrations/20231218135715_key/migration.sql new file mode 100644 index 0000000..80db33e --- /dev/null +++ b/prisma/migrations/20231218135715_key/migration.sql @@ -0,0 +1,33 @@ +/* + Warnings: + + - You are about to drop the column `result` on the `Request` table. All the data in the column will be lost. + +*/ +-- CreateTable +CREATE TABLE "Key" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "keyName" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deletedAt" DATETIME, + "pubkey" TEXT NOT NULL +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Request" ( + "id" TEXT NOT NULL PRIMARY KEY, + "keyName" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "requestId" TEXT NOT NULL, + "remotePubkey" TEXT NOT NULL, + "method" TEXT NOT NULL, + "params" TEXT, + "allowed" BOOLEAN +); +INSERT INTO "new_Request" ("allowed", "createdAt", "id", "keyName", "method", "params", "remotePubkey", "requestId") SELECT "allowed", "createdAt", "id", "keyName", "method", "params", "remotePubkey", "requestId" FROM "Request"; +DROP TABLE "Request"; +ALTER TABLE "new_Request" RENAME TO "Request"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20231218140114_/migration.sql b/prisma/migrations/20231218140114_/migration.sql new file mode 100644 index 0000000..c9fb116 --- /dev/null +++ b/prisma/migrations/20231218140114_/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[keyName]` on the table `Key` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Key_keyName_key" ON "Key"("keyName"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1efef8c..e4d1fbd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,6 +34,15 @@ model KeyUser { @@unique([keyName, userPubkey], name: "unique_key_user") } +model Key { + id Int @id @default(autoincrement()) + keyName String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + deletedAt DateTime? + pubkey String +} + model SigningCondition { id Int @id @default(autoincrement()) method String? diff --git a/src/client.ts b/src/client.ts index f3ee823..76f908b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,14 +14,14 @@ if (!command) { console.log(`\t: command to run (ping, sign)`); console.log(`\t: npub that should be published as`); console.log(`\t: event JSON to sign (no need for pubkey or id fields) | or kind:1 content string to sign`); - console.log('\t--dont-publish: do not publish the event to the relay'); console.log('\t--debug: enable debug mode'); process.exit(1); } async function createNDK(): Promise { const ndk = new NDK({ - explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://relay.damus.io', 'wss://nos.lol'], + explicitRelayUrls: ['wss://relay.nsecbunker.com'], + enableOutboxModel: false }); if (debug) { ndk.pool.on('connect', () => console.log('✅ connected')); @@ -114,18 +114,11 @@ function loadPrivateKey(): string | undefined { try { await event.sign(); if (debug) { - console.log({ - event: event.rawEvent(), - signature: event.sig, - }); + console.log(event.rawEvent()); } else { console.log(event.sig); } - if (!dontPublish) { - await event.publish(); - } - process.exit(0); } catch(e) { console.log('sign error', e); diff --git a/src/commands/start.ts b/src/commands/start.ts index 006e4ee..7e6619f 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -76,7 +76,6 @@ async function startKey(key: string, keyData: KeyData, verbose: boolean): Promis if (verbose) { console.log(`Starting ${key}...`); - process.exit(0); } rl.close(); diff --git a/src/config/index.ts b/src/config/index.ts index 2ff72a7..0603dcc 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -28,8 +28,7 @@ const defaultConfig: IConfig = { nostr: { relays: [ 'wss://relay.damus.io', - "wss://relay.nsecbunker.com", - "wss://nos.lol" + "wss://relay.nsecbunker.com" ] }, authPort: 3000, diff --git a/src/daemon/admin/commands/create_account.ts b/src/daemon/admin/commands/create_account.ts index e87fc16..fb555a9 100644 --- a/src/daemon/admin/commands/create_account.ts +++ b/src/daemon/admin/commands/create_account.ts @@ -6,12 +6,9 @@ import { IConfig, getCurrentConfig, saveCurrentConfig } from "../../../config"; import { readFileSync, writeFileSync } from "fs"; import { allowAllRequestsFromKey } from "../../lib/acl"; import { requestAuthorization } from "../../authorize"; +import prisma from "../../../db"; export async function validate(currentConfig, email: string, username: string, domain: string) { - if (!email) { - throw new Error('email is required'); - } - if (!username) { throw new Error('username is required'); } @@ -41,6 +38,9 @@ async function getCurrentNip05File(currentConfig: any, domain: string) { async function addNip05(currentConfig: IConfig, username: string, domain: string, pubkey: Hexpubkey) { const currentNip05s = await getCurrentNip05File(currentConfig, domain); currentNip05s.names[username] = pubkey; + currentNip05s.relays ??= {}; + currentNip05s.nip46Relays ??= {}; + currentNip05s.nip46Relays[username] = currentConfig.nostr.relays; // save file const nip05File = currentConfig.domains![domain].nip05; @@ -97,6 +97,8 @@ export async function createAccountReal(admin: AdminInterface, req: NDKRpcReques await admin.loadNsec!(keyName, nsec); + await prisma.key.create({ data: { keyName, pubkey: generatedUser.pubkey } }); + // Immediately grant access to the creator key await grantPermissions(req, keyName); diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts index 25cfb73..bd5bf4e 100644 --- a/src/daemon/admin/index.ts +++ b/src/daemon/admin/index.ts @@ -45,7 +45,7 @@ class AdminInterface { constructor(opts: IAdminOpts, configFile: string) { this.configFile = configFile; - this.npubs = opts.npubs; + this.npubs = opts.npubs||[]; this.ndk = new NDK({ explicitRelayUrls: opts.adminRelays, signer: new NDKPrivateKeySigner(opts.key), @@ -83,7 +83,7 @@ class AdminInterface { }); await blastrNdk.connect(2500); - for (const npub of this.npubs) { + for (const npub of this.npubs||[]) { dmUser(blastrNdk, npub, `nsecBunker has started; use ${connectionString} to connect to it and unlock your key(s)`); } } diff --git a/src/daemon/authorize.ts b/src/daemon/authorize.ts index 9b41ac9..927ab4d 100644 --- a/src/daemon/authorize.ts +++ b/src/daemon/authorize.ts @@ -54,10 +54,9 @@ async function createRecord( let params: string | undefined; if (param?.rawEvent) { - console.log("Treating as NDKEvent", typeof param); - params = JSON.stringify(param.rawEvent()); + const e = param as NDKEvent; + params = JSON.stringify(e.rawEvent()); } else if (param) { - console.log("Treating as string", typeof param); params = param.toString(); } @@ -100,20 +99,16 @@ export function urlAuthFlow( where: { id: request.id } }); - console.log('record', record); - if (!record) { clearInterval(checkingInterval); return; } - console.log(`request ${request.id} = ${request.allowed}`); - if (record.allowed !== undefined && record.allowed !== null) { clearInterval(checkingInterval); resolve(!!record.allowed); } - }, 1000); + }, 100); } function generatePendingAuthUrl(baseUrl: string, request: Request): string { diff --git a/src/daemon/backend/index.ts b/src/daemon/backend/index.ts index e6f134b..5dd240a 100644 --- a/src/daemon/backend/index.ts +++ b/src/daemon/backend/index.ts @@ -3,7 +3,7 @@ import prisma from '../../db.js'; import type {FastifyInstance} from "fastify"; export class Backend extends NDKNip46Backend { - public baseUrl: string; + public baseUrl?: string; public fastify: FastifyInstance; constructor( diff --git a/src/daemon/backend/publish-event.ts b/src/daemon/backend/publish-event.ts index 55d22ef..3302523 100644 --- a/src/daemon/backend/publish-event.ts +++ b/src/daemon/backend/publish-event.ts @@ -2,12 +2,13 @@ import { NDKNip46Backend } from "@nostr-dev-kit/ndk"; import { IEventHandlingStrategy } from '@nostr-dev-kit/ndk'; export default class PublishEventHandlingStrategy implements IEventHandlingStrategy { - async handle(backend: NDKNip46Backend, remotePubkey: string, params: string[]): Promise { + async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]): Promise { const event = await backend.signEvent(remotePubkey, params); if (!event) return undefined; - backend.ndk.publish(event); + console.log('Publishing event', event); + await event.publish(); return JSON.stringify(await event.toNostrEvent()); } -} \ No newline at end of file +} diff --git a/src/daemon/lib/profile.ts b/src/daemon/lib/profile.ts index e24498c..54ce45b 100644 --- a/src/daemon/lib/profile.ts +++ b/src/daemon/lib/profile.ts @@ -49,8 +49,6 @@ export async function setupSkeletonProfile(key: NDKPrivateKeySigner, profile?: N } as NostrEvent); await event.sign(key); - console.log(`trying to publish profile`, event.rawEvent()); - const t = await event.publish(); console.log(t); @@ -62,6 +60,7 @@ export async function setupSkeletonProfile(key: NDKPrivateKeySigner, profile?: N pubkey: user.pubkey, } as NostrEvent); await event.sign(key); + console.log(`trying to publish profile`, event.rawEvent()); await event.publish(); const relays = new NDKEvent(ndk, { diff --git a/src/daemon/run.ts b/src/daemon/run.ts index 0b54684..bb200f1 100644 --- a/src/daemon/run.ts +++ b/src/daemon/run.ts @@ -248,7 +248,7 @@ class Daemon { this.fastify.post('/register/:id', processRegistrationWebHandler); setTimeout(async () => { - console.log('🔑 Starting keys', this.config.keys); + console.log('🔑 Starting keys', Object.keys(this.config.keys)); for (const [name, nsec] of Object.entries(this.config.keys)) { await this.startKey(name, nsec); } diff --git a/src/daemon/web/authorize.ts b/src/daemon/web/authorize.ts index d640f78..c862d1d 100644 --- a/src/daemon/web/authorize.ts +++ b/src/daemon/web/authorize.ts @@ -8,6 +8,9 @@ export async function authorizeRequestWebHandler(request, reply) { }); const reqCookies = request.cookies; + const url = new URL(request.url, `http://${request.headers.host}`); + const callbackUrl = url.searchParams.get("callbackUrl"); + const method = record.method; let email: string | undefined; let username: string | undefined; @@ -22,15 +25,14 @@ export async function authorizeRequestWebHandler(request, reply) { domain = payload.domain; nip05 = `${username}@${domain}`; - return reply.view("/templates/createAccount.handlebar", { record, email, username, domain, nip05 }); + return reply.view("/templates/createAccount.handlebar", { record, email, username, domain, nip05, callbackUrl }); } else { - return reply.view("/templates/authorizeRequest.handlebar", { record, email, username, domain, nip05 }); + return reply.view("/templates/authorizeRequest.handlebar", { record, email, username, domain, nip05, callbackUrl }); } // return record; } export async function processRequestWebHandler(request, reply) { - console.log(request); const record = await prisma.request.findUnique({ where: { id: request.params.id } }); @@ -74,7 +76,7 @@ export async function processRegistrationWebHandler(request, reply) { }); if (!record || record.allowed) { - return; + return { ok: false, error: "Request not found or already processed" }; } await prisma.request.update({ @@ -82,9 +84,38 @@ export async function processRegistrationWebHandler(request, reply) { data: { allowed: true } }); - const body = request.body; + let createdPubkey: string | undefined; - console.log({body}); + // here I need to wait for the account + createdPubkey = await new Promise((resolve) => { + const interval = setInterval(async () => { + const keyName = record.keyName; + const keyRecord = await prisma.key.findUnique({ where: { keyName } }); + + if (keyRecord) { + console.log(keyRecord); + clearInterval(interval); + resolve(keyRecord.pubkey); + } + }, 100); + }); + + const body = request.body; + const callbackUrlString = body.callbackUrl; + let callbackUrl: string | undefined; + + if (callbackUrlString) { + const u = new URL(callbackUrlString); + + if (createdPubkey) { + u.searchParams.append("pubkey", createdPubkey); + callbackUrl = u.toString(); + } + } + + // const url = new URL(callbackUrl); + + // add to url a query param with the user's pubkey await allowAllRequestsFromKey( record.remotePubkey, @@ -94,5 +125,12 @@ export async function processRegistrationWebHandler(request, reply) { undefined, ); - return { ok: true }; + // redirect to login page + if (callbackUrl) { + return reply + .view("/templates/redirect.handlebar", { callbackUrl }) + .redirect(callbackUrl); + } + + return reply.view("/templates/redirect.handlebar", { callbackUrl }); } \ No newline at end of file diff --git a/src/utils/dm-user.ts b/src/utils/dm-user.ts index 1769c98..0f922e4 100644 --- a/src/utils/dm-user.ts +++ b/src/utils/dm-user.ts @@ -14,7 +14,7 @@ export async function dmUser(ndk: NDK, recipient: NDKUser | string, content: str await event.encrypt(targetUser); await event.sign(); try { - event.publish(); + await event.publish(); } catch (e) { console.log(e); } diff --git a/src/utils/prompts/boolean.ts b/src/utils/prompts/boolean.ts deleted file mode 100644 index 855fc5e..0000000 --- a/src/utils/prompts/boolean.ts +++ /dev/null @@ -1,72 +0,0 @@ -import readline from 'readline'; - -export interface IAskYNquestionOpts { - timeoutLength?: number; - yes: any; - no: any; - always?: any; - never?: any; - response?: any; - timeout?: any; -} - -export async function askYNquestion( - question: string, - opts: IAskYNquestionOpts -) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - let timeout: NodeJS.Timeout | undefined; - - if (opts.timeoutLength) { - timeout = setTimeout(() => { - rl.close(); - opts.timeout && opts.timeout(); - }, opts.timeoutLength); - } - - const prompts = ['y', 'n']; - - if (opts.always) prompts.push('always'); - if (opts.never) prompts.push('never'); - - question += ` (${prompts.join('/')}) `; - - rl.question(question, (answer) => { - timeout && clearTimeout(timeout); - - switch (answer) { - case 'y': - case 'Y': - opts.yes(); - opts.response && opts.response(answer); - break; - case 'n': - case 'N': - opts.no(); - opts.response && opts.response(answer); - break; - case 'always': - case 'a': - opts.yes(); - opts.always(); - opts.response && opts.response(answer); - break; - case 'never': - opts.no(); - opts.never(); - opts.response && opts.response(answer); - break; - default: - console.log('Invalid answer'); - askYNquestion(question, opts); - break; - } - - rl.close(); - }); - - return rl; -} \ No newline at end of file diff --git a/templates/authorizeRequest.handlebar b/templates/authorizeRequest.handlebar index 949a527..367b602 100644 --- a/templates/authorizeRequest.handlebar +++ b/templates/authorizeRequest.handlebar @@ -56,14 +56,6 @@
- - - - - - - -

Do you want to allow this client to use account
diff --git a/templates/createAccount.handlebar b/templates/createAccount.handlebar index e20efef..653966c 100644 --- a/templates/createAccount.handlebar +++ b/templates/createAccount.handlebar @@ -29,42 +29,42 @@ - -
- - - - - - - - + +
+
+

+ + Welcome to Nostr! +

+
+ +
+ + + + + + + + + +
- -
- - - - - - - -
\ No newline at end of file diff --git a/templates/redirect.handlebar b/templates/redirect.handlebar new file mode 100644 index 0000000..acc98a8 --- /dev/null +++ b/templates/redirect.handlebar @@ -0,0 +1,77 @@ + + + + + + Document + + + + + + + + + +
+
+

+ + Welcome to Nostr! +

+
+ +
+ {{#if callbackUrl}} +
+

+ You are being redirected to {{callbackUrl}} +

+
+ +
+

+ If you are not redirected automatically, follow this link +

+
+ {{else}} +
+

+ You can close this window now. +

+
+ + + {{/if}} +
+
+ + + + \ No newline at end of file