full code

This commit is contained in:
2023-12-07 12:04:19 +01:00
parent 484d2ad22a
commit 7656f57a11
45 changed files with 9659 additions and 0 deletions

102
middlewares/checkHeaders.js Executable file
View File

@@ -0,0 +1,102 @@
const conf = require(`../../conf/townconf.json`);
/**
* @api {get} http://header/CheckHeaders - CheckHeaders
* @apiGroup Middlewares
* @apiName CheckHeaders
* @apiDescription a list of headers are mandatory to access apxtri see in your space town /conf.json.exposedHeaders
*
* @apiHeader {string} xalias 'anonymous' or unique alias
* @apiHeader {string} xapp name of the webapp store in tribe/tribeid/www/{xapp}
* @apiHeader {string} xlang the 2 letter request langage (if does not exist then return en = english).
* @apiHeader {string} xtribe unique tribe name where xapp exist
* @apiHeader {string} xdays a timestamp 0 or generate during the authentifyme process
* @apiHeader {string} xhash anonymous or signature of message: xalias_xdays created by alias private key during authentifyme process
* @apiHeader {array[]} xprofils list of string profil apply into xtribe for xapp
* @apiHeader {string} xuuid a unique number uuid.v4 created the fisrt time a domain is visited on a device
* @apiHeader {integer} xtrkversion a version number link to tracking system
*
* @apiHeaderExample {json} Header-Example:
* {
* Cache-Control: "no-cache",
* Expires: 0, Pragma:"no-cache",
* xalias:"jojo",
* xapp:"presentation",
* xdays:1700733068298
* xhash:"LS0tLS1CRUdJTiBQR1AgU0lHTkVEIE1FU1NBR0UtLS0tLQpIYXNoOiBTSEE1MTIKCmpvam9fMTcwMDczMzA2ODI5OAotLS0tLUJFR0lOIFBHUCBTSUdOQVRVUkUtLS0tLQoKd25VRUFSWUtBQ2NGZ21WZklJd0prTmFVQ0daRHVUYnBGaUVFTjZlc1ZMSWdURmtPRGFVaDFwUUlaa081Ck51a0FBR09MQVA5OS96c21YeEd0b0VuYnpnekppZDJMcDA3YlBNZ1gwNUdhOUFVWjlCQm91Z0VBOVlYVworYjZIM2JHWHVhbEVOc3BrdUk1alNlTFNUWGNkSStjTExTZk5OQTg9Cj1uVjhNCi0tLS0tRU5EIFBHUCBTSUdOQVRVUkUtLS0tLQo=",
* xlang:"fr",
* xprofils:["anonymous", "pagans"],
* xtribe:"smatchit",
* xtrkversion:1,
* xuuid:"ea1cf73f-27f5-4c69-ab53-197a0feab9b2"
* }
* @apiErrorExample {json} Error-Response:
* HTTP/1/1 400 Not Found
* {
* status:400,
* ref:"middlewares",
* msg:"missingheaders",
* data:["headermissing1"]
* }
* @apiErrorExample {json} Error-Response:
* HTTP/1/1 404 Not Found
* {
* status:404,
* ref:"middlewares"
* msg:"tribeiddoesnotexist",
* data: {xalias}
* }
*/
const checkHeaders = (req, res, next) => {
req.session = {};
const header = {};
if (!req.header("xlang") && req.header("Content-Language"))
req.params.xlang = req.header("Content-Language");
let missingheader = [];
//console.log("req.headers", req.headers);
for (const h of conf.api.exposedHeaders) {
//console.log( h, req.header( h ) )
if (req.params[h]) {
header[h] = req.params[h];
} else if (req.header(h)) {
header[h] = req.header(h);
} else {
missingheader.push(h);
}
}
// console.log( 'pass header', header )
// store in session the header information
req.session.header = header;
// Each header have to be declared
if (missingheader != "") {
// bad request
return res.status(400).json({
status:400,
ref: "middlewares",
msg: "missingheader",
data: missingheader,
});
}
//console.log( req.app.locals.tribeids )
// xtribe == "town" is used during the setup process
// xtribe == "adminapi" is used to access /adminapi
if (
!(
["town","adminapi"].includes(header.xtribe) || req.app.locals.tribeids.includes(header.xtribe)
)
) {
return res.status(404).json({
status:404,
ref: "middlewares",
msg: "tribeiddoesnotexist",
data: { xtribe: header.xtribe },
});
}
if (!conf.api.languages.includes(header.xlang)) {
console.log("warning language requested does not exist force to english");
header.xlang = "en";
}
//set anonymous profil
req.session.header.xprofils=["anonymous"]
next();
};
module.exports = checkHeaders;

