Salt and hash passwords
This commit is contained in:
1 parent
88ad9451e2
commit
d5b194b9e8
13 files changed
+410
-83
No files matched your search
@@ -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 * as store from "./store";
|
||||
import * as notifications from "./notifications";
|
||||
import * as overlay from "./overlay";
|
||||
import changePasswordComponent from "./change-password-component";
|
||||
|
||||
export function init(args): Promise<{}> {
|
||||
return Promise.resolve(args);
|
||||
@@ -63,6 +65,24 @@ export const component: ClosureComponent = (): Component => {
|
||||
"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",
|
||||
provisions: "provision",
|
||||
virtualParameters: "virtual parameter",
|
||||
files: "file"
|
||||
files: "file",
|
||||
users: "user"
|
||||
};
|
||||
|
||||
function createField(current, attr, focus): Children {
|
||||
|
||||
+15
@@ -549,6 +549,21 @@ export function evaluateExpression(exp, obj): Expression {
|
||||
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> {
|
||||
return m.request({
|
||||
method: "POST",
|
||||
|
||||
+92
-54
@@ -11,6 +11,7 @@ import * as overlay from "./overlay";
|
||||
import * as smartQuery from "./smart-query";
|
||||
import { map, parse, stringify } from "../lib/common/expression-parser";
|
||||
import filterComponent from "./filter-component";
|
||||
import changePasswordComponent from "./change-password-component";
|
||||
import { Children, ClosureComponent, Component } from "mithril";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
@@ -20,7 +21,6 @@ const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes = [
|
||||
{ id: "_id", label: "Username" },
|
||||
{ id: "password", label: "Password", type: "password", unsortable: true },
|
||||
{
|
||||
id: "roles",
|
||||
label: "Roles",
|
||||
@@ -47,19 +47,27 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
||||
const object = Object.assign({}, _object);
|
||||
if (action === "save") {
|
||||
const id = object["_id"];
|
||||
const password = object["password"];
|
||||
const confirm = object["confirm"];
|
||||
delete object["_id"];
|
||||
delete object["password"];
|
||||
delete object["confirm"];
|
||||
|
||||
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)
|
||||
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(",");
|
||||
|
||||
store
|
||||
@@ -78,12 +86,20 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
||||
store
|
||||
.putResource("users", id, object)
|
||||
.then(() => {
|
||||
notifications.push(
|
||||
"success",
|
||||
`User ${exists ? "updated" : "created"}`
|
||||
);
|
||||
store.fulfill(0, Date.now());
|
||||
resolve();
|
||||
if (isNew) {
|
||||
store
|
||||
.changePassword(id, password)
|
||||
.then(() => {
|
||||
notifications.push("success", "User created");
|
||||
store.fulfill(0, Date.now());
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
} else {
|
||||
notifications.push("success", "User updated");
|
||||
store.fulfill(0, Date.now());
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
})
|
||||
@@ -106,15 +122,9 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
||||
});
|
||||
}
|
||||
|
||||
const formData = {
|
||||
resource: "users",
|
||||
attributes: attributes
|
||||
};
|
||||
|
||||
const getDownloadUrl = memoize(filter => {
|
||||
const cols = {};
|
||||
for (const attr of attributes)
|
||||
if (attr.id !== "password") cols[attr.label] = attr.id;
|
||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||
|
||||
return `/api/users.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
@@ -158,8 +168,6 @@ function renderTable(
|
||||
|
||||
const labels = [m("th", selectAll)];
|
||||
for (const attr of attributes) {
|
||||
if (attr.id === "password") continue;
|
||||
|
||||
const label = attr.label;
|
||||
|
||||
if (attr.unsortable) {
|
||||
@@ -187,6 +195,8 @@ function renderTable(
|
||||
labels.push(m("th", [label, sortable]));
|
||||
}
|
||||
|
||||
const canChangePasswords = window.authorizer.hasAccess("users", 3);
|
||||
|
||||
const rows = [];
|
||||
for (const user of users) {
|
||||
const checkbox = m("input", {
|
||||
@@ -203,8 +213,7 @@ function renderTable(
|
||||
});
|
||||
|
||||
const tds = [m("td", checkbox)];
|
||||
for (const attr of attributes)
|
||||
if (attr.id !== "password") tds.push(m("td", user[attr.id]));
|
||||
for (const attr of attributes) tds.push(m("td", user[attr.id]));
|
||||
|
||||
tds.push(
|
||||
m(
|
||||
@@ -214,39 +223,59 @@ function renderTable(
|
||||
{
|
||||
onclick: () => {
|
||||
const cb = (): Children => {
|
||||
return m(
|
||||
putFormComponent,
|
||||
Object.assign(
|
||||
{
|
||||
base: {
|
||||
_id: user._id,
|
||||
roles: user.roles.split(",")
|
||||
const children = [
|
||||
m(
|
||||
putFormComponent,
|
||||
Object.assign(
|
||||
{
|
||||
base: {
|
||||
_id: user._id,
|
||||
roles: user.roles.split(",")
|
||||
},
|
||||
actionHandler: (action, object) => {
|
||||
return new Promise(resolve => {
|
||||
putActionHandler(action, object, false)
|
||||
.then(errors => {
|
||||
const errorList = errors
|
||||
? Object.values(errors)
|
||||
: [];
|
||||
if (errorList.length) {
|
||||
for (const err of errorList)
|
||||
notifications.push("error", err);
|
||||
} else {
|
||||
overlay.close(cb);
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
notifications.push("error", err.message);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
actionHandler: (action, object) => {
|
||||
return new Promise(resolve => {
|
||||
putActionHandler(action, object, false)
|
||||
.then(errors => {
|
||||
const errorList = errors
|
||||
? Object.values(errors)
|
||||
: [];
|
||||
if (errorList.length) {
|
||||
for (const err of errorList)
|
||||
notifications.push("error", err);
|
||||
} else {
|
||||
overlay.close(cb);
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
notifications.push("error", err.message);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
{
|
||||
resource: "users",
|
||||
attributes: attributes
|
||||
}
|
||||
},
|
||||
formData
|
||||
)
|
||||
)
|
||||
);
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -331,6 +360,15 @@ function renderTable(
|
||||
];
|
||||
|
||||
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(
|
||||
m(
|
||||
"button.primary",
|
||||
|
||||
Reference in new issue
Block a user