mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-20 02:43:49 +08:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
cfc6015aca | |||
1600a6799a | |||
ca60cc3a33 | |||
df295717f0 | |||
e3001671a2 | |||
bbe2162e27 | |||
92b5ce3722 | |||
bad21fb6bb | |||
5a78dcf06d | |||
558b168477 | |||
802b6812a9 | |||
a5a627f92e | |||
9701818a6e | |||
06986fbd41 | |||
3d12ac8dc2 | |||
f01839123f | |||
e1b3b0ac6a | |||
4b0a2fdbfc | |||
db551eb24a | |||
18b49bb731 | |||
17653888a3 |
@ -306,6 +306,35 @@ func isProxyProviderType(providerType string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
|
||||||
|
if object.IsNeedPromptMfa(organization, user) {
|
||||||
|
// The prompt page needs the user to be srigned in
|
||||||
|
c.SetSessionUsername(user.GetId())
|
||||||
|
c.ResponseOk(object.RequiredMfa)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.IsMfaEnabled() {
|
||||||
|
c.setMfaUserSession(user.GetId())
|
||||||
|
mfaList := object.GetAllMfaProps(user, true)
|
||||||
|
mfaAllowList := []*object.MfaProps{}
|
||||||
|
for _, prop := range mfaList {
|
||||||
|
if prop.MfaType == verificationType || !prop.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mfaAllowList = append(mfaAllowList, prop)
|
||||||
|
}
|
||||||
|
if len(mfaAllowList) >= 1 {
|
||||||
|
c.SetSession("verificationCodeType", verificationType)
|
||||||
|
c.Ctx.Input.CruSession.SessionRelease(c.Ctx.ResponseWriter)
|
||||||
|
c.ResponseOk(object.NextMfa, mfaAllowList)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Login ...
|
// Login ...
|
||||||
// @Title Login
|
// @Title Login
|
||||||
// @Tag Login API
|
// @Tag Login API
|
||||||
@ -331,6 +360,8 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verificationType := ""
|
||||||
|
|
||||||
if authForm.Username != "" {
|
if authForm.Username != "" {
|
||||||
if authForm.Type == ResponseTypeLogin {
|
if authForm.Type == ResponseTypeLogin {
|
||||||
if c.GetSessionUsername() != "" {
|
if c.GetSessionUsername() != "" {
|
||||||
@ -425,6 +456,12 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if verificationCodeType == object.VerifyTypePhone {
|
||||||
|
verificationType = "sms"
|
||||||
|
} else {
|
||||||
|
verificationType = "email"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||||
@ -515,16 +552,7 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if object.IsNeedPromptMfa(organization, user) {
|
if checkMfaEnable(c, user, organization, verificationType) {
|
||||||
// The prompt page needs the user to be signed in
|
|
||||||
c.SetSessionUsername(user.GetId())
|
|
||||||
c.ResponseOk(object.RequiredMfa)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.IsMfaEnabled() {
|
|
||||||
c.setMfaUserSession(user.GetId())
|
|
||||||
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,6 +688,11 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if checkMfaEnable(c, user, organization, verificationType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||||
|
|
||||||
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
||||||
@ -866,8 +899,12 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if authForm.Passcode != "" {
|
if authForm.Passcode != "" {
|
||||||
|
if authForm.MfaType == c.GetSession("verificationCodeType") {
|
||||||
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
|
return
|
||||||
|
}
|
||||||
user.CountryCode = user.GetCountryCode(user.CountryCode)
|
user.CountryCode = user.GetCountryCode(user.CountryCode)
|
||||||
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
|
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetMfaProps(authForm.MfaType, false))
|
||||||
if mfaUtil == nil {
|
if mfaUtil == nil {
|
||||||
c.ResponseError("Invalid multi-factor authentication type")
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
return
|
return
|
||||||
@ -878,6 +915,7 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.SetSession("verificationCodeType", "")
|
||||||
} else if authForm.RecoveryCode != "" {
|
} else if authForm.RecoveryCode != "" {
|
||||||
err = object.MfaRecover(user, authForm.RecoveryCode)
|
err = object.MfaRecover(user, authForm.RecoveryCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -890,7 +928,11 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
if authForm.ClientId == "" {
|
||||||
|
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||||
|
} else {
|
||||||
|
application, err = object.GetApplicationByClientId(authForm.ClientId)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
@ -920,6 +962,10 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if authForm.Provider == "" {
|
||||||
|
authForm.Provider = authForm.ProviderBack
|
||||||
|
}
|
||||||
|
|
||||||
user := c.getCurrentUser()
|
user := c.getCurrentUser()
|
||||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||||
|
|
||||||
|
@ -15,11 +15,15 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
||||||
@ -57,6 +61,11 @@ func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
|||||||
// @Success 200 {object} controllers.Response The Response object
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
// @router /run-casbin-command [get]
|
// @router /run-casbin-command [get]
|
||||||
func (c *ApiController) RunCasbinCommand() {
|
func (c *ApiController) RunCasbinCommand() {
|
||||||
|
if err := validateIdentifier(c); err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
language := c.Input().Get("language")
|
language := c.Input().Get("language")
|
||||||
argString := c.Input().Get("args")
|
argString := c.Input().Get("args")
|
||||||
|
|
||||||
@ -112,3 +121,58 @@ func (c *ApiController) RunCasbinCommand() {
|
|||||||
output = strings.TrimSuffix(output, "\n")
|
output = strings.TrimSuffix(output, "\n")
|
||||||
c.ResponseOk(output)
|
c.ResponseOk(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateIdentifier
|
||||||
|
// @Title validateIdentifier
|
||||||
|
// @Description Validate the request hash and timestamp
|
||||||
|
// @Param hash string The SHA-256 hash string
|
||||||
|
// @Return error Returns error if validation fails, nil if successful
|
||||||
|
func validateIdentifier(c *ApiController) error {
|
||||||
|
language := c.Input().Get("language")
|
||||||
|
args := c.Input().Get("args")
|
||||||
|
hash := c.Input().Get("m")
|
||||||
|
timestamp := c.Input().Get("t")
|
||||||
|
|
||||||
|
if hash == "" || timestamp == "" || language == "" || args == "" {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
timeDiff := time.Since(requestTime)
|
||||||
|
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]string{
|
||||||
|
"language": language,
|
||||||
|
"args": args,
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k := range params {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var paramParts []string
|
||||||
|
for _, k := range keys {
|
||||||
|
paramParts = append(paramParts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||||
|
}
|
||||||
|
paramString := strings.Join(paramParts, "&")
|
||||||
|
|
||||||
|
version := "casbin-editor-v1"
|
||||||
|
rawString := fmt.Sprintf("%s|%s|%s", version, timestamp, paramString)
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write([]byte(rawString))
|
||||||
|
|
||||||
|
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
||||||
|
if calculatedHash != strings.ToLower(hash) {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -70,15 +70,33 @@ func (c *ApiController) GetGroups() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
err = object.ExtendGroupsWithUsers(groups)
|
groupsHaveChildrenMap, err := object.GetGroupsHaveChildrenMap(groups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
_, ok := groupsHaveChildrenMap[group.Name]
|
||||||
|
if ok {
|
||||||
|
group.HaveChildren = true
|
||||||
}
|
}
|
||||||
|
|
||||||
c.ResponseOk(groups, paginator.Nums())
|
parent, ok := groupsHaveChildrenMap[group.ParentId]
|
||||||
|
if ok {
|
||||||
|
group.ParentName = parent.DisplayName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = object.ExtendGroupsWithUsers(groups)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ResponseOk(groups, paginator.Nums())
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +124,9 @@ func (c *ApiController) UpdateOrganization() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
isGlobalAdmin, _ := c.isGlobalAdmin()
|
||||||
|
|
||||||
|
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *RootController) HandleScim() {
|
func (c *RootController) HandleScim() {
|
||||||
|
_, ok := c.RequireAdmin()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path := c.Ctx.Request.URL.Path
|
path := c.Ctx.Request.URL.Path
|
||||||
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
||||||
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||||
|
@ -93,7 +93,7 @@ func (c *ApiController) SendEmail() {
|
|||||||
|
|
||||||
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
||||||
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
||||||
err = object.DailSmtpServer(provider)
|
err = object.TestSmtpServer(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -574,7 +574,11 @@ func (c *ApiController) SetPassword() {
|
|||||||
if user.Ldap == "" {
|
if user.Ldap == "" {
|
||||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
||||||
} else {
|
} else {
|
||||||
err = object.ResetLdapPassword(targetUser, newPassword, c.GetAcceptLanguage())
|
if isAdmin {
|
||||||
|
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())
|
||||||
|
} else {
|
||||||
|
err = object.ResetLdapPassword(targetUser, oldPassword, newPassword, c.GetAcceptLanguage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
15
form/auth.go
15
form/auth.go
@ -37,13 +37,14 @@ type AuthForm struct {
|
|||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
InvitationCode string `json:"invitationCode"`
|
InvitationCode string `json:"invitationCode"`
|
||||||
|
|
||||||
Application string `json:"application"`
|
Application string `json:"application"`
|
||||||
ClientId string `json:"clientId"`
|
ClientId string `json:"clientId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Code string `json:"code"`
|
ProviderBack string `json:"providerBack"`
|
||||||
State string `json:"state"`
|
Code string `json:"code"`
|
||||||
RedirectUri string `json:"redirectUri"`
|
State string `json:"state"`
|
||||||
Method string `json:"method"`
|
RedirectUri string `json:"redirectUri"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
|
||||||
EmailCode string `json:"emailCode"`
|
EmailCode string `json:"emailCode"`
|
||||||
PhoneCode string `json:"phoneCode"`
|
PhoneCode string `json:"phoneCode"`
|
||||||
|
@ -481,7 +481,10 @@ func GetApplicationByClientId(clientId string) (*Application, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetApplication(id string) (*Application, error) {
|
func GetApplication(id string) (*Application, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return getApplication(owner, name)
|
return getApplication(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,6 +241,10 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
|||||||
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
|
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if password == "" {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
|
||||||
|
}
|
||||||
|
|
||||||
passwordType := user.PasswordType
|
passwordType := user.PasswordType
|
||||||
if passwordType == "" {
|
if passwordType == "" {
|
||||||
passwordType = organization.PasswordType
|
passwordType = organization.PasswordType
|
||||||
|
@ -16,23 +16,18 @@
|
|||||||
|
|
||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import "github.com/casdoor/casdoor/email"
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/email"
|
// TestSmtpServer Test the SMTP server
|
||||||
"github.com/casdoor/gomail/v2"
|
func TestSmtpServer(provider *Provider) error {
|
||||||
)
|
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl)
|
||||||
|
sender, err := smtpEmailProvider.Dialer.Dial()
|
||||||
func getDialer(provider *Provider) *gomail.Dialer {
|
if err != nil {
|
||||||
dialer := &gomail.Dialer{}
|
return err
|
||||||
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
|
|
||||||
if provider.Type == "SUBMAIL" {
|
|
||||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
}
|
||||||
|
defer sender.Close()
|
||||||
|
|
||||||
dialer.SSL = !provider.DisableSsl
|
return nil
|
||||||
|
|
||||||
return dialer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||||
@ -50,16 +45,3 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
|
|||||||
|
|
||||||
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DailSmtpServer Dail Smtp server
|
|
||||||
func DailSmtpServer(provider *Provider) error {
|
|
||||||
dialer := getDialer(provider)
|
|
||||||
|
|
||||||
sender, err := dialer.Dial()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sender.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -17,7 +17,6 @@ package object
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
@ -36,12 +35,14 @@ type Group struct {
|
|||||||
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
||||||
Type string `xorm:"varchar(100)" json:"type"`
|
Type string `xorm:"varchar(100)" json:"type"`
|
||||||
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
||||||
|
ParentName string `xorm:"-" json:"parentName"`
|
||||||
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
||||||
Users []string `xorm:"-" json:"users"`
|
Users []string `xorm:"-" json:"users"`
|
||||||
|
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty"`
|
||||||
Children []*Group `json:"children,omitempty"`
|
HaveChildren bool `xorm:"-" json:"haveChildren"`
|
||||||
|
Children []*Group `json:"children,omitempty"`
|
||||||
|
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
}
|
}
|
||||||
@ -79,6 +80,26 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
|
|||||||
return groups, nil
|
return groups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
|
||||||
|
groupsHaveChildren := []*Group{}
|
||||||
|
resultMap := make(map[string]*Group)
|
||||||
|
|
||||||
|
groupIds := []string{}
|
||||||
|
for _, group := range groups {
|
||||||
|
groupIds = append(groupIds, group.Name)
|
||||||
|
groupIds = append(groupIds, group.ParentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, group := range groups {
|
||||||
|
resultMap[group.Name] = group
|
||||||
|
}
|
||||||
|
return resultMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getGroup(owner string, name string) (*Group, error) {
|
func getGroup(owner string, name string) (*Group, error) {
|
||||||
if owner == "" || name == "" {
|
if owner == "" || name == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -298,17 +319,11 @@ func ExtendGroupWithUsers(group *Group) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := GetUsers(group.Owner)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupId := group.GetId()
|
groupId := group.GetId()
|
||||||
userIds := []string{}
|
userIds := []string{}
|
||||||
for _, user := range users {
|
userIds, err := userEnforcer.GetAllUsersByGroup(groupId)
|
||||||
if util.InSlice(user.Groups, groupId) {
|
if err != nil {
|
||||||
userIds = append(userIds, user.GetId())
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.Users = userIds
|
group.Users = userIds
|
||||||
@ -316,29 +331,14 @@ func ExtendGroupWithUsers(group *Group) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExtendGroupsWithUsers(groups []*Group) error {
|
func ExtendGroupsWithUsers(groups []*Group) error {
|
||||||
var wg sync.WaitGroup
|
|
||||||
errChan := make(chan error, len(groups))
|
|
||||||
|
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
wg.Add(1)
|
users, err := userEnforcer.GetAllUsersByGroup(group.GetId())
|
||||||
go func(group *Group) {
|
|
||||||
defer wg.Done()
|
|
||||||
err := ExtendGroupWithUsers(group)
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(errChan)
|
|
||||||
|
|
||||||
for err := range errChan {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
group.Users = users
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ type Ldap struct {
|
|||||||
Filter string `xorm:"varchar(200)" json:"filter"`
|
Filter string `xorm:"varchar(200)" json:"filter"`
|
||||||
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
||||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||||
|
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||||
|
|
||||||
AutoSync int `json:"autoSync"`
|
AutoSync int `json:"autoSync"`
|
||||||
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
||||||
@ -149,7 +150,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
||||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group").Update(ldap)
|
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").Update(ldap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -373,7 +375,7 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
|
|||||||
return existUuids, nil
|
return existUuids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetLdapPassword(user *User, newPassword string, lang string) error {
|
func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang string) error {
|
||||||
ldaps, err := GetLdaps(user.Owner)
|
ldaps, err := GetLdaps(user.Owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -416,8 +418,32 @@ func ResetLdapPassword(user *User, newPassword string, lang string) error {
|
|||||||
}
|
}
|
||||||
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
||||||
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
|
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
|
||||||
|
} else if oldPassword != "" {
|
||||||
|
modifyPasswordRequestWithOldPassword := goldap.NewPasswordModifyRequest(userDn, oldPassword, newPassword)
|
||||||
|
_, err = conn.Conn.PasswordModify(modifyPasswordRequestWithOldPassword)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
} else {
|
} else {
|
||||||
pwdEncoded = newPassword
|
switch ldapServer.PasswordType {
|
||||||
|
case "SSHA":
|
||||||
|
pwdEncoded, err = generateSSHA(newPassword)
|
||||||
|
break
|
||||||
|
case "MD5":
|
||||||
|
md5Byte := md5.Sum([]byte(newPassword))
|
||||||
|
md5Password := base64.StdEncoding.EncodeToString(md5Byte[:])
|
||||||
|
pwdEncoded = "{MD5}" + md5Password
|
||||||
|
break
|
||||||
|
case "Plain":
|
||||||
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
}
|
||||||
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
object/ldap_password_type.go
Normal file
36
object/ldap_password_type.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2025 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.
|
||||||
|
|
||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSSHA(password string) (string, error) {
|
||||||
|
salt := make([]byte, 4)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := append([]byte(password), salt...)
|
||||||
|
hash := sha1.Sum(combined)
|
||||||
|
hashWithSalt := append(hash[:], salt...)
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(hashWithSalt)
|
||||||
|
|
||||||
|
return "{SSHA}" + encoded, nil
|
||||||
|
}
|
@ -79,6 +79,7 @@ type Organization struct {
|
|||||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||||
EnableTour bool `json:"enableTour"`
|
EnableTour bool `json:"enableTour"`
|
||||||
IpRestriction string `json:"ipRestriction"`
|
IpRestriction string `json:"ipRestriction"`
|
||||||
|
NavItems []string `xorm:"varchar(500)" json:"navItems"`
|
||||||
|
|
||||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||||
@ -151,7 +152,10 @@ func getOrganization(owner string, name string) (*Organization, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetOrganization(id string) (*Organization, error) {
|
func GetOrganization(id string) (*Organization, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return getOrganization(owner, name)
|
return getOrganization(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,9 +196,10 @@ func GetMaskedOrganizations(organizations []*Organization, errs ...error) ([]*Or
|
|||||||
return organizations, nil
|
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)
|
owner, name := util.GetOwnerAndNameFromId(id)
|
||||||
if org, err := getOrganization(owner, name); err != nil {
|
org, err := getOrganization(owner, name)
|
||||||
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if org == nil {
|
} else if org == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -219,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()
|
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||||
|
|
||||||
if organization.MasterPassword == "***" {
|
if organization.MasterPassword == "***" {
|
||||||
|
@ -123,8 +123,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
|||||||
|
|
||||||
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||||
switch tokenTypeHint {
|
switch tokenTypeHint {
|
||||||
case "access_token":
|
case "access_token", "access-token":
|
||||||
case "access-token":
|
|
||||||
token, err := GetTokenByAccessToken(tokenValue)
|
token, err := GetTokenByAccessToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -132,8 +131,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
|||||||
if token != nil {
|
if token != nil {
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
case "refresh_token":
|
case "refresh_token", "refresh-token":
|
||||||
case "refresh-token":
|
|
||||||
token, err := GetTokenByRefreshToken(tokenValue)
|
token, err := GetTokenByRefreshToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -146,13 +144,13 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
|||||||
return nil, nil
|
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)
|
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return affected != 0
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetToken(id string) (*Token, error) {
|
func GetToken(id string) (*Token, error) {
|
||||||
|
@ -248,7 +248,10 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
|||||||
|
|
||||||
token.CodeIsUsed = true
|
token.CodeIsUsed = true
|
||||||
|
|
||||||
go updateUsedByCode(token)
|
_, err = updateUsedByCode(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tokenWrapper := &TokenWrapper{
|
tokenWrapper := &TokenWrapper{
|
||||||
AccessToken: token.AccessToken,
|
AccessToken: token.AccessToken,
|
||||||
|
@ -965,6 +965,11 @@ func DeleteUser(user *User) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = userEnforcer.DeleteGroupsForUser(user.GetId())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
organization, err := GetOrganizationByUser(user)
|
organization, err := GetOrganizationByUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -80,6 +80,15 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAllowed, err := object.CheckLoginPermission(userId, application)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAllowed {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -133,7 +142,10 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
path += urlPath
|
path += urlPath
|
||||||
}
|
}
|
||||||
|
|
||||||
err := appendThemeCookie(ctx, urlPath)
|
// Preventing synchronization problems from concurrency
|
||||||
|
ctx.Input.CruSession = nil
|
||||||
|
|
||||||
|
organizationThemeCookie, err := appendThemeCookie(ctx, urlPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
@ -154,13 +166,13 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oldStaticBaseUrl == newStaticBaseUrl {
|
if oldStaticBaseUrl == newStaticBaseUrl {
|
||||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path)
|
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
} else {
|
} else {
|
||||||
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path)
|
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
f, err := os.Open(filepath.Clean(name))
|
f, err := os.Open(filepath.Clean(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -173,7 +185,13 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
oldContent := util.ReadStringFromPath(name)
|
oldContent := util.ReadStringFromPath(name)
|
||||||
newContent := strings.ReplaceAll(oldContent, oldStaticBaseUrl, newStaticBaseUrl)
|
newContent := oldContent
|
||||||
|
if organizationThemeCookie != nil {
|
||||||
|
newContent = strings.ReplaceAll(newContent, "https://cdn.casbin.org/img/favicon.png", organizationThemeCookie.Favicon)
|
||||||
|
newContent = strings.ReplaceAll(newContent, "<title>Casdoor</title>", fmt.Sprintf("<title>%s</title>", organizationThemeCookie.DisplayName))
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = strings.ReplaceAll(newContent, oldStaticBaseUrl, newStaticBaseUrl)
|
||||||
|
|
||||||
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
||||||
}
|
}
|
||||||
@ -187,14 +205,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
|||||||
return w.Writer.Write(b)
|
return w.Writer.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string) {
|
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
serveFileWithReplace(w, r, path)
|
serveFileWithReplace(w, r, path, organizationThemeCookie)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
gz := gzip.NewWriter(w)
|
gz := gzip.NewWriter(w)
|
||||||
defer gz.Close()
|
defer gz.Close()
|
||||||
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
serveFileWithReplace(gzw, r, path)
|
serveFileWithReplace(gzw, r, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
|
@ -23,79 +23,97 @@ import (
|
|||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
func appendThemeCookie(ctx *context.Context, urlPath string) error {
|
type OrganizationThemeCookie struct {
|
||||||
|
ThemeData *object.ThemeData
|
||||||
|
LogoUrl string
|
||||||
|
FooterHtml string
|
||||||
|
Favicon string
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendThemeCookie(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
|
||||||
|
organizationThemeCookie, err := getOrganizationThemeCookieFromUrlPath(ctx, urlPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if organizationThemeCookie != nil {
|
||||||
|
return organizationThemeCookie, setThemeDataCookie(ctx, organizationThemeCookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
|
||||||
|
var application *object.Application
|
||||||
|
var organization *object.Organization
|
||||||
|
var err error
|
||||||
if urlPath == "/login" {
|
if urlPath == "/login" {
|
||||||
application, err := object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
|
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
if application.ThemeData != nil {
|
|
||||||
return setThemeDataCookie(ctx, application.ThemeData, application.Logo, application.FooterHtml)
|
|
||||||
}
|
|
||||||
organization := application.OrganizationObj
|
|
||||||
if organization == nil {
|
|
||||||
organization, err = object.GetOrganization(fmt.Sprintf("admin/built-in"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if organization != nil {
|
|
||||||
return setThemeDataCookie(ctx, organization.ThemeData, organization.Logo, application.FooterHtml)
|
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(urlPath, "/login/oauth/authorize") {
|
} else if strings.HasPrefix(urlPath, "/login/oauth/authorize") {
|
||||||
clientId := ctx.Input.Query("client_id")
|
clientId := ctx.Input.Query("client_id")
|
||||||
if clientId == "" {
|
if clientId == "" {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
application, err := object.GetApplicationByClientId(clientId)
|
application, err = object.GetApplicationByClientId(clientId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if application != nil {
|
} else if strings.HasPrefix(urlPath, "/login/saml") {
|
||||||
organization, err := object.GetOrganization(fmt.Sprintf("admin/%s", application.Organization))
|
owner, _ := strings.CutPrefix(urlPath, "/login/saml/authorize/")
|
||||||
if err != nil {
|
application, err = object.GetApplication(owner)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
if application.ThemeData != nil {
|
|
||||||
return setThemeDataCookie(ctx, application.ThemeData, application.Logo, application.FooterHtml)
|
|
||||||
}
|
|
||||||
if organization != nil {
|
|
||||||
return setThemeDataCookie(ctx, organization.ThemeData, organization.Logo, application.FooterHtml)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(urlPath, "/login/") {
|
} else if strings.HasPrefix(urlPath, "/login/") {
|
||||||
owner := strings.Replace(urlPath, "/login/", "", -1)
|
owner, _ := strings.CutPrefix(urlPath, "/login/")
|
||||||
if owner != "undefined" && owner != "oauth/undefined" {
|
if owner == "undefined" || strings.Count(owner, "/") > 0 {
|
||||||
application, err := object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
|
return nil, nil
|
||||||
if err != nil {
|
}
|
||||||
return err
|
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
|
||||||
}
|
if err != nil {
|
||||||
if application.ThemeData != nil {
|
return nil, err
|
||||||
return setThemeDataCookie(ctx, application.ThemeData, application.Logo, application.FooterHtml)
|
}
|
||||||
}
|
} else if strings.HasPrefix(urlPath, "/cas/") && strings.HasSuffix(urlPath, "/login") {
|
||||||
organization := application.OrganizationObj
|
owner, _ := strings.CutPrefix(urlPath, "/cas/")
|
||||||
if organization == nil {
|
owner, _ = strings.CutSuffix(owner, "/login")
|
||||||
organization, err = object.GetOrganization(fmt.Sprintf("admin/%s", owner))
|
application, err = object.GetApplication(owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
}
|
|
||||||
if organization != nil {
|
|
||||||
return setThemeDataCookie(ctx, organization.ThemeData, organization.Logo, application.FooterHtml)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if application == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
organization = application.OrganizationObj
|
||||||
|
if organization == nil {
|
||||||
|
organization, err = object.GetOrganization(fmt.Sprintf("admin/%s", application.Organization))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
organizationThemeCookie := &OrganizationThemeCookie{
|
||||||
|
application.ThemeData,
|
||||||
|
application.Logo,
|
||||||
|
application.FooterHtml,
|
||||||
|
organization.Favicon,
|
||||||
|
organization.DisplayName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizationThemeCookie, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setThemeDataCookie(ctx *context.Context, themeData *object.ThemeData, logoUrl string, footerHtml string) error {
|
func setThemeDataCookie(ctx *context.Context, organizationThemeCookie *OrganizationThemeCookie) error {
|
||||||
themeDataString, err := json.Marshal(themeData)
|
themeDataString, err := json.Marshal(organizationThemeCookie.ThemeData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx.SetCookie("organizationTheme", string(themeDataString))
|
ctx.SetCookie("organizationTheme", string(themeDataString))
|
||||||
ctx.SetCookie("organizationLogo", logoUrl)
|
ctx.SetCookie("organizationLogo", organizationThemeCookie.LogoUrl)
|
||||||
ctx.SetCookie("organizationFootHtml", footerHtml)
|
ctx.SetCookie("organizationFootHtml", organizationThemeCookie.FooterHtml)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,6 @@ func IsValidOrigin(origin string) (bool, error) {
|
|||||||
originHostOnly = fmt.Sprintf("%s://%s", urlObj.Scheme, urlObj.Hostname())
|
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
|
return res, nil
|
||||||
}
|
}
|
||||||
|
@ -361,6 +361,14 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onLoginSuccess(redirectUrl) {
|
||||||
|
window.google?.accounts?.id?.cancel();
|
||||||
|
if (redirectUrl) {
|
||||||
|
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
||||||
|
}
|
||||||
|
this.getAccount();
|
||||||
|
}
|
||||||
|
|
||||||
renderPage() {
|
renderPage() {
|
||||||
if (this.isDoorPages()) {
|
if (this.isDoorPages()) {
|
||||||
let themeData = this.state.themeData;
|
let themeData = this.state.themeData;
|
||||||
@ -401,19 +409,13 @@ class App extends Component {
|
|||||||
application: application,
|
application: application,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onLoginSuccess={(redirectUrl) => {
|
onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
|
||||||
window.google?.accounts?.id?.cancel();
|
|
||||||
if (redirectUrl) {
|
|
||||||
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
|
||||||
}
|
|
||||||
this.getAccount();
|
|
||||||
}}
|
|
||||||
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
||||||
updataThemeData={this.setTheme}
|
updataThemeData={this.setTheme}
|
||||||
/> :
|
/> :
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/callback" component={AuthCallback} />
|
<Route exact path="/callback" render={(props) => <AuthCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||||
<Route exact path="/callback/saml" component={SamlCallback} />
|
<Route exact path="/callback/saml" render={(props) => <SamlCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||||
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
|
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
|
||||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
|
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -33,18 +33,6 @@ class GroupListPage extends BaseListPage {
|
|||||||
}
|
}
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
super.UNSAFE_componentWillMount();
|
super.UNSAFE_componentWillMount();
|
||||||
this.getGroups(this.state.owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
getGroups(organizationName) {
|
|
||||||
GroupBackend.getGroups(organizationName)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.status === "ok") {
|
|
||||||
this.setState({
|
|
||||||
groups: res.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newGroup() {
|
newGroup() {
|
||||||
@ -188,12 +176,8 @@ class GroupListPage extends BaseListPage {
|
|||||||
{record.parentId}
|
{record.parentId}
|
||||||
</Link>;
|
</Link>;
|
||||||
}
|
}
|
||||||
const parentGroup = this.state.groups.find((group) => group.name === text);
|
return <Link to={`/groups/${record.owner}/${record.parentId}`}>
|
||||||
if (parentGroup === undefined) {
|
{record?.parentName}
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return <Link to={`/groups/${parentGroup.owner}/${parentGroup.name}`}>
|
|
||||||
{parentGroup?.displayName}
|
|
||||||
</Link>;
|
</Link>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -215,12 +199,11 @@ class GroupListPage extends BaseListPage {
|
|||||||
width: "180px",
|
width: "180px",
|
||||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
const haveChildren = this.state.groups.find((group) => group.parentId === record.id) !== undefined;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||||
<PopconfirmModal
|
<PopconfirmModal
|
||||||
disabled={haveChildren}
|
disabled={record.haveChildren}
|
||||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||||
onConfirm={() => this.deleteGroup(index)}
|
onConfirm={() => this.deleteGroup(index)}
|
||||||
>
|
>
|
||||||
|
@ -228,6 +228,21 @@ class LdapEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Password type"), i18next.t("general:Password type - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={21}>
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.ldap.passwordType ?? []} onChange={(value => {
|
||||||
|
this.updateLdapField("passwordType", value);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Option key={"Plain"} value={"Plain"}>{i18next.t("general:Plain")}</Option>
|
||||||
|
<Option key={"SSHA"} value={"SSHA"} >SSHA</Option>
|
||||||
|
<Option key={"MD5"} value={"MD5"} >MD5</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||||
|
@ -241,7 +241,7 @@ function ManagementPage(props) {
|
|||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
||||||
</Link>,
|
</Link>,
|
||||||
disabled: true,
|
disabled: true, key: "logo",
|
||||||
style: {
|
style: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
height: "auto",
|
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) {
|
function renderLoginIfNotLoggedIn(component) {
|
||||||
|
@ -26,6 +26,7 @@ import LdapTable from "./table/LdapTable";
|
|||||||
import AccountTable from "./table/AccountTable";
|
import AccountTable from "./table/AccountTable";
|
||||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||||
import MfaTable from "./table/MfaTable";
|
import MfaTable from "./table/MfaTable";
|
||||||
|
import {NavItemTree} from "./common/NavItemTree";
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -522,6 +523,21 @@ class OrganizationEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Select, Tag, Tooltip, message, theme} from "antd";
|
import {Button, Select, Tag, Tooltip, message, theme} from "antd";
|
||||||
import {QuestionCircleTwoTone} from "@ant-design/icons";
|
import {QuestionCircleTwoTone} from "@ant-design/icons";
|
||||||
import {isMobile as isMobileDevice} from "react-device-detect";
|
import {isMobile as isMobileDevice} from "react-device-detect";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
@ -25,6 +25,8 @@ import {Helmet} from "react-helmet";
|
|||||||
import * as Conf from "./Conf";
|
import * as Conf from "./Conf";
|
||||||
import * as phoneNumber from "libphonenumber-js";
|
import * as phoneNumber from "libphonenumber-js";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./auth/mfa/MfaAuthVerifyForm";
|
||||||
|
import {EmailMfaType, SmsMfaType, TotpMfaType} from "./auth/MfaSetupPage";
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -1588,3 +1590,114 @@ export function getCurrencyText(product) {
|
|||||||
export function isDarkTheme(themeAlgorithm) {
|
export function isDarkTheme(themeAlgorithm) {
|
||||||
return themeAlgorithm && themeAlgorithm.includes("dark");
|
return themeAlgorithm && themeAlgorithm.includes("dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPreferredMfaProp(mfaProps) {
|
||||||
|
for (const i in mfaProps) {
|
||||||
|
if (mfaProps[i].isPreffered) {
|
||||||
|
return mfaProps[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mfaProps[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkLoginMfa(res, body, params, handleLogin, componentThis, requireRedirect = null) {
|
||||||
|
if (res.data === RequiredMfa) {
|
||||||
|
if (!requireRedirect) {
|
||||||
|
componentThis.props.onLoginSuccess(window.location.href);
|
||||||
|
} else {
|
||||||
|
componentThis.props.onLoginSuccess(requireRedirect);
|
||||||
|
}
|
||||||
|
} else if (res.data === NextMfa) {
|
||||||
|
componentThis.setState({
|
||||||
|
mfaProps: res.data2,
|
||||||
|
selectedMfaProp: getPreferredMfaProp(res.data2),
|
||||||
|
}, () => {
|
||||||
|
body["providerBack"] = body["provider"];
|
||||||
|
body["provider"] = "";
|
||||||
|
componentThis.setState({
|
||||||
|
getVerifyTotp: () => renderMfaAuthVerifyForm(body, params, handleLogin, componentThis),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (res.data === "SelectPlan") {
|
||||||
|
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
|
||||||
|
const pricing = res.data2;
|
||||||
|
goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${body.username}`);
|
||||||
|
} else if (res.data === "BuyPlanResult") {
|
||||||
|
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
|
||||||
|
const sub = res.data2;
|
||||||
|
goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
|
||||||
|
} else {
|
||||||
|
handleLogin(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApplicationObj(componentThis) {
|
||||||
|
return componentThis.props.application;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseOffset(offset) {
|
||||||
|
if (offset === 2 || offset === 4 || inIframe() || isMobile()) {
|
||||||
|
return "0 auto";
|
||||||
|
}
|
||||||
|
if (offset === 1) {
|
||||||
|
return "0 10%";
|
||||||
|
}
|
||||||
|
if (offset === 3) {
|
||||||
|
return "0 60%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMfaAuthVerifyForm(values, authParams, onSuccess, componentThis) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MfaAuthVerifyForm
|
||||||
|
mfaProps={componentThis.state.selectedMfaProp}
|
||||||
|
formValues={values}
|
||||||
|
authParams={authParams}
|
||||||
|
application={getApplicationObj(componentThis)}
|
||||||
|
onFail={(errorMessage) => {
|
||||||
|
showMessage("error", errorMessage);
|
||||||
|
}}
|
||||||
|
onSuccess={(res) => onSuccess(res)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
componentThis.state.mfaProps.map((mfa) => {
|
||||||
|
if (componentThis.state.selectedMfaProp.mfaType === mfa.mfaType) {return null;}
|
||||||
|
let mfaI18n = "";
|
||||||
|
switch (mfa.mfaType) {
|
||||||
|
case SmsMfaType: mfaI18n = i18next.t("mfa:Use SMS"); break;
|
||||||
|
case TotpMfaType: mfaI18n = i18next.t("mfa:Use Authenticator App"); break ;
|
||||||
|
case EmailMfaType: mfaI18n = i18next.t("mfa:Use Email") ;break;
|
||||||
|
}
|
||||||
|
return <div key={mfa.mfaType}><Button type={"link"} onClick={() => {
|
||||||
|
componentThis.setState({
|
||||||
|
selectedMfaProp: mfa,
|
||||||
|
});
|
||||||
|
}}>{mfaI18n}</Button></div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderLoginPanel(application, getInnerComponent, componentThis) {
|
||||||
|
return (
|
||||||
|
<div className="login-content" style={{margin: componentThis.props.preview ?? parseOffset(application.formOffset)}}>
|
||||||
|
{inIframe() || isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
||||||
|
{inIframe() || !isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
||||||
|
<div className={isDarkTheme(componentThis.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
|
||||||
|
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
|
||||||
|
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
|
||||||
|
</div>
|
||||||
|
<div className="login-form">
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
getInnerComponent()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import {authConfig} from "./Auth";
|
|||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import RedirectForm from "../common/RedirectForm";
|
import RedirectForm from "../common/RedirectForm";
|
||||||
|
import {renderLoginPanel} from "../Setting";
|
||||||
|
|
||||||
class AuthCallback extends React.Component {
|
class AuthCallback extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -131,19 +132,23 @@ class AuthCallback extends React.Component {
|
|||||||
// user is using casdoor as cas sso server, and wants the ticket to be acquired
|
// user is using casdoor as cas sso server, and wants the ticket to be acquired
|
||||||
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
|
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
let msg = "Logged in successfully.";
|
const handleCasLogin = (res) => {
|
||||||
if (casService === "") {
|
let msg = "Logged in successfully.";
|
||||||
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
|
if (casService === "") {
|
||||||
msg += "Now you can visit apps protected by Casdoor.";
|
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
|
||||||
}
|
msg += "Now you can visit apps protected by Casdoor.";
|
||||||
Setting.showMessage("success", msg);
|
}
|
||||||
|
Setting.showMessage("success", msg);
|
||||||
|
|
||||||
if (casService !== "") {
|
if (casService !== "") {
|
||||||
const st = res.data;
|
const st = res.data;
|
||||||
const newUrl = new URL(casService);
|
const newUrl = new URL(casService);
|
||||||
newUrl.searchParams.append("ticket", st);
|
newUrl.searchParams.append("ticket", st);
|
||||||
window.location.href = newUrl.toString();
|
window.location.href = newUrl.toString();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||||
}
|
}
|
||||||
@ -159,54 +164,58 @@ class AuthCallback extends React.Component {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const responseType = this.getResponseType();
|
const responseType = this.getResponseType();
|
||||||
if (responseType === "login") {
|
const handleLogin = (res) => {
|
||||||
if (res.data2) {
|
if (responseType === "login") {
|
||||||
sessionStorage.setItem("signinUrl", signinUrl);
|
if (res.data2) {
|
||||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Setting.showMessage("success", "Logged in successfully");
|
|
||||||
// Setting.goToLinkSoft(this, "/");
|
|
||||||
const link = Setting.getFromLink();
|
|
||||||
Setting.goToLink(link);
|
|
||||||
} else if (responseType === "code") {
|
|
||||||
if (res.data2) {
|
|
||||||
sessionStorage.setItem("signinUrl", signinUrl);
|
|
||||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const code = res.data;
|
|
||||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
|
||||||
// Setting.showMessage("success", `Authorization code: ${res.data}`);
|
|
||||||
} else if (responseType === "token" || responseType === "id_token") {
|
|
||||||
if (res.data2) {
|
|
||||||
sessionStorage.setItem("signinUrl", signinUrl);
|
|
||||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const token = res.data;
|
|
||||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
|
|
||||||
} else if (responseType === "link") {
|
|
||||||
const from = innerParams.get("from");
|
|
||||||
Setting.goToLinkSoftOrJumpSelf(this, from);
|
|
||||||
} else if (responseType === "saml") {
|
|
||||||
if (res.data2.method === "POST") {
|
|
||||||
this.setState({
|
|
||||||
samlResponse: res.data,
|
|
||||||
redirectUrl: res.data2.redirectUrl,
|
|
||||||
relayState: oAuthParams.relayState,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (res.data2.needUpdatePassword) {
|
|
||||||
sessionStorage.setItem("signinUrl", signinUrl);
|
sessionStorage.setItem("signinUrl", signinUrl);
|
||||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const SAMLResponse = res.data;
|
Setting.showMessage("success", "Logged in successfully");
|
||||||
const redirectUri = res.data2.redirectUrl;
|
// Setting.goToLinkSoft(this, "/");
|
||||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
const link = Setting.getFromLink();
|
||||||
|
Setting.goToLink(link);
|
||||||
|
} else if (responseType === "code") {
|
||||||
|
if (res.data2) {
|
||||||
|
sessionStorage.setItem("signinUrl", signinUrl);
|
||||||
|
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const code = res.data;
|
||||||
|
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||||
|
// Setting.showMessage("success", `Authorization code: ${res.data}`);
|
||||||
|
} else if (responseType === "token" || responseType === "id_token") {
|
||||||
|
if (res.data2) {
|
||||||
|
sessionStorage.setItem("signinUrl", signinUrl);
|
||||||
|
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = res.data;
|
||||||
|
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
|
||||||
|
} else if (responseType === "link") {
|
||||||
|
const from = innerParams.get("from");
|
||||||
|
Setting.goToLinkSoftOrJumpSelf(this, from);
|
||||||
|
} else if (responseType === "saml") {
|
||||||
|
if (res.data2.method === "POST") {
|
||||||
|
this.setState({
|
||||||
|
samlResponse: res.data,
|
||||||
|
redirectUrl: res.data2.redirectUrl,
|
||||||
|
relayState: oAuthParams.relayState,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (res.data2.needUpdatePassword) {
|
||||||
|
sessionStorage.setItem("signinUrl", signinUrl);
|
||||||
|
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const SAMLResponse = res.data;
|
||||||
|
const redirectUri = res.data2.redirectUrl;
|
||||||
|
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Setting.checkLoginMfa(res, body, oAuthParams, handleLogin, this, window.location.origin);
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
msg: res.msg,
|
msg: res.msg,
|
||||||
@ -220,6 +229,11 @@ class AuthCallback extends React.Component {
|
|||||||
return <RedirectForm samlResponse={this.state.samlResponse} redirectUrl={this.state.redirectUrl} relayState={this.state.relayState} />;
|
return <RedirectForm samlResponse={this.state.samlResponse} redirectUrl={this.state.redirectUrl} relayState={this.state.relayState} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.getVerifyTotp !== undefined) {
|
||||||
|
const application = Setting.getApplicationObj(this);
|
||||||
|
return renderLoginPanel(application, this.state.getVerifyTotp, this);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||||
{
|
{
|
||||||
|
@ -264,6 +264,9 @@ class ForgetPage extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onValuesChange={(changedValues, allValues) => {
|
onValuesChange={(changedValues, allValues) => {
|
||||||
|
if (!changedValues.dest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email";
|
const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email";
|
||||||
this.setState({
|
this.setState({
|
||||||
dest: changedValues.dest,
|
dest: changedValues.dest,
|
||||||
|
@ -34,7 +34,7 @@ import {SendCodeInput} from "../common/SendCodeInput";
|
|||||||
import LanguageSelect from "../common/select/LanguageSelect";
|
import LanguageSelect from "../common/select/LanguageSelect";
|
||||||
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
||||||
import RedirectForm from "../common/RedirectForm";
|
import RedirectForm from "../common/RedirectForm";
|
||||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||||
import * as ProviderButton from "./ProviderButton";
|
import * as ProviderButton from "./ProviderButton";
|
||||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||||
@ -438,25 +438,7 @@ class LoginPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
if (res.data === NextMfa) {
|
Setting.checkLoginMfa(res, values, casParams, loginHandler, this);
|
||||||
this.setState({
|
|
||||||
getVerifyTotp: () => {
|
|
||||||
return (
|
|
||||||
<MfaAuthVerifyForm
|
|
||||||
mfaProps={res.data2}
|
|
||||||
formValues={values}
|
|
||||||
authParams={casParams}
|
|
||||||
application={this.getApplicationObj()}
|
|
||||||
onFail={(errorMessage) => {
|
|
||||||
Setting.showMessage("error", errorMessage);
|
|
||||||
}}
|
|
||||||
onSuccess={(res) => loginHandler(res)}
|
|
||||||
/>);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
loginHandler(res);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||||
}
|
}
|
||||||
@ -511,33 +493,7 @@ class LoginPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
if (res.data === NextMfa) {
|
Setting.checkLoginMfa(res, values, oAuthParams, loginHandler, this);
|
||||||
this.setState({
|
|
||||||
getVerifyTotp: () => {
|
|
||||||
return (
|
|
||||||
<MfaAuthVerifyForm
|
|
||||||
mfaProps={res.data2}
|
|
||||||
formValues={values}
|
|
||||||
authParams={oAuthParams}
|
|
||||||
application={this.getApplicationObj()}
|
|
||||||
onFail={(errorMessage) => {
|
|
||||||
Setting.showMessage("error", errorMessage);
|
|
||||||
}}
|
|
||||||
onSuccess={(res) => loginHandler(res)}
|
|
||||||
/>);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (res.data === "SelectPlan") {
|
|
||||||
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
|
|
||||||
const pricing = res.data2;
|
|
||||||
Setting.goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${values.username}`);
|
|
||||||
} else if (res.data === "BuyPlanResult") {
|
|
||||||
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
|
|
||||||
const sub = res.data2;
|
|
||||||
Setting.goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
|
|
||||||
} else {
|
|
||||||
loginHandler(res);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ import KwaiLoginButton from "./KwaiLoginButton";
|
|||||||
import LoginButton from "./LoginButton";
|
import LoginButton from "./LoginButton";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
import {WechatOfficialAccountModal} from "./Util";
|
import {WechatOfficialAccountModal} from "./Util";
|
||||||
|
import * as Setting from "../Setting";
|
||||||
|
|
||||||
function getSigninButton(provider) {
|
function getSigninButton(provider) {
|
||||||
const text = i18next.t("login:Sign in with {type}").replace("{type}", provider.displayName !== "" ? provider.displayName : provider.type);
|
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}`;
|
const relayState = `${clientId}&${state}&${providerName}&${realRedirectUri}&${redirectUri}`;
|
||||||
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`, btoa(relayState)).then((res) => {
|
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`, btoa(relayState)).then((res) => {
|
||||||
if (res.data2 === "POST") {
|
if (res.status === "ok") {
|
||||||
document.write(res.data);
|
if (res.data2 === "POST") {
|
||||||
|
document.write(res.data);
|
||||||
|
} else {
|
||||||
|
window.location.href = res.data;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.location.href = res.data;
|
Setting.showMessage("error", res.msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import * as Util from "./Util";
|
|||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import {authConfig} from "./Auth";
|
import {authConfig} from "./Auth";
|
||||||
|
import {renderLoginPanel} from "../Setting";
|
||||||
|
|
||||||
class SamlCallback extends React.Component {
|
class SamlCallback extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -81,13 +82,26 @@ class SamlCallback extends React.Component {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const responseType = this.getResponseType(redirectUri);
|
const responseType = this.getResponseType(redirectUri);
|
||||||
if (responseType === "login") {
|
const handleLogin = (res2) => {
|
||||||
Setting.showMessage("success", "Logged in successfully");
|
if (responseType === "login") {
|
||||||
Setting.goToLink("/");
|
Setting.showMessage("success", "Logged in successfully");
|
||||||
} else if (responseType === "code") {
|
Setting.goToLink("/");
|
||||||
const code = res.data;
|
} else if (responseType === "code") {
|
||||||
Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
|
const code = res2.data;
|
||||||
}
|
Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Setting.checkLoginMfa(res, body, {
|
||||||
|
clientId: clientId,
|
||||||
|
responseType: responseType,
|
||||||
|
redirectUri: messages[3],
|
||||||
|
state: state,
|
||||||
|
nonce: "",
|
||||||
|
scope: "read",
|
||||||
|
challengeMethod: "",
|
||||||
|
codeChallenge: "",
|
||||||
|
type: "code",
|
||||||
|
}, handleLogin, this);
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
msg: res.msg,
|
msg: res.msg,
|
||||||
@ -97,6 +111,11 @@ class SamlCallback extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.state.getVerifyTotp !== undefined) {
|
||||||
|
const application = Setting.getApplicationObj(this);
|
||||||
|
return renderLoginPanel(application, this.state.getVerifyTotp, this, window.location.origin);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||||
{
|
{
|
||||||
|
@ -33,7 +33,8 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
|||||||
|
|
||||||
const verify = ({passcode}) => {
|
const verify = ({passcode}) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const values = {...formValues, passcode, mfaType};
|
const values = {...formValues, passcode};
|
||||||
|
values["mfaType"] = mfaProps.mfaType;
|
||||||
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
|
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
|
||||||
loginFunction(values, authParams).then((res) => {
|
loginFunction(values, authParams).then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
@ -71,7 +72,7 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
|||||||
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
||||||
{i18next.t("mfa:Multi-factor authentication")}
|
{i18next.t("mfa:Multi-factor authentication")}
|
||||||
</div>
|
</div>
|
||||||
{mfaType === SmsMfaType || mfaType === EmailMfaType ? (
|
{mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{marginBottom: 24}}>
|
<div style={{marginBottom: 24}}>
|
||||||
{i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")}
|
{i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")}
|
||||||
|
@ -9,11 +9,11 @@ export function MfaEnableForm({user, mfaType, secret, recoveryCodes, dest, count
|
|||||||
const data = {
|
const data = {
|
||||||
mfaType,
|
mfaType,
|
||||||
secret,
|
secret,
|
||||||
recoveryCodes,
|
|
||||||
dest,
|
dest,
|
||||||
countryCode,
|
countryCode,
|
||||||
...user,
|
...user,
|
||||||
};
|
};
|
||||||
|
data["recoveryCodes"] = recoveryCodes[0];
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
MfaBackend.MfaSetupEnable(data).then(res => {
|
MfaBackend.MfaSetupEnable(data).then(res => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
|
@ -27,7 +27,7 @@ export const generateCasdoorAppUrl = (accessToken, forQrCode = true) => {
|
|||||||
return {qrUrl, error};
|
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) {
|
if (forQrCode && qrUrl.length >= 2000) {
|
||||||
qrUrl = "";
|
qrUrl = "";
|
||||||
|
97
web/src/common/NavItemTree.js
Normal file
97
web/src/common/NavItemTree.js
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -105,7 +105,7 @@ export const PasswordModal = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasOldPassword = user.password !== "";
|
const hasOldPassword = (user.password !== "" || user.ldap !== "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
|
Reference in New Issue
Block a user