From 6df5752d46d6c231811176d47a106e7d86c79d49 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 16 Sep 2024 15:15:52 +0000 Subject: [PATCH] db + auth routes --- datasource.js | 8 +- proto/autogenerated/client.md | 54 +++++++++ proto/autogenerated/ts/express_server.ts | 97 +++++++++++++++ proto/autogenerated/ts/http_client.ts | 39 ++++++ proto/autogenerated/ts/nostr_client.ts | 41 +++++++ proto/autogenerated/ts/nostr_transport.ts | 79 ++++++++++++ proto/autogenerated/ts/types.ts | 112 +++++++++++++++++- proto/service/methods.proto | 20 +++- proto/service/structs.proto | 25 ++++ src/nostrMiddleware.ts | 5 + src/services/main/debitManager.ts | 37 ++++++ src/services/serverMethods/index.ts | 15 ++- src/services/storage/db.ts | 3 +- src/services/storage/debitStorage.ts | 8 ++ src/services/storage/entity/DebitAccess.ts | 3 - .../migrations/1726496225078-debit_access.ts | 16 +++ src/services/storage/migrations/runner.ts | 3 +- 17 files changed, 553 insertions(+), 12 deletions(-) create mode 100644 src/services/storage/migrations/1726496225078-debit_access.ts diff --git a/datasource.js b/datasource.js index 931ebc29..d4ed74ef 100644 --- a/datasource.js +++ b/datasource.js @@ -15,6 +15,7 @@ import { LspOrder } from "./build/src/services/storage/entity/LspOrder.js" import { LndNodeInfo } from "./build/src/services/storage/entity/LndNodeInfo.js" import { TrackedProvider } from "./build/src/services/storage/entity/TrackedProvider.js" import { InviteToken } from "./build/src/services/storage/entity/InviteToken.js" +import { DebitAccess } from "./build/src/services/storage/entity/DebitAccess.js" import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' @@ -22,13 +23,14 @@ import { LndNodeInfo1720187506189 } from './build/src/services/storage/migration import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js' import { CreateInviteTokenTable1721751414878 } from './build/src/services/storage/migrations/1721751414878-create_invite_token_table.js' import { PaymentIndex1721760297610 } from './build/src/services/storage/migrations/1721760297610-payment_index.js' +import { DebitAccess1726496225078 } from './build/src/services/storage/migrations/1726496225078-debit_access.js' export default new DataSource({ type: "sqlite", database: "db.sqlite", // logging: true, - migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610], + migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078], entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, - UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider, InviteToken], + UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider, InviteToken, DebitAccess], // synchronize: true, }) -//npx typeorm migration:generate ./src/services/storage/migrations/lnd_node_info -d ./datasource.js \ No newline at end of file +//npx typeorm migration:generate ./src/services/storage/migrations/debit_access -d ./datasource.js \ No newline at end of file diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 6884a4cb..2d76dae5 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -28,6 +28,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [AuthAppRequest](#AuthAppRequest) - output: [AuthApp](#AuthApp) +- AuthorizeDebit + - auth type: __User__ + - input: [DebitAuthorization](#DebitAuthorization) + - output: [AuthorizedDebit](#AuthorizedDebit) + - BanUser - auth type: __Admin__ - input: [BanUserRequest](#BanUserRequest) @@ -58,6 +63,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [AppsMetricsRequest](#AppsMetricsRequest) - output: [AppsMetrics](#AppsMetrics) +- GetAuthorizedDebits + - auth type: __User__ + - This methods has an __empty__ __request__ body + - output: [AuthorizedDebits](#AuthorizedDebits) + - GetHttpCreds - auth type: __User__ - This methods has an __empty__ __request__ body @@ -170,6 +180,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) +- RemoveAuthorizedDebit + - auth type: __User__ + - input: [RemoveAuthorizedDebitRequest](#RemoveAuthorizedDebitRequest) + - This methods has an __empty__ __response__ body + - UseInviteLink - auth type: __GuestWithPub__ - input: [UseInviteLinkRequest](#UseInviteLinkRequest) @@ -256,6 +271,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [AuthAppRequest](#AuthAppRequest) - output: [AuthApp](#AuthApp) +- AuthorizeDebit + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/debit/authorize__ + - input: [DebitAuthorization](#DebitAuthorization) + - output: [AuthorizedDebit](#AuthorizedDebit) + - BanUser - auth type: __Admin__ - http method: __post__ @@ -326,6 +348,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [AppsMetricsRequest](#AppsMetricsRequest) - output: [AppsMetrics](#AppsMetrics) +- GetAuthorizedDebits + - auth type: __User__ + - http method: __get__ + - http route: __/api/user/debit/get__ + - This methods has an __empty__ __request__ body + - output: [AuthorizedDebits](#AuthorizedDebits) + - GetHttpCreds - auth type: __User__ - http method: __post__ @@ -545,6 +574,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) +- RemoveAuthorizedDebit + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/debit/remove__ + - input: [RemoveAuthorizedDebitRequest](#RemoveAuthorizedDebitRequest) + - This methods has an __empty__ __response__ body + - RequestNPubLinkingToken - auth type: __App__ - http method: __post__ @@ -675,6 +711,14 @@ The nostr server will send back a message response, and inside the body there wi - __allow_user_creation__: _boolean_ *this field is optional - __name__: _string_ +### AuthorizedDebit + - __debit_id__: _string_ + - __debit_type__: _[AuthorizedDebitType](#AuthorizedDebitType)_ + - __key__: _string_ + +### AuthorizedDebits + - __debits__: ARRAY of: _[AuthorizedDebit](#AuthorizedDebit)_ + ### BanUserRequest - __user_id__: _string_ @@ -702,6 +746,9 @@ The nostr server will send back a message response, and inside the body there wi ### CreateOneTimeInviteLinkResponse - __invitation_link__: _string_ +### DebitAuthorization + - __authorize_npub__: _string_ *this field is optional + ### DecodeInvoiceRequest - __invoice__: _string_ @@ -903,6 +950,9 @@ The nostr server will send back a message response, and inside the body there wi ### RelaysMigration - __relays__: ARRAY of: _string_ +### RemoveAuthorizedDebitRequest + - __debit_id__: _string_ + ### RequestNPubLinkingTokenRequest - __user_identifier__: _string_ @@ -1003,6 +1053,10 @@ The nostr server will send back a message response, and inside the body there wi - __TAPROOT_PUBKEY__ - __WITNESS_PUBKEY_HASH__ +### AuthorizedDebitType + - __KEY__ + - __NPUB__ + ### UserOperationType - __INCOMING_INVOICE__ - __INCOMING_TX__ diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index 32c0cc62..ba89f29b 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -166,6 +166,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.AuthorizeDebit) throw new Error('method: AuthorizeDebit is not implemented') + app.post('/api/user/debit/authorize', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'AuthorizeDebit', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.AuthorizeDebit) throw new Error('method: AuthorizeDebit is not implemented') + const authContext = await opts.UserAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.DebitAuthorizationValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.AuthorizeDebit({rpcName:'AuthorizeDebit', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.BanUser) throw new Error('method: BanUser is not implemented') app.post('/api/admin/user/ban', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'BanUser', batch: false, nostr: false, batchSize: 0} @@ -221,6 +243,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'AuthorizeDebit': + if (!methods.AuthorizeDebit) { + throw new Error('method AuthorizeDebit not found' ) + } else { + const error = Types.DebitAuthorizationValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.AuthorizeDebit({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'DecodeInvoice': if (!methods.DecodeInvoice) { throw new Error('method DecodeInvoice not found' ) @@ -245,6 +279,16 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'GetAuthorizedDebits': + if (!methods.GetAuthorizedDebits) { + throw new Error('method GetAuthorizedDebits not found' ) + } else { + opStats.validate = opStats.guard + const res = await methods.GetAuthorizedDebits({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'GetLNURLChannelLink': if (!methods.GetLNURLChannelLink) { throw new Error('method GetLNURLChannelLink not found' ) @@ -379,6 +423,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'RemoveAuthorizedDebit': + if (!methods.RemoveAuthorizedDebit) { + throw new Error('method RemoveAuthorizedDebit not found' ) + } else { + const error = Types.RemoveAuthorizedDebitRequestValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + await methods.RemoveAuthorizedDebit({...operation, ctx}); responses.push({ status: 'OK' }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'UserHealth': if (!methods.UserHealth) { throw new Error('method UserHealth not found' ) @@ -572,6 +628,25 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetAuthorizedDebits) throw new Error('method: GetAuthorizedDebits is not implemented') + app.get('/api/user/debit/get', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetAuthorizedDebits', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetAuthorizedDebits) throw new Error('method: GetAuthorizedDebits is not implemented') + const authContext = await opts.UserAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + stats.validate = stats.guard + const query = req.query + const params = req.params + const response = await methods.GetAuthorizedDebits({rpcName:'GetAuthorizedDebits', ctx:authContext }) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.GetInviteLinkState) throw new Error('method: GetInviteLinkState is not implemented') app.post('/api/admin/app/invite/get', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetInviteLinkState', batch: false, nostr: false, batchSize: 0} @@ -1102,6 +1177,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.RemoveAuthorizedDebit) throw new Error('method: RemoveAuthorizedDebit is not implemented') + app.post('/api/user/debit/remove', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'RemoveAuthorizedDebit', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.RemoveAuthorizedDebit) throw new Error('method: RemoveAuthorizedDebit is not implemented') + const authContext = await opts.UserAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.RemoveAuthorizedDebitRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + await methods.RemoveAuthorizedDebit({rpcName:'RemoveAuthorizedDebit', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK'}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.RequestNPubLinkingToken) throw new Error('method: RequestNPubLinkingToken is not implemented') app.post('/api/app/user/npub/token', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 86aeac62..ef723331 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -101,6 +101,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + AuthorizeDebit: async (request: Types.DebitAuthorization): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/debit/authorize' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AuthorizedDebitValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, BanUser: async (request: Types.BanUserRequest): Promise => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') @@ -232,6 +246,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAuthorizedDebits: async (): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/debit/get' + const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AuthorizedDebitsValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetHttpCreds: async (cb: (v:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise => { throw new Error('http streams are not supported')}, GetInviteLinkState: async (request: Types.GetInviteTokenStateRequest): Promise => { const auth = await params.retrieveAdminAuth() @@ -601,6 +629,17 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + RemoveAuthorizedDebit: async (request: Types.RemoveAuthorizedDebitRequest): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/debit/remove' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + return data + } + return { status: 'ERROR', reason: 'invalid response' } + }, RequestNPubLinkingToken: async (request: Types.RequestNPubLinkingTokenRequest): Promise => { const auth = await params.retrieveAppAuth() if (auth === null) throw new Error('retrieveAppAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 6a6fb4c0..be480249 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -57,6 +57,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + AuthorizeDebit: async (request: Types.DebitAuthorization): Promise => { + const auth = await params.retrieveNostrUserAuth() + if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'AuthorizeDebit',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AuthorizedDebitValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, BanUser: async (request: Types.BanUserRequest): Promise => { const auth = await params.retrieveNostrAdminAuth() if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') @@ -140,6 +155,20 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAuthorizedDebits: async (): Promise => { + const auth = await params.retrieveNostrUserAuth() + if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') + const nostrRequest: NostrRequest = {} + const data = await send(params.pubDestination, {rpcName:'GetAuthorizedDebits',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AuthorizedDebitsValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetHttpCreds: async (cb: (res:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') @@ -460,6 +489,18 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + RemoveAuthorizedDebit: async (request: Types.RemoveAuthorizedDebitRequest): Promise => { + const auth = await params.retrieveNostrUserAuth() + if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'RemoveAuthorizedDebit',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + return data + } + return { status: 'ERROR', reason: 'invalid response' } + }, UseInviteLink: async (request: Types.UseInviteLinkRequest): Promise => { const auth = await params.retrieveNostrGuestWithPubAuth() if (auth === null) throw new Error('retrieveNostrGuestWithPubAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index 288d3169..5d9032af 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -80,6 +80,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'AuthorizeDebit': + try { + if (!methods.AuthorizeDebit) throw new Error('method: AuthorizeDebit is not implemented') + const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.DebitAuthorizationValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.AuthorizeDebit({rpcName:'AuthorizeDebit', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'BanUser': try { if (!methods.BanUser) throw new Error('method: BanUser is not implemented') @@ -127,6 +143,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'AuthorizeDebit': + if (!methods.AuthorizeDebit) { + throw new Error('method not defined: AuthorizeDebit') + } else { + const error = Types.DebitAuthorizationValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.AuthorizeDebit({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'DecodeInvoice': if (!methods.DecodeInvoice) { throw new Error('method not defined: DecodeInvoice') @@ -151,6 +179,16 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'GetAuthorizedDebits': + if (!methods.GetAuthorizedDebits) { + throw new Error('method not defined: GetAuthorizedDebits') + } else { + opStats.validate = opStats.guard + const res = await methods.GetAuthorizedDebits({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'GetLNURLChannelLink': if (!methods.GetLNURLChannelLink) { throw new Error('method not defined: GetLNURLChannelLink') @@ -285,6 +323,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'RemoveAuthorizedDebit': + if (!methods.RemoveAuthorizedDebit) { + throw new Error('method not defined: RemoveAuthorizedDebit') + } else { + const error = Types.RemoveAuthorizedDebitRequestValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + await methods.RemoveAuthorizedDebit({...operation, ctx}); responses.push({ status: 'OK' }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'UserHealth': if (!methods.UserHealth) { throw new Error('method not defined: UserHealth') @@ -369,6 +419,19 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'GetAuthorizedDebits': + try { + if (!methods.GetAuthorizedDebits) throw new Error('method: GetAuthorizedDebits is not implemented') + const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + stats.validate = stats.guard + const response = await methods.GetAuthorizedDebits({rpcName:'GetAuthorizedDebits', ctx:authContext }) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'GetHttpCreds': try { if (!methods.GetHttpCreds) throw new Error('method: GetHttpCreds is not implemented') @@ -688,6 +751,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'RemoveAuthorizedDebit': + try { + if (!methods.RemoveAuthorizedDebit) throw new Error('method: RemoveAuthorizedDebit is not implemented') + const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.RemoveAuthorizedDebitRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + await methods.RemoveAuthorizedDebit({rpcName:'RemoveAuthorizedDebit', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK'}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'UseInviteLink': try { if (!methods.UseInviteLink) throw new Error('method: UseInviteLink is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index d3c2c131..09e00639 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -34,8 +34,8 @@ export type UserContext = { app_user_id: string user_id: string } -export type UserMethodInputs = AddProduct_Input | DecodeInvoice_Input | EnrollAdminToken_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | OpenChannel_Input | PayAddress_Input | PayInvoice_Input | UserHealth_Input -export type UserMethodOutputs = AddProduct_Output | DecodeInvoice_Output | EnrollAdminToken_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | OpenChannel_Output | PayAddress_Output | PayInvoice_Output | UserHealth_Output +export type UserMethodInputs = AddProduct_Input | AuthorizeDebit_Input | DecodeInvoice_Input | EnrollAdminToken_Input | GetAuthorizedDebits_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | OpenChannel_Input | PayAddress_Input | PayInvoice_Input | RemoveAuthorizedDebit_Input | UserHealth_Input +export type UserMethodOutputs = AddProduct_Output | AuthorizeDebit_Output | DecodeInvoice_Output | EnrollAdminToken_Output | GetAuthorizedDebits_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | OpenChannel_Output | PayAddress_Output | PayInvoice_Output | RemoveAuthorizedDebit_Output | UserHealth_Output export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} @@ -56,6 +56,9 @@ export type AddProduct_Output = ResultError | ({ status: 'OK' } & Product) export type AuthApp_Input = {rpcName:'AuthApp', req: AuthAppRequest} export type AuthApp_Output = ResultError | ({ status: 'OK' } & AuthApp) +export type AuthorizeDebit_Input = {rpcName:'AuthorizeDebit', req: DebitAuthorization} +export type AuthorizeDebit_Output = ResultError | ({ status: 'OK' } & AuthorizedDebit) + export type BanUser_Input = {rpcName:'BanUser', req: BanUserRequest} export type BanUser_Output = ResultError | ({ status: 'OK' } & BanUserResponse) @@ -86,6 +89,9 @@ export type GetAppUserLNURLInfo_Output = ResultError | ({ status: 'OK' } & Lnurl export type GetAppsMetrics_Input = {rpcName:'GetAppsMetrics', req: AppsMetricsRequest} export type GetAppsMetrics_Output = ResultError | ({ status: 'OK' } & AppsMetrics) +export type GetAuthorizedDebits_Input = {rpcName:'GetAuthorizedDebits'} +export type GetAuthorizedDebits_Output = ResultError | ({ status: 'OK' } & AuthorizedDebits) + export type GetHttpCreds_Input = {rpcName:'GetHttpCreds', cb:(res: HttpCreds, err:Error|null)=> void} export type GetHttpCreds_Output = ResultError | { status: 'OK' } @@ -195,6 +201,9 @@ export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvo export type PayInvoice_Input = {rpcName:'PayInvoice', req: PayInvoiceRequest} export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) +export type RemoveAuthorizedDebit_Input = {rpcName:'RemoveAuthorizedDebit', req: RemoveAuthorizedDebitRequest} +export type RemoveAuthorizedDebit_Output = ResultError | { status: 'OK' } + export type RequestNPubLinkingToken_Input = {rpcName:'RequestNPubLinkingToken', req: RequestNPubLinkingTokenRequest} export type RequestNPubLinkingToken_Output = ResultError | ({ status: 'OK' } & RequestNPubLinkingTokenResponse) @@ -229,6 +238,7 @@ export type ServerMethods = { AddAppUserInvoice?: (req: AddAppUserInvoice_Input & {ctx: AppContext }) => Promise AddProduct?: (req: AddProduct_Input & {ctx: UserContext }) => Promise AuthApp?: (req: AuthApp_Input & {ctx: AdminContext }) => Promise + AuthorizeDebit?: (req: AuthorizeDebit_Input & {ctx: UserContext }) => Promise BanUser?: (req: BanUser_Input & {ctx: AdminContext }) => Promise CreateOneTimeInviteLink?: (req: CreateOneTimeInviteLink_Input & {ctx: AdminContext }) => Promise DecodeInvoice?: (req: DecodeInvoice_Input & {ctx: UserContext }) => Promise @@ -238,6 +248,7 @@ export type ServerMethods = { GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise GetAppsMetrics?: (req: GetAppsMetrics_Input & {ctx: MetricsContext }) => Promise + GetAuthorizedDebits?: (req: GetAuthorizedDebits_Input & {ctx: UserContext }) => Promise GetHttpCreds?: (req: GetHttpCreds_Input & {ctx: UserContext }) => Promise GetInviteLinkState?: (req: GetInviteLinkState_Input & {ctx: AdminContext }) => Promise GetLNURLChannelLink?: (req: GetLNURLChannelLink_Input & {ctx: UserContext }) => Promise @@ -267,6 +278,7 @@ export type ServerMethods = { PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise + RemoveAuthorizedDebit?: (req: RemoveAuthorizedDebit_Input & {ctx: UserContext }) => Promise RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise ResetNPubLinkingToken?: (req: ResetNPubLinkingToken_Input & {ctx: AppContext }) => Promise SendAppUserToAppPayment?: (req: SendAppUserToAppPayment_Input & {ctx: AppContext }) => Promise @@ -287,6 +299,14 @@ export const enumCheckAddressType = (e?: AddressType): boolean => { for (const v in AddressType) if (e === v) return true return false } +export enum AuthorizedDebitType { + KEY = 'KEY', + NPUB = 'NPUB', +} +export const enumCheckAuthorizedDebitType = (e?: AuthorizedDebitType): boolean => { + for (const v in AuthorizedDebitType) if (e === v) return true + return false +} export enum UserOperationType { INCOMING_INVOICE = 'INCOMING_INVOICE', INCOMING_TX = 'INCOMING_TX', @@ -668,6 +688,57 @@ export const AuthAppRequestValidate = (o?: AuthAppRequest, opts: AuthAppRequestO return null } +export type AuthorizedDebit = { + debit_id: string + debit_type: AuthorizedDebitType + key: string +} +export const AuthorizedDebitOptionalFields: [] = [] +export type AuthorizedDebitOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + debit_id_CustomCheck?: (v: string) => boolean + debit_type_CustomCheck?: (v: AuthorizedDebitType) => boolean + key_CustomCheck?: (v: string) => boolean +} +export const AuthorizedDebitValidate = (o?: AuthorizedDebit, opts: AuthorizedDebitOptions = {}, path: string = 'AuthorizedDebit::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.debit_id !== 'string') return new Error(`${path}.debit_id: is not a string`) + if (opts.debit_id_CustomCheck && !opts.debit_id_CustomCheck(o.debit_id)) return new Error(`${path}.debit_id: custom check failed`) + + if (!enumCheckAuthorizedDebitType(o.debit_type)) return new Error(`${path}.debit_type: is not a valid AuthorizedDebitType`) + if (opts.debit_type_CustomCheck && !opts.debit_type_CustomCheck(o.debit_type)) return new Error(`${path}.debit_type: custom check failed`) + + if (typeof o.key !== 'string') return new Error(`${path}.key: is not a string`) + if (opts.key_CustomCheck && !opts.key_CustomCheck(o.key)) return new Error(`${path}.key: custom check failed`) + + return null +} + +export type AuthorizedDebits = { + debits: AuthorizedDebit[] +} +export const AuthorizedDebitsOptionalFields: [] = [] +export type AuthorizedDebitsOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + debits_ItemOptions?: AuthorizedDebitOptions + debits_CustomCheck?: (v: AuthorizedDebit[]) => boolean +} +export const AuthorizedDebitsValidate = (o?: AuthorizedDebits, opts: AuthorizedDebitsOptions = {}, path: string = 'AuthorizedDebits::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (!Array.isArray(o.debits)) return new Error(`${path}.debits: is not an array`) + for (let index = 0; index < o.debits.length; index++) { + const debitsErr = AuthorizedDebitValidate(o.debits[index], opts.debits_ItemOptions, `${path}.debits[${index}]`) + if (debitsErr !== null) return debitsErr + } + if (opts.debits_CustomCheck && !opts.debits_CustomCheck(o.debits)) return new Error(`${path}.debits: custom check failed`) + + return null +} + export type BanUserRequest = { user_id: string } @@ -830,6 +901,25 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL return null } +export type DebitAuthorization = { + authorize_npub?: string +} +export type DebitAuthorizationOptionalField = 'authorize_npub' +export const DebitAuthorizationOptionalFields: DebitAuthorizationOptionalField[] = ['authorize_npub'] +export type DebitAuthorizationOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: DebitAuthorizationOptionalField[] + authorize_npub_CustomCheck?: (v?: string) => boolean +} +export const DebitAuthorizationValidate = (o?: DebitAuthorization, opts: DebitAuthorizationOptions = {}, path: string = 'DebitAuthorization::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.authorize_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('authorize_npub')) && typeof o.authorize_npub !== 'string') return new Error(`${path}.authorize_npub: is not a string`) + if (opts.authorize_npub_CustomCheck && !opts.authorize_npub_CustomCheck(o.authorize_npub)) return new Error(`${path}.authorize_npub: custom check failed`) + + return null +} + export type DecodeInvoiceRequest = { invoice: string } @@ -2028,6 +2118,24 @@ export const RelaysMigrationValidate = (o?: RelaysMigration, opts: RelaysMigrati return null } +export type RemoveAuthorizedDebitRequest = { + debit_id: string +} +export const RemoveAuthorizedDebitRequestOptionalFields: [] = [] +export type RemoveAuthorizedDebitRequestOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + debit_id_CustomCheck?: (v: string) => boolean +} +export const RemoveAuthorizedDebitRequestValidate = (o?: RemoveAuthorizedDebitRequest, opts: RemoveAuthorizedDebitRequestOptions = {}, path: string = 'RemoveAuthorizedDebitRequest::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.debit_id !== 'string') return new Error(`${path}.debit_id: is not a string`) + if (opts.debit_id_CustomCheck && !opts.debit_id_CustomCheck(o.debit_id)) return new Error(`${path}.debit_id: custom check failed`) + + return null +} + export type RequestNPubLinkingTokenRequest = { user_identifier: string } diff --git a/proto/service/methods.proto b/proto/service/methods.proto index ed50cc94..fe419927 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -430,11 +430,29 @@ service LightningPub { } rpc GetLNURLChannelLink(structs.Empty) returns (structs.LnurlLinkResponse){ - option (auth_type) = "User"; + option (auth_type) = "User"; option (http_method) = "post"; option (http_route) = "/api/user/lnurl_channel/url"; option (nostr) = true; } + rpc GetAuthorizedDebits(structs.Empty) returns (structs.AuthorizedDebits){ + option (auth_type) = "User"; + option (http_method) = "get"; + option (http_route) = "/api/user/debit/get"; + option (nostr) = true; + } + rpc RemoveAuthorizedDebit(structs.RemoveAuthorizedDebitRequest) returns (structs.Empty){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/debit/remove"; + option (nostr) = true; + } + rpc AuthorizeDebit(structs.DebitAuthorization) returns (structs.AuthorizedDebit){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/debit/authorize"; + option (nostr) = true; + } rpc GetLiveUserOperations(structs.Empty) returns (stream structs.LiveUserOperation){ option (auth_type) = "User"; option (http_method) = "post"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index bedb45bf..c4474b67 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -476,4 +476,29 @@ message GetInviteTokenStateRequest { message GetInviteTokenStateResponse { bool used = 1; +} + + + +message DebitAuthorization { + optional string authorize_npub =1; +} + +enum AuthorizedDebitType { + NPUB = 0; + KEY = 1; +} + +message AuthorizedDebit { + string debit_id = 1; + AuthorizedDebitType debit_type = 2; + string key = 3; +} + +message AuthorizedDebits { + repeated AuthorizedDebit debits = 1; +} + +message RemoveAuthorizedDebitRequest { + string debit_id = 1; } \ No newline at end of file diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index 90579908..3aaf360c 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -7,6 +7,7 @@ import { ERROR, getLogger } from "./services/helpers/logger.js"; import { UnsignedEvent } from "./services/nostr/tools/event.js"; import { defaultInvoiceExpiry } from "./services/storage/paymentStorage.js"; import { Application } from "./services/storage/entity/Application.js"; +import { NdebitData } from "./services/main/debitManager.js"; export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend } => { const log = getLogger({}) @@ -52,6 +53,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett const offerReq = j as NofferData mainHandler.handleNip69Noffer(offerReq, event) return + } else if (event.kind === 21002) { + const debitReq = j as NdebitData + mainHandler.handleNip68Debit(debitReq, event) + return } if (!j.rpcName) { onClientEvent(j as { requestId: string }, event.pub) diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 88d1a13a..a7cfa4a1 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto'; import * as Types from "../../../proto/autogenerated/ts/types.js"; import ApplicationManager from "./applicationManager.js"; import { DebitKeyType } from "../storage/entity/DebitAccess.js"; @@ -18,6 +19,9 @@ const nip68errs = { export type NdebitResponse = NdebitSuccess | NdebitFailure type HandleNdebitRes = { ok: false, debitRes: NdebitFailure } | { ok: true, op: Types.UserOperation, appUserId: string, debitRes: NdebitSuccess } export class DebitManager { + + + applicationManager: ApplicationManager storage: Storage lnd: LND @@ -26,6 +30,37 @@ export class DebitManager { this.storage = storage } + AuthorizeDebit = async (ctx: Types.UserContext, req: Types.DebitAuthorization): Promise => { + const debitType = this.getDebitType(req.authorize_npub) + const access = await this.storage.debitStorage.AddDebitAccess(ctx.app_user_id, debitType.key, debitType.keyType) + return { + debit_id: access.serial_id.toString(), + debit_type: debitType.debitType, + key: debitType.key, + } + } + + getDebitType = (authorizeNpub?: string): { keyType: DebitKeyType, key: string, debitType: Types.AuthorizedDebitType } => { + if (authorizeNpub) { + return { keyType: 'pubKey', key: authorizeNpub, debitType: Types.AuthorizedDebitType.NPUB } + } + return { keyType: 'simpleId', key: crypto.randomBytes(32).toString('hex'), debitType: Types.AuthorizedDebitType.KEY } + } + + GetAuthorizedDebits = async (ctx: Types.UserContext): Promise => { + const allDebitsAccesses = await this.storage.debitStorage.GetAllUserDebitAccess(ctx.app_user_id) + const debits: Types.AuthorizedDebit[] = allDebitsAccesses.map(access => ({ + debit_id: access.serial_id.toString(), + debit_type: access.key_type === 'pubKey' ? Types.AuthorizedDebitType.NPUB : Types.AuthorizedDebitType.KEY, + key: access.key + })) + return { debits } + } + + RemoveAuthorizedDebit = async (ctx: Types.UserContext, req: Types.RemoveAuthorizedDebitRequest): Promise => { + await this.storage.debitStorage.RemoveDebitAccess(ctx.app_user_id, +req.debit_id) + } + payNdebitInvoice = async (appId: string, requestorPub: string, pointerdata: NdebitData): Promise => { try { return await this.doNdebit(appId, requestorPub, pointerdata) @@ -90,5 +125,7 @@ export class DebitManager { internal: payment.network_fee === 0 } } + + } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index d440d53a..b6de933a 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -261,7 +261,20 @@ export default (mainHandler: Main): Types.ServerMethods => { }) if (err != null) throw new Error(err.message) return mainHandler.adminManager.GetInviteTokenState(ctx, req); - } + }, + AuthorizeDebit: async ({ ctx, req }) => { + return mainHandler.debitManager.AuthorizeDebit(ctx, req) + }, + GetAuthorizedDebits: async ({ ctx }) => { + return mainHandler.debitManager.GetAuthorizedDebits(ctx) + }, + RemoveAuthorizedDebit: async ({ ctx, req }) => { + const err = Types.RemoveAuthorizedDebitRequestValidate(req, { + debit_id_CustomCheck: id => id !== '', + }) + if (err != null) throw new Error(err.message) + return mainHandler.debitManager.RemoveAuthorizedDebit(ctx, req) + }, } } \ No newline at end of file diff --git a/src/services/storage/db.ts b/src/services/storage/db.ts index 27e47dc8..2ee113d9 100644 --- a/src/services/storage/db.ts +++ b/src/services/storage/db.ts @@ -21,6 +21,7 @@ import { Product } from "./entity/Product.js" import { LndNodeInfo } from "./entity/LndNodeInfo.js" import { TrackedProvider } from "./entity/TrackedProvider.js" import { InviteToken } from "./entity/InviteToken.js" +import { DebitAccess } from "./entity/DebitAccess.js" export type DbSettings = { @@ -61,7 +62,7 @@ export default async (settings: DbSettings, migrations: Function[]): Promise<{ s // logging: true, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider, - InviteToken + InviteToken, DebitAccess ], //synchronize: true, migrations diff --git a/src/services/storage/debitStorage.ts b/src/services/storage/debitStorage.ts index 0e52ac62..5fb344ee 100644 --- a/src/services/storage/debitStorage.ts +++ b/src/services/storage/debitStorage.ts @@ -20,6 +20,10 @@ export default class { return this.txQueue.PushToQueue({ exec: async db => db.getRepository(DebitAccess).save(entry), dbTx: false }) } + async GetAllUserDebitAccess(appUserId: string) { + return this.DB.getRepository(DebitAccess).find({ where: { app_user_id: appUserId } }) + } + async GetDebitAccess(appUserId: string, key: string, keyType: DebitKeyType) { return this.DB.getRepository(DebitAccess).findOne({ where: { app_user_id: appUserId, key, key_type: keyType } }) } @@ -27,4 +31,8 @@ export default class { async IncrementDebitAccess(appUserId: string, key: string, keyType: DebitKeyType, amount: number) { return this.DB.getRepository(DebitAccess).increment({ app_user_id: appUserId, key, key_type: keyType }, 'total_debits', amount) } + + async RemoveDebitAccess(appUserId: string, serialId: number) { + return this.DB.getRepository(DebitAccess).delete({ app_user_id: appUserId, serial_id: serialId }) + } } \ No newline at end of file diff --git a/src/services/storage/entity/DebitAccess.ts b/src/services/storage/entity/DebitAccess.ts index 70d65673..3dc9df90 100644 --- a/src/services/storage/entity/DebitAccess.ts +++ b/src/services/storage/entity/DebitAccess.ts @@ -1,7 +1,4 @@ import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" -import { User } from "./User.js" -import { Application } from "./Application.js" -import { ApplicationUser } from "./ApplicationUser.js" export type DebitKeyType = 'simpleId' | 'pubKey' @Entity() @Index("unique_debit_access", ["app_user_id", "key", "key_type"], { unique: true }) diff --git a/src/services/storage/migrations/1726496225078-debit_access.ts b/src/services/storage/migrations/1726496225078-debit_access.ts new file mode 100644 index 00000000..60554464 --- /dev/null +++ b/src/services/storage/migrations/1726496225078-debit_access.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DebitAccess1726496225078 implements MigrationInterface { + name = 'DebitAccess1726496225078' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "debit_access" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "app_user_id" varchar NOT NULL, "key" varchar NOT NULL, "key_type" varchar NOT NULL, "total_debits" integer NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`); + await queryRunner.query(`CREATE UNIQUE INDEX "unique_debit_access" ON "debit_access" ("app_user_id", "key", "key_type") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "unique_debit_access"`); + await queryRunner.query(`DROP TABLE "debit_access"`); + } + +} diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index 5d4b8171..3483973d 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -12,7 +12,8 @@ import { CreateInviteTokenTable1721751414878 } from "./1721751414878-create_invi import { PaymentIndex1721760297610 } from './1721760297610-payment_index.js' import { HtlcCount1724266887195 } from './1724266887195-htlc_count.js' import { BalanceEvents1724860966825 } from './1724860966825-balance_events.js' -const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610] +import { DebitAccess1726496225078 } from './1726496225078-debit_access.js' +const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078] const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825] export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { if (arg === 'fake_initial_migration') {