From 32ed36b93ffcab761afda7a23f6d99265d16a582 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Sun, 6 Aug 2023 18:37:21 +0200 Subject: [PATCH] ticket generation working --- README.md | 2 + src/server/admin/adminAPIRoutes.js | 2 +- src/server/admin/api/postHandler.js | 6 + src/server/app.js | 3 +- src/server/backend/db/data/tickets.json | 1 + src/server/backend/db/db.js | 1 - src/server/backend/db/jsonDataHelper.js | 2 +- src/server/backend/db/jsondb.js | 92 +++++++++++- src/server/backend/db/mysqldb.js | 2 +- src/server/backend/mail/mailSender.js | 1 + src/server/backend/payments/paymentHandler.js | 24 ---- .../payments/payrexx/payrexxRoutes.js} | 0 .../plugins/payments/stripe/stripeRoutes.js | 114 +++++++++------ src/server/backend/tickets/store/README.md | 3 + src/server/backend/tickets/test.js | 24 ++++ src/server/backend/tickets/ticketGenerator.js | 106 +++++++++++--- src/server/backend/userRoutes.js | 3 + src/server/package-lock.json | 17 +++ src/server/package.json | 1 + src/server/ui/en/payments/canceled.html | 53 ++++++- src/server/ui/en/payments/ticketMail.html | 69 +++++++++ src/webapp/main/notes.md | 2 + src/webapp/main/src/router/mainRoutes.js | 9 ++ .../views/admin/events/TicketEditorView.vue | 19 ++- .../views/purchasing/PaymentSuccessView.vue | 133 ++++++++++++++++++ src/webapp/main/src/views/user/SignupView.vue | 1 + 26 files changed, 594 insertions(+), 96 deletions(-) delete mode 100644 src/server/backend/payments/paymentHandler.js rename src/server/backend/{db/nedbDB.js => plugins/payments/payrexx/payrexxRoutes.js} (100%) create mode 100644 src/server/backend/tickets/store/README.md create mode 100644 src/server/backend/tickets/test.js create mode 100644 src/server/ui/en/payments/ticketMail.html create mode 100644 src/webapp/main/src/views/purchasing/PaymentSuccessView.vue diff --git a/README.md b/README.md index 8e171bc..469c48a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ Visit our [website](https://libreevent.janishutz.com) You may download this project using the GitHub releases page or the direct links on the [libreevent website](https://libreevent.janishutz.com/download) as this only downloads the ready-to-distribute version, not the development version. Alternatively, you may download the project directly from GitHub (by cloning it or downloading the code) but you'll have to compile and package the project [manually](https://libreevent.janishutz.com/docs/contributing/packaging). +THIS IS FREE SOFTWARE. IT IS PROVIDED "AS IS" AND AS SUCH COMES WITH ABSOLUTELY NO WARRANTY TO THE EXTENT PERMITTED BY APPLICABLE LAW. If anything does not work, please report it back, but do not expect it to be fixed immediately, as this software is developed by volunteers in their free time. + # Contributing If you want to contribute to this project, please read more [here](https://libreevent.janishutz.com/docs/contributing). Until the end of October 2023, no contributions can be accepted into master. diff --git a/src/server/admin/adminAPIRoutes.js b/src/server/admin/adminAPIRoutes.js index 18273f0..f4ddba4 100644 --- a/src/server/admin/adminAPIRoutes.js +++ b/src/server/admin/adminAPIRoutes.js @@ -31,7 +31,7 @@ module.exports = ( app ) => { } } ); - app.post( '/admin/API/:call', bodyParser.json(), ( req, res ) => { + app.post( '/admin/API/:call', bodyParser.json( { limit: '20mb' } ), ( req, res ) => { if ( req.session.loggedInAdmin ) { postHandler.handleCall( req.params.call, req.body, req.query.lang ).then( data => { res.send( data ); diff --git a/src/server/admin/api/postHandler.js b/src/server/admin/api/postHandler.js index a1012a2..28b793f 100644 --- a/src/server/admin/api/postHandler.js +++ b/src/server/admin/api/postHandler.js @@ -54,6 +54,12 @@ class POSTHandler { } ).catch( error => { reject( { 'code': 500, 'error': error } ); } ); + } else if ( call === 'saveTickets' ) { + db.writeJSONDataSimple( 'tickets', data.location, data.data ).then( resp => { + resolve( resp ); + } ).catch( error => { + reject( { 'code': 500, 'error': error } ); + } ); } else { reject( { 'code': 404, 'error': 'Route not found' } ); } diff --git a/src/server/app.js b/src/server/app.js index 97897e5..78cca10 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -92,7 +92,8 @@ if ( settings.init ) { console.log( '[ Server ] loading plugins' ); // TODO: load dynamically -require( './backend/plugins/payments/stripe/stripeRoutes.js' )( app, settings ); // setup routes +require( './backend/plugins/payments/stripe/stripeRoutes.js' )( app, settings ); // stripe routes +require( './backend/payments/paymentRoutes.js' )( app, settings ); // payment routes app.use( ( request, response ) => { response.sendFile( file ); diff --git a/src/server/backend/db/data/tickets.json b/src/server/backend/db/data/tickets.json index e69de29..9e26dfe 100644 --- a/src/server/backend/db/data/tickets.json +++ b/src/server/backend/db/data/tickets.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/server/backend/db/db.js b/src/server/backend/db/db.js index 6555955..233926d 100644 --- a/src/server/backend/db/db.js +++ b/src/server/backend/db/db.js @@ -33,7 +33,6 @@ module.exports.getDataSimple = ( db, column, searchQuery ) => { } ).catch( error => { reject( error ); } ); - // resolve( '$2b$05$ElMYWoMjk7567lXkIkee.e.6cxCrWU4gkfuNLB8gmGYLQQPm7gT3O' ); } ); }; diff --git a/src/server/backend/db/jsonDataHelper.js b/src/server/backend/db/jsonDataHelper.js index 0bb9b3c..ea7e50c 100644 --- a/src/server/backend/db/jsonDataHelper.js +++ b/src/server/backend/db/jsonDataHelper.js @@ -13,7 +13,7 @@ class DataHelper { constructor () { - + } } diff --git a/src/server/backend/db/jsondb.js b/src/server/backend/db/jsondb.js index 24fe39a..cd0b804 100644 --- a/src/server/backend/db/jsondb.js +++ b/src/server/backend/db/jsondb.js @@ -7,8 +7,98 @@ * */ +const fs = require( 'fs' ); +const path = require( 'path' ); + class JSONDB { constructor () { - + this.db = {}; + } + + connect ( ) { + this.db = JSON.parse( fs.readFileSync( path.join( __dirname + '/data/db.json' ) ) ); + setInterval( async () => { + fs.writeFile( path.join( __dirname + '/data/db.json' ), JSON.stringify( this.db ) ); + }, 10000 ); + console.log( '[ JSON-DB ] Database initialized successfully' ); + return 'connection'; + } + + query ( operation, table ) { + return new Promise( ( resolve, reject ) => { + /* + Possible operation.command values (all need the table argument of the method call): + - getAllData: no additional instructions needed + + - getFilteredData: + - operation.property (the column to search for the value), + - operation.searchQuery (the value to search for [will be sanitised by method]) + + - InnerJoin (Select values that match in both tables): + - operation.property (the column to search for the value), + - operation.searchQuery (the value to search for [will be sanitised by method]) + - operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id) + - operation.secondTable (The second table to perform Join operation with) + - operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id) + + - LeftJoin (Select values in first table and return all corresponding values of second table): + - operation.property (the column to search for the value), + - operation.searchQuery (the value to search for [will be sanitised by method]) + - operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id) + - operation.secondTable (The second table to perform Join operation with) + - operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id) + + - RightJoin (Select values in second table and return all corresponding values of first table): + - operation.property (the column to search for the value), + - operation.searchQuery (the value to search for [will be sanitised by method]) + - operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id) + - operation.secondTable (The second table to perform Join operation with) + - operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id) + + - addData: + - operation.data (key-value pair with all data as values and column to insert into as key) + + - updateData: + - operation.newValues (a object with keys being the column and value being the value to be inserted into that column, values are being + sanitised by the function) + - operation.property (the column to search for the value), + - operation.searchQuery (the value to search for [will be sanitised by method]) + + - checkDataAvailability: + - operation.property (the column to search for the value), + - operation.searchQuery (the value to search for [will be sanitised by method]) + + - fullCustomCommand: + - operation.query (the SQL instruction to be executed) --> NOTE: This command will not be sanitised, so use only with proper sanitisation! + */ + + if ( operation.command === 'getAllData' ) { + resolve( this.db[ table ] ); + } else if ( operation.command === 'getFilteredData' ) { + // + } else if ( operation.command === 'fullCustomCommand' ) { + // + } else if ( operation.command === 'addData' ) { + // + } else if ( operation.command === 'updateData' ) { + if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' ); + else { + // + } + } else if ( operation.command === 'deleteData' ) { + if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' ); + else { + // + } + } else if ( operation.command === 'InnerJoin' ) { + // + } else if ( operation.command === 'LeftJoin' ) { + // + } else if ( operation.command === 'RightJoin' ) { + // + } else if ( operation.command === 'checkDataAvailability' ) { + // + } + } ); } } \ No newline at end of file diff --git a/src/server/backend/db/mysqldb.js b/src/server/backend/db/mysqldb.js index f1b66e8..ad254ee 100644 --- a/src/server/backend/db/mysqldb.js +++ b/src/server/backend/db/mysqldb.js @@ -57,7 +57,7 @@ class SQLDB { } ); this.sqlConnection.query( 'CREATE TABLE libreevent_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, pass TEXT, name TEXT, first_name TEXT, two_fa TINYTEXT, user_data VARCHAR( 60000 ), mail_confirmed TINYTEXT, marketing_ok TINYTEXT, PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', ( error ) => { if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error; - this.sqlConnection.query( 'CREATE TABLE libreevent_orders ( order_id INT ( 10 ) NOT NULL AUTO_INCREMENT, account_id INT ( 10 ) NOT NULL, seats VARCHAR( 60000 ), PRIMARY KEY ( order_id ), FOREIGN KEY ( account_id ) REFERENCES libreevent_users( account_id ) ) ENGINE=INNODB;', ( error ) => { + this.sqlConnection.query( 'CREATE TABLE libreevent_orders ( order_id INT ( 10 ) NOT NULL AUTO_INCREMENT, order_name TINYTEXT, account_id INT ( 10 ) NOT NULL, tickets VARCHAR( 60000 ), PRIMARY KEY ( order_id ), FOREIGN KEY ( account_id ) REFERENCES libreevent_users( account_id ) ) ENGINE=INNODB;', ( error ) => { if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error; this.sqlConnection.query( 'CREATE TABLE libreevent_admin ( account_id INT NOT NULL AUTO_INCREMENT, email TINYTEXT, pass TEXT, permissions VARCHAR( 1000 ), username TINYTEXT, two_fa TINYTEXT, PRIMARY KEY ( account_id ) );', ( error ) => { if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error; diff --git a/src/server/backend/mail/mailSender.js b/src/server/backend/mail/mailSender.js index 7feaf6c..ee404b4 100644 --- a/src/server/backend/mail/mailSender.js +++ b/src/server/backend/mail/mailSender.js @@ -43,6 +43,7 @@ class MailManager { } sendMailWithAttachment ( recipient, html, subject, attachments, from ) { + // Attachments have to be an array of objects that have filename and path as their keys let text = html2text.convert( html, this.options ); let mailOptions = { from: from, diff --git a/src/server/backend/payments/paymentHandler.js b/src/server/backend/payments/paymentHandler.js deleted file mode 100644 index b2aeb0d..0000000 --- a/src/server/backend/payments/paymentHandler.js +++ /dev/null @@ -1,24 +0,0 @@ -/* -* libreevent - successHandler.js -* -* Created by Janis Hutz 08/02/2023, Licensed under the GPL V3 License -* https://janishutz.com, development@janishutz.com -* -* -*/ - -class PaymentHandler { - constructor () { - this.canceledTransactions = {}; - } - - async handleSuccess ( token ) { - console.log( token ); - } - - async handleError ( token ) { - - } -} - -module.exports = PaymentHandler; \ No newline at end of file diff --git a/src/server/backend/db/nedbDB.js b/src/server/backend/plugins/payments/payrexx/payrexxRoutes.js similarity index 100% rename from src/server/backend/db/nedbDB.js rename to src/server/backend/plugins/payments/payrexx/payrexxRoutes.js diff --git a/src/server/backend/plugins/payments/stripe/stripeRoutes.js b/src/server/backend/plugins/payments/stripe/stripeRoutes.js index 16d397d..00ced39 100644 --- a/src/server/backend/plugins/payments/stripe/stripeRoutes.js +++ b/src/server/backend/plugins/payments/stripe/stripeRoutes.js @@ -13,57 +13,63 @@ 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 bodyParser = require( 'body-parser' ); -const ph = require( '../../../payments/paymentHandler.js' ); -const paymentHandler = new ph(); +const ticket = require( '../../../tickets/ticketGenerator.js' ); +const TicketGenerator = new ticket(); const endpointSecret = stripConfig[ 'endpointSecret' ]; let sessionReference = {}; +let waitingClients = {}; +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 ) => { - let purchase = { - 'line_items': [], - 'mode': 'payment', - 'success_url': settings.yourDomain + '/payments/success', - 'cancel_url': settings.yourDomain + '/payments/canceled', - 'submit_type': 'book', - 'customer_email': req.body.mail - }; + if ( req.session.loggedInUser ) { + let purchase = { + 'line_items': [], + 'mode': 'payment', + 'success_url': settings.yourDomain + '/payments/success', + 'cancel_url': settings.yourDomain + '/payments/canceled', + 'submit_type': 'book', + 'customer_email': req.session.username + }; - 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[ 'line_items' ].push( { - 'price_data': { - 'product_data': { - 'name': data[ event ][ item ].name, + 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[ 'line_items' ].push( { + 'price_data': { + 'product_data': { + 'name': data[ event ][ item ].name, + }, + 'currency': events[ event ].currency, + 'unit_amount': Math.round( parseFloat( events[ event ][ 'categories' ][ data[ event ][ item ].category ].price[ data[ event ][ item ][ 'ticketOption' ] ] ) * 100 ), }, - 'currency': events[ event ].currency, - 'unit_amount': Math.round( parseFloat( events[ event ][ 'categories' ][ data[ event ][ item ].category ].price[ data[ event ][ item ][ 'ticketOption' ] ] ) * 100 ), - }, - 'quantity': data[ event ][ item ].count ?? 1, - } ); + 'quantity': data[ event ][ item ].count ?? 1, + } ); + } } - } - const session = await stripe.checkout.sessions.create( purchase ); - sessionReference[ session.id ] = req.session.id; - res.send( session.url ); - } )(); - } ); - } else { - res.status( 400 ).send( 'ERR_UID_NOT_FOUND' ); - } - } ).catch( error => { - console.error( '[ STRIPE ] DB ERROR: ' + error ); - res.status( 500 ).send( 'ERR_DB' ); - } ); + const session = await stripe.checkout.sessions.create( purchase ); + sessionReference[ session.id ] = { 'tok': req.session.id, 'email': req.session.username }; + res.send( session.url ); + } )(); + } ); + } 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 ) => { @@ -75,7 +81,16 @@ module.exports = ( app, settings ) => { response.status( 200 ); response.flushHeaders(); response.write( 'data: connected\n\n' ); - // TODO: Finish up + waitingClients[ request.session.id ] = response; + } ); + + app.get( '/user/2fa/ping', ( request, response ) => { + if ( paymentOk[ request.session.token ] === 'ok' ) { + delete paymentOk[ request.session.token ]; + response.send( { 'status': 'ok' } ); + } else { + response.send( '' ); + } } ); app.post( '/payments/webhook', bodyParser.raw( { type: 'application/json' } ), ( req, res ) => { @@ -92,7 +107,24 @@ module.exports = ( app, settings ) => { } if ( event.type === 'checkout.session.completed' ) { - paymentHandler.handleSuccess( sessionReference[ event.data.object.id ] ); + setTimeout( () => { + waitingClients[ sessionReference[ event.data.object.id ][ 'tok' ] ].write( 'data: paymentOk\n\n' ); + }, 2000 ); + db.getDataSimple( 'temp', 'user_id', sessionReference[ event.data.object.id ][ 'tok' ] ).then( dat => { + db.getDataSimple( 'users', 'email', sessionReference[ event.data.object.id ][ 'email' ] ).then( user => { + if ( user[ 0 ] ) { + console.log( sessionReference[ event.data.object.id ][ 'tok' ] ); + db.writeDataSimple( 'orders', 'account_id', user[ 0 ].account_id, { 'account_id': user[ 0 ].account_id, 'tickets': dat[ 0 ].data, 'order_name': sessionReference[ event.data.object.id ][ 'tok' ] } ).then( () => { + TicketGenerator.generateTickets( sessionReference[ event.data.object.id ] ); + } ); + } else { + console.log( sessionReference[ event.data.object.id ][ 'email' ] ); + console.error( 'user not found' ); + } + } ); + } ).catch( err => { + console.error( err ); + } ); } res.status( 200 ).end(); diff --git a/src/server/backend/tickets/store/README.md b/src/server/backend/tickets/store/README.md new file mode 100644 index 0000000..8d693c1 --- /dev/null +++ b/src/server/backend/tickets/store/README.md @@ -0,0 +1,3 @@ +# Ticket Store + +Here, all the tickets for all the orders are saved. You may delete the tickets here at any point. Should the user request their tickets once again, the tickets can simply be regenerated from the data stored in the database. \ No newline at end of file diff --git a/src/server/backend/tickets/test.js b/src/server/backend/tickets/test.js new file mode 100644 index 0000000..35a7734 --- /dev/null +++ b/src/server/backend/tickets/test.js @@ -0,0 +1,24 @@ +/* +* libreevent - test.js +* +* Created by Janis Hutz 08/06/2023, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +const express = require( 'express' ); +let app = express(); +const ticket = require( './ticketGenerator.js' ); +const TicketGenerator = new ticket(); +const http = require( 'http' ); + + +app.get( '/', ( request, response ) => { + response.send( 'ok' ); + TicketGenerator.generateTickets( { 'tok': 'hGids5PVsHm_KiK-Wd-8ekvwxpuUPrUX', 'email': 'info@janishutz.com' } ); +} ); + + +const PORT = process.env.PORT || 8080; +http.createServer( app ).listen( PORT ); \ No newline at end of file diff --git a/src/server/backend/tickets/ticketGenerator.js b/src/server/backend/tickets/ticketGenerator.js index 6dba778..f056f05 100644 --- a/src/server/backend/tickets/ticketGenerator.js +++ b/src/server/backend/tickets/ticketGenerator.js @@ -10,18 +10,34 @@ const pdfme = require( '@pdfme/generator' ); const db = require( '../db/db.js' ); +const pdfLib = require( 'pdf-lib' ); +const fs = require( 'fs' ); +const path = require( 'path' ); +const mm = require( '../mail/mailSender.js' ); +const mailManager = new mm(); +let createSSRApp = require( 'vue' ).createSSRApp; +let renderToString = require( 'vue/server-renderer' ).renderToString; + +const settings = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../config/settings.config.json' ) ) ); class TicketGenerator { constructor () { this.ticketQueue = {}; this.jobId = 0; + this.currentlyRunningJob = 0; this.isRunning = false; + db.getJSONData( 'tickets' ).then( tickets => { + this.tickets = tickets; + } ); + db.getJSONData( 'events' ).then( events => { + this.events = events; + } ); } // TODO: Save to disk in case of crash of server / reboot / whatever // and continue processing once back online - generateTicket ( event, data ) { - this.ticketQueue [ this.jobId ] = { 'event': event, 'data': data }; + generateTickets ( order ) { + this.ticketQueue [ this.jobId ] = { 'order': order }; this.jobId += 1; this.queueHandler(); } @@ -30,28 +46,78 @@ class TicketGenerator { queueHandler () { if ( !this.isRunning ) { this.isRunning = true; - this.ticketGenerator( this.ticketQueue[ this.jobId ][ 'event' ], this.ticketQueue[ this.jobId ][ 'data' ] ).then( pdf => { - console.log( pdf ); - // TODO: Maybe write to disk - this.isRunning = false; - this.queueHandler(); - } ).catch( error => { - console.error( '[ PDF GENERATOR ] ERROR: ' + error ); - this.isRunning = false; - this.queueHandler(); - // TODO: Add to FAILED db - } ); + if ( this.ticketQueue[ this.currentlyRunningJob ] ) { + this.ticketGenerator( this.ticketQueue[ this.currentlyRunningJob ][ 'order' ] ).then( res => { + this.currentlyRunningJob += 1; + if ( res.status ) { + db.getDataSimple( 'users', 'account_id', res.user ).then( dat => { + if ( dat[ 0 ] ) { + ( async () => { + const app = createSSRApp( { + data() { + return { + host: settings.yourDomain, + pageName: settings.name, + }; + }, + template: '' + fs.readFileSync( path.join( __dirname + '/../../ui/en/payments/ticketMail.html' ) ) + } ); + + console.log( dat[ 0 ].email ); + mailManager.sendMailWithAttachment( dat[ 0 ].email, await renderToString( app ), 'Thank you for your order', [ + { + 'filename': 'tickets.pdf', + 'path': res.file, + } + ], settings.mailSender + ); + // db.writeDataSimple( 'orders', 'order_name', res.order, { 'processed': 'true' } ); + } )(); + } + } ); + } + this.isRunning = false; + this.queueHandler(); + } ).catch( error => { + console.error( '[ PDF GENERATOR ] ERROR: ' + error ); + this.isRunning = false; + // TODO: Add to FAILED db + } ); + } } } - ticketGenerator ( event, data ) { + ticketGenerator ( order ) { return new Promise( ( resolve, reject ) => { - db.getJSONDataSimple( event ).then( template => { - pdfme.generate( { template, data } ).then( pdf => { - resolve( pdf ); - } ).catch( error => { - reject( error ); - } ); + db.getDataSimple( 'orders', 'order_name', order.tok ).then( ord => { + if ( ord[ 0 ] ) { + ( async () => { + let doc = await pdfLib.PDFDocument.create(); + let pages = []; + const order = JSON.parse( ord[ 0 ].tickets ); + for ( let event in order ) { + const template = this.tickets[ event ]; + for ( let ticket in order[ event ] ) { + const data = [ { + 'locationAndTime': this.events[ event ][ 'date' ], + 'ticketName': order[ event ][ ticket ][ 'name' ], + 'ticketQRCode': ord[ 0 ].order_name + '_' + order[ event ][ ticket ][ 'id' ], + } ]; + const page = await pdfLib.PDFDocument.load( await pdfme.generate( { 'template': template, 'inputs': data } ) ); + const p = await doc.copyPages( page, page.getPageIndices() ); + pages.push( p ); + p.forEach( ( page ) => doc.addPage( page ) ); + } + } + const f = path.join( __dirname + '/store/' + ord[ 0 ].order_name + '.pdf' ); + fs.writeFileSync( f, await doc.save() ); + resolve( { 'status': true, 'file': f, 'user': ord[ 0 ].account_id, 'order': ord[ 0 ].order_name } ); + } )(); + } else { + reject( 'ERR_NO_ORDER' ); + } + } ).catch( err => { + console.error( err ); } ); } ); } diff --git a/src/server/backend/userRoutes.js b/src/server/backend/userRoutes.js index ca3ad76..1c3ae8e 100644 --- a/src/server/backend/userRoutes.js +++ b/src/server/backend/userRoutes.js @@ -106,6 +106,8 @@ module.exports = ( app, settings ) => { request.session.loggedInUser = true; if ( responseObjects[ request.body.token ] ) { responseObjects[ request.body.token ].write( 'data: authenticated\n\n' ); + responseObjects[ request.body.token ].end(); + delete responseObjects[ request.body.token ]; } else { authOk[ request.body.token ] = 'ok'; } @@ -127,6 +129,7 @@ module.exports = ( app, settings ) => { app.get( '/user/2fa/ping', ( request, response ) => { if ( authOk[ request.session.token ] === 'ok' ) { + delete authOk[ request.session.token ]; response.send( { 'status': 'ok' } ); } else { response.send( '' ); diff --git a/src/server/package-lock.json b/src/server/package-lock.json index 430722a..d341965 100644 --- a/src/server/package-lock.json +++ b/src/server/package-lock.json @@ -19,6 +19,7 @@ "html-to-text": "^9.0.5", "mysql": "^2.18.1", "nodemailer": "^6.9.3", + "pdf-lib": "^1.17.1", "serve-favicon": "^2.5.0", "serve-static": "^1.15.0", "stripe": "^12.14.0", @@ -1991,6 +1992,22 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/peberminta": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", diff --git a/src/server/package.json b/src/server/package.json index 8e8db35..1547f92 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -52,6 +52,7 @@ "html-to-text": "^9.0.5", "mysql": "^2.18.1", "nodemailer": "^6.9.3", + "pdf-lib": "^1.17.1", "serve-favicon": "^2.5.0", "serve-static": "^1.15.0", "stripe": "^12.14.0", diff --git a/src/server/ui/en/payments/canceled.html b/src/server/ui/en/payments/canceled.html index 6b560d0..d645a23 100644 --- a/src/server/ui/en/payments/canceled.html +++ b/src/server/ui/en/payments/canceled.html @@ -4,11 +4,56 @@ Payment Canceled + -

Payment Canceled

-

You have canceled your payment!

-

This was a mistake? Head back to the payment page!

- +
+

Payment Canceled

+

You have canceled your payment!

+

This was a mistake? Head back to the payment page!

+ + +
\ No newline at end of file diff --git a/src/server/ui/en/payments/ticketMail.html b/src/server/ui/en/payments/ticketMail.html new file mode 100644 index 0000000..a43e17f --- /dev/null +++ b/src/server/ui/en/payments/ticketMail.html @@ -0,0 +1,69 @@ + + + + + + Two-Factor Authentication + + + +
+ +

Thank you for your purchase at {{ pageName }}

+

Attached you may find your tickets. Enjoy the event!

+

You may also find the tickets on your account page.

+
+ + \ No newline at end of file diff --git a/src/webapp/main/notes.md b/src/webapp/main/notes.md index bc2da71..fa294ac 100644 --- a/src/webapp/main/notes.md +++ b/src/webapp/main/notes.md @@ -7,6 +7,8 @@ - Require user to confirm email before purchasing +- Guest purchase in the future (remove from matura shit) + - Create password changing endpoint (to reset forgotten pwd) - Add Admin profile (page to change account settings per person like changing pwd) diff --git a/src/webapp/main/src/router/mainRoutes.js b/src/webapp/main/src/router/mainRoutes.js index c9456ea..8e96fa3 100644 --- a/src/webapp/main/src/router/mainRoutes.js +++ b/src/webapp/main/src/router/mainRoutes.js @@ -125,6 +125,15 @@ export default [ transition: 'scale' } }, + { + path: '/payments/success', + name: 'paymentSuccess', + component: () => import( '@/views/purchasing/PaymentSuccessView.vue' ), + meta: { + title: 'Payment successful - ', + transition: 'scale' + } + }, { path: '/admin/seatplan', name: 'adminSeatplanEditor', diff --git a/src/webapp/main/src/views/admin/events/TicketEditorView.vue b/src/webapp/main/src/views/admin/events/TicketEditorView.vue index 4135f0e..a46f81f 100644 --- a/src/webapp/main/src/views/admin/events/TicketEditorView.vue +++ b/src/webapp/main/src/views/admin/events/TicketEditorView.vue @@ -33,7 +33,24 @@ saveTemplate() { // Save to server instead this.$refs.notification.createNotification( 'Saving...', 5, 'progress', 'normal' ); - this.$refs.notification.createNotification( 'Saved successfully', 5, 'ok', 'normal' ); + let fetchOptions = { + method: 'post', + body: JSON.stringify( { 'location': sessionStorage.getItem( 'selectedTicket' ), 'data': this.designer.getTemplate() } ), + headers: { + 'Content-Type': 'application/json', + 'charset': 'utf-8' + } + }; + + fetch( '/admin/api/saveTickets', fetchOptions ).then( res => { + if ( res.status === 200 ) { + res.text().then( text => { + // TODO: Finish up + console.log( text ); + this.$refs.notification.createNotification( 'Saved successfully', 5, 'ok', 'normal' ); + } ); + } + } ); console.log( this.designer.getTemplate() ); }, testNotifications () { diff --git a/src/webapp/main/src/views/purchasing/PaymentSuccessView.vue b/src/webapp/main/src/views/purchasing/PaymentSuccessView.vue new file mode 100644 index 0000000..0208a67 --- /dev/null +++ b/src/webapp/main/src/views/purchasing/PaymentSuccessView.vue @@ -0,0 +1,133 @@ + + + + + + + \ No newline at end of file diff --git a/src/webapp/main/src/views/user/SignupView.vue b/src/webapp/main/src/views/user/SignupView.vue index a5f1b8e..2f41192 100644 --- a/src/webapp/main/src/views/user/SignupView.vue +++ b/src/webapp/main/src/views/user/SignupView.vue @@ -44,6 +44,7 @@ +