Integrate new account backend

This commit is contained in:
2025-09-28 14:23:31 +02:00
parent 6e93cfdf2c
commit 0315241d76
9 changed files with 4086 additions and 3793 deletions

7245
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,43 @@
{ {
"name": "musicplayer-v2-backend", "name": "musicplayer-v2-backend",
"version": "1.0.0", "version": "1.0.0",
"description": "The backend for MusicPlayerV2", "description": "The backend for MusicPlayerV2",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/simplePCBuilding/MusicPlayerV2.git" "url": "git+https://github.com/simplePCBuilding/MusicPlayerV2.git"
}, },
"author": "Janis Hutz", "author": "Janis Hutz",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/simplePCBuilding/MusicPlayerV2/issues" "url": "https://github.com/simplePCBuilding/MusicPlayerV2/issues"
}, },
"homepage": "https://github.com/simplePCBuilding/MusicPlayerV2#readme", "homepage": "https://github.com/simplePCBuilding/MusicPlayerV2#readme",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.29.0", "@eslint/js": "^9.29.0",
"@stylistic/eslint-plugin": "^5.0.0", "@stylistic/eslint-plugin": "^5.0.0",
"@types/express-session": "^1.18.0", "@types/express-session": "^1.18.0",
"eslint-plugin-vue": "^10.2.0", "eslint-plugin-vue": "^10.2.0",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^8.35.0" "typescript-eslint": "^8.35.0"
}, },
"dependencies": { "dependencies": {
"@types/body-parser": "^1.19.5", "@janishutz/login-sdk-server": "^1.2.0",
"@types/cors": "^2.8.17", "@janishutz/login-sdk-server-stubs": "^1.0.0",
"@types/express": "^4.17.21", "@janishutz/store-sdk": "^1.0.0",
"@types/jsonwebtoken": "^9.0.6", "@types/body-parser": "^1.19.5",
"body-parser": "^1.20.2", "@types/cors": "^2.8.17",
"cors": "^2.8.5", "@types/express": "^4.17.21",
"express": "^4.19.2", "@types/jsonwebtoken": "^9.0.6",
"express-session": "^1.18.0", "body-parser": "^1.20.2",
"jsonwebtoken": "^9.0.2", "cors": "^2.8.5",
"node-mysql": "^0.4.2", "express": "^4.19.2",
"oauth-janishutz-client-server": "file:../../oauth/client/server/dist", "express-session": "^1.18.0",
"socket.io": "^4.7.5", "jsonwebtoken": "^9.0.2",
"store.janishutz.com-sdk": "file:../../store/sdk/dist" "node-mysql": "^0.4.2",
} "socket.io": "^4.7.5"
}
} }

View File

@@ -3,48 +3,58 @@ import db from './storage/db';
const createUser = ( uid: string, username: string, email: string ): Promise<boolean> => { const createUser = ( uid: string, username: string, email: string ): Promise<boolean> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
db.writeDataSimple( 'users', 'uid', uid, { 'uid': uid, 'username': username, 'email': email } ).then( () => { db.writeDataSimple( 'users', 'uid', uid, {
'uid': uid,
'username': username,
'email': email
} ).then( () => {
resolve( true ); resolve( true );
} ).catch( err => { } )
reject( err ); .catch( err => {
} ); reject( err );
} );
} ); } );
} };
const saveUserData = ( uid: string, data: object ): Promise<boolean> => { const saveUserData = ( uid: string, data: object ): Promise<boolean> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
db.writeDataSimple( 'users', 'uid', uid, { 'data': data } ).then( () => { db.writeDataSimple( 'users', 'uid', uid, {
'data': data
} ).then( () => {
resolve( true ); resolve( true );
} ).catch( err => { } )
reject( err ); .catch( err => {
} ); reject( err );
} );
} ); } );
} };
const checkUser = ( uid: string ): Promise<boolean> => { const checkUser = ( uid: string ): Promise<boolean> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
db.checkDataAvailability( 'users', 'uid', uid ).then( res => { db.checkDataAvailability( 'users', 'uid', uid ).then( res => {
resolve( res ); resolve( res );
} ).catch( err => {
reject( err );
} ) } )
.catch( err => {
reject( err );
} );
} ); } );
} };
const getUserData = ( uid: string ): Promise<object> => { const getUserData = ( uid: string ): Promise<object> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
db.getDataSimple( 'users', 'uid', uid ).then( data => { db.getDataSimple( 'users', 'uid', uid ).then( data => {
resolve( data ); resolve( data );
} ).catch( err => { } )
reject( err ); .catch( err => {
} ); reject( err );
} );
} ); } );
} };
export default { export default {
createUser, createUser,
saveUserData, saveUserData,
checkUser, checkUser,
getUserData getUserData
} };

