From 67c5252983afd4a7b382fcbedb3116526d904e66 Mon Sep 17 00:00:00 2001 From: Pablo Fernandez Date: Tue, 2 Jan 2024 11:11:06 +0000 Subject: [PATCH] better error handling --- package.json | 6 +- pnpm-lock.yaml | 322 +++++++++++++++++++- src/config/index.ts | 14 +- src/daemon/admin/commands/account/wallet.ts | 89 ++++++ src/daemon/admin/commands/create_account.ts | 82 ++++- src/daemon/admin/index.ts | 10 +- src/daemon/authorize.ts | 8 +- src/daemon/lib/profile.ts | 21 +- src/daemon/run.ts | 1 + src/daemon/web/authorize.ts | 317 +++++++++++++------ src/daemon/web/registration-validations.ts | 25 ++ templates/authorizeRequest.handlebar | 55 +++- templates/createAccount.handlebar | 43 ++- templates/error.handlebar | 33 ++ templates/login.handlebar | 49 +++ templates/redirect.handlebar | 1 + 16 files changed, 941 insertions(+), 135 deletions(-) create mode 100644 src/daemon/admin/commands/account/wallet.ts create mode 100644 src/daemon/web/registration-validations.ts create mode 100644 templates/error.handlebar create mode 100644 templates/login.handlebar diff --git a/package.json b/package.json index 39c7540..ce3c750 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nsecbunkerd", - "version": "0.9.0", + "version": "0.10.0", "description": "nsecbunker daemon", "main": "dist/index.js", "bin": { @@ -43,6 +43,8 @@ "@prisma/client": "^5.4.1", "@scure/base": "^1.1.1", "@types/yargs": "^17.0.24", + "axios": "^1.6.2", + "bcrypt": "^5.1.1", "crypto-js": "^4.2.0", "debug": "^4.3.4", "dotenv": "^16.3.1", @@ -54,6 +56,8 @@ "gravatar": "^1.8.2", "handlebars": "^4.7.8", "isomorphic-ws": "^5.0.0", + "lnbits": "^1.1.5", + "lnbits-ts": "^0.0.2", "nostr-tools": "^1.17.0", "websocket-polyfill": "^0.0.3", "ws": "^8.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 257a7ca..7de7ee7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,12 @@ dependencies: '@types/yargs': specifier: ^17.0.24 version: 17.0.24 + axios: + specifier: ^1.6.2 + version: 1.6.2(debug@4.3.4) + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 crypto-js: specifier: ^4.2.0 version: 4.2.0 @@ -62,6 +68,12 @@ dependencies: isomorphic-ws: specifier: ^5.0.0 version: 5.0.0(ws@8.13.0) + lnbits: + specifier: ^1.1.5 + version: 1.1.5(debug@4.3.4) + lnbits-ts: + specifier: ^0.0.2 + version: 0.0.2(debug@4.3.4) nostr-tools: specifier: ^1.17.0 version: 1.17.0(typescript@5.1.3) @@ -562,6 +574,24 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@mapbox/node-pre-gyp@1.0.11: + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + dependencies: + detect-libc: 2.0.2 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.5.4 + tar: 6.2.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@noble/ciphers@0.2.0: resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==} dev: false @@ -717,6 +747,10 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -761,6 +795,15 @@ packages: hasBin: true dev: true + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -821,10 +864,22 @@ packages: picomatch: 2.3.1 dev: true + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + dev: false + /archy@1.0.0: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} dev: false + /are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true @@ -880,6 +935,10 @@ packages: es-shim-unscopables: 1.0.0 dev: false + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -900,6 +959,24 @@ packages: - supports-color dev: false + /axios@0.21.4(debug@4.3.4): + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + dependencies: + follow-redirects: 1.15.3(debug@4.3.4) + transitivePeerDependencies: + - debug + dev: false + + /axios@1.6.2(debug@4.3.4): + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} + dependencies: + follow-redirects: 1.15.3(debug@4.3.4) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -907,6 +984,18 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false + /bcrypt@5.1.1: + resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -1028,6 +1117,11 @@ packages: fsevents: 2.3.3 dev: true + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + /cli-spinners@2.9.0: resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==} engines: {node: '>=6'} @@ -1066,6 +1160,18 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: false + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1074,6 +1180,10 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: false + /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -1173,6 +1283,15 @@ packages: object-keys: 1.1.1 dev: false + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1183,6 +1302,11 @@ packages: engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dev: false + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: false + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -1802,12 +1926,33 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: false + /follow-redirects@1.15.3(debug@4.3.4): + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.4 + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 dev: false + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1825,6 +1970,13 @@ packages: engines: {node: '>= 0.6'} dev: false + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1854,6 +2006,21 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: false + /gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2009,6 +2176,10 @@ packages: has-symbols: 1.0.3 dev: false + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: false + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -2031,6 +2202,16 @@ packages: toidentifier: 1.0.1 dev: false + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2320,6 +2501,23 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /lnbits-ts@0.0.2(debug@4.3.4): + resolution: {integrity: sha512-3OnkL/IILpPQ0SUkN1kBdAO1dBmV+QqQNIh1GSz3koBqwjnFM8+RoZDKMsiq8WWHRaVFH49OA+a2ODgrC9sH/Q==} + dependencies: + axios: 1.6.2(debug@4.3.4) + transitivePeerDependencies: + - debug + dev: false + + /lnbits@1.1.5(debug@4.3.4): + resolution: {integrity: sha512-RPCBNsKKxlyQTHPKdU66iiXFBz6SuISVVkxJoSZY3Z+CBEzOu6xpgzZtQcZTbc1BCLqQc6HeK4qtfByKWjBTmg==} + dependencies: + axios: 0.21.4(debug@4.3.4) + typescript: 4.9.5 + transitivePeerDependencies: + - debug + dev: false + /load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2354,6 +2552,13 @@ packages: yallist: 4.0.0 dev: false + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + dev: false + /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true @@ -2421,6 +2626,32 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: false + + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: false @@ -2462,11 +2693,27 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: false + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} dev: false + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2481,6 +2728,14 @@ packages: hasBin: true dev: false + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2510,10 +2765,18 @@ packages: path-key: 3.1.1 dev: true + /npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: true /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} @@ -2740,6 +3003,10 @@ packages: ipaddr.js: 1.9.1 dev: false + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -2785,6 +3052,15 @@ packages: unpipe: 1.0.0 dev: false + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + /readable-stream@4.4.2: resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2997,7 +3273,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -3120,6 +3395,18 @@ packages: engines: {node: '>= 0.4'} dev: false + /tar@6.2.0: + resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: false @@ -3167,6 +3454,10 @@ packages: engines: {node: '>=0.6'} dev: false + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: @@ -3317,6 +3608,12 @@ packages: resolution: {integrity: sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==} dev: false + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + /typescript@5.1.3: resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} engines: {node: '>=14.17'} @@ -3363,6 +3660,10 @@ packages: engines: {node: '>=8'} dev: false + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -3382,6 +3683,10 @@ packages: engines: {node: '>= 8'} dev: false + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true @@ -3409,6 +3714,13 @@ packages: - supports-color dev: false + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -3450,6 +3762,12 @@ packages: dependencies: isexe: 2.0.0 + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: false diff --git a/src/config/index.ts b/src/config/index.ts index 6711a70..a3ee162 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,13 +1,25 @@ import { readFileSync, writeFileSync } from 'fs'; -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; +import { NDKPrivateKeySigner, NDKUserProfile } from '@nostr-dev-kit/ndk'; import { IAdminOpts } from '../daemon/admin'; import { version } from '../../package.json'; const generatedKey = NDKPrivateKeySigner.generate(); +export type LNBitsWalletConfig = { + url: string, + key: string, + nostdressUrl: string, +} + +export interface IWalletConfig { + lnbits?: LNBitsWalletConfig; +} + export interface DomainConfig { nip05: string; + wallet?: IWalletConfig; + defaultProfile?: Record; }; export interface IConfig { diff --git a/src/daemon/admin/commands/account/wallet.ts b/src/daemon/admin/commands/account/wallet.ts new file mode 100644 index 0000000..35d73aa --- /dev/null +++ b/src/daemon/admin/commands/account/wallet.ts @@ -0,0 +1,89 @@ +import axios from "axios"; +import createDebug from "debug"; +import { IWalletConfig, LNBitsWalletConfig } from "../../../../config"; + +const debug = createDebug("nsecbunker:wallet"); + +export async function generateWallet( + walletConfig: IWalletConfig, + username: string, + domain: string, + npub: string +) { + debug("generateWallet", walletConfig, username, domain, npub); + if (walletConfig.lnbits) { + return generateLNBitsWallet(walletConfig.lnbits, username, domain, npub); + } +} + +export async function generateLNBitsWallet( + lnbitsConfig: LNBitsWalletConfig, + username: string, + domain: string, + npub: string +) { + debug("generateLNBitsWallet", lnbitsConfig, username, domain, npub); + + const url = new URL(lnbitsConfig.url); + url.pathname = '/usermanager/api/v1/users'; + + const res = await axios.post(url.toString(), { + user_name: username, + wallet_name: `${username}@${domain}`, + }, { + headers: { + "X-Api-Key": lnbitsConfig.key, + }, + }); + + const user = res.data; + const wallet = user.wallets[0]; + + debug("lnbits response: ", {status: res.status, data: res.data}); + + return await generateLNAddress( + username, + domain, + wallet.inkey, + npub, + 'lnbits', + lnbitsConfig.url, + lnbitsConfig.nostdressUrl, + ); +} + +export async function generateLNAddress( + username: string, + domain: string, + userInvoiceKey: string, + userNpub: string, + kind: string, + host: string, + nostdressUrl: string +) { + debug("generateLNAddress", username, domain, userInvoiceKey, userNpub, kind, host, nostdressUrl); + const formData = new URLSearchParams(); + formData.append('name', username); + formData.append('domain', domain); + formData.append('kind', kind); + formData.append('host', host); + formData.append('key', userInvoiceKey); + formData.append('pin', ' '); + formData.append('npub', userNpub); + formData.append('currentName', ' '); + + const url = new URL(nostdressUrl); + url.pathname = '/api/easy/'; + + debug("nostdress urL: ", url.toString()); + + const res = await axios.post(url.toString(), formData, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + + debug("nostdress response: ", res.data); + + return `${username}@${domain}`; +} \ No newline at end of file diff --git a/src/daemon/admin/commands/create_account.ts b/src/daemon/admin/commands/create_account.ts index b242369..0165c25 100644 --- a/src/daemon/admin/commands/create_account.ts +++ b/src/daemon/admin/commands/create_account.ts @@ -6,7 +6,11 @@ import { IConfig, getCurrentConfig, saveCurrentConfig } from "../../../config"; import { readFileSync, writeFileSync } from "fs"; import { allowAllRequestsFromKey } from "../../lib/acl"; import { requestAuthorization } from "../../authorize"; +import { generateWallet } from "./account/wallet"; import prisma from "../../../db"; +import createDebug from "debug"; + +const debug = createDebug("nsecbunker:createAccount"); export async function validate(currentConfig, username: string, domain: string, email?: string) { if (!username) { @@ -26,10 +30,19 @@ export async function validate(currentConfig, username: string, domain: string, } } +const emptyNip05File = { + names: {}, + relays: {}, +} + async function getCurrentNip05File(currentConfig: any, domain: string) { - const nip05File = currentConfig.domains[domain].nip05; - const file = readFileSync(nip05File, 'utf8'); - return JSON.parse(file); + try { + const nip05File = currentConfig.domains[domain].nip05; + const file = readFileSync(nip05File, 'utf8'); + return JSON.parse(file); + } catch (e: any) { + return emptyNip05File; + } } /** @@ -47,12 +60,26 @@ async function addNip05(currentConfig: IConfig, username: string, domain: string writeFileSync(nip05File, JSON.stringify(currentNip05s, null, 2)); } +/** + * Reserved usernames that cannot be used since someone might + * confuse them with some type of authority of this domain + * and scammers are scoundrels + */ +const RESERVED_USERNAMES = [ + "admin", "root", "_", "administrator", "__" +]; + async function validateUsername(username: string | undefined, domain: string, admin: AdminInterface, req: NDKRpcRequest) { if (!username || username.length === 0) { // create a random username of 10 characters username = Math.random().toString(36).substring(2, 15); } + // check if the username is available + if (RESERVED_USERNAMES.includes(username)) { + throw new Error('username not available'); + } + return username; } @@ -87,8 +114,7 @@ export default async function createAccount(admin: AdminInterface, req: NDKRpcRe const payload: string[] = [ username, domain ]; if (email) payload.push(email); - console.log('requesting authorization', payload); - + debug(`Requesting authorization for ${nip05}`); const authorizationWithPayload = await requestAuthorization( admin, nip05, @@ -97,7 +123,7 @@ export default async function createAccount(admin: AdminInterface, req: NDKRpcRe req.method, JSON.stringify(payload) ); - console.log('authorizationWithPayload', authorizationWithPayload); + debug(`Authorization for ${nip05} ${authorizationWithPayload ? 'granted' : 'denied'}`); if (authorizationWithPayload) { const payload = JSON.parse(authorizationWithPayload); @@ -108,6 +134,9 @@ export default async function createAccount(admin: AdminInterface, req: NDKRpcRe } } +/** + * This is where the real work of creating the private key, wallet, nip-05, granting access, etc happen + */ export async function createAccountReal( admin: AdminInterface, req: NDKRpcRequest, @@ -115,13 +144,15 @@ export async function createAccountReal( domain: string, email?: string ) { - // Fetch record since the authorization backend might have changed it - - - console.log('creating account'); try { const currentConfig = await getCurrentConfig(admin.configFile); + if (!currentConfig.domains) { + throw new Error('no domains configured'); + } + + const domainConfig = currentConfig.domains[domain]; + await validate(currentConfig, username, domain, email); const nip05 = `${username}@${domain}`; @@ -129,14 +160,39 @@ export async function createAccountReal( const profile: NDKUserProfile = { display_name: username, name: username, - nip05 + nip05, + ...(domainConfig.defaultProfile || {}) }; - setupSkeletonProfile(key, profile, email); const generatedUser = await key.user(); + debug(`Created user ${generatedUser.npub} for ${nip05}`); + + // Add NIP-05 await addNip05(currentConfig, username, domain, generatedUser.pubkey); + debug(`Added NIP-05 for ${nip05}`); + + // Create wallet + if (domainConfig.wallet) { + generateWallet( + domainConfig.wallet, + username, domain, generatedUser.npub + ).then((lnaddress) => { + debug(`wallet for ${nip05}`, {lnaddress}); + if (lnaddress) profile.lud16 = lnaddress; + }).catch((e) => { + debug(`error generating wallet for ${nip05}`, e); + }).finally(() => { + debug(`saving profile for ${nip05}`, profile); + setupSkeletonProfile(key, profile, email); + }) + } else { + debug(`no wallet configuration for ${domain}`); + // Create user profile + setupSkeletonProfile(key, profile, email); + } + const keyName = nip05; const nsec = nip19.nsecEncode(key.privateKey!); currentConfig.keys[keyName] = { key: key.privateKey }; @@ -148,6 +204,8 @@ export async function createAccountReal( await prisma.key.create({ data: { keyName, pubkey: generatedUser.pubkey } }); // Immediately grant access to the creator key + // This means that the client creating this account can immediately + // access it without having to go through an approval flow await grantPermissions(req, keyName); return admin.rpc.sendResponse(req.id, req.pubkey, generatedUser.pubkey, NDKKind.NostrConnectAdmin); diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts index 35b2bfa..7902005 100644 --- a/src/daemon/admin/index.ts +++ b/src/daemon/admin/index.ts @@ -1,7 +1,7 @@ import "websocket-polyfill"; -import NDK, { NDKEvent, NDKKind, NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk'; +import NDK, { NDKKind, NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser } from '@nostr-dev-kit/ndk'; import { NDKNostrRpc } from '@nostr-dev-kit/ndk'; -import { debug } from 'debug'; +import createDebug from 'debug'; import { Key, KeyUser } from '../run'; import { allowAllRequestsFromKey } from '../lib/acl/index.js'; import prisma from '../../db'; @@ -18,6 +18,8 @@ import { validateRequestFromAdmin } from './validations/request-from-admin'; import { dmUser } from '../../utils/dm-user'; import { IConfig, getCurrentConfig } from "../../config"; +const debug = createDebug("nsecbunker:admin"); + export type IAdminOpts = { npubs: string[]; adminRelays: string[]; @@ -74,7 +76,7 @@ class AdminInterface { }); }); - this.rpc = new NDKNostrRpc(this.ndk, this.ndk.signer!, debug("ndk:rpc")); + this.rpc = new NDKNostrRpc(this.ndk, this.ndk.signer!, debug); } public async config(): Promise { @@ -149,7 +151,7 @@ class AdminInterface { ); } } catch (err: any) { - console.error(`Error handling request ${req.method}: ${err?.message??err}`, req.params); + debug(`Error handling request ${req.method}: ${err?.message??err}`, req.params); return this.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin, err?.message); } } diff --git a/src/daemon/authorize.ts b/src/daemon/authorize.ts index 58ff39d..cb9721b 100644 --- a/src/daemon/authorize.ts +++ b/src/daemon/authorize.ts @@ -32,8 +32,9 @@ export async function requestAuthorization( if (baseUrl) { // If we have a URL, request authorization through web urlAuthFlow(baseUrl, admin, remotePubkey, requestId, request, resolve, reject); + } else { + adminAuthFlow(admin, keyName, remotePubkey, method, param, resolve, reject); } - adminAuthFlow(admin, keyName, remotePubkey, method, param, resolve, reject); }); } @@ -41,8 +42,10 @@ async function adminAuthFlow(adminInterface, keyName, remotePubkey, method, para const requestedPerm = await adminInterface.requestPermission(keyName, remotePubkey, method, param); if (requestedPerm) { + console.log('resolve adminAuthFlow', !!requestedPerm); resolve(); } else { + console.log('reject adminAuthFlow', !!requestedPerm); reject(); } } @@ -54,8 +57,6 @@ async function createRecord( method: string, param?: string | NDKEvent, ) { - console.trace('createRecord', { keyName, requestId, remotePubkey, method, param}); - let params: string | undefined; if (param?.rawEvent) { @@ -114,6 +115,7 @@ export function urlAuthFlow( if (record.allowed === false) { reject(record.payload); } + console.log('resolve urlAuthFlow', !!record.params); resolve(record.params); } }, 100); diff --git a/src/daemon/lib/profile.ts b/src/daemon/lib/profile.ts index 54ce45b..f433452 100644 --- a/src/daemon/lib/profile.ts +++ b/src/daemon/lib/profile.ts @@ -1,17 +1,23 @@ import NDK, { NDKEvent, NDKPrivateKeySigner, NostrEvent, type NDKUserProfile } from "@nostr-dev-kit/ndk"; import * as CryptoJS from 'crypto-js'; +import createDebug from "debug"; + +const debug = createDebug("nsecbunker:profile"); const explicitRelayUrls = [ 'wss://purplepag.es', 'wss://relay.damus.io', 'wss://relay.nostr.band', 'wss://nos.lol', + "wss://nostr.mutinywallet.com" ]; /** * Setup a skeleton profile for a new key since * the experience of a completely empty profile - * is pretty bad when logging in with Coracle + * is pretty bad when logging in with Coracle. + * + * @param email - if provided, will fetch the gravatar */ export async function setupSkeletonProfile(key: NDKPrivateKeySigner, profile?: NDKUserProfile, email?: string) { const rand = Math.random().toString(36).substring(7); @@ -27,9 +33,9 @@ export async function setupSkeletonProfile(key: NDKPrivateKeySigner, profile?: N const hash = CryptoJS.MD5(trimmedEmail); const shash = hash.toString(CryptoJS.enc.Hex); profile.image = `https://robohash.org/${shash}?gravatar=hashed&set=set5`; - console.log('fetching gravatar', profile.image); + debug('fetching gravatar', profile.image); } catch (e) { - console.log('error fetching gravatar', e); + debug('error fetching gravatar', e); } } @@ -50,17 +56,18 @@ export async function setupSkeletonProfile(key: NDKPrivateKeySigner, profile?: N await event.sign(key); const t = await event.publish(); - console.log(t); + debug(`Published to ${t.size} relays`); event = new NDKEvent(ndk, { kind: 3, tags: [ ['p', 'fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52'], + ['p', user.pubkey], ], pubkey: user.pubkey, } as NostrEvent); await event.sign(key); - console.log(`trying to publish profile`, event.rawEvent()); + debug(`follow list event`, event.rawEvent()); await event.publish(); const relays = new NDKEvent(ndk, { @@ -69,7 +76,9 @@ export async function setupSkeletonProfile(key: NDKPrivateKeySigner, profile?: N ['r', 'wss://purplepag.es'], ['r', 'wss://relay.f7z.io'], ['r', 'wss://relay.damus.io'], - ['r', 'wss://nos.lol'], + ['r', 'wss://relayable.org'], + ['r', 'wss://relay.nostr.band'], + ['r', 'wss://relay.primal.net'], ], pubkey: user.pubkey, } as NostrEvent); diff --git a/src/daemon/run.ts b/src/daemon/run.ts index c355a03..19f3e65 100644 --- a/src/daemon/run.ts +++ b/src/daemon/run.ts @@ -190,6 +190,7 @@ class Daemon { async startKeys() { console.log('🔑 Starting keys', Object.keys(this.config.keys)); for (const [name, nsec] of Object.entries(this.config.keys)) { + console.log(`🔑 Starting ${name}...`); await this.startKey(name, nsec); } diff --git a/src/daemon/web/authorize.ts b/src/daemon/web/authorize.ts index 79cc9e7..cc2639f 100644 --- a/src/daemon/web/authorize.ts +++ b/src/daemon/web/authorize.ts @@ -1,32 +1,107 @@ import prisma from "../../db"; -import type { Request } from "@prisma/client"; +import bcrypt from "bcrypt"; import { IAllowScope, allowAllRequestsFromKey } from "../lib/acl"; +import createDebug from "debug"; +import { validateRegistration } from "./registration-validations"; -export async function authorizeRequestWebHandler(request, reply) { +const debug = createDebug("nsecbunker:authorize"); + +/** + * TODO: This is still nto being used as no JWT is ever created + */ +async function validateAuthCookie(request) { + const cookies = request.cookies || {}; + const jwt = cookies.jwt; + + if (!jwt) { + return false; + } + + const user = await prisma.user.findUnique({ + where: { pubkey: jwt } + }); + + if (!user) { + return false; + } + + return true; +} + +async function getAndValidateStateOfRequest(request) { const record = await prisma.request.findUnique({ where: { id: request.params.id } }); - 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; - let domain: string | undefined; - let nip05: string | undefined; - - if (method === "create_account") { - const payload = JSON.parse(record.params); - const [ username, domain, email ] = payload; - nip05 = `${username}@${domain}`; - - return reply.view("/templates/createAccount.handlebar", { record, email, username, domain, nip05, callbackUrl }); - } else { - return reply.view("/templates/authorizeRequest.handlebar", { record, email, username, domain, nip05, callbackUrl }); + if (!record || record.allowed !== null) { + throw new Error("Request not found or already processed"); + } + + return record; +} + + +/** + * Generates the view to authorize a request + */ +export async function authorizeRequestWebHandler(request, reply) { + try { + const record = await getAndValidateStateOfRequest(request); + const url = new URL(request.url, `http://${request.headers.host}`); + const callbackUrl = url.searchParams.get("callbackUrl"); + + const method = record.method; + let nip05: string | undefined; + + if (method === "create_account") { + const [ username, domain, email ] = JSON.parse(record.params!); + nip05 = `${username}@${domain}`; + + return reply.view("/templates/createAccount.handlebar", { record, email, username, domain, nip05, callbackUrl }); + } else { + const authorized = validateAuthCookie(request); + return reply.view("/templates/authorizeRequest.handlebar", { record, callbackUrl, authorized }); + } + } catch (error: any) { + debug(`Error processing request`, error, request); + return reply.view("/templates/error.handlebar", { error: error.message }); + } +} + +/** + * Validates + authenticates a request POSTed to the authorize endpoint + */ +export async function validateRequest(request, record) { + if (await validateAuthCookie(request)) { + debug("Already authenticated"); + return true; + } + + const keyName = record.keyName; + const [username, domain] = keyName.split("@"); + + if (!username || !domain) { + throw new Error("Invalid keyName"); + } + + const password = request.body.password; + + const userRecord = await prisma.user.findUnique({ + where: { username, domain } + }); + + if (!userRecord) { + debug("No user record found"); + throw new Error("No user record found"); + } + + const hashedPassword = userRecord.password; + const match = await bcrypt.compare(password, hashedPassword); + + if (!match) { + debug("Provided password didn't match") + throw new Error("Invalid password"); } - // return record; } export async function processRequestWebHandler(request, reply) { @@ -34,26 +109,25 @@ export async function processRequestWebHandler(request, reply) { where: { id: request.params.id } }); - if (!record) { + if (!record || !record.keyName) { return; } + try { + await validateRequest(request, record); + } catch (e: any) { + reply.status(401); + reply.type("application/json"); + return reply.send({ ok: false, error: e.message }); + } + await prisma.request.update({ where: { id: request.params.id }, data: { allowed: true } }); let allowScope: IAllowScope | undefined; - - const body = request.body; - - console.log({body}); - - // if (body.permissions === 'all') { - allowScope = {kind: 'all'}; - // } - - console.log({allowScope}); + allowScope = {kind: 'all'}; await allowAllRequestsFromKey( record.remotePubkey, @@ -64,82 +138,133 @@ export async function processRequestWebHandler(request, reply) { allowScope ); + if (record.method === "connect") { + debug("connect, adding sign_event capability"); + await allowAllRequestsFromKey( + record.remotePubkey, + record.keyName, + "sign_event", + undefined, + undefined, + allowScope + ); + } + return { ok: true }; } export async function processRegistrationWebHandler(request, reply) { - const record = await prisma.request.findUnique({ - where: { id: request.params.id } - }); - const body = request.body; + try { + const record = await getAndValidateStateOfRequest(request); + const body = request.body; - if (!record || record.allowed) { - return { ok: false, error: "Request not found or already processed" }; - } + // we serialize the payload again and store it + // along with the allowed flag + // so that the original caller can get the current state + // to be processed + const payload: string[] = []; + payload.push(body.username); + payload.push(body.domain); - // we serialize the payload again and store it - // along with the allowed flag - // so that the original caller can get the current state - // to be processed - const payload: string[] = []; - payload.push(body.username); - payload.push(body.domain); - payload.push(body.email); - payload.push(body.password); + // TODO: validations here + try { + await validateRegistration(request, record); + } catch (e: any) { + const [ username, domain, email ] = JSON.parse(record.params!); + const nip05 = `${username}@${domain}`; - // TODO: validations here - - await prisma.request.update({ - where: { id: request.params.id }, - data: { params: JSON.stringify(payload), allowed: true } - }); - - let createdPubkey: string | undefined; - - // 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 callbackUrlString = body.callbackUrl; - let callbackUrl: string | undefined; - - if (callbackUrlString) { - const u = new URL(callbackUrlString); - - if (createdPubkey) { - u.searchParams.append("pubkey", createdPubkey); - callbackUrl = u.toString(); + return reply.view("/templates/createAccount.handlebar", { record, email, username, domain, nip05, error: e.message}); } + + await prisma.request.update({ + where: { id: request.params.id }, + data: { params: JSON.stringify(payload), allowed: true } + }); + + let createdPubkey: string | undefined; + + // here I need to wait for the account + createdPubkey = await new Promise((resolve) => { + const interval = setInterval(async () => { + const keyName = record.keyName; + + if (!keyName) throw new Error("Invalid keyName on generated account"); + + const keyRecord = await prisma.key.findUnique({ where: { keyName } }); + + if (keyRecord) { + console.log(keyRecord); + clearInterval(interval); + resolve(keyRecord.pubkey); + } + }, 100); + }); + + if (!createdPubkey) throw new Error("No pubkey found for keyName"); + + await createUserRecord( + body.username, + body.domain, + createdPubkey, + body.email, + body.password, + ) + + 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(); + } + } + + await allowAllRequestsFromKey( + record.remotePubkey, + record.keyName, + record.method, + undefined, + undefined, + ); + + // redirect to callbackUrl + if (callbackUrl) { + return reply + .view("/templates/redirect.handlebar", { callbackUrl }) + .redirect(callbackUrl); + } + + return reply.view("/templates/redirect.handlebar", { callbackUrl }); + } catch (error: any) { + debug(`Error processing registration request`, error, request); + return reply.view("/templates/error.handlebar", { error: error.message }); } +} - // const url = new URL(callbackUrl); +async function createUserRecord( + username: string, + domain: string, + pubkey: string, + email: string, + password: string, +) { - // add to url a query param with the user's pubkey + const hashedPassword = await bcrypt.hash(password, 10); - await allowAllRequestsFromKey( - record.remotePubkey, - record.keyName, - record.method, - undefined, - undefined, - ); + debug(`Creating user record for ${username}@${domain}`, {hashedPassword}) - // redirect to login page - if (callbackUrl) { - return reply - .view("/templates/redirect.handlebar", { callbackUrl }) - .redirect(callbackUrl); - } + const userRecord = await prisma.user.create({ + data: { + username, + domain, + pubkey, + email, + password: hashedPassword, + } + }); - return reply.view("/templates/redirect.handlebar", { callbackUrl }); + return userRecord; } \ No newline at end of file diff --git a/src/daemon/web/registration-validations.ts b/src/daemon/web/registration-validations.ts new file mode 100644 index 0000000..7c5affa --- /dev/null +++ b/src/daemon/web/registration-validations.ts @@ -0,0 +1,25 @@ +import prisma from "../../db"; + +export async function validateRegistration(request, record) { + // validate username uniqueness + const body = request.body; + const { username, domain, email, password } = body; + + const userRecord = await prisma.user.findUnique({ + where: { username, domain } + }); + + if (userRecord) throw new Error("Username already exists. If this is your account, please login instead."); + + // validate password length + if (password.length < 8) throw new Error("Password is too short"); + + // validate email (if present) + if (email) { + if (!email.includes("@")) throw new Error("Invalid email address"); + + // validate email uniqueness (if one was provided) + const emailRecord = await prisma.user.findUnique({ where: { email } }); + if (emailRecord) throw new Error("Email already exists"); + } +} \ No newline at end of file diff --git a/templates/authorizeRequest.handlebar b/templates/authorizeRequest.handlebar index 367b602..2058072 100644 --- a/templates/authorizeRequest.handlebar +++ b/templates/authorizeRequest.handlebar @@ -12,8 +12,10 @@ + + + + +
+
+

+ + Login to Nostr +

+
+ +
+ {{error}} +
+
+ + diff --git a/templates/login.handlebar b/templates/login.handlebar new file mode 100644 index 0000000..ca4e90a --- /dev/null +++ b/templates/login.handlebar @@ -0,0 +1,49 @@ + + + + + + Login - Nostr + + + + + +
+
+

+ + Login to Nostr +

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + diff --git a/templates/redirect.handlebar b/templates/redirect.handlebar index acc98a8..5389a3f 100644 --- a/templates/redirect.handlebar +++ b/templates/redirect.handlebar @@ -68,6 +68,7 @@