diff --git a/.gitignore b/.gitignore index b1b5db6..ce1137d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules *.secret.json apple_private_key.p8 -musicplayerv2-server.zip \ No newline at end of file +musicplayerv2-server.zip +dist \ No newline at end of file diff --git a/backend/dist/config b/backend/config/config similarity index 100% rename from backend/dist/config rename to backend/config/config diff --git a/backend/config/sdk.config.testing.json b/backend/config/sdk.config.testing.json new file mode 100644 index 0000000..9cc41f2 --- /dev/null +++ b/backend/config/sdk.config.testing.json @@ -0,0 +1,6 @@ +{ + "token": "phafowegoväbwpb$weapvbpvfwcvfäawef39'ü0wtäqgpt5^ü62q'ẗ9wäa3g", + "name": "localhost:8082", + "client": "localhost:8081", + "backendURL": "http://localhost:8080" +} \ No newline at end of file diff --git a/backend/dist/app.js b/backend/dist/app.js deleted file mode 100644 index 84d928d..0000000 --- a/backend/dist/app.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const express_1 = __importDefault(require("express")); -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); -const cors_1 = __importDefault(require("cors")); -if (typeof (__dirname) === 'undefined') { - __dirname = path_1.default.resolve(path_1.default.dirname('')); -} -const run = () => { - let app = (0, express_1.default)(); - app.use((0, cors_1.default)({ - credentials: true, - origin: true - })); - app.get('/', (request, response) => { - response.send('HELLO WORLD'); - }); - app.get('/getAppleMusicDevToken', (req, res) => { - // sign dev token - const privateKey = fs_1.default.readFileSync(path_1.default.join(__dirname + '/config/apple_private_key.p8')).toString(); - // TODO: Remove secret - const config = JSON.parse('' + fs_1.default.readFileSync(path_1.default.join(__dirname + '/config/apple-music-api.config.secret.json'))); - const now = new Date().getTime(); - const tomorrow = now + 24 * 3600 * 1000; - const jwtToken = jsonwebtoken_1.default.sign({ - 'iss': config.teamID, - 'iat': Math.floor(now / 1000), - 'exp': Math.floor(tomorrow / 1000), - }, privateKey, { - algorithm: "ES256", - keyid: config.keyID - }); - res.send(jwtToken); - }); - app.use((request, response, next) => { - response.status(404).send('ERR_NOT_FOUND'); - // response.sendFile( path.join( __dirname + '' ) ) - }); - const PORT = process.env.PORT || 8081; - app.listen(PORT); -}; -exports.default = { - run -}; diff --git a/backend/package-lock.json b/backend/package-lock.json index f771de5..eddb572 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,12 +16,30 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "express": "^4.19.2", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "node-mysql": "^0.4.2", + "oauth-janishutz-client-server": "file:../../oauth/client/server/dist" }, "devDependencies": { "typescript": "^5.4.5" } }, + "../../oauth/client/server/dist": { + "name": "oauth-janishutz-client-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/body-parser": "^1.19.5", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "body-parser": "^1.20.2", + "express": "^4.19.2", + "express-session": "^1.18.0" + }, + "devDependencies": { + "typescript": "^5.3.3" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -156,6 +174,20 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/better-js-class": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/better-js-class/-/better-js-class-0.1.3.tgz", + "integrity": "sha512-EMgtYym2RSQ75pUfOqKmCDL5EKur62Ec83aggzkJ942RM/mwskb6QDLaJJz93EwSK1dsfkPUFe+nlmxEbtBKDQ==" + }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -250,6 +282,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -263,6 +301,11 @@ "node": ">= 0.10" } }, + "node_modules/cps": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cps/-/cps-1.0.2.tgz", + "integrity": "sha512-kFE6vY9PBrN1sogxTxWa8ktnmfcKeR2I7+8tQgxqWndSgToerwITuprBs4FeQWEeMf+IGxo+ILixT/RK0wcIPQ==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -577,6 +620,12 @@ "node": ">= 0.10" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -731,6 +780,27 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "license": "MIT", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -740,6 +810,21 @@ "node": ">= 0.6" } }, + "node_modules/node-mysql": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-mysql/-/node-mysql-0.4.2.tgz", + "integrity": "sha512-V/AVfW/d47Wsb2c8AfEu9Q8PiHUG7/3SbDJ03HFycMMrlKSXj5bNItnHgjSSA6N60/1JXmhoJYXJESSS5BYV9A==", + "dependencies": { + "better-js-class": "*", + "cps": "*", + "mysql": "*", + "underscore": "*" + } + }, + "node_modules/oauth-janishutz-client-server": { + "resolved": "../../oauth/client/server/dist", + "link": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -785,6 +870,12 @@ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "license": "MIT" }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -837,6 +928,27 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -961,6 +1073,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -970,6 +1091,21 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1006,6 +1142,12 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -1021,6 +1163,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 3c9abfe..8849a82 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,6 +27,8 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "express": "^4.19.2", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "node-mysql": "^0.4.2", + "oauth-janishutz-client-server": "file:../../oauth/client/server/dist" } } diff --git a/backend/src/account.ts b/backend/src/account.ts new file mode 100644 index 0000000..21fdbef --- /dev/null +++ b/backend/src/account.ts @@ -0,0 +1,50 @@ +import db from './storage/db'; + + +const createUser = ( uid: string, username: string, email: string ): Promise => { + return new Promise( ( resolve, reject ) => { + db.writeDataSimple( 'users', 'uid', uid, { 'uid': uid, 'username': username, 'email': email } ).then( () => { + resolve( true ); + } ).catch( err => { + reject( err ); + } ); + } ); +} + +const saveUserData = ( uid: string, data: object ): Promise => { + return new Promise( ( resolve, reject ) => { + db.writeDataSimple( 'users', 'uid', uid, { 'data': data } ).then( () => { + resolve( true ); + } ).catch( err => { + reject( err ); + } ); + } ); +} + +const checkUser = ( uid: string ): Promise => { + return new Promise( ( resolve, reject ) => { + db.checkDataAvailability( 'users', 'uid', uid ).then( res => { + resolve( res ); + } ).catch( err => { + reject( err ); + } ) + } ); +} + + +const getUserData = ( uid: string ): Promise => { + return new Promise( ( resolve, reject ) => { + db.getDataSimple( 'users', 'uid', uid ).then( data => { + resolve( data ); + } ).catch( err => { + reject( err ); + } ); + } ); +} + +export default { + createUser, + saveUserData, + checkUser, + getUserData +} \ No newline at end of file diff --git a/backend/src/app.ts b/backend/src/app.ts index 9d541c1..bccc914 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,15 +1,20 @@ import express from 'express'; import path from 'path'; import fs from 'fs'; -import bodyParser from 'body-parser'; import jwt from 'jsonwebtoken'; import cors from 'cors'; +import account from './account'; +import sdk from 'oauth-janishutz-client-server'; declare let __dirname: string | undefined if ( typeof( __dirname ) === 'undefined' ) { __dirname = path.resolve( path.dirname( '' ) ); } +// TODO: Change config file, as well as in main.ts, index.html, oauth, if deploying there +const sdkConfig = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/sdk.config.testing.json' ) ) ); +// const sdkConfig: AuthSDKConfig = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/sdk.config.secret.json' ) ) ); + const run = () => { let app = express(); app.use( cors( { @@ -17,8 +22,29 @@ const run = () => { origin: true } ) ); + // Load id.janishutz.com SDK and allow signing in + sdk.routes( app, ( uid: string ) => { + return new Promise( ( resolve, reject ) => { + account.checkUser( uid ).then( stat => { + resolve( stat ); + } ).catch( e => { + reject( e ); + } ); + } ); + }, + ( uid: string, email: string, username: string ) => { + return new Promise( ( resolve, reject ) => { + account.createUser( uid, username, email ).then( stat => { + resolve( stat ); + } ).catch( e => { + reject( e ); + } ); + } ); + }, sdkConfig ); + + app.get( '/', ( request, response ) => { - response.send( 'HELLO WORLD' ); + response.send( 'Please visit https://music.janishutz.com to use this service' ); } ); diff --git a/backend/src/storage/db.ts b/backend/src/storage/db.ts new file mode 100644 index 0000000..d43d824 --- /dev/null +++ b/backend/src/storage/db.ts @@ -0,0 +1,330 @@ +/* +* libreevent - db.js +* +* Created by Janis Hutz 03/26/2023, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +import path from 'path'; +import fs from 'fs'; +import * as sqlDB from './mysqldb.js'; + +declare let __dirname: string | undefined +if ( typeof( __dirname ) === 'undefined' ) { + __dirname = path.resolve( path.dirname( '' ) ); +} else { + __dirname = __dirname + '/../'; +} + +const dbRef = { + 'user': 'jh_store_users', + 'users': 'jh_store_users', +}; + + +let dbh = new sqlDB.SQLDB(); +dbh.connect(); + +/** + * Initialize database (create tables, etc) + * @returns {undefined} + */ +const initDB = (): undefined => { + ( async() => { + console.log( '[ DB ] Setting up...' ); + dbh.setupDB(); + console.log( '[ DB ] Setting up complete!' ); + } )(); +}; + +/** + * Retrieve data from the database + * @param {string} db The name of the database + * @param {string} column The name of the column of the data-table in which to search for the searchQuery + * @param {string} searchQuery The query for the selected column + * @returns {Promise} Returns a promise that resolves to an object containing the results. + */ +const getDataSimple = ( db: string, column: string, searchQuery: string ): Promise => { + return new Promise( ( resolve, reject ) => { + dbh.query( { 'command': 'getFilteredData', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( data => { + resolve( data ); + } ).catch( error => { + reject( error ); + } ); + } ); +}; + +/** + * Use the SQL LeftJoin function to obtain data from DB. + * @param {string} db DB name to get data from + * @param {string} column The column in the DB in which to search for the searchQuery + * @param {string} searchQuery The data to look for in the selected column + * @param {string} secondTable The second table on which to perform the left join function + * @param {object} columns The columns to return, list of objects: { 'db': TABLE NAME, 'column': COLUMN NAME }) + * @param {string} nameOfMatchingParam Which properties should be matched to get the data, e.g. order.user_id=users.id + * @returns {Promise} Returns all records from the db and all matching data specified with the matchingParam from the secondTable. + */ +const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: string, secondTable: string, columns: object, nameOfMatchingParam: string ): Promise => { + /* + 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.columns (The columns of both tables to be selected, list of objects: { 'db': TABLE NAME, 'column': COLUMN NAME }) + - 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) + */ + return new Promise( ( resolve, reject ) => { + let settings = { + 'command': 'LeftJoin', + 'property': column, + 'searchQuery': searchQuery, + 'selection': '', + 'secondTable': dbRef[ secondTable ], + 'matchingParam': dbRef[ db ] + '.' + nameOfMatchingParam + '=' + dbRef[ secondTable ] + '.' + nameOfMatchingParam, + } + for ( let el in columns ) { + settings.selection += dbRef[ columns[ el ].db ] + '.' + columns[ el ].column + ','; + } + + settings.selection = settings.selection.slice( 0, settings.selection.length - 1 ); + dbh.query( settings, dbRef[ db ] ).then( data => { + resolve( data ); + } ).catch( error => { + reject( error ); + } ); + } ); +}; + +/** + * Get all data from the selected database + * @param {string} db The database of which all data should be retrieved + * @returns {Promise} Returns an object containing all data + */ +const getData = ( db: string ): Promise => { + return new Promise( ( resolve, reject ) => { + dbh.query( { 'command': 'getAllData' }, dbRef[ db ] ).then( data => { + resolve( data ); + } ).catch( error => { + reject( error ); + } ); + } ); +}; + +/** + * Write data to the database + * @param {string} db The database in which the data should be written + * @param {string} column The column in which to search for the data + * @param {string} searchQuery The query with which to search + * @param {string} data The data to write. Also include the column & searchQuery parameters, if they also need to be added + * @returns {Promise} Returns a promise that resolves to the interaction module return. + */ +const writeDataSimple = ( db: string, column: string, searchQuery: string, data: any ): Promise => { + return new Promise( ( resolve, reject ) => { + dbh.query( { 'command': 'checkDataAvailability', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( res => { + if ( res.length > 0 ) { + dbh.query( { 'command': 'updateData', 'property': column, 'searchQuery': searchQuery, 'newValues': data }, dbRef[ db ] ).then( dat => { + resolve( dat ); + } ).catch( error => { + reject( error ); + } ); + } else { + dbh.query( { 'command': 'addData', 'data': data }, dbRef[ db ] ).then( dat => { + resolve( dat ); + } ).catch( error => { + reject( error ); + } ); + } + } ).catch( error => { + reject( error ); + } ); + } ); +}; + +/** + * Delete data from the database + * @param {string} db The database from which the data should be deleted + * @param {string} column The column in which to search for the data + * @param {string} searchQuery The query with which to search + * @returns {Promise} Returns a promise that resolves to the interaction module return. + */ +const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Promise => { + return new Promise( ( resolve, reject ) => { + dbh.query( { 'command': 'deleteData', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( dat => { + resolve( dat ); + } ).catch( error => { + reject( error ); + } ); + } ); +}; + +/** + * Check if the data is available in the database. + * @param {string} db The database in which to check + * @param {string} column The column in which to search for the data + * @param {string} searchQuery The query with which to search + * @returns {Promise} Returns a promise that resolves to a boolean (true = is available) + */ +const checkDataAvailability = ( db: string, column: string, searchQuery: string ): Promise => { + return new Promise( ( resolve, reject ) => { + dbh.query( { 'command': 'checkDataAvailability', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( res => { + if ( res.length > 0 ) { + resolve( true ); + } else { + resolve( false ); + } + } ).catch( error => { + reject( error ); + } ); + } ); +}; + +/** + * Load multiple JSON files at once + * @param {Array} files The files which to load + * @returns {Promise} Returns the data from all files + */ +const getJSONDataBatch = async ( files: Array ): Promise => { + let allFiles = {}; + for ( let file in files ) { + try { + allFiles[ files[ file ] ] = await getJSONData( files[ file ] ); + } catch( err ) { + allFiles[ files[ file ] ] = 'ERROR: ' + err; + } + } + return allFiles; +} + +/** + * Load all data from a JSON file + * @param {string} file The file to load (just file name, file must be in "/data/" folder, no file extension) + * @returns {Promise} The data that was loaded + */ +const getJSONData = ( file: string ): Promise => { + return new Promise( ( resolve, reject ) => { + fs.readFile( path.join( __dirname + '/' + file + '.json' ), ( error, data ) => { + if ( error ) { + reject( 'Error occurred: Error trace: ' + error ); + } else { + if ( data.byteLength > 0 ) { + resolve( JSON.parse( data.toString() ) ?? {} ); + } else { + resolve( { } ); + } + } + } ); + } ); +}; + +/** + * Load some data from a JSON file + * @param {string} file The file to load (just file name, file must be in "/data/" folder, no file extension) + * @param {string} identifier The identifier of the element which should be loaded + * @returns {Promise} The data that was loaded + */ +const getJSONDataSimple = ( file: string, identifier: string ): Promise => { + return new Promise( ( resolve, reject ) => { + fs.readFile( path.join( __dirname + '/' + file + '.json' ), ( error, data ) => { + if ( error ) { + reject( 'Error occurred: Error trace: ' + error ); + } else { + if ( data.byteLength > 0 ) { + resolve( JSON.parse( data.toString() )[ identifier ] ?? {} ); + } else { + resolve( { } ); + } + } + } ); + } ); +}; + +/** + * Get JSON data, but synchronous (blocking) + * @param {string} file The file to be loaded (path relative to root) + * @returns {object} Returns the JSON file + */ +const getJSONDataSync = ( file: string ): Object => { + return JSON.parse( fs.readFileSync( path.join( __dirname + '/' + file ) ).toString() ); +}; + +/** + * Description + * @param {any} db:string + * @param {any} identifier:string + * @param {any} values:any + * @returns {any} + */ +const writeJSONDataSimple = ( db: string, identifier: string, values: any ) => { + return new Promise( ( resolve, reject ) => { + fs.readFile( path.join( __dirname + '/../../data/' + db + '.json' ), ( error, data ) => { + if ( error ) { + reject( 'Error occurred: Error trace: ' + error ); + } else { + let dat = {}; + if ( data.byteLength > 0 ) { + dat = JSON.parse( data.toString() ) ?? {}; + } + dat[ identifier ] = values; + fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), ( error ) => { + if ( error ) { + reject( 'Error occurred: Error trace: ' + error ); + } + resolve( true ); + } ); + } + } ); + } ); +}; + +/** + * Write data to a JSON file + * @param {string} db The database to write into + * @param {object} data The data to write + * @returns {Promise} + */ +const writeJSONData = ( db: string, data: object ): Promise => { + return new Promise( ( resolve, reject ) => { + fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( data ), ( error ) => { + if ( error ) { + reject( 'Error occurred: Error trace: ' + error ); + } else { + resolve( true ); + } + } ); + } ); +}; + +/** + * Delete data from a JSON file + * @param {string} db The file to delete from (just filename, has to be in "/data/" folder, no file extension) + * @param {string} identifier The identifier of the element to delete + * @returns {Promise} Returns a promise that resolves to a boolean + */ +const deleteJSONDataSimple = ( db: string, identifier: string ): Promise => { + return new Promise( ( resolve, reject ) => { + fs.readFile( path.join( __dirname + '/../../data/' + db + '.json' ), ( error, data ) => { + if ( error ) { + reject( 'Error occurred: Error trace: ' + error ); + } else { + let dat = {}; + if ( data.byteLength > 0 ) { + dat = JSON.parse( data.toString() ) ?? {}; + } + delete dat[ identifier ]; + fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), ( error ) => { + if ( error ) { + reject( 'Error occurred: Error trace: ' + error ); + } + resolve( true ); + } ); + } + } ); + } ); +}; + +export default { initDB, checkDataAvailability, deleteDataSimple, deleteJSONDataSimple, getData, + getDataSimple, getDataWithLeftJoinFunction, getJSONData, getJSONDataBatch, getJSONDataSimple, + getJSONDataSync, writeDataSimple, writeJSONData, writeJSONDataSimple +}; diff --git a/backend/src/storage/mysqldb.ts b/backend/src/storage/mysqldb.ts new file mode 100644 index 0000000..32ae4e7 --- /dev/null +++ b/backend/src/storage/mysqldb.ts @@ -0,0 +1,190 @@ +/* +* libreevent - mysqldb.js +* +* Created by Janis Hutz 07/12/2023, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +import mysql from 'mysql'; +import fs from 'fs'; +import path from 'path'; + +declare let __dirname: string | undefined +if ( typeof( __dirname ) === 'undefined' ) { + __dirname = path.resolve( path.dirname( '' ) ); +} else { + __dirname = __dirname + '/../'; +} + +// If the connection does not work for you, you will need to add your ip +// to the whitelist of the database + +class SQLConfig { + command: string; + property?: string; + searchQuery?: string; + selection?: string; + query?: string; + newValues?: object; + secondTable?: string; + matchingParam?: string; + data?: object; +} + +class SQLDB { + sqlConnection: mysql.Connection; + isRecovering: boolean; + config: object; + constructor () { + this.config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/db.config.secret.json' ) ) ); + this.sqlConnection = mysql.createConnection( this.config ); + this.isRecovering = false; + } + + connect () { + return new Promise( ( resolve, reject ) => { + const self = this; + if ( this.isRecovering ) { + console.log( '[ SQL ] Attempting to recover from critical error' ); + this.sqlConnection = mysql.createConnection( this.config ); + this.isRecovering = false; + } + this.sqlConnection.connect( ( err ) => { + if ( err ) { + console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack ); + reject( err ); + return; + } + console.log( '[ SQL ] Connected to database successfully' ); + self.sqlConnection.on( 'error', ( err ) => { + if ( err.code === 'ECONNRESET' ) { + console.error( '[ SQL ] Reconnecting to database, because connection was reset!' ); + self.isRecovering = true; + setTimeout( () => { + self.disconnect(); + self.connect(); + }, 1000 ); + } else { + console.error( err ); + } + } ); + resolve( 'connection' ); + } ); + } ); + } + + disconnect ( ) { + this.sqlConnection.end(); + } + + async setupDB () { + this.sqlConnection.query( 'SELECT @@default_storage_engine;', ( error, results ) => { + if ( error ) throw error; + if ( results[ 0 ][ '@@default_storage_engine' ] !== 'InnoDB' ) throw 'DB HAS TO USE InnoDB!'; + } ); + this.sqlConnection.query( 'CREATE TABLE jh_store_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, data VARCHAR( 55000 ), uid TINYTEXT, lang TINYTEXT, username TINYTEXT, stripe_user_id TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', ( error ) => { + if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error; + return 'DONE'; + } ); + } + + query ( operation: SQLConfig, table: string ): Promise> { + 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) + + - deleteData: + - operation.property (the column to search for the value) + - operation.searchQuery (the value to search for [will be sanitised by method]) + + - 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! + */ + let command = ''; + if ( operation.command === 'getAllData' ) { + command = 'SELECT * FROM ' + table; + } else if ( operation.command === 'getFilteredData' || operation.command === 'checkDataAvailability' ) { + command = 'SELECT * FROM ' + table + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery ); + } else if ( operation.command === 'fullCustomCommand' ) { + command = operation.query; + } else if ( operation.command === 'addData' ) { + let keys = ''; + let values = ''; + for ( let key in operation.data ) { + keys += String( key ) + ', '; + values += this.sqlConnection.escape( String( operation.data[ key ] ) ) + ', ' ; + } + command = 'INSERT INTO ' + table + ' (' + keys.slice( 0, keys.length - 2 ) + ') VALUES (' + values.slice( 0, values.length - 2 ) + ');'; + } else if ( operation.command === 'updateData' ) { + if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' ); + else { + command = 'UPDATE ' + table + ' SET '; + let updatedValues = ''; + for ( let value in operation.newValues ) { + updatedValues += value + ' = ' + this.sqlConnection.escape( String( operation.newValues[ value ] ) ) + ', '; + } + command += updatedValues.slice( 0, updatedValues.length - 2 ); + command += ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery ); + } + } else if ( operation.command === 'deleteData' ) { + if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' ); + else { + command = 'DELETE FROM ' + table + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery ); + } + } else if ( operation.command === 'InnerJoin' ) { + command = 'SELECT ' + operation.selection + ' FROM ' + table + ' INNER JOIN ' + operation.secondTable + ' ON ' + operation.matchingParam + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery ); + } else if ( operation.command === 'LeftJoin' ) { + command = 'SELECT ' + operation.selection + ' FROM ' + table + ' LEFT JOIN ' + operation.secondTable + ' ON ' + operation.matchingParam + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery ); + } else if ( operation.command === 'RightJoin' ) { + command = 'SELECT ' + operation.selection + ' FROM ' + table + ' RIGHT JOIN ' + operation.secondTable + ' ON ' + operation.matchingParam + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery ); + } + this.sqlConnection.query( command, ( error, results ) => { + if ( error ) reject( error ); + resolve( results ); + } ); + } ); + } +} + +export { SQLConfig, SQLDB }; \ No newline at end of file diff --git a/backend/src/storage/prepareDB.ts b/backend/src/storage/prepareDB.ts new file mode 100644 index 0000000..c6d9537 --- /dev/null +++ b/backend/src/storage/prepareDB.ts @@ -0,0 +1,12 @@ +import db from './db.js'; +// import hash from '../security/hash.js'; + +db.initDB(); +// setTimeout( () => { +// console.log( 'Setting up admin account' ); +// hash.hashPassword( 'test' ).then( hash => { +// db.writeDataSimple( 'admin', 'email', 'info@janishutz.com', { email: 'info@janishutz.com', pass: hash, two_fa: 'enhanced' } ); +// console.log( 'Complete!' ); +// } ); +// }, 5000 ); +