ticket generation working

This commit is contained in:
2023-08-06 18:37:21 +02:00
parent 2812ab9055
commit 32ed36b93f
26 changed files with 594 additions and 96 deletions

View File

@@ -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.

View File

@@ -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 );

View File

@@ -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' } );
}

View File

@@ -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 );

View File

@@ -0,0 +1 @@
{}

View File

@@ -33,7 +33,6 @@ module.exports.getDataSimple = ( db, column, searchQuery ) => {
} ).catch( error => {
reject( error );
} );
// resolve( '$2b$05$ElMYWoMjk7567lXkIkee.e.6cxCrWU4gkfuNLB8gmGYLQQPm7gT3O' );
} );
};

View File

@@ -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' ) {
//
}
} );
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -13,24 +13,27 @@ 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 ) => {
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.body.mail
'customer_email': req.session.username
};
db.getDataSimple( 'temp', 'user_id', req.session.id ).then( dat => {
@@ -53,7 +56,7 @@ module.exports = ( app, settings ) => {
}
}
const session = await stripe.checkout.sessions.create( purchase );
sessionReference[ session.id ] = req.session.id;
sessionReference[ session.id ] = { 'tok': req.session.id, 'email': req.session.username };
res.send( session.url );
} )();
} );
@@ -64,6 +67,9 @@ module.exports = ( app, settings ) => {
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();

View File

@@ -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.

View File

@@ -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 );

View File

@@ -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
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;
this.queueHandler();
// 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 );
} );
} );
}

View File

@@ -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( '' );

View File

@@ -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",

View File

@@ -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",

View File

@@ -4,11 +4,56 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment Canceled</title>
<style>
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
background-color: rgb(41, 40, 40);
color: white;
font-size: 150%;
}
.content {
width: 70%;
}
.submit {
margin-top: 2%;
background: linear-gradient(90deg, rgb(30, 36, 131), rgb(87, 66, 184), rgb(105, 115, 214), rgb(30, 36, 131), rgb(41, 128, 109), rgb(146, 50, 47));
background-size: 300px;
padding: 10px 20px;
border: none;
border-radius: 20px;
cursor: pointer;
transition: all 3s;
font-size: 75%;
color: white;
}
.submit:hover {
background-size: 200%;
background-position: -100%;
}
</style>
</head>
<body>
<div class="content">
<h1>Payment Canceled</h1>
<p>You have canceled your payment!</p>
<p>This was a mistake? Head back to the payment page!</p>
<button onclick="history.back()">Back to payment</button>
<button onclick="history.back()" class="submit">Back to payment</button>
<button onclick="location.href = '/cart'" class="submit">Back to the cart</button>
</div>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Two-Factor Authentication</title>
<style>
body {
font-family: sans-serif;
width: 100%;
height: 800px;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.content {
width: 80%;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.ip {
color: rgb(94, 94, 94);
}
.logo {
width: 70vw;
}
.verify {
padding: 20px 30px;
background-color: rgb(0, 7, 87);
text-decoration: none;
color: white;
transition: 0.5s all;
border-radius: 5px;
margin-bottom: 20px;
}
.verify:hover {
background-color: rgb(0, 12, 139);
}
@media only screen and (min-width: 999px) {
.logo {
width: 20vw;
}
.content {
width: 40vw;
}
}
</style>
</head>
<body>
<div class="content">
<img :src="host + '/otherAssets/logo.png'" alt="Logo" class="logo">
<h1>Thank you for your purchase at {{ pageName }}</h1>
<p>Attached you may find your tickets. Enjoy the event!</p>
<p>You may also find the tickets on your <a :href="host + '/account'">account page</a>.</p>
</div>
</body>
</html>

View File

@@ -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)

View File

@@ -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',

View File

@@ -33,7 +33,24 @@
saveTemplate() {
// Save to server instead
this.$refs.notification.createNotification( 'Saving...', 5, 'progress', '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 () {

View File

@@ -0,0 +1,133 @@
<!--
* libreevent - GuestPurchaseView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="wrapper">
<div class="content">
<h1 style="font-size: 250%;">Thank you for your purchase!</h1>
<p>The system is currently processing your order and you will be able to download your tickets within a moment's notice.</p>
<p>You will receive an email with your tickets within the next few minutes</p>
<p class="small">If the email does not arrive withing the next 10 minutes, please click <a href="/payments/resendTickets" target="_blank">here</a></p>
<button onclick="if ( confirm( 'Do you really want to leave this page? If you want to download the tickets directly, you will need to head to your account page and download the ticket from there or stay on this page and wait for the order to finish processing.' ) ) {
location.href = '/' }" class="submit">Back to the home page</button>
</div>
<notifications ref="notification" location="bottomright" size="bigger"></notifications>
</div>
</template>
<script>
import notifications from '@/components/notifications/notifications.vue';
export default {
name: 'PaymentSuccessView',
components: {
notifications
},
methods: {
},
data() {
return {}
},
created() {
if ( !!window.EventSource ) {
setTimeout( () => {
let startNotification = this.$refs.notification.createNotification( 'Connecting to status service...', 20, 'progress', 'normal' );
let source = new EventSource( localStorage.getItem( 'url' ) + '/payments/status', { withCredentials: true } );
let self = this;
source.onmessage = ( e ) => {
if ( e.data === 'ready' ) {
open( '/tickets/get' );
} else if ( e.data === 'paymentOk' ) {
self.$refs.notification.createNotification( 'Your payment has been marked as completed!', 5, 'ok', 'normal' );
}
}
source.onopen = e => {
self.$refs.notification.createNotification( 'Connected to status service', 5, 'ok', 'normal' );
self.$refs.notification.cancelNotification( startNotification );
};
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
if ( e.target.readyState == EventSource.CLOSED ) {
self.$refs.notification.cancelNotification( startNotification );
self.$refs.notification.createNotification( 'Could not connect to status service', 5, 'error', 'normal' );
}
}, false)
}, 300 );
} else {
setTimeout( () => {
this.$refs.notification.createNotification( 'Unsupported browser detected. Ticket generation might take longer!', 20, 'warning', 'normal' );
}, 300 );
// ping server every 5s to check if ticket ready
this.serverPing = setInterval( () => {
fetch( '/payments/status/ping' ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
if ( data ) {
if ( data.status === 'ready' ) {
open( '/tickets/get' );
} else if ( data.status === 'paymentOk' ) {
this.$refs.notification.createNotification( 'Your payment has been marked as completed!', 5, 'ok', 'normal' );
}
}
} );
} else {
console.error( 'Request failed' );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
}
} ).catch( error => {
console.error( error );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
} );
}, 5000 );
}
}
}
</script>
<style>
.wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
.content {
width: 70%;
}
.small {
font-size: 75%;
color: rgb(158, 158, 158);
}
.submit {
margin-top: 2%;
background: linear-gradient(90deg, rgb(30, 36, 131), rgb(87, 66, 184), rgb(105, 115, 214), rgb(30, 36, 131), rgb(41, 128, 109), rgb(146, 50, 47));
background-size: 300px;
padding: 10px 20px;
border: none;
border-radius: 20px;
cursor: pointer;
transition: all 3s;
font-size: 75%;
color: white;
}
.submit:hover {
background-size: 200%;
background-position: -100%;
}
</style>

View File

@@ -44,6 +44,7 @@
</td>
</tr>
</table>
<!-- TODO: Ask for permission to send emails (Make question sound really optional) -->
</form>
<notifications ref="notification" location="topright" size="bigger"></notifications>
<button @click="signup();" class="button">Sign up</button>