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
+