Compare commits

..

5 Commits

Author SHA1 Message Date
IZUMI-Zu
cfc6015aca feat: rename Casdoor app URL to authenticator (#3553) 2025-02-05 23:08:06 +08:00
hsluoyz
1600a6799a feat: return error for updateUsedByCode() 2025-02-05 13:40:41 +08:00
DacongDA
ca60cc3a33 feat: show SAML cert parse error better in frontend (#3551) 2025-02-05 10:06:02 +08:00
DacongDA
df295717f0 feat: can define what Casdoor pages an org admin can see via Organization.NavItems (#3539)
* feat: support define what Casdoor pages an org admin can see

* feat: remove useless code

* fix: fix NavItemNodes i18next invalid

* fix: only global admin can edit navItems

* fix: move navItem tree to extra file
2025-02-03 00:40:21 +08:00
DacongDA
e3001671a2 feat: fix bug that can not delete user if user doesn't belong to any group (#3544) 2025-02-02 17:54:05 +08:00
11 changed files with 172 additions and 18 deletions

View File

@@ -124,7 +124,9 @@ func (c *ApiController) UpdateOrganization() {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
isGlobalAdmin, _ := c.isGlobalAdmin()
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
c.ServeJSON()
}

View File

@@ -79,6 +79,7 @@ type Organization struct {
UseEmailAsUsername bool `json:"useEmailAsUsername"`
EnableTour bool `json:"enableTour"`
IpRestriction string `json:"ipRestriction"`
NavItems []string `xorm:"varchar(500)" json:"navItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
@@ -195,9 +196,10 @@ func GetMaskedOrganizations(organizations []*Organization, errs ...error) ([]*Or
return organizations, nil
}
func UpdateOrganization(id string, organization *Organization) (bool, error) {
func UpdateOrganization(id string, organization *Organization, isGlobalAdmin bool) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
if org, err := getOrganization(owner, name); err != nil {
org, err := getOrganization(owner, name)
if err != nil {
return false, err
} else if org == nil {
return false, nil
@@ -222,6 +224,10 @@ func UpdateOrganization(id string, organization *Organization) (bool, error) {
}
}
if !isGlobalAdmin {
organization.NavItems = org.NavItems
}
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
if organization.MasterPassword == "***" {

View File

@@ -144,13 +144,13 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
return nil, nil
}
func updateUsedByCode(token *Token) bool {
func updateUsedByCode(token *Token) (bool, error) {
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
if err != nil {
panic(err)
return false, err
}
return affected != 0
return affected != 0, nil
}
func GetToken(id string) (*Token, error) {

View File

@@ -248,7 +248,10 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
token.CodeIsUsed = true
go updateUsedByCode(token)
_, err = updateUsedByCode(token)
if err != nil {
return nil, err
}
tokenWrapper := &TokenWrapper{
AccessToken: token.AccessToken,

View File

@@ -965,13 +965,10 @@ func DeleteUser(user *User) (bool, error) {
return false, err
}
ok, err := userEnforcer.DeleteGroupsForUser(user.GetId())
_, err = userEnforcer.DeleteGroupsForUser(user.GetId())
if err != nil {
return false, err
}
if !ok {
return false, nil
}
organization, err := GetOrganizationByUser(user)
if err != nil {

View File

@@ -118,6 +118,6 @@ func IsValidOrigin(origin string) (bool, error) {
originHostOnly = fmt.Sprintf("%s://%s", urlObj.Scheme, urlObj.Hostname())
}
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-app" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-authenticator" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
return res, nil
}

View File

@@ -241,7 +241,7 @@ function ManagementPage(props) {
<Link to="/">
<img className="logo" src={logo ?? props.logo} alt="logo" />
</Link>,
disabled: true,
disabled: true, key: "logo",
style: {
padding: 0,
height: "auto",
@@ -323,7 +323,35 @@ function ManagementPage(props) {
}
}
return res;
const navItems = props.account.organization.navItems;
if (!Array.isArray(navItems)) {
return res;
}
if (navItems.includes("all")) {
return res;
}
const resFiltered = res.map(item => {
if (!Array.isArray(item.children)) {
return item;
}
const filteredChildren = [];
item.children.forEach(itemChild => {
if (navItems.includes(itemChild.key)) {
filteredChildren.push(itemChild);
}
});
item.children = filteredChildren;
return item;
});
return resFiltered.filter(item => {
if (item.key === "#" || item.key === "logo") {return true;}
return Array.isArray(item.children) && item.children.length > 0;
});
}
function renderLoginIfNotLoggedIn(component) {

View File

@@ -26,6 +26,7 @@ import LdapTable from "./table/LdapTable";
import AccountTable from "./table/AccountTable";
import ThemeEditor from "./common/theme/ThemeEditor";
import MfaTable from "./table/MfaTable";
import {NavItemTree} from "./common/NavItemTree";
const {Option} = Select;
@@ -522,6 +523,21 @@ class OrganizationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Navbar items"), i18next.t("general:Navbar items - Tooltip"))} :
</Col>
<Col span={22} >
<NavItemTree
disabled={!Setting.isAdminUser(this.props.account)}
checkedKeys={this.state.organization.navItems ?? ["all"]}
defaultExpandedKeys={["all"]}
onCheck={(checked, _) => {
this.updateOrganizationField("navItems", checked);
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :

View File

@@ -44,6 +44,7 @@ import KwaiLoginButton from "./KwaiLoginButton";
import LoginButton from "./LoginButton";
import * as AuthBackend from "./AuthBackend";
import {WechatOfficialAccountModal} from "./Util";
import * as Setting from "../Setting";
function getSigninButton(provider) {
const text = i18next.t("login:Sign in with {type}").replace("{type}", provider.displayName !== "" ? provider.displayName : provider.type);
@@ -114,10 +115,14 @@ function goToSamlUrl(provider, location) {
const relayState = `${clientId}&${state}&${providerName}&${realRedirectUri}&${redirectUri}`;
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`, btoa(relayState)).then((res) => {
if (res.data2 === "POST") {
document.write(res.data);
if (res.status === "ok") {
if (res.data2 === "POST") {
document.write(res.data);
} else {
window.location.href = res.data;
}
} else {
window.location.href = res.data;
Setting.showMessage("error", res.msg);
}
});
}

View File

@@ -27,7 +27,7 @@ export const generateCasdoorAppUrl = (accessToken, forQrCode = true) => {
return {qrUrl, error};
}
qrUrl = `casdoor-app://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
qrUrl = `casdoor-authenticator://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
if (forQrCode && qrUrl.length >= 2000) {
qrUrl = "";

View File

@@ -0,0 +1,97 @@
import i18next from "i18next";
import {Tree} from "antd";
import React from "react";
export const NavItemTree = ({disable, checkedKeys, defaultExpandedKeys, onCheck}) => {
const NavItemNodes = [
{
title: i18next.t("organization:All"),
key: "all",
children: [
{
title: i18next.t("general:Home"),
key: "/home-top",
children: [
{title: i18next.t("general:Dashboard"), key: "/"},
{title: i18next.t("general:Shortcuts"), key: "/shortcuts"},
{title: i18next.t("general:Apps"), key: "/apps"},
],
},
{
title: i18next.t("general:User Management"),
key: "/orgs-top",
children: [
{title: i18next.t("general:Organizations"), key: "/organizations"},
{title: i18next.t("general:Groups"), key: "/groups"},
{title: i18next.t("general:Users"), key: "/users"},
{title: i18next.t("general:Invitations"), key: "/invitations"},
],
},
{
title: i18next.t("general:Identity"),
key: "/applications-top",
children: [
{title: i18next.t("general:Applications"), key: "/applications"},
{title: i18next.t("general:Providers"), key: "/providers"},
{title: i18next.t("general:Resources"), key: "/resources"},
{title: i18next.t("general:Certs"), key: "/certs"},
],
},
{
title: i18next.t("general:Authorization"),
key: "/roles-top",
children: [
{title: i18next.t("general:Applications"), key: "/roles"},
{title: i18next.t("general:Permissions"), key: "/permissions"},
{title: i18next.t("general:Models"), key: "/models"},
{title: i18next.t("general:Adapters"), key: "/adapters"},
{title: i18next.t("general:Enforcers"), key: "/enforcers"},
],
},
{
title: i18next.t("general:Logging & Auditing"),
key: "/sessions-top",
children: [
{title: i18next.t("general:Sessions"), key: "/sessions"},
{title: i18next.t("general:Records"), key: "/records"},
{title: i18next.t("general:Tokens"), key: "/tokens"},
{title: i18next.t("general:Verifications"), key: "/verifications"},
],
},
{
title: i18next.t("general:Business & Payments"),
key: "/business-top",
children: [
{title: i18next.t("general:Products"), key: "/products"},
{title: i18next.t("general:Payments"), key: "/payments"},
{title: i18next.t("general:Plans"), key: "/plans"},
{title: i18next.t("general:Pricings"), key: "/pricings"},
{title: i18next.t("general:Subscriptions"), key: "/subscriptions"},
{title: i18next.t("general:Transactions"), key: "/transactions"},
],
},
{
title: i18next.t("general:Admin"),
key: "/admin-top",
children: [
{title: i18next.t("general:System Info"), key: "/sysinfo"},
{title: i18next.t("general:Syncers"), key: "/syncers"},
{title: i18next.t("general:Webhooks"), key: "/webhooks"},
{title: i18next.t("general:Swagger"), key: "/swagger"},
],
},
],
},
];
return (
<Tree
disabled={disable}
checkable
checkedKeys={checkedKeys}
defaultExpandedKeys={defaultExpandedKeys}
onCheck={onCheck}
treeData={NavItemNodes}
/>
);
};