1
middlewares/footer.md Normal file
View File

@@ -0,0 +1 @@
Documentation Best practices

127
middlewares/header.md Normal file
View File

@@ -0,0 +1,127 @@
## api users and backend developers
api documentation for routes and middleware has to respect apidoc's rules [https://apidocjs.com/](https://apidocjs.com) 
To update this doc accessible in [https://wal-ants.ndda.fr/cdn/apidoc](https://wal-ants.ndda.fr/cdn/apidoc) :
`yarn apidoc` 
For api tribe's doc  accessible in [https://smatchit.io/cdn/apidoc](https://smatchit.io/cdn/apidoc) [:](https://smatchit.io/cdn/apidoc:) 
`yarn apidoctribename`
Objects manage in apxtri: pagans, notifications, nations, towns, tribes, wwws 
All others objects are manage in town/tribe 
```plaintext
/townName_nationName/
/apxtri/ # core process
/nationchains/conf.json # town settings contain all glabl parameter
```
url: **/api/routeName** For core api apxtri in /apxtri :
```plaintext
/apxtri/middlewares/
/apxtri/routes/
/apxtri/models/
/apxtri/models/lg/ lauage accessible by https://wall-ants.ndda.fr/nationchains/models/Checkjson_fr.json
/apxtri/models/unitest/
```
url: **/api/smatchit/routeName** for tribe smatchit example api in /town\_nation/tribes/smatchit(tribeid) 
```plaintext
/nationchains/tribes/smatchit/api/routes/
/nationchains/tribes/smatchit/api/models/
/nationchains/tribes/smatchit/api/models/lg/ language customization accessible https://smatchit.io/smatchit/models/model_lg.json
```
**static files** are served by nginx, each tribe nginx conf are store and can be customize in /nationchains/tribes/{tribe}/www/nginx\_{xtribe}\_{xapp}.conf
## Object management (Odmdb)
An object has a name and is defined by a schema that contain properties key.
A propertie has a name and a list of caracteristics (type, pattern,format,...) that have to be validate to be accepted.
All properties respect the rules [https://json-schema.org/draft/2020-12/schema,](https://json-schema.org/draft/2020-12/schema,) some extra"format" can be add to mutualise recurrent regex pattern
To access a schema [https://wall-ants.ndda.fr/nationchains/schema/nations.json](https://wall-ants.ndda.fr/nationchains/schema/nations.json) and language specifique [https//:wall-ants.ndda.fr/nationchains/schema/lg/nations\_fr.json](https//:wall-ants.ndda.fr/nationchains/schema/lg/nations_fr.json)
A checkjson.js is available to manage all specific format [https://wall-ants.ndda.fr/Checkjson.js](https://wall-ants.ndda.fr/Checkjson.js) see **Odmdb - schema Checkjson**
**Additional properties that not exist in 2020-12/schema :**
**required**: an array of required properties
**apxid**: the propertie used as an unique id
**apxuniquekey**: array of unique properties
**apxidx** : array of index
**apxaccessrights**: object with key profilname and accessrights on properties {profilname:{C:\[properties array\],R:\[properties array\],U:\[\],D:\[\]}}
Items of an object are store in files into :  
```plaintext
/objectnames/idx/keyval_objkey.json
/objectnames/itm/uniqueid.json
```
## api pre-request
**Valid header see Middlewares**
App use openpgp.js lib to sign xdays\_xalias with a privatekey and store it in xhash.
/middlewares/isAuthenticated.js check if (xhash) is a valid signature of the public key a xhash is valid for 24 hours
See Pagans models that contain authentification process
**api Return in 3 data structure:**
A - data file from a classical get  [https://wall-ants.ndda.fr/Checkjson.js](https://smatchit.io/Checkjson.js)
B -  a json single answer {status, ref,msg,data}:
* status: http code return
* ref: model/route name reference where message come from
* msg: a message template key store into models/lg/name\_lg.json (where lg is 2 letters language)
* data: an object data use to render the value of the message key.
C - a json multi answer {status,multimsg:\[{ref,msg,data}\]}
         Each {ref,msg,data\] work the same way than B
To show feedback context message in a language lg => get /nationchains/models/{{ref}}\_{{lg}}.json
This contain a json {msg:"mustache template string to render with data"}  
## Accessrights:
An alias is just an identity, to access a tribe, a person must exist with an authenticated alias into /nationchains/tribes/{tribename}/persons/itm/{alias}.json
A person has a property profils with a list of profilename, common profiles are : anonymous (no identity) / pagan (an identity)  / person (an identity with access right into a tribe) / druid (the administrator of a tribe) / major (administrator of a town/server)
Each object has an apxaccessrights that is a list of profil and CRUD access per object key .
## Add tribe's api:
Accessible with https://dns/api/tribename/routes
```plaintext
/nationchains/tribes/tribename/api/routes
/nationchains/tribes/tribename/api/middlewares
/nationchains/tribes/tribename/api/models
/nationchains/tribes/tribename/schema
/nationchains/tribes/tribename/schema/lg
```
```plaintext
// Example of a route
const conf = require(`../../../../../conf/townconf.json`);
const express = require(`../../../../../apxtri/node_modules/express`);
const fs = require(`../../../../../apxtri/node_modules/fs-extra`);
const Nofications = require(`../../../../../apxtri/models/Notifications.js`);
```

207
middlewares/isAuthenticated.js Executable file
View File

@@ -0,0 +1,207 @@
const fs = require("fs-extra");
const dayjs = require("dayjs");
//const path=require('path');
const glob = require("glob");
// To debug it could be easier with source code:
// const openpgp = require("/media/phil/usbfarm/apxtri/node_modules/openpgp/dist/node/openpgp.js");
const openpgp = require("openpgp");
/**
* @api {get} http://header/istauthenticated - isAuthenticated
* @apiGroup Middlewares
* @apiName isAuthenticated
* @apiDescription - valid if exist xalias_xdays_xhash.substr(20,200) in town/tmp/tokens/
* - if not,
* - valid if xhash signature sign xalias_xdays with alias's publickey.
* - if not valid => not allowed
* - If valid =>
* - store a xalias_xdays_xhash.substr (20,200) into /tmp/tokens with xprofils array from person.
* - update header.xprofils from this token
*
* apxtri profils are anonymous, pagans, mayor (on a node server), druid (on a tribe like smatchit).
*
* pagan identity is independant of domain (tribe), by default profils are :['anonymous','pagans']. if this alias exist in a tribe domain as a person then his profils come from /tribes/{tribeId}/objects/person/itm/{alias}.json profils:['anonymous','pagans','person','seeker'] any profils allowed to act on tribe objects.
*
* Each profil have CRUD accessright on object managed in schema in apxaccessrights:{owner,profil:{"C":[],"R":[properties],"U":[properties],"D":[]}}, see Odmdb for details.
*
* A process run once each day to clean up all xhash tmp/tokens oldest than 24 hours.
*
**/
const isAuthenticated = async (req, res, next) => {
/*console.log('PASSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS')
console.log(__dirname)
console.log(path.resolve('../tmp/tokens'))
if (fs.existsSync('../tmp/tokens')) console.log('pass A')
if (fs.existsSync('../tmp/tokens')) console.log('pass B')
*/
const withlog = true;
const currentday = dayjs().date();
fs.ensureDirSync(`../tmp/tokens`);
let menagedone = fs.existsSync(
`../tmp/tokens/menagedone${currentday}`
);
if (withlog)
console.log(`menagedone${currentday} was it done today?:${menagedone}`);
if (!menagedone) {
// clean oldest
const tsday = dayjs().valueOf(); // now in timestamp format
glob.sync(`../tmp/tokens/menagedone*`).forEach((f) => {
fs.removeSync(f);
});
glob.sync(`../tmp/tokens/*.json`).forEach((f) => {
const fsplit = f.split("_");
const elapse = tsday - parseInt(fsplit[2]);
//24h 86400000 milliseconde 15mn 900000
if (elapse && elapse > 86400000) {
fs.remove(f);
}
});
fs.outputFile(
`../tmp/tokens/menagedone${currentday}`,
"done by middleware/isAUthenticated"
);
}
//Check register in tmp/tokens/
if (withlog) console.log("isAuthenticate?", req.session.header, req.body);
const resnotauth = {
ref: "middlewares",
msg: "notauthenticated",
data: {
xalias: req.session.header.xalias,
xaliasexists: true,
},
};
if (
req.session.header.xalias == "anonymous" ||
req.session.header.xhash == "anonymous"
) {
if (withlog) console.log("alias anonymous means not auth");
resnotauth.status = 401;
return res.status(resnotauth.status).json(resnotauth);
}
let tmpfs = `../tmp/tokens/${req.session.header.xalias}_${req.session.header.xtribe}_${req.session.header.xdays}`;
//max filename in ext4: 255 characters
tmpfs += `_${req.session.header.xhash.substring(
150,
150 + tmpfs.length - 249
)}.json`;
const bruteforcepenalty = async (alias, action) => {
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const failstamp = `../tmp/tokens/${alias}.json`;
if (action == "clean") {
//to reinit bruteforce checker
if (withlog) console.log("try to clean penalty file ", failstamp);
fs.remove(failstamp, (err) => {
if (err) console.log("Check forcebrut ", err);
});
} else if (action == "penalty") {
const stamp = fs.existsSync(failstamp)
? fs.readJSONSync(failstamp)
: { lastfail: dayjs().format(), numberfail: 0 };
stamp.lastfail = dayjs().format();
stamp.numberfail += 1;
fs.outputJSON(failstamp, stamp);
if (withlog) console.log("penalty:", stamp);
await sleep(stamp.numberfail * 100); //increase of 0,1 second the answer time per fail
if (withlog) console.log("time out penalty");
}
};
if (!fs.existsSync(tmpfs)) {
// need to check detached sign
let publickey = "";
console.log(process.cwd());
console.log(process.env.PWD);
console.log(__dirname);
const aliasinfo = `../nationchains/pagans/itm/${req.session.header.xalias}.json`;
if (fs.existsSync(aliasinfo)) {
publickey = fs.readJsonSync(aliasinfo).publickey;
} else if (req.body.publickey) {
resnotauth.data.xaliasexists = false;
publickey = req.body.publickey;
}
if (publickey == "") {
if (withlog) console.log("alias unknown");
resnotauth.status = 404;
resnotauth.data.xaliasexists = false;
return res.status(resnotauth.status).send(resnotauth);
}
if (withlog) console.log("publickey", publickey);
if (publickey.substring(0, 31) !== "-----BEGIN PGP PUBLIC KEY BLOCK") {
if (withlog)
console.log("Publickey is not valid as armored key:", publickey);
await bruteforcepenalty(req.session.header.xalias, "penalty");
resnotauth.status = 404;
return res.status(resnotauth.status).send(resnotauth);
}
const clearmsg = Buffer.from(req.session.header.xhash, "base64").toString();
if (clearmsg.substring(0, 10) !== "-----BEGIN") {
if (withlog)
console.log("xhash conv is not valid as armored key:", clearmsg);
await bruteforcepenalty(req.session.header.xalias, "penalty");
resnotauth.status = 404;
return res.status(resnotauth.status).send(resnotauth);
}
if (withlog) console.log("clearmsg", clearmsg);
const pubkey = await openpgp.readKey({ armoredKey: publickey });
const signedMessage = await openpgp.readCleartextMessage({
cleartextMessage: clearmsg,
});
const verificationResult = await openpgp.verify({
message: signedMessage,
verificationKeys: pubkey,
});
if (withlog) console.log(verificationResult);
if (withlog) console.log(verificationResult.signatures[0].keyID.toHex());
try {
await verificationResult.signatures[0].verified;
if (
verificationResult.data !=
`${req.session.header.xalias}_${req.session.header.xdays}`
) {
resnotauth.msg = "signaturefailled";
if (withlog)
console.log(
`message recu:${verificationResult.data} , message attendu:${req.session.header.xalias}_${req.session.header.xdays}`
);
await bruteforcepenalty(req.session.header.xalias, "penalty");
resnotauth.status = 401;
return res.status(resnotauth.status).send(resnotauth);
}
} catch (e) {
resnotauth.msg = "signaturefailled";
if (withlog) console.log("erreur", e);
await bruteforcepenalty(req.session.header.xalias, "penalty");
resnotauth.status = 401;
return res.status(resnotauth.status).send(resnotauth);
}
// authenticated then get person profils (person = pagan for a xtrib)
const person = `${process.env.dirtown}/tribes/${req.session.header.xtribe}/objects/persons/itm/${req.session.header.xalias}.json`;
if (withlog) {
console.log("Profils tribe/app management");
console.log("person", person);
}
if (fs.existsSync(person)) {
const infoperson = fs.readJSONSync(person);
console.log(infoperson);
infoperson.profils.forEach((p) => {
if (!req.session.header.xprofils.includes(p)) req.session.header.xprofils.push(p);
})
}else{
if (!req.session.header.xprofils.includes('pagans')) req.session.header.xprofils.push("pagans");
}
fs.outputJSONSync(tmpfs, req.session.header.xprofils);
} else {
//tmpfs exist get profils from identification process
req.session.header.xprofils = fs.readJSONSync(tmpfs);
}
bruteforcepenalty(req.session.header.xalias, "clean");
console.log(`${req.session.header.xalias} Authenticated`);
next();
};
module.exports = isAuthenticated;