412 lines
21 KiB
JavaScript
412 lines
21 KiB
JavaScript
const { Database, aql } = require("arangojs");
|
|
const { addRouteToScope } = require("./scopes");
|
|
const { encodePW, verifyPW } = require("./verlikifyhandler")
|
|
const { createHash} = require("crypto");
|
|
|
|
const raspi_arango = new Database({
|
|
url: "https://arango.ftt.gmbh:8529",
|
|
databaseName: "raspi_thern_wtf",
|
|
auth: {
|
|
username: "raspi_phil",
|
|
password: "v6u}LQN=P<0R"
|
|
}
|
|
})
|
|
const requiredCollections = [{name: "share_pw", type:"collection", index:[{type:"ttl", name:"expire", expireAfter:3600, fields:["createdAt"]}, {type:"persistent", name:"sharer", fields:["sharer_id"]}, {type:"persistent", name:"target", fields:["target_id"]}]}, { name: "user", type: "collection", index: [{ unique: true, type: "persistent", name: "name", fields: ["name"] }, { unique: true, type: "persistent", name: "id", fields: ["id"] }] }, { name: "scope", type: "collection", index: [{ unique: true, type: "persistent", fields: ["scope"], name: "scope" }] }, { name: "shopping", type: "collection", index: [{ unique: true, type: "persistent", fields: ["item"], name: "item" }] }, { name: "extshares", type: "collection", index: [{ unique: true, type: "persistent", fields: ["path"], name: "path" }] }, { name: "passman", type: "collection", index: [{ unique: true, type: "persistent", fields: ["hio"], name: "hio" }] }]
|
|
const collections = [];
|
|
const setup = () => new Promise((res, rej) => {
|
|
raspi_arango.collections(true).then((gotcollections) => {
|
|
// console.log(gotcollections)
|
|
collections.push(...gotcollections)
|
|
if (collections.find(({ name }) => name == "scope")) {
|
|
getDocuments("scope", ["scope", "content"]).then((scopes) => {
|
|
scopes.forEach(({ scope, content }) => {
|
|
content.forEach((route) => {
|
|
addRouteToScope(route, scope)
|
|
})
|
|
})
|
|
res()
|
|
}, rej)
|
|
} else {
|
|
res()
|
|
}
|
|
const missingCollecitons = requiredCollections.filter((collection) => !collections.find(({ name }) => name == collection.name));
|
|
if (missingCollecitons.length) {
|
|
Promise.all(missingCollecitons.map(({ name, type }) => type != undefined && type == "edge" ? raspi_arango.createEdgeCollection(name) : raspi_arango.createCollection(name))).then((newCollections) => {
|
|
collections.push(...newCollections)
|
|
collections.forEach((collection) => {
|
|
const requiredCollection = requiredCollections.find(({ name }) => name == collection.name);
|
|
if (requiredCollection.index) {
|
|
requiredCollection.index.forEach((index) => {
|
|
collection.ensureIndex(index).then(() => { }, console.warn)
|
|
//console.log(newCollections[0].indexes())
|
|
})
|
|
}
|
|
})
|
|
|
|
})
|
|
} else {
|
|
collections.forEach((collection) => {
|
|
const requiredCollection = requiredCollections.find(({ name }) => name == collection.name);
|
|
if (requiredCollection.index) {
|
|
requiredCollection.index.forEach((index) => {
|
|
collection.ensureIndex(index).then(() => { }, console.warn)
|
|
})
|
|
}
|
|
})
|
|
|
|
}
|
|
// requiredCollections.forEach((collection)=>{
|
|
// if(!collections.find(({name})=>name==collection.name)){
|
|
// switch(collection.type){
|
|
// case "collection":
|
|
// raspi_arango.createCollection(collection.name)
|
|
// break;
|
|
// case "edge":
|
|
// raspi_arango.createEdgeCollection(collection.name)
|
|
// break;
|
|
// default:console.warn(`${collection.name} has an unrecognized collection type ${collection.type}. It should be "collection" or "edge"`)
|
|
// }
|
|
// }
|
|
// })
|
|
|
|
}, rej)
|
|
})
|
|
const getDocuments = (collection = "user", keys = ["_key"], filters = [], sort = ["_key"], limits = []) => new Promise((res, rej) => {
|
|
const query = `for ${collection.slice(0, 1)} in ${collection} ${filters.length > 0 ? "filter " : ""}${filters.map(({ key, value, andor, unequal }) => `${collection.slice(0, 1)}["${key}"] ${unequal ? "!=" : "=="} "${value}"${andor ? andor == "and" ? " &&" : " ||" : ""}`).join(" ")} ${sort.length ? `sort ${sort.filter((key) => typeof (key) == "string").map((key) => `${collection.slice(0, 1)}["${key}"]`).join(", ")}${typeof (sort[sort.length - 1]) == "boolean" && sort[sort.length - 1] ? " DESC " : " "}` : ""}${limits.length > 0 ? "limit " : ""}${limits.slice(0, 2).join(", ")} return ${keys.length ? "{ " + keys.map((key) => key + ": " + collection.slice(0, 1) + "." + key).join(", ") + " }" : collection.slice(0, 1)}`
|
|
raspi_arango.query(query).then((users) => {
|
|
users.all().then(res, rej)
|
|
}, rej)
|
|
})
|
|
const getUsers = (keys = ["_key", "pass", "id"], filters = [], sort = ["_key"], limits = []) => getDocuments("user", keys, filters, sort, limits)
|
|
const getUser = ({ name, id }, keys = ["pass", "id", "scopes", "passHint", "blocked"]) => new Promise((res, rej) => {
|
|
getUsers(keys, [{ key: id ? "id" : "name", value: id ? id : name }]).then((users) => {
|
|
if (users.length) {
|
|
res(users[0])
|
|
} else {
|
|
rej({ error: 404, message: `user with ${id ? ("id " + id) : ("name " + name)} not found` })
|
|
}
|
|
});
|
|
})
|
|
const getUsersIndex = ({ id, name, blocked }, keys = ["_key"]) => new Promise((res, rej) => {
|
|
if (name || id) {
|
|
const query = `for doc in testView search doc.${id ? "id" : "name"} == "${id ? id : name}"${blocked != undefined ? ` filter doc.blocked ${blocked ? "!=" : "=="} null` : ""} return ${keys.length ? `{${keys.map((key) => `${key}: doc["${key}"]`).join(", ")}}` : "doc"}`
|
|
// console.log(query)
|
|
raspi_arango.query(query).then((users) => {
|
|
users.all().then((users) => {
|
|
// console.log(users)
|
|
if (users.length) {
|
|
res(users[0])
|
|
} else {
|
|
rej({ error: 404, message: "can't find user matching search criteria" })
|
|
}
|
|
})
|
|
})
|
|
} else {
|
|
rej({ error: 404, message: "invalid search criteria" })
|
|
}
|
|
})
|
|
const newUser = ({ name, passwort, admin, passHint }) => new Promise((res, rej) => {
|
|
// getUser({name}, []).then((users)=>{
|
|
// if(users.length){
|
|
// rej({error: 401, message:`${name} already exists`})
|
|
// }else{
|
|
const { uid, pass } = encodePW(passwort)
|
|
collections.find(({ name }) => name == "user").save({ name, pass, id: uid, passHint, scopes: admin ? ["admin"] : [] }).then(res, ({ code, response }) => {
|
|
//console.log(Object.keys(error))
|
|
rej({ error: code, message: response.body.errorMessage })
|
|
})
|
|
// }
|
|
// }, rej)
|
|
})
|
|
const patchUser = ({ id, name, passwort, passHint }) => new Promise((res, rej) => {
|
|
const updateObject = {};
|
|
if (passwort) {
|
|
updateObject.pass = encodePW(passwort, id).pass
|
|
}
|
|
if (passHint) {
|
|
updateObject.passHint = passHint
|
|
}
|
|
if (name) {
|
|
updateObject.name = name
|
|
}
|
|
// getUsersIndex({ id }, ["_key"]).then((user) => {
|
|
// collections.find(({ name }) => name == "user").update(user._key, updateObject)
|
|
|
|
// }, rej)
|
|
raspi_arango.query(`for doc in testView search doc.id == "${id}" update { _key: doc._key, ${Object.keys(updateObject).map((key) => `${key}: "${updateObject[key]}"`).join(", ")} } in user`).then(res, ({ code, response }) => {
|
|
rej({ error: code, message: response.body.errorMessage })
|
|
})
|
|
|
|
})
|
|
const login = ({ name, passwort }) => new Promise((res, rej) => {
|
|
// getUser({name}).then((user)=>{
|
|
// const {id, pass, scopes, passHint, blocked} = user;
|
|
// if(blocked){
|
|
// rej({error: 403, message: blocked})
|
|
// }else if(verifyPW(id, passwort, pass)){
|
|
// res({scopes, id})
|
|
// }else{
|
|
// rej({error: 401, message:passHint})
|
|
// }
|
|
// }, rej)
|
|
getUsersIndex({ name, blocked: false }, []).then((user) => {
|
|
const { id, pass, scopes, passHint } = user;
|
|
if (verifyPW(id, passwort, pass)) {
|
|
res({ scopes, id })
|
|
} else {
|
|
rej({ error: 401, message: passHint })
|
|
}
|
|
}, rej)
|
|
})
|
|
const logout = ({ id }) => new Promise((res, rej) => {
|
|
// getUser({ id }, ["_key"]).then((user) => {
|
|
// collections.find(({ name }) => name == "user").update(user._key, { lockout: Date.now() }).then(res, ({ code, response }) => {
|
|
// rej({ error: code, message: response.body.errorMessage })
|
|
// })
|
|
// }, rej)
|
|
raspi_arango.query(aql`for doc in testView search doc.id == ${id} update { _key: doc._key, lockout: ${Date.now()}} in user`).then(res, ({ code, response }) => {
|
|
rej({ error: code, message: response.body.errorMessage })
|
|
})
|
|
})
|
|
const getLock = ({ id }) => new Promise((res, rej) => {
|
|
getUsersIndex({ id }, ["lockout"]).then(({ lockout }) => {
|
|
res(lockout || 0)
|
|
}, rej)
|
|
})
|
|
const blockUser = ({ id, blocked }) => new Promise((res, rej) => {
|
|
// getUser({ id }, ["_key"]).then(({ _key }) => {
|
|
// collections.find(({ name }) => name == "user").update(_key, { blocked }).then(res, rej)
|
|
// }, rej)
|
|
raspi_arango.query(aql`for doc in testView search doc.id == ${id} update { _key: doc._key, blocked: ${blocked}} in user`).then(res, ({ code, response }) => {
|
|
rej({ error: code, message: response.body.errorMessage })
|
|
})
|
|
})
|
|
const unblockUser = ({ id }) => new Promise((res, rej) => {
|
|
// getUser({ id }, ["_key"]).then(({ _key }) => {
|
|
// collections.find(({ name }) => name == "user").update(_key, { blocked: null }).then(res, rej)
|
|
// }, rej)
|
|
raspi_arango.query(aql`for doc in testView search doc.id == ${id} update { _key: doc._key, blocked: null } in user`).then(res, ({ code, response }) => {
|
|
rej({ error: code, message: response.body.errorMessage })
|
|
})
|
|
})
|
|
const addUserScope = (id, scope) => new Promise((res, rej) => {
|
|
// getUser({ id }, ["_key", "scopes"]).then(({ _key, scopes }) => {
|
|
// collections.find(({ name }) => name == "user").update(_key, { scopes: scopes ? [...scopes, scope] : [scope] }).then(res, rej)
|
|
// }, rej)
|
|
raspi_arango.query(aql`for doc in testView search doc.id == ${id} update { _key: doc._key, scopes: APPEND(doc.scopes, ${scope}, true) } in user`).then(res, ({ code, response }) => {
|
|
rej({ error: code, message: response.body.errorMessage })
|
|
})
|
|
})
|
|
const removeUserScope = (id, scope) => new Promise((res, rej) => {
|
|
// getUser({ id }, ["_key", "scopes"]).then(({ _key, scopes }) => {
|
|
// if (scopes && scopes.includes(scope)) {
|
|
// collections.find(({ name }) => name == "user").update(_key, { scopes: scopes.filter((sc) => sc != scope) }).then(res, rej)
|
|
// } else {
|
|
// rej({ error: 404, message: `${scope} not found on user ${id}` })
|
|
// }
|
|
// }, rej)
|
|
raspi_arango.query(aql`for doc in testView search doc.id == ${id} filter POSITION(doc.scopes, ${scope}) update { _key: doc._key, scopes: REMOVE_NTH(doc.scopes, POSITION(doc.scopes, ${scope}, true))} in user`).then((response) => {
|
|
response.all().then(console.log);
|
|
res()
|
|
}, ({ code, response }) => {
|
|
rej({ error: code, message: response?.body.errorMessage })
|
|
})
|
|
})
|
|
const addScopeEntry = ({ scope, content }) => new Promise((res, rej) => {
|
|
getDocuments("scope", ["content", "_key"], [{ key: "scope", value: scope }]).then((scopes) => {
|
|
// console.log(scope, content, scopes)
|
|
if (scopes.length) {
|
|
collections.find(({ name }) => name == "scope").update(scopes[0]._key, { content: [...scopes[0].content, content] }).then(res, rej)
|
|
} else {
|
|
collections.find(({ name }) => name == "scope").save({ scope, content: [content] }).then(res, rej)
|
|
}
|
|
})
|
|
})
|
|
const tryPromiseUntilResolveOrMaxTries = (fun = () => new Promise((res, rej) => { res() }), tries, origTries = tries) => new Promise((res, rej) => {
|
|
if (tries) {
|
|
fun().then(() => {
|
|
const attempt = origTries - tries + 1;
|
|
console.log("succeeded on " + attempt + (attempt % 10 == 1 ? "st" : attempt % 10 == 2 ? "nd" : attempt % 10 == 3 ? "rd" : "th") + " try");
|
|
res()
|
|
}, () => {
|
|
console.log("trying again: " + tries + " tries left")
|
|
setTimeout(() => {
|
|
tryPromiseUntilResolveOrMaxTries(fun, tries - 1, origTries)
|
|
}, 60000)
|
|
})
|
|
} else {
|
|
rej()
|
|
}
|
|
})
|
|
const implicitAllowScopes = (path) => new Promise((res, rej) => {
|
|
raspi_arango.query(aql`for tag in ${path.split("/")}
|
|
let col = (for doc in testView search ANALYZER(doc.content == tag, "pathsplit") return {tag, scope: doc.scope})
|
|
for results in col collect dir = results.tag into scopes = results.scope return scopes
|
|
`).then((result) => {
|
|
result.all().then((scopes) => {
|
|
if (scopes.length == path.split("/").length) {
|
|
res(scopes.flat().filter((value, index, self) => self.indexOf(value) == index).filter((scope) => {
|
|
let rs = true
|
|
scopes.forEach((scopecol) => {
|
|
if (!scopecol.includes(scope)) {
|
|
rs = false;
|
|
}
|
|
})
|
|
return rs
|
|
}))
|
|
} else {
|
|
res([])
|
|
}
|
|
})
|
|
}, rej)
|
|
})
|
|
const getShoppingItems = (limit = 25, shoppingList) => new Promise((res, rej) => {
|
|
// console.log(shoppingList)
|
|
raspi_arango.query(aql`let shoppingList = ${shoppingList.map(({item})=>item)} for item in shopping filter not position(shoppingList, item.item) let counting = (for timecount in item.count filter timecount.date > ${Date.now() - 1000 * 30 * 24 * 3600} collect aggregate cnt = sum(timecount.count), rec = avg(timecount.count) return {cnt, rec}) sort counting[0].cnt DESC limit ${limit} return {item: item.item, recommend: counting[0].rec, quantityType: item.quantityType} `).then((result) => {
|
|
result.all().then((items) => {
|
|
// console.log(items);
|
|
res(items.map((item) => {
|
|
item.recommend = Math.round(item.recommend)
|
|
switch (item.quantityType) {
|
|
case "grams": item.recommend *= 500;
|
|
break;
|
|
case "kg": item.recommend /= 2;
|
|
break;
|
|
case "pieces": item.recommend /= 3;
|
|
break;
|
|
case "bags": item.recommend /= 2.5;
|
|
break;
|
|
case "bottles": item.recommend /= 4;
|
|
break;
|
|
case "cases": item.recommend /= 20;
|
|
break;
|
|
}
|
|
item.recommend = Math.ceil(item.recommend)
|
|
return item
|
|
}))
|
|
}, rej)
|
|
}, rej)
|
|
})
|
|
const addShoppingItem = (item, quantity, quantityType) => new Promise((res, rej) => {
|
|
switch (quantityType) {
|
|
case "grams":
|
|
quantity = quantity / 500
|
|
break;
|
|
case "kg":
|
|
quantity *= 2;
|
|
break;
|
|
case "pieces":
|
|
quantity *= 3;
|
|
break
|
|
case "bags":
|
|
quantity = Math.floor(quantity * 2.5)
|
|
break;
|
|
case "bottles":
|
|
quantity *= 4
|
|
break;
|
|
case "cases":
|
|
quantity *= 20;
|
|
break;
|
|
}
|
|
raspi_arango.query(aql`upsert {item: ${item}} insert {item: ${item}, quantityType:${quantityType}, count:[{date:${Date.now()}, count: ${quantity}}]} update {count:APPEND(OLD.count, {date: ${Date.now()}, count: ${quantity}}, false)} in shopping`).then(res, rej)
|
|
})
|
|
const getRecentFinds = (timespan = 1) => new Promise((res, rej) => {
|
|
raspi_arango.query(aql`for it in shopping let time = (for t in it.count sort t.date DESC limit 1 return t) filter time[0].date > ${Date.now() - timespan * 3600 * 1000} return {item: it.item, quantity: time[0].count, quantityType: it.quantityType}`).then((response) => {
|
|
response.all().then((finds) => {
|
|
// console.log(finds);
|
|
res(finds.map((find) => ({ item: find.item, quantityType: find.quantityType, quantity: Math.ceil(find.quantity / (find.quantityType == "kg" ? 2 : find.quantityType == "pieces" ? 3 : find.quantityType == "bags" ? 2.5 : find.quantityType == "bottles" ? 4 : find.quantityType == "cases" ? 20 : 1)) * find.quantityType == "grams" ? 500 : 1 })))
|
|
}, rej)
|
|
}, rej)
|
|
})
|
|
const shareExt = (path) => collections.find(({ name }) => name == "extshares").save({ path })
|
|
const getExtSharePath = (_key) => collections.find(({ name }) => name == "extshares").document(_key)
|
|
const getPassword = (owner, id, hash, nonce) => new Promise((res, rej) => {
|
|
raspi_arango.query(aql`for pass in PassManView search pass.owner == ${owner} && (pass.id == ${id} || pass.id == "Master") return pass`).then((pass)=>{
|
|
pass.all().then((pass)=>{
|
|
//console.log(pass)
|
|
if(pass.length==2){
|
|
const Master = pass.find((pass)=>pass.id=="Master")
|
|
if(Master&&createHash("sha256").update(Master.hash+nonce).digest("base64")==hash){
|
|
res(pass.find((pass)=>pass.id!="Master"))
|
|
}
|
|
}else{
|
|
rej({error: 404, message:`passkey ${id} not found on ${owner}`})
|
|
}
|
|
}, rej)
|
|
}, rej)
|
|
})
|
|
const getPasswords = (owner)=>new Promise((res, rej)=>{
|
|
raspi_arango.query(aql`for pass in PassManView search pass.owner == ${owner} return {id: pass.id, salt:pass.id=="Master"?pass.salt:null}`).then((pass)=>{
|
|
pass.all().then(res, rej)
|
|
}, rej)
|
|
})
|
|
const setMasterPassword = (owner, salt, hash)=>new Promise((res, rej)=>{
|
|
|
|
raspi_arango.query(aql`insert {id: "Master", owner: ${owner}, hio:SHA1(${owner+"Master"}), salt:${salt}, hash: ${hash}} in passman`).then(res, rej)
|
|
})
|
|
const getMasterHash = (owner)=>new Promise((res, rej)=>{
|
|
raspi_arango.query(aql`for pass in PassManView search pass.owner == ${owner} && pass.id == "Master" return pass.hash`).then((hash)=>{
|
|
hash.all().then((all)=>{
|
|
if(all.length){
|
|
res(all[0])
|
|
}else{
|
|
rej({error: 404, message:owner+" has no master password"})
|
|
}
|
|
}, rej)
|
|
}, rej)
|
|
})
|
|
const setPassword = (owner, salt, crypt, id)=>new Promise((res, rej)=>{
|
|
if(id!="Master"){
|
|
raspi_arango.query(aql`upsert {hio:SHA1(${owner+id})} insert {id:${id}, owner:${owner}, hio:SHA1(${owner+id}), salt:${salt}, crypt:${crypt}} update {_key: OLD._key, salt:${salt}, crypt:${crypt}} in passman`).then(res, rej)
|
|
}else{
|
|
rej({error: 406, message:"This Method is not meant for resetting the Master Password"})
|
|
}
|
|
})
|
|
const getPassManUsers = ()=>new Promise((res, rej)=>{
|
|
// console.log("getting pwman users")
|
|
raspi_arango.query(aql`for u in user filter "pwman" in u.scopes return {name: u.name, id: u.id}` ).then((results)=>{
|
|
results.all().then((all)=>{
|
|
// console.log(all)
|
|
res(all)
|
|
}, rej)
|
|
}, (response)=>{
|
|
console.log(response)
|
|
rej({error: 500, message: "database error"})
|
|
})
|
|
})
|
|
const storeEncryptedPWshare = (sharer_id, target_id, target_pw_id, encrypted_pw, iv)=>new Promise((res, rej)=>{
|
|
raspi_arango.query(aql`for sp in share_pw filter sp.sharer_id == ${sharer_id} && sp.target_id == ${target_id} && sp.target_pw_id == ${target_pw_id} update {_key: sp._key, encrypted_pw: ${encrypted_pw}, iv:${iv}} in share_pw`).then(res, (response)=>{
|
|
console.log(response)
|
|
rej({error: 500, message: "database error"})
|
|
})
|
|
})
|
|
const acceptsharepw = (target_id, target_pw_id, sharer_id, temp_pub)=>new Promise((res, rej)=>{
|
|
raspi_arango.query(aql`for sp in share_pw filter sp.target_id == ${target_id} && sp.sharer_id == ${sharer_id} && sp.target_pw_id == ${target_pw_id} update {_key: sp._key, target_temp_pub:${temp_pub}} in share_pw`).then(res, (response)=>{
|
|
console.log(response)
|
|
rej({error: 500, message: "database error"})
|
|
})
|
|
})
|
|
const sharePWRequests = (id)=>new Promise((res, rej)=>{
|
|
raspi_arango.query(aql`for sp in share_pw filter sp.sharer_id == ${id} or sp.target_id == ${id} return sp`).then((shares)=>{
|
|
shares.all().then(res, (response)=>{
|
|
console.log(response)
|
|
rej({error: 500, message: "database error"})
|
|
})
|
|
}, (response)=>{
|
|
console.log(response)
|
|
rej({error: 500, message: "database error"})
|
|
})
|
|
})
|
|
const sharePW = (sharer_id, target_id, target_pw_id, temp_pub)=>new Promise((res, rej)=>{
|
|
raspi_arango.query(aql`insert {target_id:${target_id}, sharer_id:${sharer_id}, target_pw_id:${target_pw_id}, sharer_temp_pub:${temp_pub}, createdAt: ${Date.now()/1000}} in share_pw`).then(res, (response)=>{
|
|
console.log(response)
|
|
rej({error: 500, message: "database error"})
|
|
})
|
|
})
|
|
tryPromiseUntilResolveOrMaxTries(setup, 100).then(() => { }, (error) => {
|
|
console.error(error);
|
|
process.exit(1)
|
|
})
|
|
module.exports = {
|
|
getUsers, getUser, newUser, setup, patchUser, login, logout, blockUser, addUserScope, removeUserScope, addScopeEntry, getLock, unblockUser, getUsersIndex, implicitAllowScopes, getShoppingItems, addShoppingItem, getRecentFinds, shareExt, getExtSharePath, getPasswords, getPassword, setMasterPassword, setPassword, getMasterHash, getPassManUsers, storeEncryptedPWshare, acceptsharepw, sharePWRequests, sharePW
|
|
} |