View File

@@ -3,8 +3,6 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import cors from 'cors'; import cors from 'cors';
import account from './account';
import sdk from 'oauth-janishutz-client-server';
import { import {
createServer createServer
} from 'node:http'; } from 'node:http';
@@ -15,26 +13,19 @@ import crypto from 'node:crypto';
import type { import type {
Room, Song Room, Song
} from './definitions'; } from './definitions';
import storeSDK from 'store.janishutz.com-sdk';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
// ┌ ┐
// │ Handle FOSS vs paid version │
// └ ┘
const isFossVersion = true; const isFossVersion = true;
declare let __dirname: string | undefined; import storeSDK from '@janishutz/store-sdk';
import sdk from '@janishutz/login-sdk-server';
// import storeSDK from './sdk/store-sdk-stub';
// import sdk from '@janishutz/login-sdk-server-stubs';
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'
// ) ).toString() );
const sdkConfig = JSON.parse( fs.readFileSync( path.join(
__dirname,
'/config/sdk.config.secret.json'
) ).toString() );
const run = () => { const run = () => {
const app = express(); const app = express();
@@ -45,10 +36,6 @@ const run = () => {
} ) ); } ) );
if ( !isFossVersion ) { if ( !isFossVersion ) {
// storeSDK.configure( JSON.parse( fs.readFileSync( path.join(
// __dirname,
// '/config/store-sdk.config.testing.json'
// ) ).toString() ) );
storeSDK.configure( JSON.parse( fs.readFileSync( path.join( storeSDK.configure( JSON.parse( fs.readFileSync( path.join(
__dirname, __dirname,
'/config/store-sdk.config.secret.json' '/config/store-sdk.config.secret.json'
@@ -58,26 +45,49 @@ const run = () => {
const httpServer = createServer( app ); const httpServer = createServer( app );
if ( !isFossVersion ) { if ( !isFossVersion ) {
const sdkConfig = JSON.parse( fs.readFileSync( path.join(
__dirname,
'/config/sdk.config.secret.json'
) ).toString() );
// Load id.janishutz.com SDK and allow signing in // Load id.janishutz.com SDK and allow signing in
sdk.routes( app, ( uid: string ) => { sdk.setUp(
return new Promise( ( resolve, reject ) => { {
account.checkUser( uid ).then( stat => { 'prod': false,
resolve( stat ); 'service': {
} ) 'serviceID': 'jh-music',
.catch( e => { 'serviceToken': sdkConfig[ 'token' ]
reject( e ); },
} ); 'user-agent': sdkConfig[ 'ua' ],
} ); 'sessionType': 'memory',
}, ( uid: string, email: string, username: string ) => { 'frontendURL': 'https://music.janishutz.com',
return new Promise( ( resolve, reject ) => { 'corsWhitelist': [ 'https://music.janishutz.com' ],
account.createUser( uid, username, email ).then( stat => { 'recheckTimeout': 300 * 1000,
resolve( stat ); 'advancedVerification': 'sdk'
} ) },
.catch( e => { app,
reject( e ); () => {
} ); return new Promise( resolve => {
} ); resolve( true );
}, sdkConfig ); } );
},
() => {
return new Promise( resolve => {
resolve( true );
} );
},
() => {
return new Promise( resolve => {
resolve( true );
} );
},
() => {
return new Promise( resolve => {
resolve( true );
} );
},
sdkConfig
);
} }
// Websocket for events // Websocket for events
@@ -291,7 +301,7 @@ const run = () => {
} ) }\n\n` ); } ) }\n\n` );
const sid = sdk.getSessionID( request ); const sid = sdk.getSessionID( request );
if ( sdk.checkAuth( request ) ) { if ( sdk.getSignedIn( request ) ) {
importantClients[ sid ] = { importantClients[ sid ] = {
'response': response, 'response': response,
'room': String( request.query.room ) 'room': String( request.query.room )
@@ -488,38 +498,35 @@ const run = () => {
app.get( app.get(
'/createRoomToken', '/createRoomToken',
sdk.loginCheck(),
( request: express.Request, response: express.Response ) => { ( request: express.Request, response: express.Response ) => {
if ( sdk.checkAuth( request ) ) { // eslint-disable-next-line no-constant-binary-expression
// eslint-disable-next-line no-constant-binary-expression const roomName = String( request.query.roomName ) ?? '';
const roomName = String( request.query.roomName ) ?? '';
if ( !socketData[ roomName ] ) { if ( !socketData[ roomName ] ) {
const roomToken = crypto.randomUUID(); const roomToken = crypto.randomUUID();
socketData[ roomName ] = { socketData[ roomName ] = {
'playbackStart': 0, 'playbackStart': 0,
'playbackStatus': false, 'playbackStatus': false,
'playlist': [], 'playlist': [],
'playlistIndex': 0, 'playlistIndex': 0,
'roomName': roomName, 'roomName': roomName,
'roomToken': roomToken, 'roomToken': roomToken,
'ownerUID': sdk.getUserData( request ).uid, 'ownerUID': sdk.getUID( request ),
'useAntiTamper': request.query.useAntiTamper === 'true' 'useAntiTamper': request.query.useAntiTamper === 'true'
? true : false, ? true : false,
}; };
response.send( roomToken ); response.send( roomToken );
} else {
if (
socketData[ roomName ].ownerUID
=== sdk.getUserData( request ).uid
) {
response.send( socketData[ roomName ].roomToken );
} else {
response.status( 409 ).send( 'ERR_CONFLICT' );
}
}
} else { } else {
response.status( 403 ).send( 'ERR_FORBIDDEN' ); if (
socketData[ roomName ].ownerUID
=== sdk.getUID( request )
) {
response.send( socketData[ roomName ].roomToken );
} else {
response.status( 409 ).send( 'ERR_CONFLICT' );
}
} }
} }
); );
@@ -569,46 +576,43 @@ const run = () => {
const checkIfOwned = ( request: express.Request ): Promise<boolean> => { const checkIfOwned = ( request: express.Request ): Promise<boolean> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
if ( sdk.checkAuth( request ) ) { const uid = sdk.getUID( request );
const userData = sdk.getUserData( request );
if ( ownedCache[ userData.uid ] ) { if ( ownedCache[ uid ] ) {
resolve( ownedCache[ userData.uid ] ); resolve( ownedCache[ uid ] );
} else { } else {
storeSDK.getSubscriptions( userData.uid ) storeSDK.getSubscriptions( uid )
.then( stat => { .then( stat => {
const now = new Date().getTime(); const now = new Date().getTime();
for ( const sub in stat ) { for ( const sub in stat ) {
if ( stat[ sub ].expires - now > 0 if ( stat[ sub ].expires - now > 0
&& ( && (
stat[ sub ].id stat[ sub ].id
=== 'com.janishutz.MusicPlayer.subscription' === 'com.janishutz.MusicPlayer.subscription'
|| stat[ sub ].id || stat[ sub ].id
=== 'com.janishutz.MusicPlayer.subscription-month' === 'com.janishutz.MusicPlayer.subscription-month'
) )
) { ) {
ownedCache[ userData.uid ] = true; ownedCache[ uid ] = true;
resolve( true ); resolve( true );
}
} }
}
ownedCache[ userData.uid ] = false; ownedCache[ uid ] = false;
resolve( false ); resolve( false );
} ) } )
.catch( e => { .catch( e => {
console.error( e ); console.error( e );
reject( 'ERR_NOT_OWNED' ); reject( 'ERR_NOT_OWNED' );
} ); } );
}
} else {
reject( 'ERR_AUTH_REQUIRED' );
} }
} ); } );
}; };
app.get( app.get(
'/checkUserStatus', '/checkUserStatus',
sdk.loginCheck(),
( request: express.Request, response: express.Response ) => { ( request: express.Request, response: express.Response ) => {
checkIfOwned( request ) checkIfOwned( request )
.then( owned => { .then( owned => {

View File

@@ -1,41 +0,0 @@
import express from 'express';
import expressSession from 'express-session';
import crypto from 'node:crypto';
// TODO: Use also express-session to make it work with getSessionID and session referencing
const checkAuth = ( request: express.Request ) => {
return true;
}
export interface AuthSDKConfig {
token: string;
name: string;
client: string;
backendURL: string;
failReturnURL: string;
useSecureCookie?: boolean;
}
declare module 'express-session' {
interface SessionData {
isAuth: boolean;
uid: string;
username: string;
email: string;
additionalData: object;
}
}
const getUserData = ( request: express.Request ) => {
if ( !request.session.uid ) {
request.session.uid = crypto.randomUUID();
request.session.username = 'FOSS-Version';
request.session.email = 'example@example.com';
}
return { 'email': request.session.email, 'username': request.session.username, 'uid': request.session.uid, 'id': request.session.id };
}
export default {
checkAuth,
getUserData
}

View File

@@ -1,11 +1,15 @@
const getSubscriptions = ( uid: string ) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
return [ { const getSubscriptions = ( _uid: string ) => {
'id': 'com.janishutz.MusicPlayer.subscription', return [
'expires': new Date().getTime() + 200000, {
'status': true 'id': 'com.janishutz.MusicPlayer.subscription',
} ]; 'expires': new Date().getTime() + 200000,
} 'status': true
}
];
};
export default { export default {
getSubscriptions, getSubscriptions,
} };

View File

@@ -11,8 +11,9 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import * as sqlDB from './mysqldb.js'; import * as sqlDB from './mysqldb.js';
declare let __dirname: string | undefined declare let __dirname: string | undefined;
if ( typeof( __dirname ) === 'undefined' ) {
if ( typeof __dirname === 'undefined' ) {
__dirname = path.resolve( path.dirname( '' ) ); __dirname = path.resolve( path.dirname( '' ) );
} else { } else {
__dirname = __dirname + '/../'; __dirname = __dirname + '/../';
@@ -22,9 +23,8 @@ const dbRef = {
'user': 'music_users', 'user': 'music_users',
'users': 'music_users', 'users': 'music_users',
}; };
const dbh = new sqlDB.SQLDB();
let dbh = new sqlDB.SQLDB();
dbh.connect(); dbh.connect();
/** /**
@@ -32,7 +32,7 @@ dbh.connect();
* @returns {undefined} * @returns {undefined}
*/ */
const initDB = (): undefined => { const initDB = (): undefined => {
( async() => { ( async () => {
console.log( '[ DB ] Setting up...' ); console.log( '[ DB ] Setting up...' );
dbh.setupDB(); dbh.setupDB();
console.log( '[ DB ] Setting up complete!' ); console.log( '[ DB ] Setting up complete!' );
@@ -48,11 +48,16 @@ const initDB = (): undefined => {
*/ */
const getDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => { const getDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
dbh.query( { 'command': 'getFilteredData', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( data => { dbh.query( {
'command': 'getFilteredData',
'property': column,
'searchQuery': searchQuery
}, dbRef[ db ] ).then( data => {
resolve( data ); resolve( data );
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} ); } );
}; };
@@ -66,7 +71,7 @@ const getDataSimple = ( db: string, column: string, searchQuery: string ): Promi
* @param {string} nameOfMatchingParam Which properties should be matched to get the data, e.g. order.user_id=users.id * @param {string} nameOfMatchingParam Which properties should be matched to get the data, e.g. order.user_id=users.id
* @returns {Promise<Object | Error>} Returns all records from the db and all matching data specified with the matchingParam from the secondTable. * @returns {Promise<Object | Error>} 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<Object> => { const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: string, secondTable: string, columns: object, nameOfMatchingParam: string ): Promise<object> => {
/* /*
LeftJoin (Select values in first table and return all corresponding values of second table): LeftJoin (Select values in first table and return all corresponding values of second table):
- operation.property (the column to search for the value), - operation.property (the column to search for the value),
@@ -76,24 +81,26 @@ const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: s
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id) - operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
*/ */
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
let settings = { const settings = {
'command': 'LeftJoin', 'command': 'LeftJoin',
'property': column, 'property': column,
'searchQuery': searchQuery, 'searchQuery': searchQuery,
'selection': '', 'selection': '',
'secondTable': dbRef[ secondTable ], 'secondTable': dbRef[ secondTable ],
'matchingParam': dbRef[ db ] + '.' + nameOfMatchingParam + '=' + dbRef[ secondTable ] + '.' + nameOfMatchingParam, 'matchingParam': dbRef[ db ] + '.' + nameOfMatchingParam + '=' + dbRef[ secondTable ] + '.' + nameOfMatchingParam,
} };
for ( let el in columns ) {
for ( const el in columns ) {
settings.selection += dbRef[ columns[ el ].db ] + '.' + columns[ el ].column + ','; settings.selection += dbRef[ columns[ el ].db ] + '.' + columns[ el ].column + ',';
} }
settings.selection = settings.selection.slice( 0, settings.selection.length - 1 ); settings.selection = settings.selection.slice( 0, settings.selection.length - 1 );
dbh.query( settings, dbRef[ db ] ).then( data => { dbh.query( settings, dbRef[ db ] ).then( data => {
resolve( data ); resolve( data );
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} ); } );
}; };
@@ -102,13 +109,16 @@ const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: s
* @param {string} db The database of which all data should be retrieved * @param {string} db The database of which all data should be retrieved
* @returns {Promise<object>} Returns an object containing all data * @returns {Promise<object>} Returns an object containing all data
*/ */
const getData = ( db: string ): Promise<Object> => { const getData = ( db: string ): Promise<object> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
dbh.query( { 'command': 'getAllData' }, dbRef[ db ] ).then( data => { dbh.query( {
'command': 'getAllData'
}, dbRef[ db ] ).then( data => {
resolve( data ); resolve( data );
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} ); } );
}; };
@@ -120,25 +130,40 @@ const getData = ( db: string ): Promise<Object> => {
* @param {string} data The data to write. Also include the column & searchQuery parameters, if they also need to be added * @param {string} data The data to write. Also include the column & searchQuery parameters, if they also need to be added
* @returns {Promise<object>} Returns a promise that resolves to the interaction module return. * @returns {Promise<object>} Returns a promise that resolves to the interaction module return.
*/ */
const writeDataSimple = ( db: string, column: string, searchQuery: string, data: any ): Promise<Object> => { const writeDataSimple = ( db: string, column: string, searchQuery: string, data: any ): Promise<object> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
dbh.query( { 'command': 'checkDataAvailability', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( res => { dbh.query( {
'command': 'checkDataAvailability',
'property': column,
'searchQuery': searchQuery
}, dbRef[ db ] ).then( res => {
if ( res.length > 0 ) { if ( res.length > 0 ) {
dbh.query( { 'command': 'updateData', 'property': column, 'searchQuery': searchQuery, 'newValues': data }, dbRef[ db ] ).then( dat => { dbh.query( {
'command': 'updateData',
'property': column,
'searchQuery': searchQuery,
'newValues': data
}, dbRef[ db ] ).then( dat => {
resolve( dat ); resolve( dat );
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} else { } else {
dbh.query( { 'command': 'addData', 'data': data }, dbRef[ db ] ).then( dat => { dbh.query( {
'command': 'addData',
'data': data
}, dbRef[ db ] ).then( dat => {
resolve( dat ); resolve( dat );
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} }
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} ); } );
}; };
@@ -151,11 +176,16 @@ const writeDataSimple = ( db: string, column: string, searchQuery: string, data:
*/ */
const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => { const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
dbh.query( { 'command': 'deleteData', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( dat => { dbh.query( {
'command': 'deleteData',
'property': column,
'searchQuery': searchQuery
}, dbRef[ db ] ).then( dat => {
resolve( dat ); resolve( dat );
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} ); } );
}; };
@@ -168,15 +198,20 @@ const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Pr
*/ */
const checkDataAvailability = ( db: string, column: string, searchQuery: string ): Promise<boolean> => { const checkDataAvailability = ( db: string, column: string, searchQuery: string ): Promise<boolean> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
dbh.query( { 'command': 'checkDataAvailability', 'property': column, 'searchQuery': searchQuery }, dbRef[ db ] ).then( res => { dbh.query( {
'command': 'checkDataAvailability',
'property': column,
'searchQuery': searchQuery
}, dbRef[ db ] ).then( res => {
if ( res.length > 0 ) { if ( res.length > 0 ) {
resolve( true ); resolve( true );
} else { } else {
resolve( false ); resolve( false );
} }
} ).catch( error => { } )
reject( error ); .catch( error => {
} ); reject( error );
} );
} ); } );
}; };
@@ -186,16 +221,18 @@ const checkDataAvailability = ( db: string, column: string, searchQuery: string
* @returns {Promise<object>} Returns the data from all files * @returns {Promise<object>} Returns the data from all files
*/ */
const getJSONDataBatch = async ( files: Array<string> ): Promise<object> => { const getJSONDataBatch = async ( files: Array<string> ): Promise<object> => {
let allFiles = {}; const allFiles = {};
for ( let file in files ) {
for ( const file in files ) {
try { try {
allFiles[ files[ file ] ] = await getJSONData( files[ file ] ); allFiles[ files[ file ] ] = await getJSONData( files[ file ] );
} catch( err ) { } catch ( err ) {
allFiles[ files[ file ] ] = 'ERROR: ' + err; allFiles[ files[ file ] ] = 'ERROR: ' + err;
} }
} }
return allFiles; return allFiles;
} };
/** /**
* Load all data from a JSON file * Load all data from a JSON file
@@ -245,7 +282,7 @@ const getJSONDataSimple = ( file: string, identifier: string ): Promise<object>
* @param {string} file The file to be loaded (path relative to root) * @param {string} file The file to be loaded (path relative to root)
* @returns {object} Returns the JSON file * @returns {object} Returns the JSON file
*/ */
const getJSONDataSync = ( file: string ): Object => { const getJSONDataSync = ( file: string ): object => {
return JSON.parse( fs.readFileSync( path.join( __dirname + '/' + file ) ).toString() ); return JSON.parse( fs.readFileSync( path.join( __dirname + '/' + file ) ).toString() );
}; };
@@ -263,14 +300,17 @@ const writeJSONDataSimple = ( db: string, identifier: string, values: any ) => {
reject( 'Error occurred: Error trace: ' + error ); reject( 'Error occurred: Error trace: ' + error );
} else { } else {
let dat = {}; let dat = {};
if ( data.byteLength > 0 ) { if ( data.byteLength > 0 ) {
dat = JSON.parse( data.toString() ) ?? {}; dat = JSON.parse( data.toString() ) ?? {};
} }
dat[ identifier ] = values; dat[ identifier ] = values;
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), ( error ) => { fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), error => {
if ( error ) { if ( error ) {
reject( 'Error occurred: Error trace: ' + error ); reject( 'Error occurred: Error trace: ' + error );
} }
resolve( true ); resolve( true );
} ); } );
} }
@@ -286,7 +326,7 @@ const writeJSONDataSimple = ( db: string, identifier: string, values: any ) => {
*/ */
const writeJSONData = ( db: string, data: object ): Promise<boolean> => { const writeJSONData = ( db: string, data: object ): Promise<boolean> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( data ), ( error ) => { fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( data ), error => {
if ( error ) { if ( error ) {
reject( 'Error occurred: Error trace: ' + error ); reject( 'Error occurred: Error trace: ' + error );
} else { } else {
@@ -309,14 +349,17 @@ const deleteJSONDataSimple = ( db: string, identifier: string ): Promise<boolean
reject( 'Error occurred: Error trace: ' + error ); reject( 'Error occurred: Error trace: ' + error );
} else { } else {
let dat = {}; let dat = {};
if ( data.byteLength > 0 ) { if ( data.byteLength > 0 ) {
dat = JSON.parse( data.toString() ) ?? {}; dat = JSON.parse( data.toString() ) ?? {};
} }
delete dat[ identifier ]; delete dat[ identifier ];
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), ( error ) => { fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), error => {
if ( error ) { if ( error ) {
reject( 'Error occurred: Error trace: ' + error ); reject( 'Error occurred: Error trace: ' + error );
} }
resolve( true ); resolve( true );
} ); } );
} }
@@ -324,7 +367,19 @@ const deleteJSONDataSimple = ( db: string, identifier: string ): Promise<boolean
} ); } );
}; };
export default { initDB, checkDataAvailability, deleteDataSimple, deleteJSONDataSimple, getData, export default {
getDataSimple, getDataWithLeftJoinFunction, getJSONData, getJSONDataBatch, getJSONDataSimple, initDB,
getJSONDataSync, writeDataSimple, writeJSONData, writeJSONDataSimple checkDataAvailability,
deleteDataSimple,
deleteJSONDataSimple,
getData,
getDataSimple,
getDataWithLeftJoinFunction,
getJSONData,
getJSONDataBatch,
getJSONDataSimple,
getJSONDataSync,
writeDataSimple,
writeJSONData,
writeJSONDataSimple
}; };

View File

@@ -11,8 +11,9 @@ import mysql from 'mysql';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
declare let __dirname: string | undefined declare let __dirname: string | undefined;
if ( typeof( __dirname ) === 'undefined' ) {
if ( typeof __dirname === 'undefined' ) {
__dirname = path.resolve( path.dirname( '' ) ); __dirname = path.resolve( path.dirname( '' ) );
} else { } else {
__dirname = __dirname + '/../'; __dirname = __dirname + '/../';
@@ -22,21 +23,35 @@ if ( typeof( __dirname ) === 'undefined' ) {
// to the whitelist of the database // to the whitelist of the database
class SQLConfig { class SQLConfig {
command: string; command: string;
property?: string; property?: string;
searchQuery?: string; searchQuery?: string;
selection?: string; selection?: string;
query?: string; query?: string;
newValues?: object; newValues?: object;
secondTable?: string; secondTable?: string;
matchingParam?: string; matchingParam?: string;
data?: object; data?: object;
} }
class SQLDB { class SQLDB {
sqlConnection: mysql.Connection; sqlConnection: mysql.Connection;
isRecovering: boolean; isRecovering: boolean;
config: object; config: object;
constructor () { constructor () {
this.config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/db.config.secret.json' ) ) ); this.config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/db.config.secret.json' ) ) );
this.sqlConnection = mysql.createConnection( this.config ); this.sqlConnection = mysql.createConnection( this.config );
@@ -46,19 +61,23 @@ class SQLDB {
connect () { connect () {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
const self = this; const self = this;
if ( this.isRecovering ) { if ( this.isRecovering ) {
console.log( '[ SQL ] Attempting to recover from critical error' ); console.log( '[ SQL ] Attempting to recover from critical error' );
this.sqlConnection = mysql.createConnection( this.config ); this.sqlConnection = mysql.createConnection( this.config );
this.isRecovering = false; this.isRecovering = false;
} }
this.sqlConnection.connect( ( err ) => {
this.sqlConnection.connect( err => {
if ( err ) { if ( err ) {
console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack ); console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
reject( err ); reject( err );
return; return;
} }
console.log( '[ SQL ] Connected to database successfully' ); console.log( '[ SQL ] Connected to database successfully' );
self.sqlConnection.on( 'error', ( err ) => { self.sqlConnection.on( 'error', err => {
if ( err.code === 'ECONNRESET' ) { if ( err.code === 'ECONNRESET' ) {
self.isRecovering = true; self.isRecovering = true;
setTimeout( () => { setTimeout( () => {
@@ -81,15 +100,17 @@ class SQLDB {
async setupDB () { async setupDB () {
this.sqlConnection.query( 'SELECT @@default_storage_engine;', ( error, results ) => { this.sqlConnection.query( 'SELECT @@default_storage_engine;', ( error, results ) => {
if ( error ) throw error; if ( error ) throw error;
if ( results[ 0 ][ '@@default_storage_engine' ] !== 'InnoDB' ) throw 'DB HAS TO USE InnoDB!'; if ( results[ 0 ][ '@@default_storage_engine' ] !== 'InnoDB' ) throw 'DB HAS TO USE InnoDB!';
} ); } );
this.sqlConnection.query( 'CREATE TABLE music_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, uid TINYTEXT, lang TINYTEXT, username TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', ( error ) => { this.sqlConnection.query( 'CREATE TABLE music_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, uid TINYTEXT, lang TINYTEXT, username TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', error => {
if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error; if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error;
return 'DONE'; return 'DONE';
} ); } );
} }
query ( operation: SQLConfig, table: string ): Promise<Array<Object>> { query ( operation: SQLConfig, table: string ): Promise<Array<object>> {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
/* /*
Possible operation.command values (all need the table argument of the method call): Possible operation.command values (all need the table argument of the method call):
@@ -141,6 +162,7 @@ class SQLDB {
- operation.query (the SQL instruction to be executed) --> NOTE: This command will not be sanitised, so use only with proper sanitisation! - operation.query (the SQL instruction to be executed) --> NOTE: This command will not be sanitised, so use only with proper sanitisation!
*/ */
let command = ''; let command = '';
if ( operation.command === 'getAllData' ) { if ( operation.command === 'getAllData' ) {
command = 'SELECT * FROM ' + table; command = 'SELECT * FROM ' + table;
} else if ( operation.command === 'getFilteredData' || operation.command === 'checkDataAvailability' ) { } else if ( operation.command === 'getFilteredData' || operation.command === 'checkDataAvailability' ) {
@@ -150,19 +172,23 @@ class SQLDB {
} else if ( operation.command === 'addData' ) { } else if ( operation.command === 'addData' ) {
let keys = ''; let keys = '';
let values = ''; let values = '';
for ( let key in operation.data ) {
for ( const key in operation.data ) {
keys += String( key ) + ', '; keys += String( key ) + ', ';
values += this.sqlConnection.escape( String( operation.data[ 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 ) + ');';
command = 'INSERT INTO ' + table + ' (' + keys.slice( 0, keys.length - 2 ) + ') VALUES (' + values.slice( 0, values.length - 2 ) + ');';
} else if ( operation.command === 'updateData' ) { } else if ( operation.command === 'updateData' ) {
if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' ); if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' );
else { else {
command = 'UPDATE ' + table + ' SET '; command = 'UPDATE ' + table + ' SET ';
let updatedValues = ''; let updatedValues = '';
for ( let value in operation.newValues ) {
for ( const value in operation.newValues ) {
updatedValues += value + ' = ' + this.sqlConnection.escape( String( operation.newValues[ value ] ) ) + ', '; updatedValues += value + ' = ' + this.sqlConnection.escape( String( operation.newValues[ value ] ) ) + ', ';
} }
command += updatedValues.slice( 0, updatedValues.length - 2 ); command += updatedValues.slice( 0, updatedValues.length - 2 );
command += ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery ); command += ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
} }
@@ -178,12 +204,17 @@ class SQLDB {
} else if ( operation.command === 'RightJoin' ) { } 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 ); 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 ) => { this.sqlConnection.query( command, ( error, results ) => {
if ( error ) reject( error ); if ( error ) reject( error );
resolve( results ); resolve( results );
} ); } );
} ); } );
} }
} }
export { SQLConfig, SQLDB }; export {
SQLConfig, SQLDB
};