ci: add password complexity options to organization edit page (#1949)

* Support uploading roles and permissions via xlsx file.

* Template xlsx file for uploading users and permissions.

* reformat according to gofumpt.

* fix typo.

* add password complexity options to organization edit page.

* add password complexity options to organization edit page.

* Fixed Typos.

* Fixed Typos.

* feat:add password complexity options to organization edit page

* Auto generate i18n fields.

* Refactor code according to instructions

* Support autocheck passwd complexity in frontend when setting passwd in user edit page.

* feat:Backend Support for password validation in signup and forget page.

* feat:Frontend Support for password validation in signup and forget page.

* Add default password complex option & Update historical empty filed with default option.

* Migrator for field `password_complex_options` in org table.

* feat: support frontend password complex option check in user_edit/forget/signup page.

* frontend update for user edit page

* update i18n file

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
leoil
2023-06-17 00:01:20 +08:00
committed by Yang Luo
parent edc6aa0d50
commit 0f57ac297b
24 changed files with 420 additions and 24 deletions

View File

@ -0,0 +1,82 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import i18next from "i18next";
function isValidOption_AtLeast6(password) {
if (password.length < 6) {
return i18next.t("user:The password must have at least 6 characters");
}
return "";
}
function isValidOption_AtLeast8(password) {
if (password.length < 8) {
return i18next.t("user:The password must have at least 8 characters");
}
return "";
}
function isValidOption_Aa123(password) {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).+$/;
if (!regex.test(password)) {
return i18next.t("user:The password must contain at least one uppercase letter, one lowercase letter and one digit");
}
return "";
}
function isValidOption_SpecialChar(password) {
const regex = /^(?=.*[!@#$%^&*]).+$/;
if (!regex.test(password)) {
return i18next.t("user:The password must contain at least one special character");
}
return "";
}
function isValidOption_NoRepeat(password) {
const regex = /(.)\1+/;
if (regex.test(password)) {
return i18next.t("user:The password must not contain any repeated characters");
}
return "";
}
export function checkPasswordComplexity(password, options) {
if (password.length === 0) {
return i18next.t("login:Please input your password!");
}
if (options.length === 0) {
options = ["AtLeast6"];
}
const checkers = {
AtLeast6: isValidOption_AtLeast6,
AtLeast8: isValidOption_AtLeast8,
Aa123: isValidOption_Aa123,
SpecialChar: isValidOption_SpecialChar,
NoRepeat: isValidOption_NoRepeat,
};
for (const option of options) {
const checkerFunc = checkers[option];
if (checkerFunc) {
const errorMsg = checkerFunc(password);
if (errorMsg !== "") {
return errorMsg;
}
}
}
return "";
}

View File

@ -17,6 +17,8 @@ import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
import * as Setting from "../../Setting";
import * as OrganizationBackend from "../../backend/OrganizationBackend";
import * as PasswordChecker from "../PasswordChecker";
export const PasswordModal = (props) => {
const [visible, setVisible] = React.useState(false);
@ -27,6 +29,26 @@ export const PasswordModal = (props) => {
const {user} = props;
const {account} = props;
const [passwordOptions, setPasswordOptions] = React.useState([]);
const [newPasswordValid, setNewPasswordValid] = React.useState(false);
const [rePasswordValid, setRePasswordValid] = React.useState(false);
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = React.useState("");
const [rePasswordErrorMessage, setRePasswordErrorMessage] = React.useState("");
React.useEffect(() => {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
const organizations = (res.msg === undefined) ? res : [];
// Find the user's corresponding organization
const organization = organizations.find((org) => org.name === user.owner);
if (organization) {
setPasswordOptions(organization.passwordOptions);
}
})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}, [user.owner]);
const showModal = () => {
setVisible(true);
};
@ -34,6 +56,24 @@ export const PasswordModal = (props) => {
const handleCancel = () => {
setVisible(false);
};
const handleNewPassword = (value) => {
setNewPassword(value);
const errorMessage = PasswordChecker.checkPasswordComplexity(value, passwordOptions);
setNewPasswordValid(errorMessage === "");
setNewPasswordErrorMessage(errorMessage);
};
const handleRePassword = (value) => {
setRePassword(value);
if (value !== newPassword) {
setRePasswordErrorMessage(i18next.t("signup:Your confirmed password is inconsistent with the password!"));
setRePasswordValid(false);
} else {
setRePasswordValid(true);
}
};
const handleOk = () => {
if (newPassword === "" || rePassword === "") {
@ -45,12 +85,44 @@ export const PasswordModal = (props) => {
return;
}
setConfirmLoading(true);
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword).then((res) => {
setConfirmLoading(false);
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Password set successfully"));
setVisible(false);
} else {Setting.showMessage("error", i18next.t(`user:${res.msg}`));}
OrganizationBackend.getOrganizations("admin").then((res) => {
const organizations = (res.msg === undefined) ? res : [];
// find the users' corresponding organization
let organization = null;
for (let i = 0; i < organizations.length; i++) {
if (organizations[i].name === user.owner) {
organization = organizations[i];
break;
}
}
if (organization === null) {
Setting.showMessage("error", "organization is null");
setConfirmLoading(false);
return;
}
const errorMsg = PasswordChecker.checkPasswordComplexity(newPassword, organization.passwordOptions);
if (errorMsg !== "") {
Setting.showMessage("error", errorMsg);
setConfirmLoading(false);
return;
}
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Password set successfully"));
setVisible(false);
} else {
Setting.showMessage("error", i18next.t(`user:${res.msg}`));
}
})
.finally(() => {
setConfirmLoading(false);
});
});
};
@ -79,11 +151,23 @@ export const PasswordModal = (props) => {
</Row>
) : null}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:New Password")} placeholder={i18next.t("user:input password")} onChange={(e) => setNewPassword(e.target.value)} />
<Input.Password
addonBefore={i18next.t("user:New Password")}
placeholder={i18next.t("user:input password")}
onChange={(e) => {handleNewPassword(e.target.value);}}
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
/>
</Row>
{!newPasswordValid && newPasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{newPasswordErrorMessage}</div>}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:Re-enter New")} placeholder={i18next.t("user:input password")} onChange={(e) => setRePassword(e.target.value)} />
<Input.Password
addonBefore={i18next.t("user:Re-enter New")}
placeholder={i18next.t("user:input password")}
onChange={(e) => handleRePassword(e.target.value)}
status={(!rePasswordValid && rePasswordErrorMessage) ? "error" : undefined}
/>
</Row>
{!rePasswordValid && rePasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{rePasswordErrorMessage}</div>}
</Col>
</Modal>
</Row>