Salt and hash passwords
This commit is contained in:
1 parent
88ad9451e2
commit
d5b194b9e8
13 files changed
+381
-54
No files matched your search
+19
-1
@@ -17,7 +17,7 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createHash } from "crypto";
|
import { createHash, randomBytes, pbkdf2 } from "crypto";
|
||||||
|
|
||||||
export function parseAuthHeader(authHeader): {} {
|
export function parseAuthHeader(authHeader): {} {
|
||||||
authHeader = authHeader.trim();
|
authHeader = authHeader.trim();
|
||||||
@@ -119,3 +119,21 @@ export function digest(
|
|||||||
|
|
||||||
return authString;
|
return authString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateSalt(length): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
randomBytes(length, (err, rand) => {
|
||||||
|
if (err) return void reject(err);
|
||||||
|
resolve(rand.toString("hex"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashPassword(pass, salt): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pbkdf2(pass, salt, 10000, 128, "sha512", (err, hash) => {
|
||||||
|
if (err) return void reject(err);
|
||||||
|
resolve(hash.toString("hex"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -954,7 +954,7 @@ export function getPermissions(
|
|||||||
export function getUsers(
|
export function getUsers(
|
||||||
callback: (
|
callback: (
|
||||||
err?: Error,
|
err?: Error,
|
||||||
users?: { _id: string; password: string; roles: string }[]
|
users?: { _id: string; password: string; salt: string; roles: string }[]
|
||||||
) => void
|
) => void
|
||||||
): void {
|
): void {
|
||||||
usersCollection.find().toArray(callback);
|
usersCollection.find().toArray(callback);
|
||||||
|
|||||||
@@ -74,6 +74,27 @@ function computeHash(snapshot): string {
|
|||||||
h.update(JSON.stringify(keys));
|
h.update(JSON.stringify(keys));
|
||||||
for (const k of keys) h.update(JSON.stringify(snapshot.config[k]));
|
for (const k of keys) h.update(JSON.stringify(snapshot.config[k]));
|
||||||
|
|
||||||
|
keys = Object.keys(snapshot.files).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys) h.update(JSON.stringify(snapshot.files[k]));
|
||||||
|
|
||||||
|
keys = Object.keys(snapshot.users).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys) h.update(JSON.stringify(snapshot.users[k]));
|
||||||
|
|
||||||
|
const roles = Object.keys(snapshot.permissions).sort();
|
||||||
|
h.update(JSON.stringify(roles));
|
||||||
|
for (const r of roles) {
|
||||||
|
const levels = Object.keys(snapshot.permissions[r]).sort();
|
||||||
|
h.update(JSON.stringify(levels));
|
||||||
|
for (const l of levels) {
|
||||||
|
keys = Object.keys(snapshot.permissions[r][l]).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys)
|
||||||
|
h.update(JSON.stringify(snapshot.permissions[r][l][k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return h.digest("hex");
|
return h.digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +375,7 @@ function refresh(callback): void {
|
|||||||
for (const user of _users) {
|
for (const user of _users) {
|
||||||
users[user._id] = {
|
users[user._id] = {
|
||||||
password: user.password,
|
password: user.password,
|
||||||
|
salt: user.salt,
|
||||||
roles: user.roles.split(",").map(s => s.trim())
|
roles: user.roles.split(",").map(s => s.trim())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Authorizer from "./common/authorizer";
|
|||||||
import * as logger from "./logger";
|
import * as logger from "./logger";
|
||||||
import * as localCache from "./local-cache";
|
import * as localCache from "./local-cache";
|
||||||
import { PermissionSet } from "./types";
|
import { PermissionSet } from "./types";
|
||||||
|
import { authSimple } from "./ui/api-functions";
|
||||||
|
|
||||||
declare module "koa" {
|
declare module "koa" {
|
||||||
interface Request {
|
interface Request {
|
||||||
@@ -33,14 +34,6 @@ function getPermissionSets(ctx): PermissionSet[] {
|
|||||||
return permissionSets;
|
return permissionSets;
|
||||||
}
|
}
|
||||||
|
|
||||||
function authSimple(ctx, username, password): Promise<string[]> {
|
|
||||||
const users = localCache.getUsers(ctx.state.configSnapshot);
|
|
||||||
const user = users[username];
|
|
||||||
if (user && user.password === password) return Promise.resolve(user.roles);
|
|
||||||
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
koa.on("error", async err => {
|
koa.on("error", async err => {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -100,7 +93,7 @@ router.post("/login", async ctx => {
|
|||||||
logger.accessWarn(log);
|
logger.accessWarn(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roles = await authSimple(ctx, username, password);
|
const roles = await authSimple(ctx.state.configSnapshot, username, password);
|
||||||
|
|
||||||
if (roles) return void success(roles, "simple");
|
if (roles) return void success(roles, "simple");
|
||||||
|
|
||||||
|
|||||||
+30
-6
@@ -2,6 +2,8 @@ import { exec } from "child_process";
|
|||||||
import { ObjectID } from "mongodb";
|
import { ObjectID } from "mongodb";
|
||||||
import * as db from "./db";
|
import * as db from "./db";
|
||||||
import { del } from "../cache";
|
import { del } from "../cache";
|
||||||
|
import { getUsers } from "../local-cache";
|
||||||
|
import { hashPassword } from "../auth";
|
||||||
import {
|
import {
|
||||||
insertTasks,
|
insertTasks,
|
||||||
watchTask,
|
watchTask,
|
||||||
@@ -186,13 +188,21 @@ export function putResource(resource, id, data): Promise<void> {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let promise;
|
let promise;
|
||||||
|
|
||||||
if (resource === "presets") promise = db.putPreset(id, data);
|
if (resource === "presets") {
|
||||||
else if (resource === "provisions") promise = db.putProvision(id, data);
|
promise = db.putPreset(id, data);
|
||||||
else if (resource === "virtualParameters")
|
} else if (resource === "provisions") {
|
||||||
|
promise = db.putProvision(id, data);
|
||||||
|
} else if (resource === "virtualParameters") {
|
||||||
promise = db.putVirtualParameter(id, data);
|
promise = db.putVirtualParameter(id, data);
|
||||||
else if (resource === "config") promise = db.putConfig(id, data);
|
} else if (resource === "config") {
|
||||||
else if (resource === "permissions") promise = db.putPermission(id, data);
|
promise = db.putConfig(id, data);
|
||||||
else if (resource === "users") promise = db.putUser(id, data);
|
} else if (resource === "permissions") {
|
||||||
|
promise = db.putPermission(id, data);
|
||||||
|
} else if (resource === "users") {
|
||||||
|
delete data.password;
|
||||||
|
delete data.salt;
|
||||||
|
promise = db.putUser(id, data);
|
||||||
|
}
|
||||||
|
|
||||||
promise
|
promise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -204,3 +214,17 @@ export function putResource(resource, id, data): Promise<void> {
|
|||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function authSimple(snapshot, username, password): Promise<string[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const users = getUsers(snapshot);
|
||||||
|
const user = users[username];
|
||||||
|
if (!user || !user.password) return void resolve(null);
|
||||||
|
hashPassword(password, user.salt)
|
||||||
|
.then(hash => {
|
||||||
|
if (hash === user.password) resolve(user.roles);
|
||||||
|
else resolve(null);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import { parse } from "../common/expression-parser";
|
|||||||
import * as logger from "../logger";
|
import * as logger from "../logger";
|
||||||
import { getConfig } from "../local-cache";
|
import { getConfig } from "../local-cache";
|
||||||
import { QueryOptions, Expression } from "../types";
|
import { QueryOptions, Expression } from "../types";
|
||||||
|
import { generateSalt, hashPassword } from "../auth";
|
||||||
|
import { del } from "../cache";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
export default router;
|
export default router;
|
||||||
@@ -497,3 +499,64 @@ router.get("/ping/:host", async ctx => {
|
|||||||
ctx.body = `${err.name}: ${err.message}`;
|
ctx.body = `${err.name}: ${err.message}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.put("/users/:id/password", async (ctx, next) => {
|
||||||
|
const username = ctx.params.id;
|
||||||
|
const log = {
|
||||||
|
message: "Change password",
|
||||||
|
context: ctx,
|
||||||
|
username: username
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorizer = ctx.state.authorizer;
|
||||||
|
|
||||||
|
if (!ctx.state.user) {
|
||||||
|
// User not logged in
|
||||||
|
if (
|
||||||
|
!(await apiFunctions.authSimple(
|
||||||
|
ctx.state.configSnapshot,
|
||||||
|
username,
|
||||||
|
ctx.request.body.authPassword
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
logUnauthorizedWarning(log);
|
||||||
|
ctx.status = 401;
|
||||||
|
ctx.body = "Authentication failed, check your username and password";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!authorizer.hasAccess("users", 3)) {
|
||||||
|
logUnauthorizedWarning(log);
|
||||||
|
return void next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = ["=", ["PARAM", RESOURCE_IDS.users], username];
|
||||||
|
const res = await db.query("users", filter);
|
||||||
|
if (!res.length) return void next();
|
||||||
|
|
||||||
|
const newPassword = ctx.request.body.newPassword;
|
||||||
|
if (ctx.state.user) {
|
||||||
|
const validate = authorizer.getValidator("users", res[0]);
|
||||||
|
if (!validate("password", { password: newPassword })) {
|
||||||
|
logUnauthorizedWarning(log);
|
||||||
|
return void (ctx.status = 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = await generateSalt(64);
|
||||||
|
const password = await hashPassword(newPassword, salt);
|
||||||
|
await db.putUser(username, { password, salt });
|
||||||
|
|
||||||
|
function delCache(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
del("presets_hash", err => {
|
||||||
|
if (err) return void reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await delCache();
|
||||||
|
|
||||||
|
logger.accessInfo(log);
|
||||||
|
ctx.body = "";
|
||||||
|
});
|
||||||
+11
-4
@@ -82,12 +82,19 @@ export function query(
|
|||||||
filter = expression.evaluate(filter, null, Date.now());
|
filter = expression.evaluate(filter, null, Date.now());
|
||||||
|
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
if (resource === "devices")
|
if (resource === "devices") {
|
||||||
filter = mongodbFunctions.processDeviceFilter(filter);
|
filter = mongodbFunctions.processDeviceFilter(filter);
|
||||||
else if (resource === "tasks")
|
} else if (resource === "tasks") {
|
||||||
filter = mongodbFunctions.processTasksFilter(filter);
|
filter = mongodbFunctions.processTasksFilter(filter);
|
||||||
else if (resource === "faults")
|
} else if (resource === "faults") {
|
||||||
filter = mongodbFunctions.processFaultsFilter(filter);
|
filter = mongodbFunctions.processFaultsFilter(filter);
|
||||||
|
} else if (resource === "users") {
|
||||||
|
// Protect against brute force, and dictionary attacks
|
||||||
|
const params = expression.extractParams(filter);
|
||||||
|
if (params.includes("password") || params.includes("salt"))
|
||||||
|
return Promise.reject(new Error("Invalid users filter"));
|
||||||
|
}
|
||||||
|
|
||||||
q = mongodbFunctions.filterToMongoQuery(filter);
|
q = mongodbFunctions.filterToMongoQuery(filter);
|
||||||
} else if (filter != null && !filter) {
|
} else if (filter != null && !filter) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
@@ -111,7 +118,7 @@ export function query(
|
|||||||
cursor.project(projection);
|
cursor.project(projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource === "users") cursor.project({ password: 0 });
|
if (resource === "users") cursor.project({ password: 0, salt: 0 });
|
||||||
|
|
||||||
if (options.skip) cursor.skip(options.skip);
|
if (options.skip) cursor.skip(options.skip);
|
||||||
if (options.limit) cursor.limit(options.limit);
|
if (options.limit) cursor.limit(options.limit);
|
||||||
|
|||||||
+16
-6
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import { get } from "../lib/config";
|
import { get } from "../lib/config";
|
||||||
|
import { hashPassword, generateSalt } from "../lib/auth";
|
||||||
|
|
||||||
const permissionsData = [
|
const permissionsData = [
|
||||||
{ role: "admin", resource: "devices", access: 3, validate: ["pass"] },
|
{ role: "admin", resource: "devices", access: 3, validate: ["pass"] },
|
||||||
@@ -202,12 +203,21 @@ async function run(): Promise<void> {
|
|||||||
.collection("permissions")
|
.collection("permissions")
|
||||||
.insertMany(perms)
|
.insertMany(perms)
|
||||||
);
|
);
|
||||||
const users = usersData.map(p => {
|
const saltingProms = [];
|
||||||
p.roles = (p.roles || []).join(",");
|
for (const u of usersData) {
|
||||||
p._id = p.username;
|
saltingProms.push(
|
||||||
delete p.username;
|
new Promise(async resolve => {
|
||||||
return p;
|
u.salt = await generateSalt(64);
|
||||||
});
|
u.password = await hashPassword(u.password, u.salt);
|
||||||
|
u.roles = (u.roles || []).join(",");
|
||||||
|
u._id = u.username;
|
||||||
|
delete u.username;
|
||||||
|
resolve(u);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await Promise.all(saltingProms);
|
||||||
inserts.push(
|
inserts.push(
|
||||||
client
|
client
|
||||||
.db()
|
.db()
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { VnodeDOM, ClosureComponent, Component } from "mithril";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
import { changePassword } from "./store";
|
||||||
|
import { m } from "./components";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const onPasswordChange = vnode.attrs["onPasswordChange"];
|
||||||
|
const enforceAuth = !vnode.attrs["noAuth"];
|
||||||
|
const username = vnode.attrs["username"];
|
||||||
|
|
||||||
|
if (username) vnode.state["username"] = username;
|
||||||
|
|
||||||
|
const form = [
|
||||||
|
m(
|
||||||
|
"p",
|
||||||
|
m("label", { for: "username" }, "Username"),
|
||||||
|
m("input", {
|
||||||
|
name: "username",
|
||||||
|
type: "text",
|
||||||
|
value: vnode.state["username"],
|
||||||
|
disabled: !!username,
|
||||||
|
oninput: m.withAttr("value", v => (vnode.state["username"] = v)),
|
||||||
|
oncreate: _vnode => {
|
||||||
|
(_vnode.dom as HTMLSelectElement).focus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
let fields = {
|
||||||
|
newPassword: "New password",
|
||||||
|
confirmPassword: "Confirm password"
|
||||||
|
};
|
||||||
|
if (enforceAuth)
|
||||||
|
fields = Object.assign({ authPassword: "Your password" }, fields);
|
||||||
|
|
||||||
|
for (const [f, l] of Object.entries(fields)) {
|
||||||
|
form.push(
|
||||||
|
m(
|
||||||
|
"p",
|
||||||
|
m("label", { for: f }, l),
|
||||||
|
m("input", {
|
||||||
|
name: f,
|
||||||
|
type: "password",
|
||||||
|
value: vnode.state[f],
|
||||||
|
oninput: m.withAttr("value", v => (vnode.state[f] = v))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
type: "submit"
|
||||||
|
},
|
||||||
|
"Change password"
|
||||||
|
) as VnodeDOM;
|
||||||
|
|
||||||
|
form.push(m(".actions-bar", submit));
|
||||||
|
|
||||||
|
const children = [
|
||||||
|
m("h1", "Change password"),
|
||||||
|
m(
|
||||||
|
"form",
|
||||||
|
{
|
||||||
|
onsubmit: e => {
|
||||||
|
e.redraw = false;
|
||||||
|
e.preventDefault();
|
||||||
|
if (
|
||||||
|
!vnode.state["username"] ||
|
||||||
|
!vnode.state["newPassword"] ||
|
||||||
|
(enforceAuth && !vnode.state["authPassword"])
|
||||||
|
) {
|
||||||
|
notifications.push("error", "Please fill all fields");
|
||||||
|
} else if (
|
||||||
|
vnode.state["newPassword"] !== vnode.state["confirmPassword"]
|
||||||
|
) {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
"Password confirm doesn't match new password"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
(submit.dom as HTMLFormElement).disabled = true;
|
||||||
|
changePassword(
|
||||||
|
vnode.state["username"],
|
||||||
|
vnode.state["newPassword"],
|
||||||
|
vnode.state["authPassword"]
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
"Password updated successfully"
|
||||||
|
);
|
||||||
|
if (onPasswordChange) onPasswordChange();
|
||||||
|
(submit.dom as HTMLFormElement).disabled = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
(submit.dom as HTMLFormElement).disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
form
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
return m("div.put-form", children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
+21
-1
@@ -1,7 +1,9 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
import { m } from "./components";
|
import { m } from "./components";
|
||||||
import * as store from "./store";
|
import * as store from "./store";
|
||||||
import * as notifications from "./notifications";
|
import * as notifications from "./notifications";
|
||||||
|
import * as overlay from "./overlay";
|
||||||
|
import changePasswordComponent from "./change-password-component";
|
||||||
|
|
||||||
export function init(args): Promise<{}> {
|
export function init(args): Promise<{}> {
|
||||||
return Promise.resolve(args);
|
return Promise.resolve(args);
|
||||||
@@ -63,6 +65,24 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"Login"
|
"Login"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
onclick: () => {
|
||||||
|
const cb = (): Children => {
|
||||||
|
const attrs = {
|
||||||
|
onPasswordChange: () => {
|
||||||
|
overlay.close(cb);
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return m(changePasswordComponent, attrs);
|
||||||
|
};
|
||||||
|
overlay.open(cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Change password"
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const singular = {
|
|||||||
presets: "preset",
|
presets: "preset",
|
||||||
provisions: "provision",
|
provisions: "provision",
|
||||||
virtualParameters: "virtual parameter",
|
virtualParameters: "virtual parameter",
|
||||||
files: "file"
|
files: "file",
|
||||||
|
users: "user"
|
||||||
};
|
};
|
||||||
|
|
||||||
function createField(current, attr, focus): Children {
|
function createField(current, attr, focus): Children {
|
||||||
|
|||||||
+15
@@ -549,6 +549,21 @@ export function evaluateExpression(exp, obj): Expression {
|
|||||||
return memoizedEvaluate(exp, obj, fulfillTimestamp);
|
return memoizedEvaluate(exp, obj, fulfillTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function changePassword(
|
||||||
|
username,
|
||||||
|
newPassword,
|
||||||
|
authPassword?
|
||||||
|
): Promise<void> {
|
||||||
|
const data = { newPassword };
|
||||||
|
if (authPassword) data["authPassword"] = authPassword;
|
||||||
|
return m.request({
|
||||||
|
method: "PUT",
|
||||||
|
url: `/api/users/${username}/password`,
|
||||||
|
background: true,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function logIn(username, password): Promise<void> {
|
export function logIn(username, password): Promise<void> {
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
+63
-25
@@ -11,6 +11,7 @@ import * as overlay from "./overlay";
|
|||||||
import * as smartQuery from "./smart-query";
|
import * as smartQuery from "./smart-query";
|
||||||
import { map, parse, stringify } from "../lib/common/expression-parser";
|
import { map, parse, stringify } from "../lib/common/expression-parser";
|
||||||
import filterComponent from "./filter-component";
|
import filterComponent from "./filter-component";
|
||||||
|
import changePasswordComponent from "./change-password-component";
|
||||||
import { Children, ClosureComponent, Component } from "mithril";
|
import { Children, ClosureComponent, Component } from "mithril";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||||
@@ -20,7 +21,6 @@ const memoizedJsonParse = memoize(JSON.parse);
|
|||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
{ id: "_id", label: "Username" },
|
{ id: "_id", label: "Username" },
|
||||||
{ id: "password", label: "Password", type: "password", unsortable: true },
|
|
||||||
{
|
{
|
||||||
id: "roles",
|
id: "roles",
|
||||||
label: "Roles",
|
label: "Roles",
|
||||||
@@ -47,19 +47,27 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
const object = Object.assign({}, _object);
|
const object = Object.assign({}, _object);
|
||||||
if (action === "save") {
|
if (action === "save") {
|
||||||
const id = object["_id"];
|
const id = object["_id"];
|
||||||
|
const password = object["password"];
|
||||||
|
const confirm = object["confirm"];
|
||||||
delete object["_id"];
|
delete object["_id"];
|
||||||
|
delete object["password"];
|
||||||
|
delete object["confirm"];
|
||||||
|
|
||||||
if (!id) return void resolve({ _id: "ID can not be empty" });
|
if (!id) return void resolve({ _id: "ID can not be empty" });
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
if (!password) {
|
||||||
|
return void resolve({ password: "Password can not be empty" });
|
||||||
|
} else if (password !== confirm) {
|
||||||
|
return void resolve({
|
||||||
|
confirm: "Confirm password doesn't match password"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(object.roles) || !object.roles.length)
|
if (!Array.isArray(object.roles) || !object.roles.length)
|
||||||
return void resolve({ roles: "Role(s) must be selected" });
|
return void resolve({ roles: "Role(s) must be selected" });
|
||||||
|
|
||||||
if (typeof object.password !== "string" || !object.password.length) {
|
|
||||||
if (isNew)
|
|
||||||
return void resolve({ password: "Password can not be empty" });
|
|
||||||
else delete object.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
object.roles = object.roles.join(",");
|
object.roles = object.roles.join(",");
|
||||||
|
|
||||||
store
|
store
|
||||||
@@ -78,14 +86,22 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
store
|
store
|
||||||
.putResource("users", id, object)
|
.putResource("users", id, object)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push(
|
if (isNew) {
|
||||||
"success",
|
store
|
||||||
`User ${exists ? "updated" : "created"}`
|
.changePassword(id, password)
|
||||||
);
|
.then(() => {
|
||||||
|
notifications.push("success", "User created");
|
||||||
store.fulfill(0, Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
notifications.push("success", "User updated");
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
@@ -106,15 +122,9 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = {
|
|
||||||
resource: "users",
|
|
||||||
attributes: attributes
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDownloadUrl = memoize(filter => {
|
const getDownloadUrl = memoize(filter => {
|
||||||
const cols = {};
|
const cols = {};
|
||||||
for (const attr of attributes)
|
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||||
if (attr.id !== "password") cols[attr.label] = attr.id;
|
|
||||||
|
|
||||||
return `/api/users.csv?${m.buildQueryString({
|
return `/api/users.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: stringify(filter),
|
||||||
@@ -158,8 +168,6 @@ function renderTable(
|
|||||||
|
|
||||||
const labels = [m("th", selectAll)];
|
const labels = [m("th", selectAll)];
|
||||||
for (const attr of attributes) {
|
for (const attr of attributes) {
|
||||||
if (attr.id === "password") continue;
|
|
||||||
|
|
||||||
const label = attr.label;
|
const label = attr.label;
|
||||||
|
|
||||||
if (attr.unsortable) {
|
if (attr.unsortable) {
|
||||||
@@ -187,6 +195,8 @@ function renderTable(
|
|||||||
labels.push(m("th", [label, sortable]));
|
labels.push(m("th", [label, sortable]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canChangePasswords = window.authorizer.hasAccess("users", 3);
|
||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
const checkbox = m("input", {
|
const checkbox = m("input", {
|
||||||
@@ -203,8 +213,7 @@ function renderTable(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tds = [m("td", checkbox)];
|
const tds = [m("td", checkbox)];
|
||||||
for (const attr of attributes)
|
for (const attr of attributes) tds.push(m("td", user[attr.id]));
|
||||||
if (attr.id !== "password") tds.push(m("td", user[attr.id]));
|
|
||||||
|
|
||||||
tds.push(
|
tds.push(
|
||||||
m(
|
m(
|
||||||
@@ -214,7 +223,8 @@ function renderTable(
|
|||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
const cb = (): Children => {
|
const cb = (): Children => {
|
||||||
return m(
|
const children = [
|
||||||
|
m(
|
||||||
putFormComponent,
|
putFormComponent,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
@@ -244,9 +254,28 @@ function renderTable(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formData
|
{
|
||||||
|
resource: "users",
|
||||||
|
attributes: attributes
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (canChangePasswords) {
|
||||||
|
children.push(m("hr"));
|
||||||
|
const attrs = {
|
||||||
|
noAuth: true,
|
||||||
|
username: user._id,
|
||||||
|
onPasswordChange: () => {
|
||||||
|
overlay.close(cb);
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
children.push(m(changePasswordComponent, attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
};
|
};
|
||||||
overlay.open(cb);
|
overlay.open(cb);
|
||||||
}
|
}
|
||||||
@@ -331,6 +360,15 @@ function renderTable(
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("users", 3)) {
|
if (window.authorizer.hasAccess("users", 3)) {
|
||||||
|
const formData = {
|
||||||
|
resource: "users",
|
||||||
|
attributes: [
|
||||||
|
attributes[0],
|
||||||
|
{ id: "password", label: "Password", type: "password" },
|
||||||
|
{ id: "confirm", label: "Confirm password", type: "password" },
|
||||||
|
attributes[1]
|
||||||
|
]
|
||||||
|
};
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.primary",
|
||||||
|
|||||||
Reference in new issue
Block a user