From 4cb56c8cbd0878fe2eaf1e1fd588aa1530818bbc Mon Sep 17 00:00:00 2001 From: STEINNI Date: Wed, 3 Sep 2025 20:49:15 +0000 Subject: [PATCH] switched to imports, debugged sessions --- corsMiddleware.js | 3 +- helpers/pwd.js | 2 +- helpers/utils.js | 113 +++++++++++++++++++++++++ p42ApiEndpoints.js | 203 +++++++++++++-------------------------------- p42api.js | 67 +++++++++------ package.json | 21 ++--- 6 files changed, 225 insertions(+), 184 deletions(-) create mode 100644 helpers/utils.js diff --git a/corsMiddleware.js b/corsMiddleware.js index 52b11b3..97e17e5 100644 --- a/corsMiddleware.js +++ b/corsMiddleware.js @@ -1,4 +1,4 @@ -function corsResolver(req, res, next) { +export function corsResolver(req, res, next) { if(1==0) { // allow browser / postman / world // Allow only from Mike & Nike devs @@ -22,4 +22,3 @@ function corsResolver(req, res, next) { next(); } -module.exports = corsResolver; \ No newline at end of file diff --git a/helpers/pwd.js b/helpers/pwd.js index 0c4ddfd..d524ba6 100644 --- a/helpers/pwd.js +++ b/helpers/pwd.js @@ -1,4 +1,4 @@ -import argon2 from 'argon2' +const argon2 = require('argon2') // --- Hash a password (e.g. at signup) --- export async function hashPassword(plainPassword) { diff --git a/helpers/utils.js b/helpers/utils.js new file mode 100644 index 0000000..ccf1006 --- /dev/null +++ b/helpers/utils.js @@ -0,0 +1,113 @@ +export class Utils { + constructor(options){ + } + + isValidIsoTimestamp(val) { + const isoTimestampRegex = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]|60)(\.\d+)?(Z|([+-])(0[0-9]|1[0-3]):([0-5][0-9])|(14):00)?$/ + return(isoTimestampRegex.test(val)) + } + + isValidMail(val) { + const mailRFC5322Regex = /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/ + return(mailRFC5322Regex.test(val)) + } + + isValidUUID(val) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + return(uuidRegex.test(val)) + } + + isIterable(obj) { + if (obj == null) { + return false; + } + return typeof obj[Symbol.iterator] === "function"; + } + + FilterMapArray(rows, condition) { + if(!rows || (typeof(rows[Symbol.iterator]) != 'function')) return([]) + let filteredRows = [] + for(let row of rows) { + if(condition(row)) filteredRows.push(row) + } + return(filteredRows) + } + + CheckMapArray(rows, remap, transformers) { + if(!rows || (typeof(rows[Symbol.iterator]) != 'function')) return([]) + let filteredRows = [] + for(let row of rows) { + filteredRows.push(this.CheckMapObject(row, remap, transformers)) + } + return(filteredRows) + } + + CheckMapObject(row, remap, transformers) { + let filteredRow = {} + let tempTransformers = { ...transformers } + for(let key of Object.keys(row)){ + if(Object.keys(remap).indexOf(key)>-1) { + if(tempTransformers && tempTransformers[key] && (typeof(tempTransformers[key])=='function')) { + this.remap(filteredRow, remap[key], tempTransformers[key](row)) + delete(tempTransformers[remap[key]]) + } else this.remap(filteredRow, remap[key], row[key]) //filteredRow[remap[key]] = row[key] + } + } + + // now use remaining transformers for other virtual, non-original keys + for(let virtkey of Object.keys(tempTransformers)){ + filteredRow[virtkey] = tempTransformers[virtkey](row) + } + + return(filteredRow) + } + + validateMapObject(row, validators, remap){ + let isok = true + let badkeys = [] + + for(let key of Object.keys(validators)){ + if(typeof(validators[key])=='function') { + let result = validators[key](row[key], row) + if(result!==true) { + isok = false + badkeys.push(key + ((typeof(result)=='string') ? ' - '+result : '') ) + } + } + } + let remappedRow = {} + if(!isok) return([false, {}, badkeys]) + + for(let key of Object.keys(row)){ + if(Object.keys(remap).indexOf(key)>-1){ + this.remap(remappedRow, remap[key], row[key]) + } + } + return([true, remappedRow, badkeys]) + } + + validateMapArray(rows, validators, remap){ + if(!rows || (typeof(rows[Symbol.iterator]) != 'function')) return([false, [], ['First parameter of validateMapArray is not an array']]) + let remappedRows = [] + let allErrors = [] + let isOk = true + for(let row of rows){ + let isValid,remappedRow, errors + [isValid, remappedRow, errors] = this.validateMapObject(row, validators, remap) + isOk = isOk && isValid + remappedRows.push(remappedRow) + allErrors = allErrors.concat(errors) + } + return([isOk, remappedRows, allErrors]) + } + + remap(obj, newKey, value){ + let ref = obj + // create sub-struct as needed + newKey.split('.').slice(0,-1).forEach( (keyPart) => { + if(!ref.hasOwnProperty(keyPart)) ref[keyPart]={} + ref = ref[keyPart] + }) + ref[newKey.split('.').slice(-1)[0]] = value + } +} \ No newline at end of file diff --git a/p42ApiEndpoints.js b/p42ApiEndpoints.js index 9dcd719..6e01169 100644 --- a/p42ApiEndpoints.js +++ b/p42ApiEndpoints.js @@ -1,29 +1,21 @@ -const mysql = require('mysql2/promise'); - -class P42ApiEndpoints{ - constructor(app) { - this.db = null +import { Utils } from './helpers/utils.js' +export class P42ApiEndpoints{ + constructor(app, db) { + this.db = db this.app = app this.userinfos = null + this.utils = new Utils() this.registerPaths() + setInterval(() => { + this.db.query('SELECT 1'); + }, 5000); } registerPaths(){ this.app.get('/hw', this.hw.bind(this)) - } - - async connectDB(mysqlCreds) { - this.db = await mysql.createConnection({ - host: mysqlCreds.host, - port: mysqlCreds.port, - socketPath: mysqlCreds.socketPath, - database: mysqlCreds.database, - user: mysqlCreds.user, - password: mysqlCreds.password - }); - setInterval(() => { - this.db.query('SELECT 1'); - }, 5000); + this.app.get('/checkauth', this.checkauth.bind(this)) + this.app.post('/login', this.login.bind(this)) + } err(req, res, msg, debug, status=500) { @@ -56,55 +48,7 @@ class P42ApiEndpoints{ } getSession(req, res) { - this.userinfos = { - "at_hash": "fhaNqJbWprmseino7D7vQhdEIWzlss6a08DvgY_Y7ik", - "sub": "steinic", - "amr": [ - "pwd" - ], - "iss": "https://ecas.acceptance.ec.europa.eu/cas/oauth2", - - // Impersonate here - "preferred_username": "fallimi", //"steinic", - - - - "locale": "en", - "https://ecas.ec.europa.eu/claims/domain": "eu.europa.ec", - "acr": "https://ecas.ec.europa.eu/loa/basic", - "auth_time": 1686415198, - "nickname": "steinic", - "https://ecas.ec.europa.eu/claims/teleworking_priority": false, - "exp": 1686415501, - "iat": 1686415201, - "email": "Nicolas.STEIN@ext.ec.europa.eu", - "https://ecas.ec.europa.eu/claims/employee_number": "90218167", - "email_verified": true, - "https://ecas.ec.europa.eu/claims/department_number": "EISMEA.C.02.2", - "https://ecas.ec.europa.eu/claims/employee_type": "x", - "given_name": "Nicolas", - "https://ecas.ec.europa.eu/claims/org_id": "232619", - "aud": "zjDAOobFg2JJzMxhzfoTyPg1BrOzPzG4EMUJOoqUbF1mYTkwddaZwL4o9YzzK3unIZAEunze7fQAfOoOgXnq9Xhr-NaAc23CqASenqizgfAeUl6", - "c_hash": "8pzkBbmGEZW48yLZYoEoR_H3QC0GIeWYxlzUCfRMElg", - "https://ecas.ec.europa.eu/claims/sso": false, - "https://ecas.ec.europa.eu/claims/authentication_factors": [ - { - "username": "steinic" - } - ], - "name": "Nicolas STEIN", - "https://ecas.ec.europa.eu/claims/uid": "steinic", - "family_name": "STEIN", - "userRoles": [ - "BP_PO", - "APPLICANT", - ] - } - return(true) - - if((!req.session.userinfo) || (!req.session.userinfo.isAuthenticated)) { - this.err(req, res, 'Not authenticated !') this.userinfos = null return(false) } else { @@ -125,87 +69,56 @@ class P42ApiEndpoints{ return(false) } - CheckMapOutput(data, remap, transformers) { - if(!data) return(null) - let rows = Array.isArray(data) ? data : [data] - let filteredRows = [] - for(let row of rows) { - let filteredRow = {} - Object.keys(row).forEach((key, index) => { - if(Object.keys(remap).indexOf(key)>-1) { - if(transformers && transformers[key] && (typeof(transformers[key])=='function')) { - filteredRow[remap[key]] = transformers[key](row[key]) - } else filteredRow[remap[key]] = row[key] - } - }); - filteredRows.push(filteredRow) - } - if(Array.isArray(data)) return(filteredRows) - else return(filteredRows[0]) - } - - CheckMapInput(dataIn, remap, checks) { - let dataOut = {} - for(let field in checks) { - let dbName = checks[field](dataIn[field]) - if(dbName && (dataIn[field]!=null)) dataOut[remap[field]] = dataIn[field] - } - return(dataOut) - } - - async isMemberOf(pic) { - let [rows, fields] = await this.db.query(` - SELECT count(*) as cnt FROM organisation_members - WHERE (om_pic=?) - AND (om_uid=?) - `, - [pic, this.userinfos.preferred_username]); - return(rows[0]['cnt']>0) - } - - async isOrgAdminOf(pic) { - let [rows, fields] = await this.db.query(` - SELECT count(*) as cnt FROM organisation_members - WHERE (om_pic=?) - AND (om_uid=?) - AND om_administrator=1 - `, - [pic, this.userinfos.preferred_username]); - return(rows[0]['cnt']>0) - } - - async isPropAdminOf(pid) { - let [rows, fields] = await this.db.query(` - SELECT count(*) as cnt FROM shortprops_members - WHERE (spm_prop_id=?) - AND (spm_uid=?) - AND spm_administrator=1 - `, - [pid, this.userinfos.preferred_username]); - return(rows[0]['cnt']>0) - } - - async isPropMemberOf(pid) { - let [rows, fields] = await this.db.query(` - SELECT count(*) as cnt FROM shortprops_members - WHERE (spm_prop_id=?) - AND (spm_uid=?) - `, - [pid, this.userinfos.preferred_username]); - return(rows[0]['cnt']>0) - } - - - async merge(table, where, whereVals, data) { - let [rows, field] = await this.db.query(`SELECT * FROM ${table} WHERE ${where}`, whereVals) - if(rows.length==0) return(data) - else return(Object.assign(rows[0], data)) - } - ///////////////////////////API starts here...///////////////////////////// async hw(req, res) { this.ok(req, res, {hello:'world'}) } + + async checkauth(req, res) { + if(this.getSession(req, res)) { + this.ok(req, res, { + authenticated: true, + userInfos: this.userinfos, + }) + } else { + this.ok(req, res, { + authenticated: false, + userInfos: null, + }) + } + + } + + async login(req, res) { + console.log('====>req.json', req.body) + let [isValid, payload, errors] = this.utils.validateMapObject(req.body, { + username: ((val, obj) => (typeof(val)=='string') && (val.length>3) ), + passwd: ((val, obj) => (typeof(val)=='string') && (val.length>7) ), + },{ + 'username': 'username', + 'passwd': 'passwd', + }) + + if((!isValid)){ + this.err(req, res, `Invalid request', 'Invalid login payload:: ${errors}`, 401) + return + } + + if((payload.username=='toto') && (payload.passwd=='azertyuiop')){ + req.session.userinfo = { + username: payload.username, + roles: ['admin'] + } + this.ok(req, res, { + authenticated: true, + userInfos: req.session.userinfo, + }) + } else { + this.ok(req, res, { + authenticated: false, + userInfos: null, + }) + } + } } -module.exports = P42ApiEndpoints; \ No newline at end of file diff --git a/p42api.js b/p42api.js index 8592046..890807c 100755 --- a/p42api.js +++ b/p42api.js @@ -1,44 +1,59 @@ #!/usr/bin/env node 'use strict' -const p42apiConfig = require("./p42api.json"); -const http = require('http'); -const express = require("express"); -const bodyParser = require('body-parser'); -const session = require('express-session') -const MySQLStore = require('express-mysql-session')(session); +import p42apiConfig from './p42api.json' with { type: 'json' } +import mysql from 'mysql2/promise' +import http from 'http' +import express from 'express' +import session from 'express-session' +import connectMySQL from 'express-mysql-session' +import { corsResolver } from './corsMiddleware.js' +import { P42ApiEndpoints } from './p42ApiEndpoints.js' -const corsResolver = require('./corsMiddleware') -const P42ApiEndpoints = require('./p42ApiEndpoints') +const MySQLStore = connectMySQL(session) +const app = express(); +app.set('trust proxy', 1) // trust first proxy (nginx), so we serve http to nginx, but we still behave as if we're in https +app.use(express.json()) +app.use(corsResolver) + +//TOTO: kick this const mysqlCreds = { // host: '127.0.0.1', // port: 3306, socketPath: '/var/run/mysqld/mysqld.sock', user: 'p42', password: 'C3h=V9!r>Mvc>skxPf9?W2P3duJTk', - database: 'p42' + database: 'p42', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 } -const sessionStore = new MySQLStore({ ...mysqlCreds, + +const db = await mysql.createConnection(mysqlCreds) +// { +// host: mysqlCreds.host, +// port: mysqlCreds.port, +// socketPath: mysqlCreds.socketPath, +// database: mysqlCreds.database, +// user: mysqlCreds.user, +// password: mysqlCreds.password +// }); + +const sessionStore = new MySQLStore({ createDatabaseTable: false, clearExpired: true, schema: { - tableName: 'p42_sessions', - columnNames: { - session_id: 'session_id', - expires: 'expires', - data: 'data' - } - } -}); + tableName: 'p42_sessions', + columnNames: { + session_id: 'session_id', + expires: 'expires', + data: 'data' + } + } +}, db) -const app = express(); -app.set('trust proxy', 1) // trust first proxy (nginx), so we serve http to nginx, but we still behave as if we're in https -app.use(bodyParser.urlencoded({ extended: false })); -app.use(bodyParser.json()) -app.use(corsResolver); - app.use(session({ name: 'p42.api.sid', secret: 'qNhy555Y9vyxj?!3yaYA=aKfgk+Wy5eymNtP*?4i', @@ -53,8 +68,8 @@ app.use(session({ } )) -let eps = new P42ApiEndpoints(app) -eps.connectDB(mysqlCreds) + +let eps = new P42ApiEndpoints(app, db) const server = http.createServer(app) .listen(p42apiConfig.listenPort, p42apiConfig.listenHost, function (req, res) { diff --git a/package.json b/package.json index df5312d..55121c7 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { - "dependencies": { - "argon2": "^0.44.0", - "body-parser": "^2.2.0", - "express": "^5.1.0", - "express-mysql-session": "^3.0.3", - "express-session": "^1.18.2", - "mysql": "^2.18.1", - "mysql2": "^3.14.3", - "swagger-ui-express": "^5.0.1" - } + "type": "module", + "dependencies": { + "argon2": "^0.44.0", + "body-parser": "^2.2.0", + "express": "^5.1.0", + "express-mysql-session": "^3.0.3", + "express-session": "^1.18.2", + "mysql": "^2.18.1", + "mysql2": "^3.14.3", + "swagger-ui-express": "^5.0.1" + } }