diff --git a/src/server/admin/api/getHandler.js b/src/server/admin/api/getHandler.js index 9d659ec..fcf7402 100644 --- a/src/server/admin/api/getHandler.js +++ b/src/server/admin/api/getHandler.js @@ -74,6 +74,10 @@ class GETHandler { } ); } else if ( call === 'getCurrency' ) { resolve( this.settings.currency ); + } else if ( call === 'getAdminAccounts' ) { + // TODO: Finish + } else if ( call === 'getSettings' ) { + resolve( this.settings ); } else { reject( { 'code': 404, 'error': 'Route not found' } ); } diff --git a/src/server/admin/api/postHandler.js b/src/server/admin/api/postHandler.js index 709bcc0..653a794 100644 --- a/src/server/admin/api/postHandler.js +++ b/src/server/admin/api/postHandler.js @@ -106,6 +106,15 @@ class POSTHandler { } ).catch( error => { reject( { 'code': 500, 'error': error } ); } ); + } else if ( call === 'createAdminAccount' ) { + // TODO: Finish + } else if ( call === 'updateAdminAccount' ) { + // TODO: Finish + } else if ( call === 'deleteAdminAccount' ) { + // TODO: Finish + } else if ( call === 'updateSettings' ) { + // TODO: Finish + // TODO: Parse all events and update currency } else { reject( { 'code': 404, 'error': 'Route not found' } ); } diff --git a/src/server/app.js b/src/server/app.js index 06f2e63..74eb305 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -91,7 +91,8 @@ if ( settings.init ) { console.log( '[ Server ] loading plugins' ); // TODO: load dynamically -require( './backend/plugins/payments/stripe/stripeRoutes.js' )( app, settings ); // stripe routes +// require( './backend/plugins/payments/stripe/stripeRoutes.js' )( app, settings ); // stripe routes +require( './backend/plugins/payments/payrexx/payrexxRoutes.js' )( app, settings ); // payrexx routes require( './backend/payments/paymentRoutes.js' )( app, settings ); // payment routes app.use( ( request, response ) => { diff --git a/src/server/backend/payments/paymentRoutes.js b/src/server/backend/payments/paymentRoutes.js index 6b1ce5e..21b9091 100644 --- a/src/server/backend/payments/paymentRoutes.js +++ b/src/server/backend/payments/paymentRoutes.js @@ -17,6 +17,10 @@ module.exports = ( app, settings ) => { res.sendFile( path.join( __dirname + '/../../ui/en/payments/canceled.html' ) ); } ); + app.get( '/payments/failed', ( req, res ) => { + res.sendFile( path.join( __dirname + '/../../ui/en/payments/failed.html' ) ); + } ); + app.get( '/tickets/tickets.pdf', ( req, res ) => { if ( req.session.id ) { fs.readFile( path.join( __dirname + '/../tickets/store/' + req.session.id + '.pdf' ), ( error, data ) => { diff --git a/src/server/backend/plugins/payments/README.md b/src/server/backend/plugins/payments/README.md index 7329b3a..9d80b44 100644 --- a/src/server/backend/plugins/payments/README.md +++ b/src/server/backend/plugins/payments/README.md @@ -4,28 +4,7 @@ All payment gateway integration plugins have to be in their own folders. This fo You will also need to add documentation for the user to set up the payment gateway. Read on below to find out how. ## Setting up the routes.js file -In the routes.js file you should have at least the following code: - -``` -module.exports = ( app, settings ) => { - app.post( '/payments/prepare', ( req, res ) => { - - } ); - - app.get( '/payments/status', ( request, response ) => { - response.writeHead( 200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - } ); - response.status( 200 ); - response.flushHeaders(); - response.write( 'data: connected\n\n' ); - } ); -} -``` - -Take some inspiration of the stripe or adyen setup as these are officially supported by the system and have been developed by the original creator. +Take some inspiration of the stripe or payrexx setup as these are officially supported by the system and have been developed by the original creator. The express.js routes it has to expose are the following: @@ -36,4 +15,8 @@ It can contain any number of (not interfering) routes. Please always use the /pa ## The plugin.json file -The plugin.json file should look as follows: \ No newline at end of file +The plugin.json file should look as follows: + +## configOption.json +This file contains the settings that should be available in the settings page of libreevent. It should contain the following fields, as required by the settings.vue module. + diff --git a/src/server/backend/plugins/payments/payrexx/configOptions.json b/src/server/backend/plugins/payments/payrexx/configOptions.json new file mode 100644 index 0000000..e69de29 diff --git a/src/server/backend/plugins/payments/payrexx/module.payrexx.js b/src/server/backend/plugins/payments/payrexx/module.payrexx.js new file mode 100644 index 0000000..4757eab --- /dev/null +++ b/src/server/backend/plugins/payments/payrexx/module.payrexx.js @@ -0,0 +1,58 @@ +/* +* libreevent - module.payrexx.js +* +* Created by Janis Hutz 08/12/2023, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +const fs = require( 'fs' ); +const path = require( 'path' ); +const qs = require( 'qs' ); +const axios = require( 'axios' ); +const Base64 = require( 'crypto-js/enc-base64' ); +const hmacSHA256 = require( 'crypto-js/hmac-sha256' ); +const payrexxConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'payrexx' ]; + +const baseUrl = 'https://api.payrexx.com/v1.0/'; +const instance = payrexxConfig.instance; +const secret = payrexxConfig.APISecret; + +exports.init = function () { + function buildSignature ( query = '' ) { + return Base64.stringify( hmacSHA256( query, secret ) ); + } + + function buildBaseUrl ( path ) { + return baseUrl + path + '?instance=' + instance; + } + + return { + getGateway: function ( id ) { + const baseUrl = buildBaseUrl( 'Gateway/' + id + '/' ); + const url = baseUrl + '&ApiSignature=' + buildSignature(); + return axios.get( url ); + }, + createGateway: function ( params ) { + if ( !params.amount ) { + throw new Error( 'Amount required!' ); + } + + const defaultParams = { + currency: 'CHF' + }; + + let queryParams = Object.assign( {}, defaultParams, params ); + + const queryStr = qs.stringify( queryParams, { format: 'RFC1738' } ); + const signature = buildSignature( queryStr ); + + queryParams.ApiSignature = signature; + const queryStrSigned = qs.stringify( queryParams ); + + const baseUrl = buildBaseUrl( 'Gateway/' ); + return axios.post( baseUrl, queryStrSigned ); + } + }; +}; diff --git a/src/server/backend/plugins/payments/payrexx/payrexxRoutes.js b/src/server/backend/plugins/payments/payrexx/payrexxRoutes.js index e69de29..baa6cde 100644 --- a/src/server/backend/plugins/payments/payrexx/payrexxRoutes.js +++ b/src/server/backend/plugins/payments/payrexx/payrexxRoutes.js @@ -0,0 +1,177 @@ +/* +* libreevent - payrexxRoutes.js +* +* Created by Janis Hutz 08/12/2023, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +const fs = require( 'fs' ); +const path = require( 'path' ); +const db = require( '../../../db/db.js' ); +const payrexxConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'payrexx' ]; +const bodyParser = require( 'body-parser' ); +const ticket = require( '../../../tickets/ticketGenerator.js' ); +const payrexxModule = require( './module.payrexx.js' ); +const payrexx = payrexxModule.init(); +const TicketGenerator = new ticket(); + + +let sessionReference = {}; +let waitingClients = {}; +let pendingPayments = {}; +let paymentOk = {}; + +module.exports = ( app, settings ) => { + app.post( '/payments/prepare', bodyParser.json(), ( req, res ) => { + if ( req.session.loggedInUser ) { + let purchase = { + 'successRedirectUrl': settings.yourDomain + '/payments/success', + 'cancelRedirectUrl': settings.yourDomain + '/payments/canceled', + 'failedRedirectUrl': settings.yourDomain + '/payments/failed', + 'currency': settings.currency, + 'basket': [], + 'pm': payrexxConfig.paymentMethods, + 'amount': 0, + }; + + db.getDataSimple( 'temp', 'user_id', req.session.id ).then( dat => { + if ( dat[ 0 ] ) { + db.getJSONData( 'events' ).then( events => { + let data = JSON.parse( dat[ 0 ].data ); + ( async () => { + for ( let event in data ) { + for ( let item in data[ event ] ) { + purchase[ 'basket' ].push( { + 'name': data[ event ][ item ].name, + 'quantity': data[ event ][ item ].count ?? 1, + 'amount': Math.round( parseFloat( events[ event ][ 'categories' ][ data[ event ][ item ].category ].price[ data[ event ][ item ][ 'ticketOption' ] ] ) * 100 ), + } ); + purchase[ 'amount' ] += Math.round( parseFloat( events[ event ][ 'categories' ][ data[ event ][ item ].category ].price[ data[ event ][ item ][ 'ticketOption' ] ] ) * 100 ) * ( data[ event ][ item ].count ?? 1 ); + } + } + const response = await payrexx.createGateway( purchase ); + if ( response.status === 200 ) { + const session = response.data.data[ 0 ]; + sessionReference[ session.id ] = { 'tok': req.session.id, 'email': req.session.username }; + pendingPayments[ req.session.id ] = true; + res.send( session.link ); + } else { + res.status( 500 ).send( 'ERR_PAYMENT' ); + } + } )(); + } ); + } else { + res.status( 400 ).send( 'ERR_UID_NOT_FOUND' ); + } + } ).catch( error => { + console.error( '[ STRIPE ] DB ERROR: ' + error ); + res.status( 500 ).send( 'ERR_DB' ); + } ); + } else { + res.status( 403 ).send( 'ERR_UNAUTHORIZED' ); + } + } ); + + app.get( '/payments/status', ( request, response ) => { + response.writeHead( 200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + } ); + response.status( 200 ); + response.flushHeaders(); + response.write( 'data: connected\n\n' ); + waitingClients[ request.session.id ] = response; + const ping = setInterval( () => { + if ( !pendingPayments[ request.session.id ] ) { + const stat = TicketGenerator.getGenerationStatus( request.session.id ); + if ( stat === 'done' ) { + clearInterval( ping ); + setTimeout( () => { + response.write( 'data: ready\n\n' ); + response.end(); + delete waitingClients[ request.session.id ]; + }, 2000 ); + } else if ( stat === 'noTicket' ) { + clearInterval( ping ); + response.write( 'data: noData\n\n' ); + response.end(); + delete waitingClients[ request.session.id ]; + } + } + }, 2000 ); + } ); + + app.get( '/user/2fa/ping', ( request, response ) => { + if ( paymentOk[ request.session.id ] === 'ok' ) { + delete paymentOk[ request.session.id ]; + response.send( { 'status': 'paymentOk' } ); + } else { + if ( !pendingPayments[ request.session.id ] ) { + const stat = TicketGenerator.getGenerationStatus( request.session.id ); + if ( stat === 'done' ) { + response.send( { 'status': 'ticketOk' } ); + } else if ( stat === 'noTicket' ) { + response.send( { 'status': 'noTicket' } ); + } else { + response.send( '' ); + } + } else { + response.send( '' ); + } + } + } ); + + app.post( '/payments/webhook', bodyParser.raw( { type: 'application/json' } ), async ( req, res ) => { + console.error( req.body ); + const response = await payrexx.getGateway( req.body.id ); + + if ( response.status === 200 ) { + const gateway = response.data.data[ 0 ]; + + res.status( 200 ).end(); + if ( gateway.status === 'confirmed' ) { + setTimeout( () => { + if ( waitingClients[ sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ] ) { + waitingClients[ sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ].write( 'data: paymentOk\n\n' ); + } + }, 1000 ); + db.getDataSimple( 'temp', 'user_id', sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ).then( dat => { + db.getDataSimple( 'users', 'email', sessionReference[ response.data.data[ 0 ].id ][ 'email' ] ).then( user => { + if ( user[ 0 ] ) { + const tickets = JSON.parse( dat[ 0 ].data ); + db.writeDataSimple( 'orders', 'account_id', user[ 0 ].account_id, { 'account_id': user[ 0 ].account_id, 'tickets': dat[ 0 ].data, 'order_name': sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] } ).then( () => { + console.log( sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ); + TicketGenerator.generateTickets( sessionReference[ response.data.data[ 0 ].id ] ); + db.getJSONData( 'booked' ).then( ret => { + let booked = ret ?? {}; + for ( let event in tickets ) { + if ( !booked[ String( event ) ] ) { + booked[ String( event ) ] = {}; + } + for ( let tik in tickets[ event ] ) { + booked[ event ][ tik ] = tickets[ event ][ tik ]; + } + } + db.writeJSONData( 'booked', booked ).then( () => { + delete pendingPayments[ sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ]; + } ); + db.deleteDataSimple( 'temp', 'user_id', sessionReference[ response.data.data[ 0 ].id ][ 'tok' ] ).catch( error => { + console.error( '[ STRIPE ] ERROR whilst deleting data from DB: ' + error ); + } ); + } ); + } ); + } else { + console.log( sessionReference[ response.data.data[ 0 ].id ][ 'email' ] ); + console.error( 'user not found' ); + } + } ); + } ).catch( err => { + console.error( err ); + } ); + } + } + } ); +}; \ No newline at end of file diff --git a/src/server/backend/plugins/payments/stripe/configOptions.json b/src/server/backend/plugins/payments/stripe/configOptions.json new file mode 100644 index 0000000..e69de29 diff --git a/src/server/backend/plugins/payments/stripe/stripeRoutes.js b/src/server/backend/plugins/payments/stripe/stripeRoutes.js index 4f0b2f6..adf9095 100644 --- a/src/server/backend/plugins/payments/stripe/stripeRoutes.js +++ b/src/server/backend/plugins/payments/stripe/stripeRoutes.js @@ -10,21 +10,19 @@ const fs = require( 'fs' ); const path = require( 'path' ); const db = require( '../../../db/db.js' ); -const stripConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'stripe' ]; -const stripe = require( 'stripe' )( stripConfig[ 'APIKey' ] ); +const stripeConfig = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../../../config/payments.config.secret.json' ) ) )[ 'stripe' ]; +const stripe = require( 'stripe' )( stripeConfig[ 'APIKey' ] ); const bodyParser = require( 'body-parser' ); const ticket = require( '../../../tickets/ticketGenerator.js' ); const TicketGenerator = new ticket(); -const endpointSecret = stripConfig[ 'endpointSecret' ]; +const endpointSecret = stripeConfig[ 'endpointSecret' ]; let sessionReference = {}; let waitingClients = {}; let pendingPayments = {}; let paymentOk = {}; -// TODO: Remove all selected tickets if timestamp more than user defined amount ago - module.exports = ( app, settings ) => { app.post( '/payments/prepare', bodyParser.json(), ( req, res ) => { if ( req.session.loggedInUser ) { diff --git a/src/server/config/payments.config.json b/src/server/config/payments.config.json index b8e48e7..9b31ed4 100644 --- a/src/server/config/payments.config.json +++ b/src/server/config/payments.config.json @@ -1,5 +1,11 @@ { "stripe": { - "APIKey": "" + "APIKey": "", + "endpointSecret": "" + }, + "payrexx": { + "APISecret": "", + "instance": "", + "pm": [] } } \ No newline at end of file diff --git a/src/server/config/settings.config.json b/src/server/config/settings.config.json index 9a1ec7b..4a05847 100644 --- a/src/server/config/settings.config.json +++ b/src/server/config/settings.config.json @@ -4,7 +4,7 @@ "setupKey": "hello world", "twoFAMode": "enhanced", "db": "mysql", - "payments": "stripe", + "payments": "payrexx", "name": "libreevent", "yourDomain": "http://localhost:8080", "mailSender": "libreevent ", diff --git a/src/server/package-lock.json b/src/server/package-lock.json index 72c73bc..bb78bd5 100644 --- a/src/server/package-lock.json +++ b/src/server/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "@pdfme/generator": "^1.2.6", "@seald-io/nedb": "^4.0.2", + "axios": "^1.4.0", "bcrypt": "^5.0.1", "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", + "crypto-js": "^4.1.1", "express": "^4.18.2", "express-session": "^1.17.3", "html-to-text": "^9.0.5", @@ -21,6 +23,7 @@ "mysql": "^2.18.1", "nodemailer": "^6.9.3", "pdf-lib": "^1.17.1", + "qs": "^6.11.2", "serve-favicon": "^2.5.0", "serve-static": "^1.15.0", "stripe": "^12.14.0", @@ -432,6 +435,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -454,6 +462,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -535,6 +553,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -665,6 +697,17 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -774,6 +817,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "node_modules/css-b64-images": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz", @@ -820,6 +868,14 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -934,6 +990,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "~0.4.13" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1087,6 +1153,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/express/node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -1152,6 +1232,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fontkit": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.2.tgz", @@ -1176,6 +1275,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2158,10 +2270,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { "side-channel": "^1.0.4" }, diff --git a/src/server/package.json b/src/server/package.json index e6a5461..562db19 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -44,9 +44,11 @@ "dependencies": { "@pdfme/generator": "^1.2.6", "@seald-io/nedb": "^4.0.2", + "axios": "^1.4.0", "bcrypt": "^5.0.1", "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", + "crypto-js": "^4.1.1", "express": "^4.18.2", "express-session": "^1.17.3", "html-to-text": "^9.0.5", @@ -54,6 +56,7 @@ "mysql": "^2.18.1", "nodemailer": "^6.9.3", "pdf-lib": "^1.17.1", + "qs": "^6.11.2", "serve-favicon": "^2.5.0", "serve-static": "^1.15.0", "stripe": "^12.14.0", diff --git a/src/server/ui/en/payments/failed.html b/src/server/ui/en/payments/failed.html new file mode 100644 index 0000000..3a992a5 --- /dev/null +++ b/src/server/ui/en/payments/failed.html @@ -0,0 +1,57 @@ + + + + + + Payment Canceled + + + +
+

Payment Failed

+

Your payment has failed! Please try again or use a different payment method

+ +
+ + \ No newline at end of file diff --git a/src/webapp/main/src/views/admin/SettingsView.vue b/src/webapp/main/src/views/admin/SettingsView.vue index cdf0865..a874088 100644 --- a/src/webapp/main/src/views/admin/SettingsView.vue +++ b/src/webapp/main/src/views/admin/SettingsView.vue @@ -9,6 +9,9 @@