add middleware security to prevent injection
This commit is contained in:
+147
-34
@@ -43,6 +43,14 @@ SEE README.md to start
|
|||||||
********************************************/
|
********************************************/
|
||||||
const apxtri = {};
|
const apxtri = {};
|
||||||
|
|
||||||
|
apxtri.istribe = (tribe) => {
|
||||||
|
if (typeof tribe !== "string" || !/^[a-z0-9_-]{1,64}$/.test(tribe)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tribe === "adminapi") return true;
|
||||||
|
return fs.existsSync(`../${tribe}/apxtri/routes`);
|
||||||
|
};
|
||||||
|
|
||||||
apxtri.main = async () => {
|
apxtri.main = async () => {
|
||||||
if (process.env.NODE_MODE !== 'dev' && !fs.existsSync("/etc/nginx/nginx.conf")) {
|
if (process.env.NODE_MODE !== 'dev' && !fs.existsSync("/etc/nginx/nginx.conf")) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -51,39 +59,97 @@ apxtri.main = async () => {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
//Check prerequest data
|
//Check prerequest data
|
||||||
if (
|
const adminapipath = "../adminapi/objects/tribes/itm/adminapi.json";
|
||||||
fs.existsSync("../adminapi/objects/tribes/idx/tribes_dns.json") &&
|
const tribednspath = "../adminapi/objects/tribes/idx/tribes_dns.json";
|
||||||
fs.existsSync("../adminapi/objects/tribes/itm/adminapi.json")
|
// An existing adminapi.json means this is not a first install: never
|
||||||
) {
|
// overwrite it from the template, even if tribes_dns.json is missing.
|
||||||
// check all tribes are in tribes_dns.json
|
if (fs.existsSync(adminapipath)) {
|
||||||
const conf = fs.readJSONSync(
|
// check all tribes are in tribes_dns.json (rebuild it if needed)
|
||||||
"../adminapi/objects/tribes/itm/adminapi.json"
|
const conf = fs.readJSONSync(adminapipath);
|
||||||
|
const tribesdns = fs.existsSync(tribednspath)
|
||||||
|
? fs.readJsonSync(tribednspath)
|
||||||
|
: {};
|
||||||
|
Object.keys(tribesdns).forEach((tribe) => {
|
||||||
|
if (!apxtri.istribe(tribe)) {
|
||||||
|
console.log(
|
||||||
|
`\x1b[31m drop unknown tribe from tribes_dns.json: ${tribe}\x1b[0m`
|
||||||
|
);
|
||||||
|
delete tribesdns[tribe];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// A partial/restored adminapi.json must not crash the whole server: fill
|
||||||
|
// the runtime configuration from the install template and warn loudly.
|
||||||
|
const initconf = fs.readJsonSync(
|
||||||
|
"../adminapi/apxtri/setup/initadminapi.json"
|
||||||
);
|
);
|
||||||
const tribesdns = fs.readJsonSync(
|
if (!conf.api) {
|
||||||
`../adminapi/objects/tribes/idx/tribes_dns.json`
|
conf.api = JSON.parse(JSON.stringify(initconf.api));
|
||||||
);
|
console.log(
|
||||||
//check if new tribe was add
|
"\x1b[31m adminapi.json is missing 'api', using defaults from apxtri/setup/initadminapi.json. Restore this file from a backup.\x1b[0m"
|
||||||
glob
|
);
|
||||||
|
}
|
||||||
|
if (!conf.socket) {
|
||||||
|
conf.socket = JSON.parse(JSON.stringify(initconf.socket));
|
||||||
|
console.log(
|
||||||
|
"\x1b[31m adminapi.json is missing 'socket', using default port from apxtri/setup/initadminapi.json.\x1b[0m"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//check if new tribe was add (only real tribe folders, never tmp/.git/...)
|
||||||
|
const tribedirs = glob
|
||||||
.sync("../*")
|
.sync("../*")
|
||||||
.filter((f) => fs.lstatSync(f).isDirectory())
|
.filter((f) => fs.lstatSync(f).isDirectory())
|
||||||
.forEach(async (t) => {
|
.filter((f) => fs.existsSync(`${f}/apxtri/routes`));
|
||||||
const tribe = path.basename(t);
|
for (const t of tribedirs) {
|
||||||
//console.log(tribe);
|
const tribe = path.basename(t);
|
||||||
if (![".", ".."].includes(tribe) && !tribesdns[tribe]) {
|
if ([".", ".."].includes(tribe) || tribesdns[tribe]) continue;
|
||||||
await apxtri.setuptribe(tribe, conf);
|
// Never re-initialize an already configured tribe: if its itm file
|
||||||
|
// exists, just restore its dns entry instead of overwriting the config.
|
||||||
|
const itmpath = `../adminapi/objects/tribes/itm/${tribe}.json`;
|
||||||
|
if (fs.existsSync(itmpath)) {
|
||||||
|
try {
|
||||||
|
const existing = fs.readJSONSync(itmpath);
|
||||||
|
if (Array.isArray(existing.dns) && existing.dns.length > 0) {
|
||||||
|
tribesdns[tribe] = existing.dns;
|
||||||
|
console.log(
|
||||||
|
`tribe ${tribe} already configured, restored in tribes_dns.json`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(
|
||||||
|
`unreadable itm/${tribe}.json, skipping:`,
|
||||||
|
err.message
|
||||||
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
await apxtri.setuptribe(tribe, conf);
|
||||||
|
}
|
||||||
|
// merge with the on-disk index so setuptribe writes are preserved
|
||||||
|
const ondiskdns = fs.existsSync(tribednspath)
|
||||||
|
? fs.readJsonSync(tribednspath)
|
||||||
|
: {};
|
||||||
|
Object.keys(ondiskdns).forEach((tribe) => {
|
||||||
|
if (!apxtri.istribe(tribe)) delete ondiskdns[tribe];
|
||||||
|
});
|
||||||
|
fs.outputJSONSync(
|
||||||
|
tribednspath,
|
||||||
|
Object.assign({}, ondiskdns, tribesdns),
|
||||||
|
{ space: 2 }
|
||||||
|
);
|
||||||
|
// run api with the (possibly repaired) in-memory conf
|
||||||
|
await apxtri.runexpress(fs.readJsonSync(tribednspath), conf);
|
||||||
} else {
|
} else {
|
||||||
const initconf = fs.readJsonSync(
|
const initconf = fs.readJsonSync(
|
||||||
"../adminapi/apxtri/setup/initadminapi.json"
|
"../adminapi/apxtri/setup/initadminapi.json"
|
||||||
);
|
);
|
||||||
await apxtri.setuptribe("adminapi", initconf);
|
await apxtri.setuptribe("adminapi", initconf);
|
||||||
|
// run api with update conf
|
||||||
|
await apxtri.runexpress(
|
||||||
|
fs.readJsonSync(`../adminapi/objects/tribes/idx/tribes_dns.json`),
|
||||||
|
fs.readJSONSync("../adminapi/objects/tribes/itm/adminapi.json")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// run api with update conf
|
|
||||||
apxtri.runexpress(
|
|
||||||
fs.readJsonSync(`../adminapi/objects/tribes/idx/tribes_dns.json`),
|
|
||||||
fs.readJSONSync("../adminapi/objects/tribes/itm/adminapi.json")
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
apxtri.getip = async () => {
|
apxtri.getip = async () => {
|
||||||
/**
|
/**
|
||||||
@@ -112,6 +178,21 @@ apxtri.getip = async () => {
|
|||||||
return network;
|
return network;
|
||||||
};
|
};
|
||||||
apxtri.setuptribe = async (tribe, conf) => {
|
apxtri.setuptribe = async (tribe, conf) => {
|
||||||
|
if (!apxtri.istribe(tribe)) {
|
||||||
|
console.log(
|
||||||
|
`\x1b[31m skip setuptribe: ${tribe} is not an existing tribe folder\x1b[0m`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tribe != "adminapi") {
|
||||||
|
const existingitm = `../adminapi/objects/tribes/itm/${tribe}.json`;
|
||||||
|
if (fs.existsSync(existingitm)) {
|
||||||
|
console.log(
|
||||||
|
`\x1b[31m skip setuptribe: ${tribe} is already configured\x1b[0m`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
let inittribe;
|
let inittribe;
|
||||||
if (tribe == "adminapi") {
|
if (tribe == "adminapi") {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -131,9 +212,13 @@ apxtri.setuptribe = async (tribe, conf) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`a new tribe called ${tribe} was detected`);
|
console.log(`a new tribe called ${tribe} was detected`);
|
||||||
const adminapiconf = fs.readJSONSync(
|
const initconf = fs.readJsonSync(
|
||||||
"../adminapi/objects/tribes/itm/adminapi.json"
|
"../adminapi/apxtri/setup/initadminapi.json"
|
||||||
);
|
);
|
||||||
|
const adminapiconf =
|
||||||
|
conf && conf.api && conf.socket
|
||||||
|
? conf
|
||||||
|
: fs.readJSONSync("../adminapi/objects/tribes/itm/adminapi.json");
|
||||||
inittribe = {
|
inittribe = {
|
||||||
tribeId: tribe,
|
tribeId: tribe,
|
||||||
townpath: __dirname.replace("/adminapi/apxtri", ""),
|
townpath: __dirname.replace("/adminapi/apxtri", ""),
|
||||||
@@ -142,8 +227,8 @@ apxtri.setuptribe = async (tribe, conf) => {
|
|||||||
nationId: conf.nationId,
|
nationId: conf.nationId,
|
||||||
townId: conf.townId,
|
townId: conf.townId,
|
||||||
activelog: [],
|
activelog: [],
|
||||||
api: { port: adminapiconf.api.port },
|
api: { port: (adminapiconf.api || initconf.api).port },
|
||||||
socket: { port: adminapiconf.socket.port },
|
socket: { port: (adminapiconf.socket || initconf.socket).port },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
inittribe.sudoUser = process.env.USER;
|
inittribe.sudoUser = process.env.USER;
|
||||||
@@ -171,7 +256,7 @@ apxtri.setuptribe = async (tribe, conf) => {
|
|||||||
let lsttribe = fs.existsSync(lst_tribeIdpath)
|
let lsttribe = fs.existsSync(lst_tribeIdpath)
|
||||||
? fs.readJsonSync(lst_tribeIdpath)
|
? fs.readJsonSync(lst_tribeIdpath)
|
||||||
: [];
|
: [];
|
||||||
lsttribe.push(tribe);
|
if (!lsttribe.includes(tribe)) lsttribe.push(tribe);
|
||||||
fs.outputJSONSync(lst_tribeIdpath, lsttribe);
|
fs.outputJSONSync(lst_tribeIdpath, lsttribe);
|
||||||
const tribes_dnspath = "../adminapi/objects/tribes/idx/tribes_dns.json";
|
const tribes_dnspath = "../adminapi/objects/tribes/idx/tribes_dns.json";
|
||||||
const tribedns = fs.existsSync(tribes_dnspath)
|
const tribedns = fs.existsSync(tribes_dnspath)
|
||||||
@@ -248,7 +333,12 @@ apxtri.setuptribe = async (tribe, conf) => {
|
|||||||
|
|
||||||
apxtri.runexpress = async (tribesdns, conf) => {
|
apxtri.runexpress = async (tribesdns, conf) => {
|
||||||
const Odmdb = require(path.resolve("./apxtri/models/Odmdb.js"));
|
const Odmdb = require(path.resolve("./apxtri/models/Odmdb.js"));
|
||||||
let tribeIds = Object.keys(tribesdns);
|
const checkRequestSafety = require(
|
||||||
|
path.resolve("./apxtri/middlewares/checkRequestSafety.js")
|
||||||
|
);
|
||||||
|
let tribeIds = Object.keys(tribesdns).filter((tribe) =>
|
||||||
|
apxtri.istribe(tribe)
|
||||||
|
);
|
||||||
// context is store in /itm/tribename.json ={contexte:{routes:[],models:[{model:,tplstringslg:[]}]}
|
// context is store in /itm/tribename.json ={contexte:{routes:[],models:[{model:,tplstringslg:[]}]}
|
||||||
// routes={url,route} check how to add plugin tribe route later
|
// routes={url,route} check how to add plugin tribe route later
|
||||||
// keep only the 2 last part (.) of domain name to validate requests with it (generic domain)
|
// keep only the 2 last part (.) of domain name to validate requests with it (generic domain)
|
||||||
@@ -260,6 +350,13 @@ apxtri.runexpress = async (tribesdns, conf) => {
|
|||||||
if (!doms.includes(dm)) doms.push(dm);
|
if (!doms.includes(dm)) doms.push(dm);
|
||||||
//reindex database attention check dev-ants/.. a bug was fixed
|
//reindex database attention check dev-ants/.. a bug was fixed
|
||||||
glob.sync(`../${t}/objects/*`).forEach((o) => {
|
glob.sync(`../${t}/objects/*`).forEach((o) => {
|
||||||
|
if (!fs.existsSync(o) || !fs.lstatSync(o).isDirectory()) return;
|
||||||
|
if (
|
||||||
|
!fs.existsSync(path.join(o, "itm")) &&
|
||||||
|
!fs.existsSync(path.join(o, "conf.json"))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("reindex: ", o);
|
console.log("reindex: ", o);
|
||||||
Odmdb.runidx(o);
|
Odmdb.runidx(o);
|
||||||
});
|
});
|
||||||
@@ -292,18 +389,34 @@ apxtri.runexpress = async (tribesdns, conf) => {
|
|||||||
});
|
});
|
||||||
const app = express();
|
const app = express();
|
||||||
// load express parameter from conf
|
// load express parameter from conf
|
||||||
Object.keys(conf.api.appset).forEach((p) => {
|
const apiconf = conf.api || {};
|
||||||
app.set(p, conf.api.appset[p]);
|
if (apiconf.appset && apiconf.appset["trust proxy"] === true) {
|
||||||
|
apiconf.appset["trust proxy"] = 1;
|
||||||
|
console.log(
|
||||||
|
"\x1b[31m api.appset['trust proxy']=true is unsafe, using 1 (nginx only)\x1b[0m"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Object.keys(apiconf.appset || {}).forEach((p) => {
|
||||||
|
app.set(p, apiconf.appset[p]);
|
||||||
});
|
});
|
||||||
// To set depending of data form or get size to send
|
// To set depending of data form or get size to send
|
||||||
tribeIds.forEach((t) => {
|
tribeIds.forEach((t) => {
|
||||||
app.use(`/${t}`, bodyParser.urlencoded(conf.api.bodyparse.urlencoded));
|
app.use(
|
||||||
app.use(`/${t}`, bodyParser.json(conf.api.bodyparse.json));
|
`/${t}`,
|
||||||
|
bodyParser.urlencoded(
|
||||||
|
(apiconf.bodyparse && apiconf.bodyparse.urlencoded) || {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
`/${t}`,
|
||||||
|
bodyParser.json((apiconf.bodyparse && apiconf.bodyparse.json) || {}),
|
||||||
|
);
|
||||||
// To set depending of post put json data size to send
|
// To set depending of post put json data size to send
|
||||||
app.use(`/${t}`, express.json(conf.api.json));
|
app.use(`/${t}`, express.json(apiconf.json || {}));
|
||||||
});
|
});
|
||||||
app.disable("x-powered-by"); // for security
|
app.disable("x-powered-by"); // for security
|
||||||
app.locals.tribeids = tribeIds;
|
app.locals.tribeids = tribeIds;
|
||||||
|
app.use(checkRequestSafety);
|
||||||
const currentmod = "apxtri";
|
const currentmod = "apxtri";
|
||||||
const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(currentmod) : false;
|
const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(currentmod) : false;
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(cu
|
|||||||
* data: {xalias}
|
* data: {xalias}
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
const aliaspattern = /^[a-z0-9]{3,64}$/;
|
||||||
|
const namepattern = /^[A-Za-z0-9_.:-]{1,64}$/;
|
||||||
|
const indexpattern = /^[A-Za-z0-9_.-]{1,128}$/;
|
||||||
|
const uuidpattern =
|
||||||
|
/^(0|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
|
||||||
|
const emailpattern =
|
||||||
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
|
|
||||||
const checkHeaders = (req, res, next) => {
|
const checkHeaders = (req, res, next) => {
|
||||||
req.session = {};
|
req.session = {};
|
||||||
const header = {};
|
const header = {};
|
||||||
@@ -90,14 +98,118 @@ const checkHeaders = (req, res, next) => {
|
|||||||
data: {missingheader},
|
data: {missingheader},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const allowedtribes = [
|
||||||
|
"town",
|
||||||
|
"adminapi",
|
||||||
|
...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
|
||||||
|
];
|
||||||
|
const invalidheader = (field, value) =>
|
||||||
|
res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "middlewares",
|
||||||
|
msg: "headervalueinvalid",
|
||||||
|
data: { field, value },
|
||||||
|
});
|
||||||
|
if (header.xalias !== "anonymous" && !aliaspattern.test(header.xalias)) {
|
||||||
|
return invalidheader("xalias", header.xalias);
|
||||||
|
}
|
||||||
|
if (!uuidpattern.test(header.xuuid)) {
|
||||||
|
return invalidheader("xuuid", header.xuuid);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
header.xapp === "." ||
|
||||||
|
header.xapp === ".." ||
|
||||||
|
!namepattern.test(header.xapp)
|
||||||
|
) {
|
||||||
|
return invalidheader("xapp", header.xapp);
|
||||||
|
}
|
||||||
|
if (header.xdays !== undefined && !/^\d{0,16}$/.test(header.xdays)) {
|
||||||
|
return invalidheader("xdays", header.xdays);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
header.xtrkversion !== undefined &&
|
||||||
|
!/^\d{0,10}$/.test(header.xtrkversion)
|
||||||
|
) {
|
||||||
|
return invalidheader("xtrkversion", header.xtrkversion);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
req.headers.xworkon !== undefined &&
|
||||||
|
!allowedtribes.includes(req.headers.xworkon)
|
||||||
|
) {
|
||||||
|
return res.status(404).json({
|
||||||
|
status: 404,
|
||||||
|
ref: "middlewares",
|
||||||
|
msg: "tribeiddoesnotexist",
|
||||||
|
data: { xworkon: req.headers.xworkon },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const invalidparam = (param, value) =>
|
||||||
|
res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "middlewares",
|
||||||
|
msg: "paramvalueinvalid",
|
||||||
|
data: { param, value },
|
||||||
|
});
|
||||||
|
for (const param of ["tribe", "tribeId", "tribeid"]) {
|
||||||
|
if (
|
||||||
|
req.params[param] !== undefined &&
|
||||||
|
!allowedtribes.includes(req.params[param])
|
||||||
|
) {
|
||||||
|
return res.status(404).json({
|
||||||
|
status: 404,
|
||||||
|
ref: "middlewares",
|
||||||
|
msg: "tribeiddoesnotexist",
|
||||||
|
data: { tribe: req.params[param] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const paramrules = {
|
||||||
|
alias: aliaspattern,
|
||||||
|
objectname: indexpattern,
|
||||||
|
optionname: indexpattern,
|
||||||
|
indexname: indexpattern,
|
||||||
|
primaryindex: /^[A-Za-z0-9_.@+-]{1,128}$/,
|
||||||
|
primaryid: /^[A-Za-z0-9_.@+-]{1,128}$/,
|
||||||
|
apxid: /^[A-Za-z0-9_]{1,64}$/,
|
||||||
|
articleid: indexpattern,
|
||||||
|
evaluation: /^(follow|[0-4])$/,
|
||||||
|
app: namepattern,
|
||||||
|
key: namepattern,
|
||||||
|
jobadid: indexpattern,
|
||||||
|
jobstepid: indexpattern,
|
||||||
|
personalias: aliaspattern,
|
||||||
|
recruiter: aliaspattern,
|
||||||
|
seeker: aliaspattern,
|
||||||
|
profil: namepattern,
|
||||||
|
wconame: namepattern,
|
||||||
|
appname: namepattern,
|
||||||
|
pagename: namepattern,
|
||||||
|
website: namepattern,
|
||||||
|
template: namepattern,
|
||||||
|
mlist: namepattern,
|
||||||
|
srckey: namepattern,
|
||||||
|
action: namepattern,
|
||||||
|
pluginname: namepattern,
|
||||||
|
filename: namepattern,
|
||||||
|
email: emailpattern,
|
||||||
|
siret: /^\d{14}$/,
|
||||||
|
typekey: /^[A-Za-z0-9_.-]{1,32}$/,
|
||||||
|
data: /^[A-Za-z0-9_.@+-]{1,128}$/,
|
||||||
|
validation: /^[A-Za-z0-9_-]{1,128}$/,
|
||||||
|
digit6: /^\d{1,6}$/,
|
||||||
|
version: /^\d{1,16}$/,
|
||||||
|
};
|
||||||
|
for (const param of Object.keys(paramrules)) {
|
||||||
|
const value = req.params[param];
|
||||||
|
if (value === undefined || value === "") continue;
|
||||||
|
if (value === "." || value === ".." || !paramrules[param].test(value)) {
|
||||||
|
return invalidparam(param, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
//console.log( req.app.locals.tribeids )
|
//console.log( req.app.locals.tribeids )
|
||||||
// xtribe == "town" is used during the setup process
|
// xtribe == "town" is used during the setup process
|
||||||
// xtribe == "adminapi" is used to access /adminapi
|
// xtribe == "adminapi" is used to access /adminapi
|
||||||
if (
|
if (!allowedtribes.includes(header.xtribe)) {
|
||||||
!(
|
|
||||||
["town","adminapi"].includes(header.xtribe) || req.app.locals.tribeids.includes(header.xtribe)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
status:404,
|
status:404,
|
||||||
ref: "middlewares",
|
ref: "middlewares",
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const Security = require("../models/Security.js");
|
||||||
|
|
||||||
|
const currentmod = "checkRequestSafety";
|
||||||
|
const log = process.env.ACTIVELOG
|
||||||
|
? process.env.ACTIVELOG.split(",").includes(currentmod)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const pathHeaders = ["xtribe", "xapp", "xuuid", "xalias", "xworkon"];
|
||||||
|
const enforce = process.env.SECURITY_TRAVERSAL_ENFORCE === "true";
|
||||||
|
|
||||||
|
const collect = (value, field, findings, depth = 0) => {
|
||||||
|
if (depth > 8 || findings.length >= 5) return;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (Security.freeTextFields.includes(field)) return;
|
||||||
|
if (/^imgbase64_/i.test(field)) return;
|
||||||
|
if (Security.isTraversal(value)) findings.push({ field, value });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.slice(0, 100).forEach((item) => collect(item, field, findings, depth + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
Object.keys(value)
|
||||||
|
.slice(0, 100)
|
||||||
|
.forEach((key) => collect(value[key], key, findings, depth + 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkRequestSafety = (req, res, next) => {
|
||||||
|
const findings = [];
|
||||||
|
collect(req.originalUrl, "url", findings);
|
||||||
|
collect(req.path, "path", findings);
|
||||||
|
collect(req.params, "params", findings);
|
||||||
|
collect(req.query, "query", findings);
|
||||||
|
collect(req.body, "body", findings);
|
||||||
|
pathHeaders.forEach((header) => {
|
||||||
|
const value = req.headers[header];
|
||||||
|
if (value !== undefined && Security.isTraversal(value)) {
|
||||||
|
findings.push({ field: header, value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unique = [];
|
||||||
|
findings.forEach((finding) => {
|
||||||
|
const key = `${finding.field}|${finding.value}`;
|
||||||
|
if (!unique.some((item) => `${item.field}|${item.value}` === key)) {
|
||||||
|
unique.push(finding);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unique.length > 0) {
|
||||||
|
if (log) console.log(currentmod, "suspicious request", unique);
|
||||||
|
unique.forEach((finding) => Security.alert(req, finding));
|
||||||
|
if (enforce) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "middlewares",
|
||||||
|
msg: "invalidrequest",
|
||||||
|
data: { fields: unique.map((finding) => finding.field) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = checkRequestSafety;
|
||||||
@@ -117,13 +117,33 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
if (log) console.log(currentmod,"time out penalty");
|
if (log) console.log(currentmod,"time out penalty");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const paganpath = `../adminapi/objects/pagans/itm/${req.session.header.xalias}.json`;
|
||||||
|
const paganexists = fs.existsSync(paganpath);
|
||||||
|
const base = req.baseUrl || "";
|
||||||
|
const ispaganroute = /\/pagans\/?$/.test(base);
|
||||||
|
const iscreatepagan =
|
||||||
|
req.method === "POST" &&
|
||||||
|
ispaganroute &&
|
||||||
|
(req.path === "/" || req.path === "");
|
||||||
|
if (!paganexists && !iscreatepagan) {
|
||||||
|
resnotauth.status = 401;
|
||||||
|
resnotauth.data.xaliasexists = false;
|
||||||
|
return res.status(resnotauth.status).json(resnotauth);
|
||||||
|
}
|
||||||
|
if (iscreatepagan && req.body.alias !== req.session.header.xalias) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Middlewares",
|
||||||
|
msg: "aliasmismatch",
|
||||||
|
data: { xalias: req.session.header.xalias, alias: req.body.alias },
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!fs.existsSync(tmpfs)) {
|
if (!fs.existsSync(tmpfs)) {
|
||||||
// need to check detached sign
|
// need to check detached sign
|
||||||
let publickey = "";
|
let publickey = "";
|
||||||
const aliasinfo = `../adminapi/objects/pagans/itm/${req.session.header.xalias}.json`;
|
if (paganexists) {
|
||||||
if (fs.existsSync(aliasinfo)) {
|
publickey = fs.readJsonSync(paganpath).publickey;
|
||||||
publickey = fs.readJsonSync(aliasinfo).publickey;
|
} else if (iscreatepagan && req.body.publickey) {
|
||||||
} else if (req.body.publickey) {
|
|
||||||
resnotauth.data.xaliasexists = false;
|
resnotauth.data.xaliasexists = false;
|
||||||
publickey = req.body.publickey;
|
publickey = req.body.publickey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ Checkjson.schema.properties.format = {
|
|||||||
ipv4: /^([0–9]{1,3}.){3}.([0–9]{1,3})$/,
|
ipv4: /^([0–9]{1,3}.){3}.([0–9]{1,3})$/,
|
||||||
ipv6: /^((([0–9A-Fa-f]{1,4}:){7}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){6}:[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){5}:([0–9A-Fa-f]{1,4}:)?[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){4}:([0–9A-Fa-f]{1,4}:){0,2}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){3}:([0–9A-Fa-f]{1,4}:){0,3}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){2}:([0–9A-Fa-f]{1,4}:){0,4}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){6}((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|(([0–9A-Fa-f]{1,4}:){0,5}:((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|(::([0–9A-Fa-f]{1,4}:){0,5}((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|([0–9A-Fa-f]{1,4}::([0–9A-Fa-f]{1,4}:){0,5}[0–9A-Fa-f]{1,4})|(::([0–9A-Fa-f]{1,4}:){0,6}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){1,7}:))$/,
|
ipv6: /^((([0–9A-Fa-f]{1,4}:){7}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){6}:[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){5}:([0–9A-Fa-f]{1,4}:)?[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){4}:([0–9A-Fa-f]{1,4}:){0,2}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){3}:([0–9A-Fa-f]{1,4}:){0,3}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){2}:([0–9A-Fa-f]{1,4}:){0,4}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){6}((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|(([0–9A-Fa-f]{1,4}:){0,5}:((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|(::([0–9A-Fa-f]{1,4}:){0,5}((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|([0–9A-Fa-f]{1,4}::([0–9A-Fa-f]{1,4}:){0,5}[0–9A-Fa-f]{1,4})|(::([0–9A-Fa-f]{1,4}:){0,6}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){1,7}:))$/,
|
||||||
telephonefr: /^0[1-9][0-9]{8}$/,
|
telephonefr: /^0[1-9][0-9]{8}$/,
|
||||||
imgbase64: /data:image\/(png|jpg|jpeg|svg);base64,(?:[A-Za-z0-9+\/]+)/,
|
imgbase64:
|
||||||
|
/^data:image\/(png|jpeg|jpg|webp|gif);base64,[A-Za-z0-9+/]+={0,2}$/i,
|
||||||
telephoneinter: /^\+*(\d{3})*[0-9,\-]{8,}/,
|
telephoneinter: /^\+*(\d{3})*[0-9,\-]{8,}/,
|
||||||
password:
|
password:
|
||||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&.])[A-Za-z\d$@$!%*?&.{}:|\s]{8,}/,
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&.])[A-Za-z\d$@$!%*?&.{}:|\s]{8,}/,
|
||||||
|
|||||||
@@ -21,7 +21,18 @@ const log = process.env.ACTIVELOG
|
|||||||
const Notifications = {};
|
const Notifications = {};
|
||||||
|
|
||||||
Notifications.get = (alias, tribeId) => {
|
Notifications.get = (alias, tribeId) => {
|
||||||
const notiffile = `../${req.params.tribe}/notifications/${req.params.alias}.json`;
|
if (
|
||||||
|
!/^[a-z0-9]{3,64}$/.test(String(alias)) ||
|
||||||
|
!/^[a-z0-9_-]{1,64}$/.test(String(tribeId))
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
ref: "Notification",
|
||||||
|
msg: "paramsinvalid",
|
||||||
|
data: { alias, tribeId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const notiffile = `../${tribeId}/notifications/${alias}.json`;
|
||||||
const msg = fs.existsSync(notiffile) ? fs.readJSONSync(notiffile) : {};
|
const msg = fs.existsSync(notiffile) ? fs.readJSONSync(notiffile) : {};
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -50,8 +61,6 @@ Notifications.statmaillist = (tribe) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// fichier csv stocker en local en attendant d'avoir un back pour stocker la reponse dans data.csv
|
|
||||||
fs.outputFileSync(`../${tribe}/mailinglst.csv`, csv, "utf-8");
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
ref: "Notifications",
|
ref: "Notifications",
|
||||||
|
|||||||
+170
-35
@@ -625,6 +625,145 @@ Odmdb.indexAllowed = (indexaccess, role) => {
|
|||||||
|
|
||||||
* */
|
* */
|
||||||
|
|
||||||
|
Odmdb.prepareUploadedImages = async (objectPathname, itm) => {
|
||||||
|
const getschema = Odmdb.Schema(objectPathname, true);
|
||||||
|
if (getschema.status != 200) return getschema;
|
||||||
|
const schema = getschema.data.schema;
|
||||||
|
const imagekeys = Object.keys(itm).filter(
|
||||||
|
(key) => key.startsWith("imgbase64_") && itm[key] !== ""
|
||||||
|
);
|
||||||
|
if (imagekeys.length == 0) {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imagesprepared",
|
||||||
|
data: { written: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const apxidvalue = itm[schema.apxid];
|
||||||
|
if (!apxidvalue) {
|
||||||
|
return {
|
||||||
|
status: 406,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "apxidmissing",
|
||||||
|
data: { missingkey: schema.apxid },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const allowed = /^data:image\/(png|jpeg|jpg|webp|gif);base64,([A-Za-z0-9+/=\s]+)$/i;
|
||||||
|
const validmagic = (buffer) => {
|
||||||
|
if (buffer.length < 12) return false;
|
||||||
|
const ispng =
|
||||||
|
buffer[0] === 0x89 &&
|
||||||
|
buffer[1] === 0x50 &&
|
||||||
|
buffer[2] === 0x4e &&
|
||||||
|
buffer[3] === 0x47 &&
|
||||||
|
buffer[4] === 0x0d &&
|
||||||
|
buffer[5] === 0x0a &&
|
||||||
|
buffer[6] === 0x1a &&
|
||||||
|
buffer[7] === 0x0a;
|
||||||
|
const isjpeg =
|
||||||
|
buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
||||||
|
const gif = buffer.slice(0, 6).toString();
|
||||||
|
const isgif = gif === "GIF87a" || gif === "GIF89a";
|
||||||
|
const iswebp =
|
||||||
|
buffer.slice(0, 4).toString() === "RIFF" &&
|
||||||
|
buffer.slice(8, 12).toString() === "WEBP";
|
||||||
|
return ispng || isjpeg || isgif || iswebp;
|
||||||
|
};
|
||||||
|
const sharp = require("sharp");
|
||||||
|
const objectname = path.basename(objectPathname);
|
||||||
|
const imgdir = path.resolve(objectPathname, "img");
|
||||||
|
const safeid = String(apxidvalue)
|
||||||
|
.replace(/[^A-Za-z0-9_-]/g, "_")
|
||||||
|
.slice(0, 64);
|
||||||
|
const writes = [];
|
||||||
|
for (const key of imagekeys) {
|
||||||
|
if (typeof itm[key] !== "string") {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imageinvalid",
|
||||||
|
data: { field: key },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const match = itm[key].match(allowed);
|
||||||
|
if (!match) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imageinvalid",
|
||||||
|
data: { field: key },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const newk = key.slice("imgbase64_".length);
|
||||||
|
if (!schema.properties[newk]) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imgpropertiesmissing",
|
||||||
|
data: { field: key, propertie: newk },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const buffer = Buffer.from(match[2].replace(/\s+/g, ""), "base64");
|
||||||
|
if (buffer.length == 0 || buffer.length > 5 * 1024 * 1024) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imageinvalid",
|
||||||
|
data: { field: key, size: buffer.length },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!validmagic(buffer)) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imageinvalid",
|
||||||
|
data: { field: key },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let webp;
|
||||||
|
try {
|
||||||
|
webp = await sharp(buffer, { limitInputPixels: 25000000 })
|
||||||
|
.rotate()
|
||||||
|
.webp({ quality: 80 })
|
||||||
|
.toBuffer();
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imageinvalid",
|
||||||
|
data: { field: key, error: err.message },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const safek = String(newk)
|
||||||
|
.replace(/[^A-Za-z0-9_-]/g, "_")
|
||||||
|
.slice(0, 64);
|
||||||
|
const filename = `apximg_${safeid}_${safek}.webp`;
|
||||||
|
const finalpath = path.resolve(imgdir, filename);
|
||||||
|
if (!finalpath.startsWith(`${imgdir}${path.sep}`)) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imagepathinvalid",
|
||||||
|
data: { field: key },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
writes.push({ key, newk, filename, webp, finalpath });
|
||||||
|
}
|
||||||
|
fs.ensureDirSync(imgdir);
|
||||||
|
writes.forEach((write) => {
|
||||||
|
fs.outputFileSync(write.finalpath, write.webp);
|
||||||
|
itm[write.newk] = `objectimg/${objectname}/img/${write.filename}`;
|
||||||
|
delete itm[write.key];
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imagesprepared",
|
||||||
|
data: { written: writes.map((write) => write.finalpath) },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
|
Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
|
||||||
const getschema = Odmdb.Schema(objectPathname, true);
|
const getschema = Odmdb.Schema(objectPathname, true);
|
||||||
if (getschema.status != 200) return getschema;
|
if (getschema.status != 200) return getschema;
|
||||||
@@ -637,6 +776,17 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
|
|||||||
data: { missingkey: getschema.data.schema.apxid },
|
data: { missingkey: getschema.data.schema.apxid },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const pendingimages = Object.keys(itm).filter(
|
||||||
|
(key) => key.startsWith("imgbase64_") && itm[key] !== ""
|
||||||
|
);
|
||||||
|
if (pendingimages.length > 0) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "imgbase64requirespreprocessing",
|
||||||
|
data: { fields: pendingimages },
|
||||||
|
};
|
||||||
|
}
|
||||||
fs.ensureDirSync(`${objectPathname}/itm/`);
|
fs.ensureDirSync(`${objectPathname}/itm/`);
|
||||||
fs.ensureDirSync(`${objectPathname}/idx/`);
|
fs.ensureDirSync(`${objectPathname}/idx/`);
|
||||||
const existid = fs.existsSync(
|
const existid = fs.existsSync(
|
||||||
@@ -774,40 +924,6 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
|
|||||||
//console.log(k, itmtostore[k], itmtostore[k] === "");
|
//console.log(k, itmtostore[k], itmtostore[k] === "");
|
||||||
if (itmtostore[k] === "") delete itmtostore[k];
|
if (itmtostore[k] === "") delete itmtostore[k];
|
||||||
});
|
});
|
||||||
Object.keys(itmtostore).forEach((k) => {
|
|
||||||
//Manage base64 case image to convert ans store in webp
|
|
||||||
if (k.includes("imgbase64_") && itmtostore[k] != "") {
|
|
||||||
fs.ensureDirSync(`${objectPathname}/img/`);
|
|
||||||
//console.log("check this for k:", k, " itmtostore[k]:", itmtostore[k]);
|
|
||||||
const imgb64 = itmtostore[k].replace(
|
|
||||||
/^data:image\/(png|png|gif|bmp|jpg|jpeg);base64,/,
|
|
||||||
""
|
|
||||||
); //.replace(/\+/g," ");
|
|
||||||
const extension = itmtostore[k].split(";base64,")[0].split("/")[1];
|
|
||||||
const newk = k.split("_").slice(1).join("_");
|
|
||||||
const filenameimg = `/img/${
|
|
||||||
itmtostore[getschema.data.schema.apxid]
|
|
||||||
}_${newk}.${extension}`;
|
|
||||||
if (!getschema.data.schema.properties[newk]) {
|
|
||||||
if (log)
|
|
||||||
console.log(
|
|
||||||
currentmod,
|
|
||||||
" Check your schema it miss a propertie because imgbase64_blabla must have a blabla propertie to host the pathfile image"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const objectname = objectPathname.split("/").slice(-1)[0];
|
|
||||||
itmtostore[newk] = `objectimg/${objectname}${filenameimg}`;
|
|
||||||
//const webp = require("webp-converter");
|
|
||||||
//const buf = Buffer.from(itmtostore[k], 'base64');
|
|
||||||
//const imgwebp = await webp.bufferToWebp(buf);
|
|
||||||
//@TODO use sharp to resize img with attribut sizeHW in propertie
|
|
||||||
fs.writeFileSync(`${objectPathname}/${filenameimg}`, imgb64, {
|
|
||||||
encoding: "base64",
|
|
||||||
});
|
|
||||||
delete itmtostore[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// check consistency of datatostore
|
// check consistency of datatostore
|
||||||
//console.log("itmstostore::::::", itmtostore);
|
//console.log("itmstostore::::::", itmtostore);
|
||||||
const chkdata = Checkjson.schema.data(
|
const chkdata = Checkjson.schema.data(
|
||||||
@@ -859,6 +975,17 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
|
|||||||
*/
|
*/
|
||||||
Odmdb.runidx = (objectPathname, schema) => {
|
Odmdb.runidx = (objectPathname, schema) => {
|
||||||
const starttime=process.hrtime.bigint();
|
const starttime=process.hrtime.bigint();
|
||||||
|
if (
|
||||||
|
!fs.existsSync(objectPathname) ||
|
||||||
|
!fs.lstatSync(objectPathname).isDirectory()
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: 404,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "objectdoesnotexist",
|
||||||
|
data: { objectPathname },
|
||||||
|
};
|
||||||
|
}
|
||||||
if (!schema || !schema.apxid) {
|
if (!schema || !schema.apxid) {
|
||||||
const getschema = Odmdb.Schema(objectPathname, true);
|
const getschema = Odmdb.Schema(objectPathname, true);
|
||||||
if (getschema.status != 200) {
|
if (getschema.status != 200) {
|
||||||
@@ -867,6 +994,14 @@ Odmdb.runidx = (objectPathname, schema) => {
|
|||||||
}
|
}
|
||||||
schema = getschema.data.schema;
|
schema = getschema.data.schema;
|
||||||
}
|
}
|
||||||
|
if (!Array.isArray(schema.apxidx)) {
|
||||||
|
return {
|
||||||
|
status: 406,
|
||||||
|
ref: "Odmdb",
|
||||||
|
msg: "apxidxmissing",
|
||||||
|
data: { objectPathname },
|
||||||
|
};
|
||||||
|
}
|
||||||
const ventil = {};
|
const ventil = {};
|
||||||
schema.apxidx.forEach((id) => {
|
schema.apxidx.forEach((id) => {
|
||||||
ventil[id.name] = id;
|
ventil[id.name] = id;
|
||||||
@@ -881,7 +1016,7 @@ Odmdb.runidx = (objectPathname, schema) => {
|
|||||||
} else {
|
} else {
|
||||||
if (!schema.properties[id.keyval]){
|
if (!schema.properties[id.keyval]){
|
||||||
console.log(`Check ${objectPathname} index ${id.name} property ${id.keyval} does not exist`)
|
console.log(`Check ${objectPathname} index ${id.name} property ${id.keyval} does not exist`)
|
||||||
process.exit(0)
|
return
|
||||||
}
|
}
|
||||||
ventil[id.name].isunique = schema.apxuniquekey.includes(id.keyval);
|
ventil[id.name].isunique = schema.apxuniquekey.includes(id.keyval);
|
||||||
ventil[id.name].nbobjkey = id.objkey ? id.objkey.length : 0;
|
ventil[id.name].nbobjkey = id.objkey ? id.objkey.length : 0;
|
||||||
|
|||||||
@@ -84,6 +84,20 @@ Pagans.keyrecovery = async (emailalias, tribe, search, lg) => {
|
|||||||
data: { emailalias, tribe, search, lg },
|
data: { emailalias, tribe, search, lg },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const searchvalid =
|
||||||
|
emailalias === "alias"
|
||||||
|
? /^[a-z0-9]{3,64}$/.test(String(search))
|
||||||
|
: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(
|
||||||
|
String(search),
|
||||||
|
);
|
||||||
|
if (!/^[a-z0-9_-]{1,64}$/.test(String(tribe)) || !searchvalid) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "searchinvalid",
|
||||||
|
data: { emailalias, tribe, search },
|
||||||
|
};
|
||||||
|
}
|
||||||
let emaillist = [];
|
let emaillist = [];
|
||||||
if (emailalias == "email") {
|
if (emailalias == "email") {
|
||||||
search = search.toLowerCase();
|
search = search.toLowerCase();
|
||||||
@@ -148,6 +162,14 @@ Pagans.keyrecovery = async (emailalias, tribe, search, lg) => {
|
|||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
Pagans.getalias = (alias) => {
|
Pagans.getalias = (alias) => {
|
||||||
|
if (!/^[a-z0-9]{3,64}$/.test(String(alias))) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "aliasinvalid",
|
||||||
|
data: { alias },
|
||||||
|
};
|
||||||
|
}
|
||||||
//bypass Odmdb cause all is public save ressources
|
//bypass Odmdb cause all is public save ressources
|
||||||
console.log(path.resolve(`../adminapi/objects/pagans/itm/${alias}.json`));
|
console.log(path.resolve(`../adminapi/objects/pagans/itm/${alias}.json`));
|
||||||
if (fs.existsSync(`../adminapi/objects/pagans/itm/${alias}.json`)) {
|
if (fs.existsSync(`../adminapi/objects/pagans/itm/${alias}.json`)) {
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
const crypto = require("crypto");
|
||||||
|
const conf = require(`../../../adminapi/objects/tribes/itm/adminapi.json`);
|
||||||
|
|
||||||
|
const Security = {};
|
||||||
|
|
||||||
|
Security.freeTextFields = [
|
||||||
|
"biography",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"message",
|
||||||
|
"html",
|
||||||
|
"text",
|
||||||
|
"content",
|
||||||
|
"comment",
|
||||||
|
"subject",
|
||||||
|
];
|
||||||
|
|
||||||
|
Security.hash = (value) =>
|
||||||
|
crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(String(value === undefined ? "" : value))
|
||||||
|
.digest("hex")
|
||||||
|
.slice(0, 16);
|
||||||
|
|
||||||
|
Security.normalize = (value) => {
|
||||||
|
let normalized = String(value);
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
try {
|
||||||
|
const decoded = decodeURIComponent(normalized);
|
||||||
|
if (decoded === normalized) break;
|
||||||
|
normalized = decoded;
|
||||||
|
} catch (err) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalized.replace(/\\/g, "/");
|
||||||
|
};
|
||||||
|
|
||||||
|
Security.isTraversal = (value) => {
|
||||||
|
if (typeof value !== "string" || value === "") return false;
|
||||||
|
const normalized = Security.normalize(value);
|
||||||
|
if (normalized.includes("\0")) return true;
|
||||||
|
return normalized
|
||||||
|
.split("/")
|
||||||
|
.some((segment) => segment === ".." || segment === "..;");
|
||||||
|
};
|
||||||
|
|
||||||
|
Security.redact = (value, depth = 0) => {
|
||||||
|
if (depth > 6) return "[depthlimit]";
|
||||||
|
if (value === null || value === undefined) return value;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.length > 2000 ? `${value.slice(0, 2000)}...[truncated]` : value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.slice(0, 50).map((item) => Security.redact(item, depth + 1));
|
||||||
|
}
|
||||||
|
if (typeof value === "object") {
|
||||||
|
const redacted = {};
|
||||||
|
Object.keys(value)
|
||||||
|
.slice(0, 100)
|
||||||
|
.forEach((key) => {
|
||||||
|
if (/privatekey|passphrase|xhash|password|secret|token|apikey|smtp|brevo/i.test(key)) {
|
||||||
|
redacted[key] = "[redacted]";
|
||||||
|
} else if (/^imgbase64_/i.test(key)) {
|
||||||
|
redacted[key] = "[image]";
|
||||||
|
} else {
|
||||||
|
redacted[key] = Security.redact(value[key], depth + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return redacted;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
Security.throttlekey = (ip, field, value) =>
|
||||||
|
`${ip}|${field}|${Security.hash(value)}`;
|
||||||
|
|
||||||
|
Security.throttled = new Map();
|
||||||
|
Security.emailtimes = [];
|
||||||
|
|
||||||
|
Security.alert = (req, details = {}) => {
|
||||||
|
const header = (req.session && req.session.header) || {};
|
||||||
|
const ip =
|
||||||
|
req.ip || (req.socket && req.socket.remoteAddress) || "unknown";
|
||||||
|
const field = details.field || "unknown";
|
||||||
|
const value =
|
||||||
|
typeof details.value === "string"
|
||||||
|
? details.value.slice(0, 500)
|
||||||
|
: details.value;
|
||||||
|
const now = Date.now();
|
||||||
|
const throttlekey = Security.throttlekey(ip, field, value);
|
||||||
|
const last = Security.throttled.get(throttlekey) || 0;
|
||||||
|
if (now - last < 10 * 60 * 1000) return;
|
||||||
|
Security.throttled.set(throttlekey, now);
|
||||||
|
if (Security.throttled.size > 5000) {
|
||||||
|
Security.throttled.forEach((ts, key) => {
|
||||||
|
if (now - ts > 10 * 60 * 1000) Security.throttled.delete(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
at: new Date(now).toISOString(),
|
||||||
|
method: req.method,
|
||||||
|
url: req.originalUrl,
|
||||||
|
ip,
|
||||||
|
xtribe: header.xtribe,
|
||||||
|
xalias: header.xalias,
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
body: Security.redact(req.body),
|
||||||
|
};
|
||||||
|
console.log("ALERT security", JSON.stringify(entry));
|
||||||
|
|
||||||
|
const emailalertsecu =
|
||||||
|
conf && typeof conf.emailalertsecu === "string"
|
||||||
|
? conf.emailalertsecu.trim()
|
||||||
|
: "";
|
||||||
|
if (!emailalertsecu) return;
|
||||||
|
Security.emailtimes = Security.emailtimes.filter(
|
||||||
|
(ts) => now - ts < 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
if (Security.emailtimes.length >= 30) return;
|
||||||
|
Security.emailtimes.push(now);
|
||||||
|
|
||||||
|
const Notifications = require("./Notifications.js");
|
||||||
|
Notifications.sendmail(
|
||||||
|
{
|
||||||
|
to: emailalertsecu,
|
||||||
|
subject: `[apXtri security] ${field} - ${req.method} ${req.originalUrl}`,
|
||||||
|
text: JSON.stringify(entry, null, 2),
|
||||||
|
html: `<pre>${JSON.stringify(entry, null, 2)}</pre>`,
|
||||||
|
},
|
||||||
|
"adminapi"
|
||||||
|
).catch((err) => {
|
||||||
|
console.log("ALERT security email failed", err.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Security;
|
||||||
@@ -16,6 +16,24 @@ const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(cu
|
|||||||
const Wwws = {};
|
const Wwws = {};
|
||||||
|
|
||||||
Wwws.getwco = (wconame, ctx) => {
|
Wwws.getwco = (wconame, ctx) => {
|
||||||
|
const safename = (value) =>
|
||||||
|
typeof value === "string" && /^[A-Za-z0-9_.:-]{1,64}$/.test(value);
|
||||||
|
const tagids = ctx.tagid ? String(ctx.tagid).split(",") : [];
|
||||||
|
if (
|
||||||
|
!safename(wconame) ||
|
||||||
|
!safename(ctx.tribe) ||
|
||||||
|
!safename(ctx.wcotribe) ||
|
||||||
|
!safename(ctx.xapp) ||
|
||||||
|
!safename(ctx.pagename) ||
|
||||||
|
tagids.some((tagid) => !safename(tagid))
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
ref: "Wwws",
|
||||||
|
msg: "queryinvalid",
|
||||||
|
data: { wconame, query: ctx },
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* In a webpage to add a wco
|
* In a webpage to add a wco
|
||||||
* <script src="/api/adminapi/wwws/getwco/wco.js?wcotribe=adminapi&tribe=adminapi&xapp=admin&pagename=apxid&code=enjoy&tagid=id"></script>
|
* <script src="/api/adminapi/wwws/getwco/wco.js?wcotribe=adminapi&tribe=adminapi&xapp=admin&pagename=apxid&code=enjoy&tagid=id"></script>
|
||||||
@@ -486,6 +504,22 @@ Wwws.initlocalwco = (tribwco, profils, lg) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Wwws.initlocaldata = (tribe, appname, pagename, version, profils, lg) => {
|
Wwws.initlocaldata = (tribe, appname, pagename, version, profils, lg) => {
|
||||||
|
const safename = (value) =>
|
||||||
|
typeof value === "string" && /^[A-Za-z0-9_.:-]{1,64}$/.test(value);
|
||||||
|
if (
|
||||||
|
!safename(tribe) ||
|
||||||
|
!safename(appname) ||
|
||||||
|
!safename(pagename) ||
|
||||||
|
!/^[A-Za-z]{2}$/.test(String(lg)) ||
|
||||||
|
!/^\d{1,16}$/.test(String(version))
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
ref: "Wwws",
|
||||||
|
msg: "paraminvalid",
|
||||||
|
data: { tribe, appname, pagename, version, lg },
|
||||||
|
};
|
||||||
|
}
|
||||||
const fileparam = `../${tribe}/objects/wwws/itm/${appname}.json`;
|
const fileparam = `../${tribe}/objects/wwws/itm/${appname}.json`;
|
||||||
//console.log(path.resolve(fileparam));
|
//console.log(path.resolve(fileparam));
|
||||||
if (!fs.existsSync(fileparam)) {
|
if (!fs.existsSync(fileparam)) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
|
const crypto = require("crypto");
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
// Classes
|
// Classes
|
||||||
const Notifications = require("../models/Notifications.js");
|
const Notifications = require("../models/Notifications.js");
|
||||||
|
|
||||||
|
const emailpattern =
|
||||||
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
const checkHeaders = require("../middlewares/checkHeaders");
|
const checkHeaders = require("../middlewares/checkHeaders");
|
||||||
const isAuthenticated = require("../middlewares/isAuthenticated");
|
const isAuthenticated = require("../middlewares/isAuthenticated");
|
||||||
@@ -26,7 +30,7 @@ const router = express.Router();
|
|||||||
**/
|
**/
|
||||||
router.get("/messages/:alias/:tribeId", (req, res) => {
|
router.get("/messages/:alias/:tribeId", (req, res) => {
|
||||||
const getnot = Notifications.get(req.params.alias, req.params.tribeId);
|
const getnot = Notifications.get(req.params.alias, req.params.tribeId);
|
||||||
res.status(getalias.status).send(getalias);
|
res.status(getnot.status).json(getnot);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,6 +123,34 @@ router.post("/registeranonymous", checkHeaders, (req, res) => {
|
|||||||
data: { typekey: req.body.typekey },
|
data: { typekey: req.body.typekey },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const allowedtribes = [
|
||||||
|
"adminapi",
|
||||||
|
...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
|
||||||
|
];
|
||||||
|
if (!allowedtribes.includes(req.body.tribe)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
status: 404,
|
||||||
|
ref: "Notifications",
|
||||||
|
msg: "tribeiddoesnotexist",
|
||||||
|
data: { tribe: req.body.tribe },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(req.body.mlist || "")) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Notifications",
|
||||||
|
msg: "mlistinvalid",
|
||||||
|
data: { mlist: req.body.mlist },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(req.body.srckey || "")) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Notifications",
|
||||||
|
msg: "srckeyinvalid",
|
||||||
|
data: { srckey: req.body.srckey },
|
||||||
|
});
|
||||||
|
}
|
||||||
const key = req.body.contactpoint
|
const key = req.body.contactpoint
|
||||||
? req.body.contactpoint
|
? req.body.contactpoint
|
||||||
: req.body[req.body.typekey];
|
: req.body[req.body.typekey];
|
||||||
@@ -201,7 +233,18 @@ router.get(
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
router.get("/stat/maillinglst/:tribe", checkHeaders, async (req, res) => {
|
router.get("/stat/maillinglst/:tribe", checkHeaders, async (req, res) => {
|
||||||
console.log("passe là");
|
const allowedtribes = [
|
||||||
|
"adminapi",
|
||||||
|
...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
|
||||||
|
];
|
||||||
|
if (!allowedtribes.includes(req.params.tribe)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
status: 404,
|
||||||
|
ref: "Notifications",
|
||||||
|
msg: "tribeiddoesnotexist",
|
||||||
|
data: { tribe: req.params.tribe },
|
||||||
|
});
|
||||||
|
}
|
||||||
const resstat = Notifications.statmaillist(req.params.tribe);
|
const resstat = Notifications.statmaillist(req.params.tribe);
|
||||||
res.status(resstat.status).json(resstat);
|
res.status(resstat.status).json(resstat);
|
||||||
});
|
});
|
||||||
@@ -270,8 +313,19 @@ router.get("/contact", checkHeaders, isAuthenticated, (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get("/checkemail/:email/:digit6", checkHeaders, async (req, res) => {
|
router.get("/checkemail/:email/:digit6", checkHeaders, async (req, res) => {
|
||||||
const { email, digit6 } = req.params;
|
const { email, digit6 } = req.params;
|
||||||
|
if (!emailpattern.test(email)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Notifications",
|
||||||
|
msg: "emailinvalid",
|
||||||
|
data: { email },
|
||||||
|
});
|
||||||
|
}
|
||||||
const tmpDir = path.join(__dirname, "../../tmp");
|
const tmpDir = path.join(__dirname, "../../tmp");
|
||||||
const tmpFile = path.join(tmpDir, email);
|
const tmpFile = path.join(
|
||||||
|
tmpDir,
|
||||||
|
`${crypto.createHash("sha256").update(email).digest("hex")}.txt`
|
||||||
|
);
|
||||||
|
|
||||||
if (digit6 === "0") {
|
if (digit6 === "0") {
|
||||||
const code = email.includes("smatchit.io") ? "789000" : Math.floor(100000 + Math.random() * 900000).toString();
|
const code = email.includes("smatchit.io") ? "789000" : Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
|||||||
+24
-4
@@ -82,7 +82,7 @@ router.get(
|
|||||||
checkHeaders,
|
checkHeaders,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
const objectPathname = `../${req.params.tribe}/objects/options/${req.params.optionname}_${req.header.xlang}.json`;
|
const objectPathname = `../${req.params.tribe}/objects/options/${req.params.optionname}_${req.session.header.xlang}.json`;
|
||||||
if (fs.existsSync(objectPathname)) {
|
if (fs.existsSync(objectPathname)) {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -293,14 +293,24 @@ router.post(
|
|||||||
"/itm/:tribe/:objectname",
|
"/itm/:tribe/:objectname",
|
||||||
checkHeaders,
|
checkHeaders,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
(req, res) => {
|
async (req, res) => {
|
||||||
// Create an item of an object with no specificities
|
// Create an item of an object with no specificities
|
||||||
// if specificities then create a route / model that import odmdb
|
// if specificities then create a route / model that import odmdb
|
||||||
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`;
|
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`;
|
||||||
|
const prepared = await Odmdb.prepareUploadedImages(
|
||||||
|
objectPathname,
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (prepared.status != 200) {
|
||||||
|
return res.status(prepared.status).json(prepared);
|
||||||
|
}
|
||||||
const postitm = Odmdb.cud(objectPathname, "C", req.body, {
|
const postitm = Odmdb.cud(objectPathname, "C", req.body, {
|
||||||
xprofils: req.session.header.xprofils,
|
xprofils: req.session.header.xprofils,
|
||||||
xalias: req.session.header.xalias,
|
xalias: req.session.header.xalias,
|
||||||
});
|
});
|
||||||
|
if (postitm.status != 200 && prepared.data.written.length > 0) {
|
||||||
|
prepared.data.written.forEach((file) => fs.removeSync(file));
|
||||||
|
}
|
||||||
res.status(postitm.status).json(postitm);
|
res.status(postitm.status).json(postitm);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -336,14 +346,24 @@ router.put(
|
|||||||
"/itm/:tribe/:objectname",
|
"/itm/:tribe/:objectname",
|
||||||
checkHeaders,
|
checkHeaders,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
(req, res) => {
|
async (req, res) => {
|
||||||
// Create an item of an object with no specificities
|
// Create an item of an object with no specificities
|
||||||
// if specificities then create a route / model that import odmdb
|
// if specificities then create a route / model that import odmdb
|
||||||
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`;
|
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`;
|
||||||
|
const prepared = await Odmdb.prepareUploadedImages(
|
||||||
|
objectPathname,
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (prepared.status != 200) {
|
||||||
|
return res.status(prepared.status).json(prepared);
|
||||||
|
}
|
||||||
const postitm = Odmdb.cud(objectPathname, "U", req.body, {
|
const postitm = Odmdb.cud(objectPathname, "U", req.body, {
|
||||||
xprofils: req.session.header.xprofils,
|
xprofils: req.session.header.xprofils,
|
||||||
xalias: req.session.header.xalias,
|
xalias: req.session.header.xalias,
|
||||||
});
|
});
|
||||||
|
if (postitm.status != 200 && prepared.data.written.length > 0) {
|
||||||
|
prepared.data.written.forEach((file) => fs.removeSync(file));
|
||||||
|
}
|
||||||
res.status(postitm.status).json(postitm);
|
res.status(postitm.status).json(postitm);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -761,7 +781,7 @@ router.put(
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
const fullpath = path.resolve(
|
const fullpath = path.resolve(
|
||||||
`${__dirname}/tribes/${req.session.header.xworkon}/schema/${req.params.pathobjectname}.json`
|
`${__dirname}/tribes/${req.session.header.xworkon}/schema/${req.params.objectname}.json`
|
||||||
);
|
);
|
||||||
const set = Odmdb.setObject(
|
const set = Odmdb.setObject(
|
||||||
path.resolve(`${__dirname}/tribes/${req.session.header.xworkon}`)
|
path.resolve(`${__dirname}/tribes/${req.session.header.xworkon}`)
|
||||||
|
|||||||
+86
-1
@@ -142,6 +142,31 @@ router.post("/", checkHeaders, isAuthenticated, async (req, res) => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!/^[a-z0-9]{3,64}$/.test(req.body.alias || "") ||
|
||||||
|
req.body.alias !== req.session.header.xalias
|
||||||
|
) {
|
||||||
|
res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "aliasinvalid",
|
||||||
|
data: { alias: req.body.alias, xalias: req.session.header.xalias },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowedtribes = [
|
||||||
|
"adminapi",
|
||||||
|
...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
|
||||||
|
];
|
||||||
|
if (!allowedtribes.includes(req.body.trustedtribe)) {
|
||||||
|
res.status(404).json({
|
||||||
|
status: 404,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "tribeiddoesnotexist",
|
||||||
|
data: { tribe: req.body.trustedtribe },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const objpagan = { alias: req.body.alias, publickey: req.body.publickey };
|
const objpagan = { alias: req.body.alias, publickey: req.body.publickey };
|
||||||
console.log(path.resolve(`../adminapi/objects/pagans`));
|
console.log(path.resolve(`../adminapi/objects/pagans`));
|
||||||
const newpagan = Odmdb.cud(`../adminapi/objects/pagans`, "C", objpagan, role);
|
const newpagan = Odmdb.cud(`../adminapi/objects/pagans`, "C", objpagan, role);
|
||||||
@@ -219,6 +244,14 @@ router.post("/", checkHeaders, isAuthenticated, async (req, res) => {
|
|||||||
* Delete an alias and his publickey, this mean that publickey disapear as well as alias. We set dt_delete
|
* Delete an alias and his publickey, this mean that publickey disapear as well as alias. We set dt_delete
|
||||||
* */
|
* */
|
||||||
router.delete("/alias/:alias", checkHeaders, isAuthenticated, (req, res) => {
|
router.delete("/alias/:alias", checkHeaders, isAuthenticated, (req, res) => {
|
||||||
|
if (req.params.alias !== req.session.header.xalias) {
|
||||||
|
return res.status(403).json({
|
||||||
|
status: 403,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "profilnotallow",
|
||||||
|
data: { alias: req.params.alias, xalias: req.session.header.xalias },
|
||||||
|
});
|
||||||
|
}
|
||||||
const personpath = `../adminapi/objects/pagans`;
|
const personpath = `../adminapi/objects/pagans`;
|
||||||
const role = {
|
const role = {
|
||||||
xalias: req.session.header.xalias,
|
xalias: req.session.header.xalias,
|
||||||
@@ -263,6 +296,17 @@ router.delete(
|
|||||||
checkHeaders,
|
checkHeaders,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
|
const privileged =
|
||||||
|
req.session.header.xprofils.includes("druid") ||
|
||||||
|
req.session.header.xprofils.includes("mayor");
|
||||||
|
if (req.params.alias !== req.session.header.xalias && !privileged) {
|
||||||
|
return res.status(403).json({
|
||||||
|
status: 403,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "profilnotallow",
|
||||||
|
data: { alias: req.params.alias, xalias: req.session.header.xalias },
|
||||||
|
});
|
||||||
|
}
|
||||||
const personpath = `../${req.params.tribe}/objects/persons`;
|
const personpath = `../${req.params.tribe}/objects/persons`;
|
||||||
const role = {
|
const role = {
|
||||||
xalias: req.session.header.xalias,
|
xalias: req.session.header.xalias,
|
||||||
@@ -331,8 +375,30 @@ router.get("/person/:alias", checkHeaders, isAuthenticated, (req, res) => {
|
|||||||
* @apiBody {string} [addprofils] must be alone no other Body field
|
* @apiBody {string} [addprofils] must be alone no other Body field
|
||||||
* @apiBody {string} [removeprofils] must be alone
|
* @apiBody {string} [removeprofils] must be alone
|
||||||
*/
|
*/
|
||||||
router.put("/person/:tribe", checkHeaders, isAuthenticated, (req, res) => {
|
router.put("/person/:tribe", checkHeaders, isAuthenticated, async (req, res) => {
|
||||||
//console.log(req.body);
|
//console.log(req.body);
|
||||||
|
if (!req.body || !/^[a-z0-9]{3,64}$/.test(req.body.alias || "")) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "aliasinvalid",
|
||||||
|
data: { alias: req.body && req.body.alias },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const privileged =
|
||||||
|
req.session.header.xprofils.includes("druid") ||
|
||||||
|
req.session.header.xprofils.includes("mayor");
|
||||||
|
if (
|
||||||
|
req.body.alias !== req.session.header.xalias &&
|
||||||
|
!privileged
|
||||||
|
) {
|
||||||
|
return res.status(403).json({
|
||||||
|
status: 403,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "profilnotallow",
|
||||||
|
data: { alias: req.body.alias, xalias: req.session.header.xalias },
|
||||||
|
});
|
||||||
|
}
|
||||||
const pathobj = `../${req.params.tribe}/objects/persons`;
|
const pathobj = `../${req.params.tribe}/objects/persons`;
|
||||||
const action = fs.existsSync(`${pathobj}/itm/${req.body.alias}.json`)
|
const action = fs.existsSync(`${pathobj}/itm/${req.body.alias}.json`)
|
||||||
? "U"
|
? "U"
|
||||||
@@ -386,10 +452,17 @@ router.put("/person/:tribe", checkHeaders, isAuthenticated, (req, res) => {
|
|||||||
if (!req.body.profils) {
|
if (!req.body.profils) {
|
||||||
req.body.profils = ["anonymous", "pagans", "persons"];
|
req.body.profils = ["anonymous", "pagans", "persons"];
|
||||||
}
|
}
|
||||||
|
const prepared = await Odmdb.prepareUploadedImages(pathobj, req.body);
|
||||||
|
if (prepared.status != 200) {
|
||||||
|
return res.status(prepared.status).json(prepared);
|
||||||
|
}
|
||||||
const personup = Odmdb.cud(pathobj, action, req.body, {
|
const personup = Odmdb.cud(pathobj, action, req.body, {
|
||||||
xprofils: req.session.header.xprofils,
|
xprofils: req.session.header.xprofils,
|
||||||
xalias: req.session.header.xalias,
|
xalias: req.session.header.xalias,
|
||||||
});
|
});
|
||||||
|
if (personup.status != 200 && prepared.data.written.length > 0) {
|
||||||
|
prepared.data.written.forEach((file) => fs.removeSync(file));
|
||||||
|
}
|
||||||
if (log) console.log(currentmod, " personupdate or create:", personup);
|
if (log) console.log(currentmod, " personupdate or create:", personup);
|
||||||
res.status(personup.status).json(personup);
|
res.status(personup.status).json(personup);
|
||||||
}
|
}
|
||||||
@@ -415,6 +488,18 @@ router.put("/person/:tribe", checkHeaders, isAuthenticated, (req, res) => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
router.post("/keyrecovery", checkHeaders, async (req, res) => {
|
router.post("/keyrecovery", checkHeaders, async (req, res) => {
|
||||||
|
const allowedtribes = [
|
||||||
|
"adminapi",
|
||||||
|
...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
|
||||||
|
];
|
||||||
|
if (!allowedtribes.includes(req.body.tribe)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
status: 404,
|
||||||
|
ref: "Pagans",
|
||||||
|
msg: "tribeiddoesnotexist",
|
||||||
|
data: { tribe: req.body.tribe },
|
||||||
|
});
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const recores = await Pagans.keyrecovery(
|
const recores = await Pagans.keyrecovery(
|
||||||
req.body.emailalias,
|
req.body.emailalias,
|
||||||
|
|||||||
@@ -33,6 +33,27 @@ const router = express.Router();
|
|||||||
*/
|
*/
|
||||||
router.post("/newdevice", checkHeaders, async (req, res) => {
|
router.post("/newdevice", checkHeaders, async (req, res) => {
|
||||||
if (log) console.log(currentmod, "post new device with", req.body);
|
if (log) console.log(currentmod, "post new device with", req.body);
|
||||||
|
const xuuid = req.session.header.xuuid;
|
||||||
|
if (
|
||||||
|
!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
||||||
|
xuuid
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Trackings",
|
||||||
|
msg: "xuuidinvalid",
|
||||||
|
data: { xuuid },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Trackings",
|
||||||
|
msg: "bodyinvalid",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
const newdevice = { status: 200, ref: "Trackings", msg: "success", data: {} };
|
const newdevice = { status: 200, ref: "Trackings", msg: "success", data: {} };
|
||||||
const role = {
|
const role = {
|
||||||
xalias: req.session.header.xalias,
|
xalias: req.session.header.xalias,
|
||||||
@@ -57,9 +78,9 @@ router.post("/newdevice", checkHeaders, async (req, res) => {
|
|||||||
confdevice
|
confdevice
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const devicepath = `../${req.session.header.xtribe}/objects/devices/itm/${req.session.header.xuuid}.json`;
|
const devicepath = `../${req.session.header.xtribe}/objects/devices/itm/${xuuid}.json`;
|
||||||
const device = req.body;
|
const device = req.body;
|
||||||
device.xuuid = req.session.header.xuuid;
|
device.xuuid = xuuid;
|
||||||
if (req.session.header.xalias != "anonymous") {
|
if (req.session.header.xalias != "anonymous") {
|
||||||
device.alias = req.session.header.xalias;
|
device.alias = req.session.header.xalias;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,32 @@ const checkHeaders = require( '../middlewares/checkHeaders' );
|
|||||||
const isAuthenticated = require( '../middlewares/isAuthenticated' );
|
const isAuthenticated = require( '../middlewares/isAuthenticated' );
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const disabledroutes = [
|
||||||
|
'/config',
|
||||||
|
'/www',
|
||||||
|
'/actionanonyme',
|
||||||
|
'/action',
|
||||||
|
'/clientconf',
|
||||||
|
'/spaceweb',
|
||||||
|
'/plugins',
|
||||||
|
'/dirls',
|
||||||
|
'/ls',
|
||||||
|
'/sendjson',
|
||||||
|
'/downloadls',
|
||||||
|
'/upfilepond',
|
||||||
|
'/file',
|
||||||
|
'/uploadfile',
|
||||||
|
'/uploadzip',
|
||||||
|
'/upload',
|
||||||
|
];
|
||||||
|
router.use( ( req, res, next ) => {
|
||||||
|
if( disabledroutes.some( p => req.path === p || req.path.startsWith( `${p}/` ) ) ) {
|
||||||
|
return res.status( 410 )
|
||||||
|
.json( { status: 410, ref: 'Tribes', msg: 'routedisabled' } );
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} adminapi/tribes/conf/:tribe - tribe list
|
* @api {get} adminapi/tribes/conf/:tribe - tribe list
|
||||||
|
|||||||
+23
-1
@@ -53,10 +53,32 @@ router.get("/getwco/:wconame", (req, res) => {
|
|||||||
missquery+=q+" "
|
missquery+=q+" "
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const namepattern = /^[A-Za-z0-9_.:-]{1,64}$/;
|
||||||
|
const allowedtribes = [
|
||||||
|
"adminapi",
|
||||||
|
...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
|
||||||
|
];
|
||||||
|
const wconame = req.params.wconame.replace(/\.js$/, "");
|
||||||
|
const tagids = req.query.tagid ? String(req.query.tagid).split(",") : [];
|
||||||
|
const invalid =
|
||||||
|
!namepattern.test(wconame) ||
|
||||||
|
!allowedtribes.includes(req.query.tribe) ||
|
||||||
|
!allowedtribes.includes(req.query.wcotribe) ||
|
||||||
|
!namepattern.test(req.query.xapp || "") ||
|
||||||
|
!namepattern.test(req.query.pagename || "") ||
|
||||||
|
String(req.query.code || "").length > 128 ||
|
||||||
|
tagids.some((tagid) => !namepattern.test(tagid));
|
||||||
if (missquery!=""){
|
if (missquery!=""){
|
||||||
res.status(403).json({status:403,ref:"Wwws",msg:"missquery",data:{missquery}})
|
res.status(403).json({status:403,ref:"Wwws",msg:"missquery",data:{missquery}})
|
||||||
|
}else if (invalid) {
|
||||||
|
res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
ref: "Wwws",
|
||||||
|
msg: "queryinvalid",
|
||||||
|
data: { wconame, query: req.query },
|
||||||
|
});
|
||||||
}else{
|
}else{
|
||||||
const getwco=Wwws.getwco(req.params.wconame.replace(/\.js$/,''),req.query)
|
const getwco=Wwws.getwco(wconame,req.query)
|
||||||
if (getwco.status==200){
|
if (getwco.status==200){
|
||||||
res.set('Content-Type', 'application/javascript');
|
res.set('Content-Type', 'application/javascript');
|
||||||
res.sendFile(getwco.data.file)
|
res.sendFile(getwco.data.file)
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require("fs-extra");
|
||||||
|
const path = require("path");
|
||||||
|
const glob = require("glob");
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const deletefiles = args.includes("--delete");
|
||||||
|
const quarantine = args.includes("--quarantine");
|
||||||
|
const dryrun = !deletefiles && !quarantine;
|
||||||
|
const adminapiroot = path.resolve(__dirname, "../..");
|
||||||
|
const townroot = path.resolve(adminapiroot, "..");
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const quarantinedir = path.join(
|
||||||
|
adminapiroot,
|
||||||
|
"tmp",
|
||||||
|
"quarantine",
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
const imageext = [".webp", ".png", ".jpg", ".jpeg", ".gif"];
|
||||||
|
const imagelabel = /_(imgavatar|backgroundimage|companylogo)\./i;
|
||||||
|
|
||||||
|
const readhead = (file) => {
|
||||||
|
const fd = fs.openSync(file, "r");
|
||||||
|
const buffer = Buffer.alloc(12);
|
||||||
|
fs.readSync(fd, buffer, 0, 12, 0);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validmagic = (buffer) => {
|
||||||
|
if (buffer.length < 12) return false;
|
||||||
|
const ispng =
|
||||||
|
buffer[0] === 0x89 &&
|
||||||
|
buffer[1] === 0x50 &&
|
||||||
|
buffer[2] === 0x4e &&
|
||||||
|
buffer[3] === 0x47 &&
|
||||||
|
buffer[4] === 0x0d &&
|
||||||
|
buffer[5] === 0x0a &&
|
||||||
|
buffer[6] === 0x1a &&
|
||||||
|
buffer[7] === 0x0a;
|
||||||
|
const isjpeg =
|
||||||
|
buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
||||||
|
const gif = buffer.slice(0, 6).toString();
|
||||||
|
const isgif = gif === "GIF87a" || gif === "GIF89a";
|
||||||
|
const iswebp =
|
||||||
|
buffer.slice(0, 4).toString() === "RIFF" &&
|
||||||
|
buffer.slice(8, 12).toString() === "WEBP";
|
||||||
|
return ispng || isjpeg || isgif || iswebp;
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectdirs = [];
|
||||||
|
glob
|
||||||
|
.sync(`${townroot}/*/objects`, { nodir: false, follow: false })
|
||||||
|
.forEach((dir) => {
|
||||||
|
if (!objectdirs.includes(dir)) objectdirs.push(dir);
|
||||||
|
});
|
||||||
|
const adminobjects = path.join(adminapiroot, "objects");
|
||||||
|
if (fs.existsSync(adminobjects) && !objectdirs.includes(adminobjects)) {
|
||||||
|
objectdirs.push(adminobjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagged = [];
|
||||||
|
objectdirs.forEach((objectdir) => {
|
||||||
|
if (!fs.existsSync(objectdir)) return;
|
||||||
|
glob
|
||||||
|
.sync("**/*", { cwd: objectdir, nodir: true, dot: false, follow: false })
|
||||||
|
.forEach((rel) => {
|
||||||
|
if (/(^|\/)(node_modules|tmp|logs|\.git)(\/|$)/.test(rel)) return;
|
||||||
|
const file = path.join(objectdir, rel);
|
||||||
|
let stat;
|
||||||
|
try {
|
||||||
|
stat = fs.lstatSync(file);
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!stat.isFile() || stat.isSymbolicLink()) return;
|
||||||
|
const base = path.basename(file);
|
||||||
|
const dirparts = rel.split(path.sep);
|
||||||
|
const top = dirparts[0];
|
||||||
|
const inobjectimg = dirparts.length >= 3 && dirparts[1] === "img";
|
||||||
|
const ext = path.extname(base).toLowerCase();
|
||||||
|
let reason = "";
|
||||||
|
if (base.startsWith("zz_")) {
|
||||||
|
reason = "zzartifact";
|
||||||
|
} else if (["wwws", "apidoc"].includes(top)) {
|
||||||
|
return;
|
||||||
|
} else if (!inobjectimg && imagelabel.test(base)) {
|
||||||
|
reason = "imageoutsideimg";
|
||||||
|
} else if (inobjectimg && !imageext.includes(ext)) {
|
||||||
|
reason = "extensionnotallowed";
|
||||||
|
} else if (
|
||||||
|
inobjectimg &&
|
||||||
|
imageext.includes(ext) &&
|
||||||
|
!validmagic(readhead(file))
|
||||||
|
) {
|
||||||
|
reason = "invalidmagic";
|
||||||
|
}
|
||||||
|
if (reason) {
|
||||||
|
flagged.push({ file, reason, rel, objectdir });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flagged.length == 0) {
|
||||||
|
console.log("No suspicious image found.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryrun) fs.ensureDirSync(quarantinedir);
|
||||||
|
|
||||||
|
flagged.forEach((item) => {
|
||||||
|
console.log(
|
||||||
|
`${dryrun ? "WOULD QUARANTINE" : deletefiles ? "DELETE" : "QUARANTINE"} [${item.reason}] ${item.file}`
|
||||||
|
);
|
||||||
|
if (dryrun) return;
|
||||||
|
if (deletefiles) {
|
||||||
|
fs.removeSync(item.file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dest = path.resolve(
|
||||||
|
quarantinedir,
|
||||||
|
path.relative(townroot, item.file)
|
||||||
|
);
|
||||||
|
if (!dest.startsWith(`${quarantinedir}${path.sep}`)) {
|
||||||
|
console.log(`skip unsafe quarantine path: ${dest}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.moveSync(item.file, dest, { overwrite: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dryrun) {
|
||||||
|
fs.ensureDirSync(quarantinedir);
|
||||||
|
fs.outputJSONSync(path.join(quarantinedir, "report.json"), flagged, {
|
||||||
|
spaces: 2,
|
||||||
|
});
|
||||||
|
console.log(`Report written to ${path.join(quarantinedir, "report.json")}`);
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"xtrkversion"
|
"xtrkversion"
|
||||||
],
|
],
|
||||||
"appset": {
|
"appset": {
|
||||||
"trust proxy": true
|
"trust proxy": 1
|
||||||
},
|
},
|
||||||
"json": {
|
"json": {
|
||||||
"limit": "10mb",
|
"limit": "10mb",
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
"worker_connections": 1024
|
"worker_connections": 1024
|
||||||
},
|
},
|
||||||
"emailcontact": "",
|
"emailcontact": "",
|
||||||
|
"emailalertsecu": "",
|
||||||
"smtp": {},
|
"smtp": {},
|
||||||
"sms": {}
|
"sms": {}
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,8 @@ location /api/ {
|
|||||||
proxy_pass http://localhost:{{{api.port}}};
|
proxy_pass http://localhost:{{{api.port}}};
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
include proxy_params;
|
include proxy_params;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
}
|
}
|
||||||
location /socket/ {
|
location /socket/ {
|
||||||
proxy_pass http://127.0.0.1:{{socket.port}};
|
proxy_pass http://127.0.0.1:{{socket.port}};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"title": "alias",
|
"title": "alias",
|
||||||
"description": "A unique string identifying a unique public key",
|
"description": "A unique string identifying a unique public key",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9]{3,64}$",
|
||||||
"options": {
|
"options": {
|
||||||
"$ref": "adminapi/objects/pagans/idx/lst_alias"
|
"$ref": "adminapi/objects/pagans/idx/lst_alias"
|
||||||
}
|
}
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"title": "Owner of this person",
|
"title": "Owner of this person",
|
||||||
"description": "For accessright purpose this is always equal as alias",
|
"description": "For accessright purpose this is always equal as alias",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9]{3,64}$",
|
||||||
"options": {
|
"options": {
|
||||||
"$ref": "adminapi/objects/pagans/idx/lst_alias"
|
"$ref": "adminapi/objects/pagans/idx/lst_alias"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user