mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-09 17:33:45 +08:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
1f2b0a3587 | |||
0b3feb0d5f | |||
568c0e2c3d | |||
f4ad2b4034 |
@ -536,7 +536,13 @@ func IsNeedPromptMfa(org *Organization, user *User) bool {
|
|||||||
if org == nil || user == nil {
|
if org == nil || user == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, item := range org.MfaItems {
|
|
||||||
|
mfaItems := org.MfaItems
|
||||||
|
|
||||||
|
if len(user.MfaItems) > 0 {
|
||||||
|
mfaItems = user.MfaItems
|
||||||
|
}
|
||||||
|
for _, item := range mfaItems {
|
||||||
if item.Rule == "Required" {
|
if item.Rule == "Required" {
|
||||||
if item.Name == EmailType && !user.MfaEmailEnabled {
|
if item.Name == EmailType && !user.MfaEmailEnabled {
|
||||||
return true
|
return true
|
||||||
|
@ -212,6 +212,7 @@ type User struct {
|
|||||||
|
|
||||||
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
||||||
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
||||||
|
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||||
NeedUpdatePassword bool `json:"needUpdatePassword"`
|
NeedUpdatePassword bool `json:"needUpdatePassword"`
|
||||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||||
}
|
}
|
||||||
@ -795,7 +796,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance")
|
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "mfa_items")
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = append(columns, "updated_time")
|
columns = append(columns, "updated_time")
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/beego/beego/context"
|
"github.com/beego/beego/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
var forbiddenChars = `/?:@#&%=+;`
|
var forbiddenChars = `/?:#&%=+;`
|
||||||
|
|
||||||
func FieldValidationFilter(ctx *context.Context) {
|
func FieldValidationFilter(ctx *context.Context) {
|
||||||
if ctx.Input.Method() != "POST" {
|
if ctx.Input.Method() != "POST" {
|
||||||
|
@ -696,18 +696,27 @@ export const MfaRulePrompted = "Prompted";
|
|||||||
export const MfaRuleOptional = "Optional";
|
export const MfaRuleOptional = "Optional";
|
||||||
|
|
||||||
export function isRequiredEnableMfa(user, organization) {
|
export function isRequiredEnableMfa(user, organization) {
|
||||||
if (!user || !organization || !organization.mfaItems) {
|
if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
|
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMfaItemsByRules(user, organization, mfaRules = []) {
|
export function getMfaItemsByRules(user, organization, mfaRules = []) {
|
||||||
if (!user || !organization || !organization.mfaItems) {
|
if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return organization.mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
|
let mfaItems = organization.mfaItems;
|
||||||
|
if (user.mfaItems && user.mfaItems.length !== 0) {
|
||||||
|
mfaItems = user.mfaItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mfaItems === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
|
||||||
.filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
|
.filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ import * as MfaBackend from "./backend/MfaBackend";
|
|||||||
import AccountAvatar from "./account/AccountAvatar";
|
import AccountAvatar from "./account/AccountAvatar";
|
||||||
import FaceIdTable from "./table/FaceIdTable";
|
import FaceIdTable from "./table/FaceIdTable";
|
||||||
import MfaAccountTable from "./table/MfaAccountTable";
|
import MfaAccountTable from "./table/MfaAccountTable";
|
||||||
|
import MfaTable from "./table/MfaTable";
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -926,6 +927,19 @@ class UserEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
} else if (accountItem.name === "MFA items") {
|
||||||
|
return (<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:MFA items"), i18next.t("general:MFA items - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<MfaTable
|
||||||
|
title={i18next.t("general:MFA items")}
|
||||||
|
table={this.state.user.mfaItems ?? []}
|
||||||
|
onUpdateTable={(value) => {this.updateUserField("mfaItems", value);}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>);
|
||||||
} else if (accountItem.name === "Multi-factor authentication") {
|
} else if (accountItem.name === "Multi-factor authentication") {
|
||||||
return (
|
return (
|
||||||
!this.isSelfOrAdmin() ? null : (
|
!this.isSelfOrAdmin() ? null : (
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Col, Form, Input, Row, Select, Steps} from "antd";
|
import {Button, Col, Form, Input, Popover, Row, Select, Steps} from "antd";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||||
import * as Util from "./Util";
|
import * as Util from "./Util";
|
||||||
@ -385,30 +385,48 @@ class ForgetPage extends React.Component {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Form.Item
|
<Popover placement="right" content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
|
||||||
name="newPassword"
|
<Form.Item
|
||||||
hidden={this.state.current !== 2}
|
name="newPassword"
|
||||||
rules={[
|
hidden={this.state.current !== 2}
|
||||||
{
|
rules={[
|
||||||
required: true,
|
{
|
||||||
validateTrigger: "onChange",
|
required: true,
|
||||||
validator: (rule, value) => {
|
validateTrigger: "onChange",
|
||||||
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
|
validator: (rule, value) => {
|
||||||
if (errorMsg === "") {
|
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
|
||||||
return Promise.resolve();
|
if (errorMsg === "") {
|
||||||
} else {
|
return Promise.resolve();
|
||||||
return Promise.reject(errorMsg);
|
} else {
|
||||||
}
|
return Promise.reject(errorMsg);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]}
|
||||||
]}
|
hasFeedback
|
||||||
hasFeedback
|
>
|
||||||
>
|
<Input.Password
|
||||||
<Input.Password
|
prefix={<LockOutlined />}
|
||||||
prefix={<LockOutlined />}
|
placeholder={i18next.t("general:Password")}
|
||||||
placeholder={i18next.t("general:Password")}
|
onChange={(e) => {
|
||||||
/>
|
this.setState({
|
||||||
</Form.Item>
|
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, e.target.value),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
this.setState({
|
||||||
|
passwordPopoverOpen: true,
|
||||||
|
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("newPassword") ?? ""),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
this.setState({
|
||||||
|
passwordPopoverOpen: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Popover>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="confirm"
|
name="confirm"
|
||||||
dependencies={["newPassword"]}
|
dependencies={["newPassword"]}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Form, Input, Radio, Result, Row, Select, message} from "antd";
|
import {Button, Form, Input, Popover, Radio, Result, Row, Select, message} from "antd";
|
||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
import * as ProviderButton from "./ProviderButton";
|
import * as ProviderButton from "./ProviderButton";
|
||||||
@ -607,28 +607,45 @@ class SignupPage extends React.Component {
|
|||||||
}
|
}
|
||||||
} else if (signupItem.name === "Password") {
|
} else if (signupItem.name === "Password") {
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Popover placement="right" content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
|
||||||
name="password"
|
<Form.Item
|
||||||
className="signup-password"
|
name="password"
|
||||||
label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
|
className="signup-password"
|
||||||
rules={[
|
label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
|
||||||
{
|
rules={[
|
||||||
required: required,
|
{
|
||||||
validateTrigger: "onChange",
|
required: required,
|
||||||
validator: (rule, value) => {
|
validateTrigger: "onChange",
|
||||||
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
|
validator: (rule, value) => {
|
||||||
if (errorMsg === "") {
|
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
|
||||||
return Promise.resolve();
|
if (errorMsg === "") {
|
||||||
} else {
|
return Promise.resolve();
|
||||||
return Promise.reject(errorMsg);
|
} else {
|
||||||
}
|
return Promise.reject(errorMsg);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]}
|
||||||
]}
|
hasFeedback
|
||||||
hasFeedback
|
>
|
||||||
>
|
<Input.Password className="signup-password-input" placeholder={signupItem.placeholder} onChange={(e) => {
|
||||||
<Input.Password className="signup-password-input" placeholder={signupItem.placeholder} />
|
this.setState({
|
||||||
</Form.Item>
|
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, e.target.value),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
this.setState({
|
||||||
|
passwordPopoverOpen: true,
|
||||||
|
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("password") ?? ""),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
this.setState({
|
||||||
|
passwordPopoverOpen: false,
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
</Form.Item>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
} else if (signupItem.name === "Confirm password") {
|
} else if (signupItem.name === "Confirm password") {
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
|
import {CopyOutlined} from "@ant-design/icons";
|
||||||
import {Button, Col, Form, Input, QRCode, Space} from "antd";
|
import {Button, Col, Form, Input, QRCode, Space} from "antd";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -47,11 +47,11 @@ export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
|
|||||||
name="passcode"
|
name="passcode"
|
||||||
rules={[{required: true, message: "Please input your passcode"}]}
|
rules={[{required: true, message: "Please input your passcode"}]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input.OTP
|
||||||
style={{marginTop: 24}}
|
style={{marginTop: 24}}
|
||||||
prefix={<UserOutlined />}
|
onChange={() => {
|
||||||
placeholder={i18next.t("mfa:Passcode")}
|
form.submit();
|
||||||
autoComplete="off"
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import React from "react";
|
||||||
|
import {CheckCircleTwoTone, CloseCircleTwoTone} from "@ant-design/icons";
|
||||||
|
|
||||||
function isValidOption_AtLeast6(password) {
|
function isValidOption_AtLeast6(password) {
|
||||||
if (password.length < 6) {
|
if (password.length < 6) {
|
||||||
@ -52,6 +54,33 @@ function isValidOption_NoRepeat(password) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkers = {
|
||||||
|
AtLeast6: isValidOption_AtLeast6,
|
||||||
|
AtLeast8: isValidOption_AtLeast8,
|
||||||
|
Aa123: isValidOption_Aa123,
|
||||||
|
SpecialChar: isValidOption_SpecialChar,
|
||||||
|
NoRepeat: isValidOption_NoRepeat,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOptionDescription(option, password) {
|
||||||
|
switch (option) {
|
||||||
|
case "AtLeast6": return i18next.t("user:The password must have at least 6 characters");
|
||||||
|
case "AtLeast8": return i18next.t("user:The password must have at least 8 characters");
|
||||||
|
case "Aa123": return i18next.t("user:The password must contain at least one uppercase letter, one lowercase letter and one digit");
|
||||||
|
case "SpecialChar": return i18next.t("user:The password must contain at least one special character");
|
||||||
|
case "NoRepeat": return i18next.t("user:The password must not contain any repeated characters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderPasswordPopover(options, password) {
|
||||||
|
return <div style={{width: 240}} >
|
||||||
|
{options.map((option, idx) => {
|
||||||
|
return <div key={idx}>{checkers[option](password) === "" ? <CheckCircleTwoTone twoToneColor={"#52c41a"} /> :
|
||||||
|
<CloseCircleTwoTone twoToneColor={"#ff4d4f"} />} {getOptionDescription(option, password)}</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
export function checkPasswordComplexity(password, options) {
|
export function checkPasswordComplexity(password, options) {
|
||||||
if (password.length === 0) {
|
if (password.length === 0) {
|
||||||
return i18next.t("login:Please input your password!");
|
return i18next.t("login:Please input your password!");
|
||||||
@ -61,14 +90,6 @@ export function checkPasswordComplexity(password, options) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkers = {
|
|
||||||
AtLeast6: isValidOption_AtLeast6,
|
|
||||||
AtLeast8: isValidOption_AtLeast8,
|
|
||||||
Aa123: isValidOption_Aa123,
|
|
||||||
SpecialChar: isValidOption_SpecialChar,
|
|
||||||
NoRepeat: isValidOption_NoRepeat,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
const checkerFunc = checkers[option];
|
const checkerFunc = checkers[option];
|
||||||
if (checkerFunc) {
|
if (checkerFunc) {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import {Button, Col, Input, Modal, Row} from "antd";
|
import {Button, Col, Input, Modal, Popover, Row} from "antd";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as UserBackend from "../../backend/UserBackend";
|
import * as UserBackend from "../../backend/UserBackend";
|
||||||
@ -35,6 +35,8 @@ export const PasswordModal = (props) => {
|
|||||||
const [rePasswordValid, setRePasswordValid] = React.useState(false);
|
const [rePasswordValid, setRePasswordValid] = React.useState(false);
|
||||||
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = React.useState("");
|
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = React.useState("");
|
||||||
const [rePasswordErrorMessage, setRePasswordErrorMessage] = React.useState("");
|
const [rePasswordErrorMessage, setRePasswordErrorMessage] = React.useState("");
|
||||||
|
const [passwordPopoverOpen, setPasswordPopoverOpen] = React.useState(false);
|
||||||
|
const [passwordPopover, setPasswordPopover] = React.useState();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (organization) {
|
if (organization) {
|
||||||
@ -130,12 +132,26 @@ export const PasswordModal = (props) => {
|
|||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
<Row style={{width: "100%", marginBottom: "20px"}}>
|
<Row style={{width: "100%", marginBottom: "20px"}}>
|
||||||
<Input.Password
|
<Popover placement="right" content={passwordPopover} open={passwordPopoverOpen}>
|
||||||
addonBefore={i18next.t("user:New Password")}
|
<Input.Password
|
||||||
placeholder={i18next.t("user:input password")}
|
addonBefore={i18next.t("user:New Password")}
|
||||||
onChange={(e) => {handleNewPassword(e.target.value);}}
|
placeholder={i18next.t("user:input password")}
|
||||||
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
|
onChange={(e) => {
|
||||||
/>
|
handleNewPassword(e.target.value);
|
||||||
|
setPasswordPopoverOpen(true);
|
||||||
|
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, e.target.value));
|
||||||
|
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
setPasswordPopoverOpen(true);
|
||||||
|
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, newPassword));
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setPasswordPopoverOpen(false);
|
||||||
|
}}
|
||||||
|
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
</Row>
|
</Row>
|
||||||
{!newPasswordValid && newPasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{newPasswordErrorMessage}</div>}
|
{!newPasswordValid && newPasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{newPasswordErrorMessage}</div>}
|
||||||
<Row style={{width: "100%", marginBottom: "20px"}}>
|
<Row style={{width: "100%", marginBottom: "20px"}}>
|
||||||
|
@ -110,6 +110,7 @@ class AccountTable extends React.Component {
|
|||||||
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
||||||
{name: "Face ID", label: i18next.t("user:Face ID")},
|
{name: "Face ID", label: i18next.t("user:Face ID")},
|
||||||
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
|
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
|
||||||
|
{name: "MFA items", label: i18next.t("general:MFA items")},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user