Compare commits

...

67 Commits

Author SHA1 Message Date
347b25676f feat: dark mode now works for login/signup pages too (#3252)
* fix: trying to fix dark mode not applying on login/registration interface

* fix: trying to fix dark mode not applying on login/registration interface

* fix: trying to fix dark mode not applying on login/registration interface

* fix: Clean up unused code

* fix: loginBackgroundDark move to App.less

* fix: fix typo
2024-10-05 21:26:25 +08:00
2417ff84e6 feat: support initial group assignment for new invited users via invitation.SignupGroup field (#3266) 2024-10-04 20:15:51 +08:00
468631e654 feat: support "All" in organization's country codes (#3264) 2024-10-03 22:58:09 +08:00
e1dea9f697 feat: add organization's PasswordObfuscator to obfuscate login API's password (#3260)
* feat: add PasswordObfuscator to the login API

* fix: change key error message

* fix: remove unnecessary change

* fix: fix one

* fix: fix two

* fix: fix three

* fix: fix five

* fix: disable organization update when key is invalid

* fix: fix six

* fix: use Form.Item to control key

* fix: update obfuscator.js

* Update obfuscator.go

* Update obfuscator.go

* Update auth.go

* fix: remove real-time key monitoring

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-10-03 10:38:37 +08:00
c0f22bae43 feat: better handling of organization.AccountItems on init_data import (#3263)
* Better handling of accountitems on init_data import.

* Removed commented code.

* Update init_data.go

* Update init_data.go

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-10-03 08:49:09 +08:00
c9635d9e2b feat: improve i18n (#3259) 2024-10-01 00:10:49 +08:00
3bd52172ea feat: add Hide-Password option for signin method rule field (#3258) 2024-09-30 23:31:41 +08:00
bf730050d5 feat: increase Organization.Favicon to 200 chars 2024-09-29 11:45:56 +08:00
5b733b7f15 feat: improve filterRecordIn24Hours() logic 2024-09-29 11:45:15 +08:00
034f28def9 feat: logout if app.conf's inactiveTimeoutMinutes is reached (#3244)
* feat: logout if there's no activities for a long time

* fix: change the implementation of updating LastTime

* fix: add logoutMinites to app.conf

* fix: change the implementation of judgment statement

* fix: use sync.Map to ensure thread safety

* fix: syntax standards and Apache headers

* fix: change the implementation of obtaining logoutMinutes in app.conf

* fix: follow community code standards

* fix: <=0 or empty means no restriction

* Update logout_filter.go

* Update app.conf

* Update main.go

* Update and rename logout_filter.go to timeout_filter.go

* Update app.conf

* Update timeout_filter.go

* fix: update app.conf

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-09-27 01:18:02 +08:00
c86ac8e6ad feat: fix UTF-8 charset for Alipay IdP (#3247) 2024-09-27 00:59:52 +08:00
d647eed22a feat: add OIDC WebFinger support (#3245)
* feat: add WebFinger support

* lint: used gofumpt

* oidc: ensure webfinger rel is checked
2024-09-26 13:06:36 +08:00
717c53f6e5 feat: support enableErrorMask2 config 2024-09-25 19:37:14 +08:00
097adac871 feat: support single-choice and multi-choices in signup page (#3234)
* feat: add custom signup field

* feat: support more field in signup page

* feat: support more field in signup page

* feat: support more field in signup page

* feat: Reduce code duplication in form item rendering

* feat: Simplify gender and info checks using includes

* feat: update translate

* Revert "feat: update translate"

This reverts commit 669334c716.

* feat: address feedback from hsluoyz
2024-09-25 12:48:37 +08:00
74543b9533 feat: improve QR code for casdoor-app (#3226)
* feat: simplify login url for casdoor-app

* feat: add token check

* fix: improve logic
2024-09-23 22:27:58 +08:00
110dc04179 feat: Revert "feat: fix permission problem in standard image" (#3231)
This reverts commit 6464bd10dc.
2024-09-23 22:19:27 +08:00
6464bd10dc feat: fix permission problem in standard image (#3228) 2024-09-23 18:40:39 +08:00
db878a890e feat: add type and options to signup items 2024-09-21 23:40:29 +08:00
12d6d8e6ce feat: fix cookie expire time too short bug 2024-09-21 22:45:13 +08:00
8ed6e4f934 feat: improve UI for "No account?" 2024-09-21 07:35:33 +08:00
ed9732caf9 feat: add condition for getWebBuildFolder function (#3219) 2024-09-20 23:59:13 +08:00
0de4e7da38 feat: fix organization pagination count error (#3215)
* fix(organization): ensure count includes shared organizations

Adjust the `GetOrganizationCount` function to account for shared organizations by adding
an additional parameter and modifying the count query accordingly. This change ensures that
the organization count correctly reflects shared organizations within the system.

* ```fix(organization): optimize GetOrganizationCount query

Refactor the GetOrganizationCount function to use a more efficient search
method by leveraging the 'is_shared' field directly in the query condition.
This change improves the performance for counting organizations by avoiding
unnecessary iteration over potentially large result sets.
```

---------

Co-authored-by: CuiJing <cuijing@tul.com.cn>
2024-09-20 23:58:46 +08:00
a330fbc11f docs: fix Docker link 2024-09-17 20:45:32 +08:00
ed158d4981 feat: support advanced editor in model edit page (#3176)
* feat: integrate external model editor and handle message events for model updates

* feat: add CasbinEditor and IframeEditor components for model editing

* feat: add tabbed editor interface for CasbinEditor

* fix: Synchronize content between basic and advanced editors

* refactor: simplify CasbinEditor and ModelEditPage components

* refactor: Refactor CasbinEditor for improved iframe initialization and model synchronization

* refactor: update default state of CasbinEditor active tab to "advanced

* chore: add Apache License header to CasbinEditor.js and IframeEditor.js files

* refactor: update CasbinEditor class names for consistency
2024-09-16 22:25:25 +08:00
8df965b98d feat: improve SAML XML's xmlns to fix SAML support for some clouds (#3207) 2024-09-16 08:01:28 +08:00
2c3749820e feat: add application.UseEmailAsSamlNameId field for SAML (#3203)
* feat: Add option to use email as SAML NameID based on application config

- Updated NewSamlResponse11 to accept an application parameter.
- Conditionally set SAML NameIdentifier to user's email or username based on application.UseEmailAsNameId.

* refactor: Update GetValidationBySaml to pass application to NewSamlResponse11

- Modified GetValidationBySaml function to include application parameter in NewSamlResponse11 call.

* feat: Rename field and update logic for using Email as SAML NameID

- Renamed the `UseEmailAsNameId` field to `UseEmailAsSamlNameId` in the `Application` struct.
- Updated `NewSamlResponse` and `NewSamlResponse11` functions to use `UseEmailAsSamlNameId` for setting the NameID value.
- Modified `ApplicationEditPage.js` to reflect the field name change and update the corresponding logic.
2024-09-15 23:00:50 +08:00
0b17cb9746 feat: make Organization.EnableSoftDeletion and User.IsDeleted work (#3205)
* feat: make Organization.EnableSoftDeletion and User.IsDeleted work

* fix: add handling of the situation where organization is nil
2024-09-15 14:35:44 +08:00
e2ce9ad625 feat: handle null account item issue in CheckPermissionForUpdateUser() (#3202)
* feat: improve the logic of the permission check code for users to modify account items

* fix: add skip operation for deleted account items in update-user API

* fix: add the function of removing deleted account item
2024-09-14 15:00:10 +08:00
64491abc64 feat: fix CORS issue of /api/acs for SAML IdP (#3200)
* fix: fix CORS problem of /api/acs when login with saml idp

* fix: fix origin get null when receive post with http protocol
2024-09-14 12:48:51 +08:00
934a8947c8 feat: fix CAS logout failure caused by Beego session update problem (#3194)
* feat: fix the cas logout failure caused by beego session update problem

* fix: simplify the implementation of logout timer

* fix: change the location of the login success code

* fix: add i18n to CasLogout.js
2024-09-10 21:31:37 +08:00
943edfb48b feat: support QR login for casdoor app (#3190)
* feat: add MFA devices QR code to UserEditPage

* chore: remove mfa devices
2024-09-08 22:38:13 +08:00
0d02b5e768 feat: remove disabled state in syncer.table 2024-09-07 21:08:21 +08:00
ba8d0b5f46 feat: Revert "feat: Users added through LDAP cannot log in using the set password" (#3186)
This reverts commit 973a1df6c2.
2024-09-07 20:55:14 +08:00
973a1df6c2 feat: Users added through LDAP cannot log in using the set password (#3175)
* fix: login will prioritize the use of password set in casdoor and use ldap when use LDAP option in login form or user never change their password in casdoor after sync

* fix: promote if statement
2024-09-06 10:31:34 +08:00
05bfd3a3a3 feat: fix bug that custom SAML providers are removed by GetMaskedApplication() (#3165) 2024-09-05 20:08:56 +08:00
69aa3c8a8b feat: Revert "feat: add Casbin editor's checking in model editor" (#3167)
This reverts commit a1b010a406.
2024-09-03 21:59:06 +08:00
a1b010a406 feat: add Casbin editor's checking in model editor (#3166)
* feat: add model syntax linting and update dependencies

* refactor: move model linter logic to separate module
2024-09-03 21:32:45 +08:00
89e92cbd47 feat: when using basic auth to fetch access_token will return restful response to oidc client (#3164) 2024-09-03 08:05:29 +08:00
d4c8193357 feat: support reCAPTCHA v3 captcha provider (#3160)
* feat: support reCAPTCHA v3 captcha provider

* fix: modify the implementation of row component style in CaptchaModal.js
2024-09-02 22:15:03 +08:00
9b33800b4c feat: add email_verified, phone_number and phone_number_verified field for standard jwt token (#3156)
* feat: add email_verified, phone_number and phone_number_verified field for standard jwt token

* fix: fix linter err
2024-08-31 12:49:39 +08:00
ec98785172 feat: certEditPage will be redirected to 404 when name is changed (#3154) 2024-08-30 23:04:50 +08:00
45dd4cc344 feat: fix nonce not parsed issue in fastAutoSignin() (#3153)
* fix: fix nonce none passed when auto sign enabled

* fix: fix query error
2024-08-30 22:29:23 +08:00
1adb172d6b feat: add more crypto algorithm for jwt signing (#3150)
* feat: add more algorithm support for JWT signing

* feat: add i18n support

* feat: add i18n support

* feat: optimize if statement

* fix: remove additional space line
2024-08-30 16:59:41 +08:00
c08f2b1f3f feat: support Casdoor storage provider (#3147)
* feat: support Casdoor storage provider

* fix: fix code format and nil pointer error

* feat: change cert if statement
2024-08-27 23:54:03 +08:00
62bb257c6d feat: make Resource.Url length to 500 2024-08-26 23:57:41 +08:00
230a77e3e3 feat: add captcha page (#3144) 2024-08-26 23:22:53 +08:00
dce0a96dea feat: improve uploaded file URL 2024-08-26 21:41:28 +08:00
65563fa0cd feat: Ensure MFA email and phone are validated before enabling (#3143)
Added validation checks to ensure that a user's email and phone number are provided before enabling MFA email and phone respectively. This fixes the issue where MFA could be enabled without these values, causing inconsistencies.
2024-08-26 08:40:22 +08:00
f2a94f671a feat: complete i18n translation (#3141)
* feat: complete i18n translation

* fix: fix problem in cs/data
2024-08-24 23:27:59 +08:00
1460a0498f feat: support assign a default group for synchronized from external openldap (#3140)
* feat: support default sync group for ldap (with without add i18n translate)

* feat: improve translation

* feat: update all i18n translation

* revert: remove new i18n translation
2024-08-24 00:12:52 +08:00
adc63ea726 feat: fix wrong error alert in ApiFilter's getObject() 2024-08-23 23:36:55 +08:00
0b8be016c5 feat: add enableErrorMask config 2024-08-23 22:19:17 +08:00
986dcbbda1 feat: handle error in ApiFilter 2024-08-23 21:50:48 +08:00
7d3920fb1f feat: add ManagedAccounts to JWT 2024-08-20 22:23:58 +08:00
b794ef87ee feat: Revert "feat: support reCAPTCHA v3 captcha provider" (#3135)
This reverts commit a0d6f2125e.
2024-08-20 17:56:53 +08:00
a0d6f2125e feat: support reCAPTCHA v3 captcha provider (#3130) 2024-08-20 17:29:37 +08:00
85cbb7d074 feat: add replaceAll polyfill to be compatible with Firefox 68 2024-08-17 18:37:21 +08:00
fdc1be9452 feat: add provider.Bucket to fileUrl response and TrimPrefix "/" before delete GCS object (#3129)
* feat: add provider.Bucket to fileUrl response

* feat: TrimPrefix "/" before Google Cloud Storage delete object
2024-08-17 11:46:58 +08:00
2bd7dabd33 feat: allow custom Domain of Google Cloud Storage Provider (#3128) 2024-08-15 23:28:36 +08:00
9b9a58e7ac feat: update casdoor/oss version to support Google Cloud's Application Default Credentials (#3125) 2024-08-15 13:45:27 +08:00
38e389e8c8 feat: Pagination not updating after last item deletion (#3120) 2024-08-13 16:09:16 +08:00
ab5fcf848e feat: support accessKey and accessSecret login in AutoSigninFilter (#3117) 2024-08-12 12:20:41 +08:00
b4e51b4631 feat: improve error message in GetFailedSigninConfigByUser() 2024-08-10 09:31:46 +08:00
45e25acc80 feat: fix JWT generate issue cause by shared application (#3113)
* fix: fix jwt generate cause by shared application

* fix: fix built-in org will not add -org-
2024-08-09 22:48:44 +08:00
97dcf24a91 feat: improve error message in GetAuthorizationCodeToken() 2024-08-09 21:06:23 +08:00
4c0fff66ff feat: support shared application across organizations (#3108)
* feat: support share application

* revert: revert i18n

* fix: improve code format

* fix: improve code format and move GetSharedOrgFromApp to string.go
2024-08-09 15:43:25 +08:00
e7230700e0 feat: Revert "feat: fix Beego session delete concurrent issue" (#3105)
This reverts commit f21aa9c0d2.
2024-08-07 16:51:54 +08:00
125 changed files with 2500 additions and 310 deletions

View File

@ -13,7 +13,7 @@
<a href="https://github.com/casdoor/casdoor/releases/latest"> <a href="https://github.com/casdoor/casdoor/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casdoor/casdoor.svg"> <img alt="GitHub Release" src="https://img.shields.io/github/v/release/casdoor/casdoor.svg">
</a> </a>
<a href="https://hub.docker.com/repository/docker/casbin/casdoor"> <a href="https://hub.docker.com/r/casbin/casdoor">
<img alt="Docker Image Version (latest semver)" src="https://img.shields.io/badge/Docker%20Hub-latest-brightgreen"> <img alt="Docker Image Version (latest semver)" src="https://img.shields.io/badge/Docker%20Hub-latest-brightgreen">
</a> </a>
</p> </p>

View File

@ -77,6 +77,7 @@ p, *, *, POST, /api/verify-code, *, *
p, *, *, POST, /api/reset-email-or-phone, *, * p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, * p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, * p, *, *, GET, /.well-known/openid-configuration, *, *
p, *, *, GET, /.well-known/webfinger, *, *
p, *, *, *, /.well-known/jwks, *, * p, *, *, *, /.well-known/jwks, *, *
p, *, *, GET, /api/get-saml-login, *, * p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, * p, *, *, POST, /api/acs, *, *

View File

@ -26,6 +26,10 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
return NewDefaultCaptchaProvider() return NewDefaultCaptchaProvider()
case "reCAPTCHA": case "reCAPTCHA":
return NewReCaptchaProvider() return NewReCaptchaProvider()
case "reCAPTCHA v2":
return NewReCaptchaProvider()
case "reCAPTCHA v3":
return NewReCaptchaProvider()
case "Aliyun Captcha": case "Aliyun Captcha":
return NewAliyunCaptchaProvider() return NewAliyunCaptchaProvider()
case "hCaptcha": case "hCaptcha":

View File

@ -21,7 +21,9 @@ originFrontend =
staticBaseUrl = "https://cdn.casbin.org" staticBaseUrl = "https://cdn.casbin.org"
isDemoMode = false isDemoMode = false
batchSize = 100 batchSize = 100
enableErrorMask = false
enableGzip = true enableGzip = true
inactiveTimeoutMinutes =
ldapServerPort = 389 ldapServerPort = 389
radiusServerPort = 1812 radiusServerPort = 1812
radiusSecret = "secret" radiusSecret = "secret"

View File

@ -200,6 +200,10 @@ func (c *ApiController) Signup() {
Type: userType, Type: userType,
Password: authForm.Password, Password: authForm.Password,
DisplayName: authForm.Name, DisplayName: authForm.Name,
Gender: authForm.Gender,
Bio: authForm.Bio,
Tag: authForm.Tag,
Education: authForm.Education,
Avatar: organization.DefaultAvatar, Avatar: organization.DefaultAvatar,
Email: authForm.Email, Email: authForm.Email,
Phone: authForm.Phone, Phone: authForm.Phone,
@ -234,6 +238,10 @@ func (c *ApiController) Signup() {
} }
} }
if invitation != nil && invitation.SignupGroup != "" {
user.Groups = []string{invitation.SignupGroup}
}
affected, err := object.AddUser(user) affected, err := object.AddUser(user)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())

View File

@ -463,6 +463,15 @@ func (c *ApiController) Login() {
} }
password := authForm.Password password := authForm.Password
if application.OrganizationObj != nil {
password, err = util.GetUnobfuscatedPassword(application.OrganizationObj.PasswordObfuscatorType, application.OrganizationObj.PasswordObfuscatorKey, authForm.Password)
if err != nil {
c.ResponseError(err.Error())
return
}
}
isSigninViaLdap := authForm.SigninMethod == "LDAP" isSigninViaLdap := authForm.SigninMethod == "LDAP"
var isPasswordWithLdapEnabled bool var isPasswordWithLdapEnabled bool
if authForm.SigninMethod == "Password" { if authForm.SigninMethod == "Password" {

View File

@ -60,7 +60,6 @@ func (c *ApiController) Unlink() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if application == nil { if application == nil {
c.ResponseError(c.T("link:You can't unlink yourself, you are not a member of any application")) c.ResponseError(c.T("link:You can't unlink yourself, you are not a member of any application"))
return return

View File

@ -14,7 +14,11 @@
package controllers package controllers
import "github.com/casdoor/casdoor/object" import (
"strings"
"github.com/casdoor/casdoor/object"
)
// GetOidcDiscovery // GetOidcDiscovery
// @Title GetOidcDiscovery // @Title GetOidcDiscovery
@ -42,3 +46,31 @@ func (c *RootController) GetJwks() {
c.Data["json"] = jwks c.Data["json"] = jwks
c.ServeJSON() c.ServeJSON()
} }
// GetWebFinger
// @Title GetWebFinger
// @Tag OIDC API
// @Param resource query string true "resource"
// @Success 200 {object} object.WebFinger
// @router /.well-known/webfinger [get]
func (c *RootController) GetWebFinger() {
resource := c.Input().Get("resource")
rels := []string{}
host := c.Ctx.Request.Host
for key, value := range c.Input() {
if strings.HasPrefix(key, "rel") {
rels = append(rels, value...)
}
}
webfinger, err := object.GetWebFinger(resource, rels, host)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = webfinger
c.Ctx.Output.ContentType("application/jrd+json")
c.ServeJSON()
}

View File

@ -65,7 +65,7 @@ func (c *ApiController) GetOrganizations() {
c.ResponseOk(organizations) c.ResponseOk(organizations)
} else { } else {
limit := util.ParseInt(limit) limit := util.ParseInt(limit)
count, err := object.GetOrganizationCount(owner, field, value) count, err := object.GetOrganizationCount(owner, organizationName, field, value)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -138,7 +138,7 @@ func (c *ApiController) AddOrganization() {
return return
} }
count, err := object.GetOrganizationCount("", "", "") count, err := object.GetOrganizationCount("", "", "", "")
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -257,7 +257,7 @@ func (c *ApiController) UploadResource() {
fileType, _ = util.GetOwnerAndNameFromIdNoCheck(mimeType + "/") fileType, _ = util.GetOwnerAndNameFromIdNoCheck(mimeType + "/")
} }
fullFilePath = object.GetTruncatedPath(provider, fullFilePath, 175) fullFilePath = object.GetTruncatedPath(provider, fullFilePath, 450)
if tag != "avatar" && tag != "termsOfUse" && !strings.HasPrefix(tag, "idCard") { if tag != "avatar" && tag != "termsOfUse" && !strings.HasPrefix(tag, "idCard") {
ext := filepath.Ext(filepath.Base(fullFilePath)) ext := filepath.Ext(filepath.Base(fullFilePath))
index := len(fullFilePath) - len(ext) index := len(fullFilePath) - len(ext)

View File

@ -289,6 +289,16 @@ func (c *ApiController) UpdateUser() {
} }
} }
if user.MfaEmailEnabled && user.Email == "" {
c.ResponseError(c.T("user:MFA email is enabled but email is empty"))
return
}
if user.MfaPhoneEnabled && user.Phone == "" {
c.ResponseError(c.T("user:MFA phone is enabled but phone number is empty"))
return
}
if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" { if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
return return
@ -400,6 +410,12 @@ func (c *ApiController) GetEmailAndPhone() {
organization := c.Ctx.Request.Form.Get("organization") organization := c.Ctx.Request.Form.Get("organization")
username := c.Ctx.Request.Form.Get("username") username := c.Ctx.Request.Form.Get("username")
enableErrorMask2 := conf.GetConfigBool("enableErrorMask2")
if enableErrorMask2 {
c.ResponseError("Error")
return
}
user, err := object.GetUserByFields(organization, username) user, err := object.GetUserByFields(organization, username)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())

View File

@ -45,6 +45,22 @@ func (c *ApiController) ResponseOk(data ...interface{}) {
// ResponseError ... // ResponseError ...
func (c *ApiController) ResponseError(error string, data ...interface{}) { func (c *ApiController) ResponseError(error string, data ...interface{}) {
enableErrorMask2 := conf.GetConfigBool("enableErrorMask2")
if enableErrorMask2 {
error = c.T("subscription:Error")
resp := &Response{Status: "error", Msg: error}
c.ResponseJsonData(resp, data...)
return
}
enableErrorMask := conf.GetConfigBool("enableErrorMask")
if enableErrorMask {
if strings.HasPrefix(error, "The user: ") && strings.HasSuffix(error, " doesn't exist") || strings.HasPrefix(error, "用户: ") && strings.HasSuffix(error, "不存在") {
error = c.T("check:password or code is incorrect")
}
}
resp := &Response{Status: "error", Msg: error} resp := &Response{Status: "error", Msg: error}
c.ResponseJsonData(resp, data...) c.ResponseJsonData(resp, data...)
} }

View File

@ -27,7 +27,18 @@ import (
) )
func deployStaticFiles(provider *object.Provider) { func deployStaticFiles(provider *object.Provider) {
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint) certificate := ""
if provider.Category == "Storage" && provider.Type == "Casdoor" {
cert, err := object.GetCert(util.GetId(provider.Owner, provider.Cert))
if err != nil {
panic(err)
}
if cert == nil {
panic(err)
}
certificate = cert.Certificate
}
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint, certificate, provider.Content)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -26,6 +26,10 @@ type AuthForm struct {
Name string `json:"name"` Name string `json:"name"`
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
LastName string `json:"lastName"` LastName string `json:"lastName"`
Gender string `json:"gender"`
Bio string `json:"bio"`
Tag string `json:"tag"`
Education string `json:"education"`
Email string `json:"email"` Email string `json:"email"`
Phone string `json:"phone"` Phone string `json:"phone"`
Affiliation string `json:"affiliation"` Affiliation string `json:"affiliation"`

6
go.mod
View File

@ -6,13 +6,13 @@ require (
github.com/Masterminds/squirrel v1.5.3 github.com/Masterminds/squirrel v1.5.3
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go v1.45.5 github.com/aws/aws-sdk-go v1.45.5
github.com/beego/beego v1.12.13 github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.77.2 github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.24.0 github.com/casdoor/go-sms-sender v0.24.0
github.com/casdoor/gomail/v2 v2.0.1 github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.45.0 github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.6.0 github.com/casdoor/oss v1.8.0
github.com/casdoor/xorm-adapter/v3 v3.1.0 github.com/casdoor/xorm-adapter/v3 v3.1.0
github.com/casvisor/casvisor-go-sdk v1.4.0 github.com/casvisor/casvisor-go-sdk v1.4.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
@ -30,7 +30,7 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.6.0 github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.4.0 github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/lestrrat-go/jwx v1.2.29 github.com/lestrrat-go/jwx v1.2.29
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9

13
go.sum
View File

@ -1052,8 +1052,8 @@ github.com/baidubce/bce-sdk-go v0.9.156 h1:f++WfptxGmSp5acsjl4kUxHpWDDccoFqkIrQK
github.com/baidubce/bce-sdk-go v0.9.156/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/baidubce/bce-sdk-go v0.9.156/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/beego/beego v1.12.13 h1:g39O1LGLTiPejWVqQKK/TFGrroW9BCZQz6/pf4S8IRM= github.com/beego/beego v1.12.12 h1:ARY1sNVSS23N0mEQIhSqRDTyyDlx95JY0V3GogBbZbQ=
github.com/beego/beego v1.12.13/go.mod h1:QURFL1HldOcCZAxnc1cZ7wrplsYR5dKPHFjmk6WkLAs= github.com/beego/beego v1.12.12/go.mod h1:QURFL1HldOcCZAxnc1cZ7wrplsYR5dKPHFjmk6WkLAs=
github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
@ -1083,6 +1083,8 @@ github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM= github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk= github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5TKnufWZM=
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs= github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc= github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.24.0 h1:LNLsce3EG/87I3JS6UiajF3LlQmdIiCgebEu0IE4wSM= github.com/casdoor/go-sms-sender v0.24.0 h1:LNLsce3EG/87I3JS6UiajF3LlQmdIiCgebEu0IE4wSM=
@ -1091,8 +1093,8 @@ github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q= github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk= github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ= github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.6.0 h1:IOWrGLJ+VO82qS796eaRnzFPPA1Sn3cotYTi7O/VIlQ= github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
github.com/casdoor/oss v1.6.0/go.mod h1:rJAWA0hLhtu94t6IRpotLUkXO1NWMASirywQYaGizJE= github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk= github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM= github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM=
github.com/casvisor/casvisor-go-sdk v1.4.0 h1:hbZEGGJ1cwdHFAxeXrMoNw6yha6Oyg2F0qQhBNCN/dg= github.com/casvisor/casvisor-go-sdk v1.4.0 h1:hbZEGGJ1cwdHFAxeXrMoNw6yha6Oyg2F0qQhBNCN/dg=
@ -1460,8 +1462,9 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=

View File

@ -200,7 +200,7 @@ func (idp *AlipayIdProvider) postWithBody(body interface{}, targetUrl string) ([
formData.Set("sign", sign) formData.Set("sign", sign)
resp, err := idp.Client.PostForm(targetUrl, formData) resp, err := idp.Client.Post(targetUrl, "application/x-www-form-urlencoded;charset=utf-8", strings.NewReader(formData.Encode()))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -56,6 +56,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.TimeoutFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage) beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
@ -71,6 +72,7 @@ func main() {
beego.BConfig.WebConfig.Session.SessionProviderConfig = conf.GetConfigString("redisEndpoint") beego.BConfig.WebConfig.Session.SessionProviderConfig = conf.GetConfigString("redisEndpoint")
} }
beego.BConfig.WebConfig.Session.SessionCookieLifeTime = 3600 * 24 * 30 beego.BConfig.WebConfig.Session.SessionCookieLifeTime = 3600 * 24 * 30
beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 30
// beego.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteNoneMode // beego.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteNoneMode
err := logs.SetLogger(logs.AdapterFile, conf.GetConfigString("logConfig")) err := logs.SetLogger(logs.AdapterFile, conf.GetConfigString("logConfig"))

View File

@ -31,15 +31,17 @@ type SigninMethod struct {
} }
type SignupItem struct { type SignupItem struct {
Name string `json:"name"` Name string `json:"name"`
Visible bool `json:"visible"` Visible bool `json:"visible"`
Required bool `json:"required"` Required bool `json:"required"`
Prompted bool `json:"prompted"` Prompted bool `json:"prompted"`
CustomCss string `json:"customCss"` Type string `json:"type"`
Label string `json:"label"` CustomCss string `json:"customCss"`
Placeholder string `json:"placeholder"` Label string `json:"label"`
Regex string `json:"regex"` Placeholder string `json:"placeholder"`
Rule string `json:"rule"` Options []string `json:"options"`
Regex string `json:"regex"`
Rule string `json:"rule"`
} }
type SigninItem struct { type SigninItem struct {
@ -78,24 +80,27 @@ type Application struct {
EnableSamlCompress bool `json:"enableSamlCompress"` EnableSamlCompress bool `json:"enableSamlCompress"`
EnableSamlC14n10 bool `json:"enableSamlC14n10"` EnableSamlC14n10 bool `json:"enableSamlC14n10"`
EnableSamlPostBinding bool `json:"enableSamlPostBinding"` EnableSamlPostBinding bool `json:"enableSamlPostBinding"`
UseEmailAsSamlNameId bool `json:"useEmailAsSamlNameId"`
EnableWebAuthn bool `json:"enableWebAuthn"` EnableWebAuthn bool `json:"enableWebAuthn"`
EnableLinkWithEmail bool `json:"enableLinkWithEmail"` EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
OrgChoiceMode string `json:"orgChoiceMode"` OrgChoiceMode string `json:"orgChoiceMode"`
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"` SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"` SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(3000)" json:"signupItems"`
SigninItems []*SigninItem `xorm:"mediumtext" json:"signinItems"` SigninItems []*SigninItem `xorm:"mediumtext" json:"signinItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
CertPublicKey string `xorm:"-" json:"certPublicKey"` CertPublicKey string `xorm:"-" json:"certPublicKey"`
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"` SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
IsShared bool `json:"isShared"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"` RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"` TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"` TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
ExpireInHours int `json:"expireInHours"` ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"` RefreshExpireInHours int `json:"refreshExpireInHours"`
@ -123,9 +128,9 @@ func GetApplicationCount(owner, field, value string) (int64, error) {
return session.Count(&Application{}) return session.Count(&Application{})
} }
func GetOrganizationApplicationCount(owner, Organization, field, value string) (int64, error) { func GetOrganizationApplicationCount(owner, organization, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Application{Organization: Organization}) return session.Where("organization = ? or is_shared = ? ", organization, true).Count(&Application{})
} }
func GetApplications(owner string) ([]*Application, error) { func GetApplications(owner string) ([]*Application, error) {
@ -140,7 +145,7 @@ func GetApplications(owner string) ([]*Application, error) {
func GetOrganizationApplications(owner string, organization string) ([]*Application, error) { func GetOrganizationApplications(owner string, organization string) ([]*Application, error) {
applications := []*Application{} applications := []*Application{}
err := ormer.Engine.Desc("created_time").Find(&applications, &Application{Organization: organization}) err := ormer.Engine.Desc("created_time").Where("organization = ? or is_shared = ? ", organization, true).Find(&applications, &Application{})
if err != nil { if err != nil {
return applications, err return applications, err
} }
@ -162,7 +167,7 @@ func GetPaginationApplications(owner string, offset, limit int, field, value, so
func GetPaginationOrganizationApplications(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) ([]*Application, error) { func GetPaginationOrganizationApplications(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) ([]*Application, error) {
applications := []*Application{} applications := []*Application{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder) session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&applications, &Application{Organization: organization}) err := session.Where("organization = ? or is_shared = ? ", organization, true).Find(&applications, &Application{})
if err != nil { if err != nil {
return applications, err return applications, err
} }
@ -337,12 +342,18 @@ func getApplication(owner string, name string) (*Application, error) {
return nil, nil return nil, nil
} }
application := Application{Owner: owner, Name: name} realApplicationName, sharedOrg := util.GetSharedOrgFromApp(name)
application := Application{Owner: owner, Name: realApplicationName}
existed, err := ormer.Engine.Get(&application) existed, err := ormer.Engine.Get(&application)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if application.IsShared && sharedOrg != "" {
application.Organization = sharedOrg
}
if existed { if existed {
err = extendApplicationWithProviders(&application) err = extendApplicationWithProviders(&application)
if err != nil { if err != nil {
@ -428,11 +439,18 @@ func GetApplicationByUserId(userId string) (application *Application, err error)
func GetApplicationByClientId(clientId string) (*Application, error) { func GetApplicationByClientId(clientId string) (*Application, error) {
application := Application{} application := Application{}
existed, err := ormer.Engine.Where("client_id=?", clientId).Get(&application)
realClientId, sharedOrg := util.GetSharedOrgFromApp(clientId)
existed, err := ormer.Engine.Where("client_id=?", realClientId).Get(&application)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if application.IsShared && sharedOrg != "" {
application.Organization = sharedOrg
}
if existed { if existed {
err = extendApplicationWithProviders(&application) err = extendApplicationWithProviders(&application)
if err != nil { if err != nil {
@ -516,7 +534,7 @@ func GetMaskedApplication(application *Application, userId string) *Application
providerItems := []*ProviderItem{} providerItems := []*ProviderItem{}
for _, providerItem := range application.Providers { for _, providerItem := range application.Providers {
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha") { if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML") {
providerItems = append(providerItems, providerItem) providerItems = append(providerItems, providerItem)
} }
} }
@ -626,6 +644,10 @@ func UpdateApplication(id string, application *Application) (bool, error) {
return false, err return false, err
} }
if application.IsShared == true && application.Organization != "built-in" {
return false, fmt.Errorf("only applications belonging to built-in organization can be shared")
}
for _, providerItem := range application.Providers { for _, providerItem := range application.Providers {
providerItem.Provider = nil providerItem.Provider = nil
} }

View File

@ -52,6 +52,9 @@ func GetFailedSigninConfigByUser(user *User) (int, int, error) {
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
if application == nil {
return 0, 0, fmt.Errorf("the application for user %s is not found", user.GetId())
}
failedSigninLimit := application.FailedSigninLimit failedSigninLimit := application.FailedSigninLimit
if failedSigninLimit == 0 { if failedSigninLimit == 0 {

View File

@ -182,6 +182,9 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
if organization.Tags == nil { if organization.Tags == nil {
organization.Tags = []string{} organization.Tags = []string{}
} }
if organization.AccountItems == nil {
organization.AccountItems = []*AccountItem{}
}
} }
for _, application := range data.Applications { for _, application := range data.Applications {
if application.Providers == nil { if application.Providers == nil {
@ -275,7 +278,9 @@ func initDefinedOrganization(organization *Organization) {
} }
} }
organization.CreatedTime = util.GetCurrentTime() organization.CreatedTime = util.GetCurrentTime()
organization.AccountItems = getBuiltInAccountItems() if len(organization.AccountItems) == 0 {
organization.AccountItems = getBuiltInAccountItems()
}
_, err = AddOrganization(organization) _, err = AddOrganization(organization)
if err != nil { if err != nil {

View File

@ -32,6 +32,7 @@ type Ldap struct {
BaseDn string `xorm:"varchar(100)" json:"baseDn"` BaseDn string `xorm:"varchar(100)" json:"baseDn"`
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"`
AutoSync int `json:"autoSync"` AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"` LastSync string `xorm:"varchar(100)" json:"lastSync"`
@ -148,7 +149,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").Update(ldap) "port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group").Update(ldap)
if err != nil { if err != nil {
return false, nil return false, nil
} }

View File

@ -339,6 +339,10 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
Ldap: syncUser.Uuid, Ldap: syncUser.Uuid,
} }
if ldap.DefaultGroup != "" {
newUser.Groups = []string{ldap.DefaultGroup}
}
affected, err := AddUser(newUser) affected, err := AddUser(newUser)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -44,6 +44,18 @@ type OidcDiscovery struct {
EndSessionEndpoint string `json:"end_session_endpoint"` EndSessionEndpoint string `json:"end_session_endpoint"`
} }
type WebFinger struct {
Subject string `json:"subject"`
Links []WebFingerLink `json:"links"`
Aliases *[]string `json:"aliases,omitempty"`
Properties *map[string]string `json:"properties,omitempty"`
}
type WebFingerLink struct {
Rel string `json:"rel"`
Href string `json:"href"`
}
func isIpAddress(host string) bool { func isIpAddress(host string) bool {
// Attempt to split the host and port, ignoring the error // Attempt to split the host and port, ignoring the error
hostWithoutPort, _, err := net.SplitHostPort(host) hostWithoutPort, _, err := net.SplitHostPort(host)
@ -112,7 +124,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
ResponseModesSupported: []string{"query", "fragment", "login", "code", "link"}, ResponseModesSupported: []string{"query", "fragment", "login", "code", "link"},
GrantTypesSupported: []string{"password", "authorization_code"}, GrantTypesSupported: []string{"password", "authorization_code"},
SubjectTypesSupported: []string{"public"}, SubjectTypesSupported: []string{"public"},
IdTokenSigningAlgValuesSupported: []string{"RS256"}, IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},
ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"}, ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"},
ClaimsSupported: []string{"iss", "ver", "sub", "aud", "iat", "exp", "id", "type", "displayName", "avatar", "permanentAvatar", "email", "phone", "location", "affiliation", "title", "homepage", "bio", "tag", "region", "language", "score", "ranking", "isOnline", "isAdmin", "isForbidden", "signupApplication", "ldap"}, ClaimsSupported: []string{"iss", "ver", "sub", "aud", "iat", "exp", "id", "type", "displayName", "avatar", "permanentAvatar", "email", "phone", "location", "affiliation", "title", "homepage", "bio", "tag", "region", "language", "score", "ranking", "isOnline", "isAdmin", "isForbidden", "signupApplication", "ldap"},
RequestParameterSupported: true, RequestParameterSupported: true,
@ -160,3 +172,43 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
return jwks, nil return jwks, nil
} }
func GetWebFinger(resource string, rels []string, host string) (WebFinger, error) {
wf := WebFinger{}
resourceSplit := strings.Split(resource, ":")
if len(resourceSplit) != 2 {
return wf, fmt.Errorf("invalid resource")
}
resourceType := resourceSplit[0]
resourceValue := resourceSplit[1]
oidcDiscovery := GetOidcDiscovery(host)
switch resourceType {
case "acct":
user, err := GetUserByEmailOnly(resourceValue)
if err != nil {
return wf, err
}
if user == nil {
return wf, fmt.Errorf("user not found")
}
wf.Subject = resource
for _, rel := range rels {
if rel == "http://openid.net/specs/connect/1.0/issuer" {
wf.Links = append(wf.Links, WebFingerLink{
Rel: "http://openid.net/specs/connect/1.0/issuer",
Href: oidcDiscovery.Issuer,
})
}
}
}
return wf, nil
}

View File

@ -56,10 +56,12 @@ type Organization struct {
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"` WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Logo string `xorm:"varchar(200)" json:"logo"` Logo string `xorm:"varchar(200)" json:"logo"`
LogoDark string `xorm:"varchar(200)" json:"logoDark"` LogoDark string `xorm:"varchar(200)" json:"logoDark"`
Favicon string `xorm:"varchar(100)" json:"favicon"` Favicon string `xorm:"varchar(200)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"` PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"` PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"` PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"` CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"` DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"` DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
@ -79,9 +81,9 @@ type Organization struct {
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"` AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
} }
func GetOrganizationCount(owner, field, value string) (int64, error) { func GetOrganizationCount(owner, name, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Organization{}) return session.Count(&Organization{Name: name})
} }
func GetOrganizations(owner string, name ...string) ([]*Organization, error) { func GetOrganizations(owner string, name ...string) ([]*Organization, error) {
@ -319,6 +321,7 @@ func GetDefaultApplication(id string) (*Application, error) {
if defaultApplication == nil { if defaultApplication == nil {
return nil, fmt.Errorf("The default application: %s does not exist", organization.DefaultApplication) return nil, fmt.Errorf("The default application: %s does not exist", organization.DefaultApplication)
} else { } else {
defaultApplication.Organization = organization.Name
return defaultApplication, nil return defaultApplication, nil
} }
} }

View File

@ -36,7 +36,7 @@ type Resource struct {
FileType string `xorm:"varchar(100)" json:"fileType"` FileType string `xorm:"varchar(100)" json:"fileType"`
FileFormat string `xorm:"varchar(100)" json:"fileFormat"` FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
FileSize int `json:"fileSize"` FileSize int `json:"fileSize"`
Url string `xorm:"varchar(255)" json:"url"` Url string `xorm:"varchar(500)" json:"url"`
Description string `xorm:"varchar(255)" json:"description"` Description string `xorm:"varchar(255)" json:"description"`
} }

View File

@ -65,7 +65,11 @@ func NewSamlResponse(application *Application, user *User, host string, certific
assertion.CreateAttr("IssueInstant", now) assertion.CreateAttr("IssueInstant", now)
assertion.CreateElement("saml:Issuer").SetText(host) assertion.CreateElement("saml:Issuer").SetText(host)
subject := assertion.CreateElement("saml:Subject") subject := assertion.CreateElement("saml:Subject")
subject.CreateElement("saml:NameID").SetText(user.Name) nameIDValue := user.Name
if application.UseEmailAsSamlNameId {
nameIDValue = user.Email
}
subject.CreateElement("saml:NameID").SetText(nameIDValue)
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation") subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer") subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer")
subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData") subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData")
@ -184,17 +188,17 @@ type NameIDFormat struct {
} }
type SingleSignOnService struct { type SingleSignOnService struct {
XMLName xml.Name // XMLName xml.Name
Binding string `xml:"Binding,attr"` Binding string `xml:"Binding,attr"`
Location string `xml:"Location,attr"` Location string `xml:"Location,attr"`
} }
type Attribute struct { type Attribute struct {
// XMLName xml.Name // XMLName xml.Name
Xmlns string `xml:"xmlns,attr"`
Name string `xml:"Name,attr"` Name string `xml:"Name,attr"`
NameFormat string `xml:"NameFormat,attr"` NameFormat string `xml:"NameFormat,attr"`
FriendlyName string `xml:"FriendlyName,attr"` FriendlyName string `xml:"FriendlyName,attr"`
Xmlns string `xml:"xmlns,attr"`
Values []string `xml:"AttributeValue"` Values []string `xml:"AttributeValue"`
} }
@ -386,7 +390,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
} }
// NewSamlResponse11 return a saml1.1 response(not 2.0) // NewSamlResponse11 return a saml1.1 response(not 2.0)
func NewSamlResponse11(user *User, requestID string, host string) (*etree.Element, error) { func NewSamlResponse11(application *Application, user *User, requestID string, host string) (*etree.Element, error) {
samlResponse := &etree.Element{ samlResponse := &etree.Element{
Space: "samlp", Space: "samlp",
Tag: "Response", Tag: "Response",
@ -430,7 +434,11 @@ func NewSamlResponse11(user *User, requestID string, host string) (*etree.Elemen
// nameIdentifier inside subject // nameIdentifier inside subject
nameIdentifier := subject.CreateElement("saml:NameIdentifier") nameIdentifier := subject.CreateElement("saml:NameIdentifier")
// nameIdentifier.CreateAttr("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress") // nameIdentifier.CreateAttr("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
nameIdentifier.SetText(user.Name) if application.UseEmailAsSamlNameId {
nameIdentifier.SetText(user.Email)
} else {
nameIdentifier.SetText(user.Name)
}
// subjectConfirmation inside subject // subjectConfirmation inside subject
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation") subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
@ -439,7 +447,11 @@ func NewSamlResponse11(user *User, requestID string, host string) (*etree.Elemen
attributeStatement := assertion.CreateElement("saml:AttributeStatement") attributeStatement := assertion.CreateElement("saml:AttributeStatement")
subjectInAttribute := attributeStatement.CreateElement("saml:Subject") subjectInAttribute := attributeStatement.CreateElement("saml:Subject")
nameIdentifierInAttribute := subjectInAttribute.CreateElement("saml:NameIdentifier") nameIdentifierInAttribute := subjectInAttribute.CreateElement("saml:NameIdentifier")
nameIdentifierInAttribute.SetText(user.Name) if application.UseEmailAsSamlNameId {
nameIdentifierInAttribute.SetText(user.Email)
} else {
nameIdentifierInAttribute.SetText(user.Name)
}
subjectConfirmationInAttribute := subjectInAttribute.CreateElement("saml:SubjectConfirmation") subjectConfirmationInAttribute := subjectInAttribute.CreateElement("saml:SubjectConfirmation")
subjectConfirmationInAttribute.CreateElement("saml:ConfirmationMethod").SetText("urn:oasis:names:tc:SAML:1.0:cm:artifact") subjectConfirmationInAttribute.CreateElement("saml:ConfirmationMethod").SetText("urn:oasis:names:tc:SAML:1.0:cm:artifact")

View File

@ -30,6 +30,13 @@ import (
var isCloudIntranet bool var isCloudIntranet bool
const (
ProviderTypeGoogleCloudStorage = "Google Cloud Storage"
ProviderTypeTencentCloudCOS = "Tencent Cloud COS"
ProviderTypeAzureBlob = "Azure Blob"
ProviderTypeLocalFileSystem = "Local File System"
)
func init() { func init() {
isCloudIntranet = conf.GetConfigBool("isCloudIntranet") isCloudIntranet = conf.GetConfigBool("isCloudIntranet")
} }
@ -80,27 +87,28 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath) objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
host := "" host := ""
if provider.Type != "Local File System" { if provider.Type != ProviderTypeLocalFileSystem {
// provider.Domain = "https://cdn.casbin.com/casdoor/" // provider.Domain = "https://cdn.casbin.com/casdoor/"
host = util.GetUrlHost(provider.Domain) host = util.GetUrlHost(provider.Domain)
} else { } else {
// provider.Domain = "http://localhost:8000" or "https://door.casdoor.com" // provider.Domain = "http://localhost:8000" or "https://door.casdoor.com"
host = util.UrlJoin(provider.Domain, "/files") host = util.UrlJoin(provider.Domain, "/files")
} }
if provider.Type == "Azure Blob" { if provider.Type == ProviderTypeAzureBlob || provider.Type == ProviderTypeGoogleCloudStorage {
host = util.UrlJoin(host, provider.Bucket) host = util.UrlJoin(host, provider.Bucket)
} }
fileUrl := "" fileUrl := ""
if host != "" { if host != "" {
fileUrl = util.UrlJoin(host, escapePath(objectKey)) // fileUrl = util.UrlJoin(host, escapePath(objectKey))
fileUrl = util.UrlJoin(host, objectKey)
} }
if fileUrl != "" && hasTimestamp { // if fileUrl != "" && hasTimestamp {
fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime()) // fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime())
} // }
if provider.Type == "Tencent Cloud COS" { if provider.Type == ProviderTypeTencentCloudCOS {
objectKey = escapePath(objectKey) objectKey = escapePath(objectKey)
} }
@ -109,7 +117,18 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface, error) { func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface, error) {
endpoint := getProviderEndpoint(provider) endpoint := getProviderEndpoint(provider)
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint) certificate := ""
if provider.Category == "Storage" && provider.Type == "Casdoor" {
cert, err := GetCert(util.GetId(provider.Owner, provider.Cert))
if err != nil {
return nil, err
}
if cert == nil {
return nil, fmt.Errorf("no cert for %s", provider.Cert)
}
certificate = cert.Certificate
}
storageProvider, err := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint, certificate, provider.Content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -135,17 +154,17 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
} }
fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true) fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true)
objectKeyRefined := refineObjectKey(provider, objectKey)
objectKeyRefined := objectKey object, err := storageProvider.Put(objectKeyRefined, fileBuffer)
if provider.Type == "Google Cloud Storage" {
objectKeyRefined = strings.TrimPrefix(objectKeyRefined, "/")
}
_, err = storageProvider.Put(objectKeyRefined, fileBuffer)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
if provider.Type == "Casdoor" {
fileUrl = object.Path
}
return fileUrl, objectKey, nil return fileUrl, objectKey, nil
} }
@ -184,5 +203,13 @@ func DeleteFile(provider *Provider, objectKey string, lang string) error {
return err return err
} }
return storageProvider.Delete(objectKey) objectKeyRefined := refineObjectKey(provider, objectKey)
return storageProvider.Delete(objectKeyRefined)
}
func refineObjectKey(provider *Provider, objectKey string) string {
if provider.Type == ProviderTypeGoogleCloudStorage {
return strings.TrimPrefix(objectKey, "/")
}
return objectKey
} }

View File

@ -277,12 +277,11 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
if application == nil { if application == nil {
return "", "", fmt.Errorf("the application for user %s is not found", userId) return "", "", fmt.Errorf("the application for user %s is not found", userId)
} }
samlResponse, err := NewSamlResponse11(user, request.RequestID, host) samlResponse, err := NewSamlResponse11(application, user, request.RequestID, host)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }

View File

@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"time" "time"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -128,7 +129,7 @@ type UserWithoutThirdIdp struct {
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"` SigninWrongTimes int `json:"signinWrongTimes"`
// ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
} }
type ClaimsShort struct { type ClaimsShort struct {
@ -254,6 +255,8 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
LastSigninWrongTime: user.LastSigninWrongTime, LastSigninWrongTime: user.LastSigninWrongTime,
SigninWrongTimes: user.SigninWrongTimes, SigninWrongTimes: user.SigninWrongTimes,
ManagedAccounts: user.ManagedAccounts,
} }
return res return res
@ -365,6 +368,10 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
}, },
} }
if application.IsShared {
claims.Audience = []string{application.ClientId + "-org-" + user.Owner}
}
var token *jwt.Token var token *jwt.Token
var refreshToken *jwt.Token var refreshToken *jwt.Token
@ -372,36 +379,52 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
application.TokenFormat = "JWT" application.TokenFormat = "JWT"
} }
var jwtMethod jwt.SigningMethod
if application.TokenSigningMethod == "RS256" {
jwtMethod = jwt.SigningMethodRS256
} else if application.TokenSigningMethod == "RS512" {
jwtMethod = jwt.SigningMethodRS512
} else if application.TokenSigningMethod == "ES256" {
jwtMethod = jwt.SigningMethodES256
} else if application.TokenSigningMethod == "ES512" {
jwtMethod = jwt.SigningMethodES512
} else if application.TokenSigningMethod == "ES384" {
jwtMethod = jwt.SigningMethodES384
} else {
jwtMethod = jwt.SigningMethodRS256
}
// the JWT token length in "JWT-Empty" mode will be very short, as User object only has two properties: owner and name // the JWT token length in "JWT-Empty" mode will be very short, as User object only has two properties: owner and name
if application.TokenFormat == "JWT" { if application.TokenFormat == "JWT" {
claimsWithoutThirdIdp := getClaimsWithoutThirdIdp(claims) claimsWithoutThirdIdp := getClaimsWithoutThirdIdp(claims)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp) token = jwt.NewWithClaims(jwtMethod, claimsWithoutThirdIdp)
claimsWithoutThirdIdp.ExpiresAt = jwt.NewNumericDate(refreshExpireTime) claimsWithoutThirdIdp.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claimsWithoutThirdIdp.TokenType = "refresh-token" claimsWithoutThirdIdp.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithoutThirdIdp) refreshToken = jwt.NewWithClaims(jwtMethod, claimsWithoutThirdIdp)
} else if application.TokenFormat == "JWT-Empty" { } else if application.TokenFormat == "JWT-Empty" {
claimsShort := getShortClaims(claims) claimsShort := getShortClaims(claims)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort) token = jwt.NewWithClaims(jwtMethod, claimsShort)
claimsShort.ExpiresAt = jwt.NewNumericDate(refreshExpireTime) claimsShort.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claimsShort.TokenType = "refresh-token" claimsShort.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsShort) refreshToken = jwt.NewWithClaims(jwtMethod, claimsShort)
} else if application.TokenFormat == "JWT-Custom" { } else if application.TokenFormat == "JWT-Custom" {
claimsCustom := getClaimsCustom(claims, application.TokenFields) claimsCustom := getClaimsCustom(claims, application.TokenFields)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsCustom) token = jwt.NewWithClaims(jwtMethod, claimsCustom)
refreshClaims := getClaimsCustom(claims, application.TokenFields) refreshClaims := getClaimsCustom(claims, application.TokenFields)
refreshClaims["exp"] = jwt.NewNumericDate(refreshExpireTime) refreshClaims["exp"] = jwt.NewNumericDate(refreshExpireTime)
refreshClaims["TokenType"] = "refresh-token" refreshClaims["TokenType"] = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, refreshClaims) refreshToken = jwt.NewWithClaims(jwtMethod, refreshClaims)
} else if application.TokenFormat == "JWT-Standard" { } else if application.TokenFormat == "JWT-Standard" {
claimsStandard := getStandardClaims(claims) claimsStandard := getStandardClaims(claims)
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsStandard) token = jwt.NewWithClaims(jwtMethod, claimsStandard)
claimsStandard.ExpiresAt = jwt.NewNumericDate(refreshExpireTime) claimsStandard.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
claimsStandard.TokenType = "refresh-token" claimsStandard.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsStandard) refreshToken = jwt.NewWithClaims(jwtMethod, claimsStandard)
} else { } else {
return "", "", "", fmt.Errorf("unknown application TokenFormat: %s", application.TokenFormat) return "", "", "", fmt.Errorf("unknown application TokenFormat: %s", application.TokenFormat)
} }
@ -419,34 +442,57 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
} }
} }
// RSA private key var (
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey)) tokenString string
refreshTokenString string
key interface{}
)
if strings.Contains(application.TokenSigningMethod, "RS") || application.TokenSigningMethod == "" {
// RSA private key
key, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(cert.PrivateKey))
} else if strings.Contains(application.TokenSigningMethod, "ES") {
// ES private key
key, err = jwt.ParseECPrivateKeyFromPEM([]byte(cert.PrivateKey))
} else if strings.Contains(application.TokenSigningMethod, "Ed") {
// Ed private key
key, err = jwt.ParseEdPrivateKeyFromPEM([]byte(cert.PrivateKey))
}
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
token.Header["kid"] = cert.Name token.Header["kid"] = cert.Name
tokenString, err := token.SignedString(key) tokenString, err = token.SignedString(key)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
refreshTokenString, err := refreshToken.SignedString(key) refreshTokenString, err = refreshToken.SignedString(key)
return tokenString, refreshTokenString, name, err return tokenString, refreshTokenString, name, err
} }
func ParseJwtToken(token string, cert *Cert) (*Claims, error) { func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { var (
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) certificate interface{}
} err error
)
if cert.Certificate == "" { if cert.Certificate == "" {
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert) return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
} }
// RSA certificate if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate)) // RSA certificate
certificate, err = jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
} else if _, ok := token.Method.(*jwt.SigningMethodECDSA); ok {
// ES certificate
certificate, err = jwt.ParseECPublicKeyFromPEM([]byte(cert.Certificate))
} else {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -428,22 +428,26 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
if token == nil { if token == nil {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "authorization code is invalid", ErrorDescription: fmt.Sprintf("authorization code: [%s] is invalid", code),
}, nil }, nil
} }
if token.CodeIsUsed { if token.CodeIsUsed {
// anti replay attacks // anti replay attacks
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "authorization code has been used", ErrorDescription: fmt.Sprintf("authorization code has been used for token: [%s]", token.GetId()),
}, nil }, nil
} }
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge { if token.CodeChallenge != "" {
return nil, &TokenError{ challengeAnswer := pkceChallenge(verifier)
Error: InvalidGrant, if challengeAnswer != token.CodeChallenge {
ErrorDescription: "verifier is invalid", return nil, &TokenError{
}, nil Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("verifier is invalid, challengeAnswer: [%s], token.CodeChallenge: [%s]", challengeAnswer, token.CodeChallenge),
}, nil
}
} }
if application.ClientSecret != clientSecret { if application.ClientSecret != clientSecret {
@ -452,13 +456,13 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
if token.CodeChallenge == "" { if token.CodeChallenge == "" {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidClient, Error: InvalidClient,
ErrorDescription: "client_secret is invalid", ErrorDescription: fmt.Sprintf("client_secret is invalid for application: [%s], token.CodeChallenge: empty", application.GetId()),
}, nil }, nil
} else { } else {
if clientSecret != "" { if clientSecret != "" {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidClient, Error: InvalidClient,
ErrorDescription: "client_secret is invalid", ErrorDescription: fmt.Sprintf("client_secret is invalid for application: [%s], token.CodeChallenge: [%s]", application.GetId(), token.CodeChallenge),
}, nil }, nil
} }
} }
@ -467,15 +471,16 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
if application.Name != token.Application { if application.Name != token.Application {
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "the token is for wrong application (client_id)", ErrorDescription: fmt.Sprintf("the token is for wrong application (client_id), application.Name: [%s], token.Application: [%s]", application.Name, token.Application),
}, nil }, nil
} }
if time.Now().Unix() > token.CodeExpireIn { nowUnix := time.Now().Unix()
if nowUnix > token.CodeExpireIn {
// code must be used within 5 minutes // code must be used within 5 minutes
return nil, &TokenError{ return nil, &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: "authorization code has expired", ErrorDescription: fmt.Sprintf("authorization code has expired, nowUnix: [%s], token.CodeExpireIn: [%s]", time.Unix(nowUnix, 0).Format(time.RFC3339), time.Unix(token.CodeExpireIn, 0).Format(time.RFC3339)),
}, nil }, nil
} }
return token, nil, nil return token, nil, nil

View File

@ -18,16 +18,20 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/casdoor/casdoor/util"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
type ClaimsStandard struct { type ClaimsStandard struct {
*UserShort *UserShort
Gender string `json:"gender,omitempty"` EmailVerified bool `json:"email_verified,omitempty"`
TokenType string `json:"tokenType,omitempty"` PhoneNumber string `json:"phone_number,omitempty"`
Nonce string `json:"nonce,omitempty"` PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
Scope string `json:"scope,omitempty"` Gender string `json:"gender,omitempty"`
Address OIDCAddress `json:"address,omitempty"` TokenType string `json:"tokenType,omitempty"`
Nonce string `json:"nonce,omitempty"`
Scope string `json:"scope,omitempty"`
Address OIDCAddress `json:"address,omitempty"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -43,12 +47,14 @@ func getStreetAddress(user *User) string {
func getStandardClaims(claims Claims) ClaimsStandard { func getStandardClaims(claims Claims) ClaimsStandard {
res := ClaimsStandard{ res := ClaimsStandard{
UserShort: getShortUser(claims.User), UserShort: getShortUser(claims.User),
EmailVerified: claims.User.EmailVerified,
TokenType: claims.TokenType, TokenType: claims.TokenType,
Nonce: claims.Nonce, Nonce: claims.Nonce,
Scope: claims.Scope, Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims, RegisteredClaims: claims.RegisteredClaims,
} }
res.Phone = ""
var scopes []string var scopes []string
if strings.Contains(claims.Scope, ",") { if strings.Contains(claims.Scope, ",") {
@ -62,6 +68,15 @@ func getStandardClaims(claims Claims) ClaimsStandard {
res.Address = OIDCAddress{StreetAddress: getStreetAddress(claims.User)} res.Address = OIDCAddress{StreetAddress: getStreetAddress(claims.User)}
} else if scope == "profile" { } else if scope == "profile" {
res.Gender = claims.User.Gender res.Gender = claims.User.Gender
} else if scope == "phone" && claims.User.Phone != "" {
res.PhoneNumberVerified = true
phoneNumber, ok := util.GetE164Number(claims.User.Phone, claims.User.CountryCode)
if !ok {
res.PhoneNumberVerified = false
} else {
res.PhoneNumber = phoneNumber
}
} }
} }

View File

@ -950,7 +950,17 @@ func DeleteUser(user *User) (bool, error) {
return false, err return false, err
} }
return deleteUser(user) organization, err := GetOrganizationByUser(user)
if err != nil {
return false, err
}
if organization != nil && organization.EnableSoftDeletion {
user.IsDeleted = true
user.DeletedTime = util.GetCurrentTime()
return UpdateUser(user.GetId(), user, []string{"is_deleted", "deleted_time"}, false)
} else {
return deleteUser(user)
}
} }
func GetUserInfo(user *User, scope string, aud string, host string) (*Userinfo, error) { func GetUserInfo(user *User, scope string, aud string, host string) (*Userinfo, error) {
@ -1138,7 +1148,7 @@ func (user *User) IsApplicationAdmin(application *Application) bool {
return false return false
} }
return (user.Owner == application.Organization && user.IsAdmin) || user.IsGlobalAdmin() return (user.Owner == application.Organization && user.IsAdmin) || user.IsGlobalAdmin() || (user.IsAdmin && application.IsShared)
} }
func (user *User) IsGlobalAdmin() bool { func (user *User) IsGlobalAdmin() bool {

View File

@ -271,113 +271,213 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
if oldUser.Owner != newUser.Owner { if oldUser.Owner != newUser.Owner {
item := GetAccountItemByName("Organization", organization) item := GetAccountItemByName("Organization", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Owner = oldUser.Owner
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Name != newUser.Name { if oldUser.Name != newUser.Name {
item := GetAccountItemByName("Name", organization) item := GetAccountItemByName("Name", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Name = oldUser.Name
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Id != newUser.Id { if oldUser.Id != newUser.Id {
item := GetAccountItemByName("ID", organization) item := GetAccountItemByName("ID", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Id = oldUser.Id
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.DisplayName != newUser.DisplayName { if oldUser.DisplayName != newUser.DisplayName {
item := GetAccountItemByName("Display name", organization) item := GetAccountItemByName("Display name", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.DisplayName = oldUser.DisplayName
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Avatar != newUser.Avatar { if oldUser.Avatar != newUser.Avatar {
item := GetAccountItemByName("Avatar", organization) item := GetAccountItemByName("Avatar", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Avatar = oldUser.Avatar
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Type != newUser.Type { if oldUser.Type != newUser.Type {
item := GetAccountItemByName("User type", organization) item := GetAccountItemByName("User type", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Type = oldUser.Type
} else {
itemsChanged = append(itemsChanged, item)
}
} }
// The password is *** when not modified // The password is *** when not modified
if oldUser.Password != newUser.Password && newUser.Password != "***" { if oldUser.Password != newUser.Password && newUser.Password != "***" {
item := GetAccountItemByName("Password", organization) item := GetAccountItemByName("Password", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Password = oldUser.Password
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Email != newUser.Email { if oldUser.Email != newUser.Email {
item := GetAccountItemByName("Email", organization) item := GetAccountItemByName("Email", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Email = oldUser.Email
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Phone != newUser.Phone { if oldUser.Phone != newUser.Phone {
item := GetAccountItemByName("Phone", organization) item := GetAccountItemByName("Phone", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Phone = oldUser.Phone
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.CountryCode != newUser.CountryCode { if oldUser.CountryCode != newUser.CountryCode {
item := GetAccountItemByName("Country code", organization) item := GetAccountItemByName("Country code", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.CountryCode = oldUser.CountryCode
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Region != newUser.Region { if oldUser.Region != newUser.Region {
item := GetAccountItemByName("Country/Region", organization) item := GetAccountItemByName("Country/Region", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Region = oldUser.Region
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Location != newUser.Location { if oldUser.Location != newUser.Location {
item := GetAccountItemByName("Location", organization) item := GetAccountItemByName("Location", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Location = oldUser.Location
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Affiliation != newUser.Affiliation { if oldUser.Affiliation != newUser.Affiliation {
item := GetAccountItemByName("Affiliation", organization) item := GetAccountItemByName("Affiliation", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Affiliation = oldUser.Affiliation
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Title != newUser.Title { if oldUser.Title != newUser.Title {
item := GetAccountItemByName("Title", organization) item := GetAccountItemByName("Title", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Title = oldUser.Title
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Homepage != newUser.Homepage { if oldUser.Homepage != newUser.Homepage {
item := GetAccountItemByName("Homepage", organization) item := GetAccountItemByName("Homepage", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Homepage = oldUser.Homepage
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Bio != newUser.Bio { if oldUser.Bio != newUser.Bio {
item := GetAccountItemByName("Bio", organization) item := GetAccountItemByName("Bio", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Bio = oldUser.Bio
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Tag != newUser.Tag { if oldUser.Tag != newUser.Tag {
item := GetAccountItemByName("Tag", organization) item := GetAccountItemByName("Tag", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Tag = oldUser.Tag
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.SignupApplication != newUser.SignupApplication { if oldUser.SignupApplication != newUser.SignupApplication {
item := GetAccountItemByName("Signup application", organization) item := GetAccountItemByName("Signup application", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.SignupApplication = oldUser.SignupApplication
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Gender != newUser.Gender { if oldUser.Gender != newUser.Gender {
item := GetAccountItemByName("Gender", organization) item := GetAccountItemByName("Gender", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Gender = oldUser.Gender
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Birthday != newUser.Birthday { if oldUser.Birthday != newUser.Birthday {
item := GetAccountItemByName("Birthday", organization) item := GetAccountItemByName("Birthday", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Birthday = oldUser.Birthday
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Education != newUser.Education { if oldUser.Education != newUser.Education {
item := GetAccountItemByName("Education", organization) item := GetAccountItemByName("Education", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Education = oldUser.Education
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.IdCard != newUser.IdCard { if oldUser.IdCard != newUser.IdCard {
item := GetAccountItemByName("ID card", organization) item := GetAccountItemByName("ID card", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.IdCard = oldUser.IdCard
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.IdCardType != newUser.IdCardType { if oldUser.IdCardType != newUser.IdCardType {
item := GetAccountItemByName("ID card type", organization) item := GetAccountItemByName("ID card type", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.IdCardType = oldUser.IdCardType
} else {
itemsChanged = append(itemsChanged, item)
}
} }
oldUserPropertiesJson, _ := json.Marshal(oldUser.Properties) oldUserPropertiesJson, _ := json.Marshal(oldUser.Properties)
newUserPropertiesJson, _ := json.Marshal(newUser.Properties) newUserPropertiesJson, _ := json.Marshal(newUser.Properties)
if string(oldUserPropertiesJson) != string(newUserPropertiesJson) { if string(oldUserPropertiesJson) != string(newUserPropertiesJson) {
item := GetAccountItemByName("Properties", organization) item := GetAccountItemByName("Properties", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Properties = oldUser.Properties
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.PreferredMfaType != newUser.PreferredMfaType { if oldUser.PreferredMfaType != newUser.PreferredMfaType {
item := GetAccountItemByName("Multi-factor authentication", organization) item := GetAccountItemByName("Multi-factor authentication", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.PreferredMfaType = oldUser.PreferredMfaType
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Groups == nil { if oldUser.Groups == nil {
@ -390,7 +490,11 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
newUserGroupsJson, _ := json.Marshal(newUser.Groups) newUserGroupsJson, _ := json.Marshal(newUser.Groups)
if string(oldUserGroupsJson) != string(newUserGroupsJson) { if string(oldUserGroupsJson) != string(newUserGroupsJson) {
item := GetAccountItemByName("Groups", organization) item := GetAccountItemByName("Groups", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Groups = oldUser.Groups
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Address == nil { if oldUser.Address == nil {
@ -404,65 +508,117 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
newUserAddressJson, _ := json.Marshal(newUser.Address) newUserAddressJson, _ := json.Marshal(newUser.Address)
if string(oldUserAddressJson) != string(newUserAddressJson) { if string(oldUserAddressJson) != string(newUserAddressJson) {
item := GetAccountItemByName("Address", organization) item := GetAccountItemByName("Address", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Address = oldUser.Address
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if newUser.FaceIds != nil { if newUser.FaceIds != nil {
item := GetAccountItemByName("Face ID", organization) item := GetAccountItemByName("Face ID", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.FaceIds = oldUser.FaceIds
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.IsAdmin != newUser.IsAdmin { if oldUser.IsAdmin != newUser.IsAdmin {
item := GetAccountItemByName("Is admin", organization) item := GetAccountItemByName("Is admin", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.IsAdmin = oldUser.IsAdmin
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.IsForbidden != newUser.IsForbidden { if oldUser.IsForbidden != newUser.IsForbidden {
item := GetAccountItemByName("Is forbidden", organization) item := GetAccountItemByName("Is forbidden", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.IsForbidden = oldUser.IsForbidden
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.IsDeleted != newUser.IsDeleted { if oldUser.IsDeleted != newUser.IsDeleted {
item := GetAccountItemByName("Is deleted", organization) item := GetAccountItemByName("Is deleted", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.IsDeleted = oldUser.IsDeleted
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.NeedUpdatePassword != newUser.NeedUpdatePassword { if oldUser.NeedUpdatePassword != newUser.NeedUpdatePassword {
item := GetAccountItemByName("Need update password", organization) item := GetAccountItemByName("Need update password", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.NeedUpdatePassword = oldUser.NeedUpdatePassword
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Balance != newUser.Balance { if oldUser.Balance != newUser.Balance {
item := GetAccountItemByName("Balance", organization) item := GetAccountItemByName("Balance", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Balance = oldUser.Balance
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Score != newUser.Score { if oldUser.Score != newUser.Score {
item := GetAccountItemByName("Score", organization) item := GetAccountItemByName("Score", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Score = oldUser.Score
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Karma != newUser.Karma { if oldUser.Karma != newUser.Karma {
item := GetAccountItemByName("Karma", organization) item := GetAccountItemByName("Karma", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Karma = oldUser.Karma
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Language != newUser.Language { if oldUser.Language != newUser.Language {
item := GetAccountItemByName("Language", organization) item := GetAccountItemByName("Language", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Language = oldUser.Language
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Ranking != newUser.Ranking { if oldUser.Ranking != newUser.Ranking {
item := GetAccountItemByName("Ranking", organization) item := GetAccountItemByName("Ranking", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Ranking = oldUser.Ranking
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Currency != newUser.Currency { if oldUser.Currency != newUser.Currency {
item := GetAccountItemByName("Currency", organization) item := GetAccountItemByName("Currency", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Currency = oldUser.Currency
} else {
itemsChanged = append(itemsChanged, item)
}
} }
if oldUser.Hash != newUser.Hash { if oldUser.Hash != newUser.Hash {
item := GetAccountItemByName("Hash", organization) item := GetAccountItemByName("Hash", organization)
itemsChanged = append(itemsChanged, item) if item == nil {
newUser.Hash = oldUser.Hash
} else {
itemsChanged = append(itemsChanged, item)
}
} }
for _, accountItem := range itemsChanged { for _, accountItem := range itemsChanged {

View File

@ -166,19 +166,76 @@ func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordT
return nil return nil
} }
func filterRecordIn24Hours(record *VerificationRecord) *VerificationRecord {
if record == nil {
return nil
}
now := time.Now().Unix()
if now-record.Time > 60*60*24 {
return nil
}
return record
}
func getVerificationRecord(dest string) (*VerificationRecord, error) { func getVerificationRecord(dest string) (*VerificationRecord, error) {
var record VerificationRecord record := &VerificationRecord{}
record.Receiver = dest record.Receiver = dest
has, err := ormer.Engine.Desc("time").Where("is_used = false").Get(&record) has, err := ormer.Engine.Desc("time").Where("is_used = false").Get(record)
if err != nil { if err != nil {
return nil, err return nil, err
} }
record = filterRecordIn24Hours(record)
if record == nil {
has = false
}
if !has {
record = &VerificationRecord{}
record.Receiver = dest
has, err = ormer.Engine.Desc("time").Get(record)
if err != nil {
return nil, err
}
record = filterRecordIn24Hours(record)
if record == nil {
has = false
}
if !has {
return nil, nil
}
return record, nil
}
return record, nil
}
func getUnusedVerificationRecord(dest string) (*VerificationRecord, error) {
record := &VerificationRecord{}
record.Receiver = dest
has, err := ormer.Engine.Desc("time").Where("is_used = false").Get(record)
if err != nil {
return nil, err
}
record = filterRecordIn24Hours(record)
if record == nil {
has = false
}
if !has { if !has {
return nil, nil return nil, nil
} }
return &record, nil return record, nil
} }
func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult, error) { func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult, error) {
@ -187,7 +244,9 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
return nil, err return nil, err
} }
if record == nil { if record == nil {
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet, or has already been used!")}, nil return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet!")}, nil
} else if record.IsUsed {
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has already been used!")}, nil
} }
timeoutInMinutes, err := conf.GetConfigInt64("verificationCodeTimeout") timeoutInMinutes, err := conf.GetConfigInt64("verificationCodeTimeout")
@ -196,9 +255,6 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
} }
now := time.Now().Unix() now := time.Now().Unix()
if now-record.Time > timeoutInMinutes*60*10 {
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet!")}, nil
}
if now-record.Time > timeoutInMinutes*60 { if now-record.Time > timeoutInMinutes*60 {
return &VerifyResult{timeoutError, fmt.Sprintf(i18n.Translate(lang, "verification:You should verify your code in %d min!"), timeoutInMinutes)}, nil return &VerifyResult{timeoutError, fmt.Sprintf(i18n.Translate(lang, "verification:You should verify your code in %d min!"), timeoutInMinutes)}, nil
} }
@ -211,7 +267,7 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
} }
func DisableVerificationCode(dest string) error { func DisableVerificationCode(dest string) error {
record, err := getVerificationRecord(dest) record, err := getUnusedVerificationRecord(dest)
if record == nil || err != nil { if record == nil || err != nil {
return nil return nil
} }

View File

@ -56,7 +56,7 @@ func getSubject(ctx *context.Context) (string, string) {
return util.GetOwnerAndNameFromId(username) return util.GetOwnerAndNameFromId(username)
} }
func getObject(ctx *context.Context) (string, string) { func getObject(ctx *context.Context) (string, string, error) {
method := ctx.Request.Method method := ctx.Request.Method
path := ctx.Request.URL.Path path := ctx.Request.URL.Path
@ -65,13 +65,13 @@ func getObject(ctx *context.Context) (string, string) {
if ctx.Input.Query("id") == "/" { if ctx.Input.Query("id") == "/" {
adapterId := ctx.Input.Query("adapterId") adapterId := ctx.Input.Query("adapterId")
if adapterId != "" { if adapterId != "" {
return util.GetOwnerAndNameFromIdNoCheck(adapterId) return util.GetOwnerAndNameFromIdWithError(adapterId)
} }
} else { } else {
// query == "?id=built-in/admin" // query == "?id=built-in/admin"
id := ctx.Input.Query("id") id := ctx.Input.Query("id")
if id != "" { if id != "" {
return util.GetOwnerAndNameFromIdNoCheck(id) return util.GetOwnerAndNameFromIdWithError(id)
} }
} }
} }
@ -80,34 +80,34 @@ func getObject(ctx *context.Context) (string, string) {
// query == "?id=built-in/admin" // query == "?id=built-in/admin"
id := ctx.Input.Query("id") id := ctx.Input.Query("id")
if id != "" { if id != "" {
return util.GetOwnerAndNameFromIdNoCheck(id) return util.GetOwnerAndNameFromIdWithError(id)
} }
} }
owner := ctx.Input.Query("owner") owner := ctx.Input.Query("owner")
if owner != "" { if owner != "" {
return owner, "" return owner, "", nil
} }
return "", "" return "", "", nil
} else { } else {
if path == "/api/add-policy" || path == "/api/remove-policy" || path == "/api/update-policy" { if path == "/api/add-policy" || path == "/api/remove-policy" || path == "/api/update-policy" {
id := ctx.Input.Query("id") id := ctx.Input.Query("id")
if id != "" { if id != "" {
return util.GetOwnerAndNameFromIdNoCheck(id) return util.GetOwnerAndNameFromIdWithError(id)
} }
} }
body := ctx.Input.RequestBody body := ctx.Input.RequestBody
if len(body) == 0 { if len(body) == 0 {
return ctx.Request.Form.Get("owner"), ctx.Request.Form.Get("name") return ctx.Request.Form.Get("owner"), ctx.Request.Form.Get("name"), nil
} }
var obj Object var obj Object
err := json.Unmarshal(body, &obj) err := json.Unmarshal(body, &obj)
if err != nil { if err != nil {
// panic(err) // this is not error
return "", "" return "", "", nil
} }
if path == "/api/delete-resource" { if path == "/api/delete-resource" {
@ -117,7 +117,7 @@ func getObject(ctx *context.Context) (string, string) {
} }
} }
return obj.Owner, obj.Name return obj.Owner, obj.Name, nil
} }
} }
@ -183,7 +183,12 @@ func ApiFilter(ctx *context.Context) {
objOwner, objName := "", "" objOwner, objName := "", ""
if urlPath != "/api/get-app-login" && urlPath != "/api/get-resource" { if urlPath != "/api/get-app-login" && urlPath != "/api/get-resource" {
objOwner, objName = getObject(ctx) var err error
objOwner, objName, err = getObject(ctx)
if err != nil {
responseError(ctx, err.Error())
return
}
} }
if strings.HasPrefix(urlPath, "/api/notify-payment") { if strings.HasPrefix(urlPath, "/api/notify-payment") {

View File

@ -16,6 +16,7 @@ package routers
import ( import (
"fmt" "fmt"
"strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -23,6 +24,10 @@ import (
) )
func AutoSigninFilter(ctx *context.Context) { func AutoSigninFilter(ctx *context.Context) {
urlPath := ctx.Request.URL.Path
if strings.HasPrefix(urlPath, "/api/login/oauth/access_token") {
return
}
//if getSessionUser(ctx) != "" { //if getSessionUser(ctx) != "" {
// return // return
//} //}
@ -67,6 +72,17 @@ func AutoSigninFilter(ctx *context.Context) {
return return
} }
accessKey := ctx.Input.Query("accessKey")
accessSecret := ctx.Input.Query("accessSecret")
if accessKey != "" && accessSecret != "" {
userId, err := getUsernameByKeys(ctx)
if err != nil {
responseError(ctx, err.Error())
}
setSessionUser(ctx, userId)
}
// "/page?clientId=123&clientSecret=456" // "/page?clientId=123&clientSecret=456"
userId, err := getUsernameByClientIdSecret(ctx) userId, err := getUsernameByClientIdSecret(ctx)
if err != nil { if err != nil {

View File

@ -21,7 +21,6 @@ import (
"strings" "strings"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -126,7 +125,7 @@ func setSessionUser(ctx *context.Context, user string) {
} }
// https://github.com/beego/beego/issues/3445#issuecomment-455411915 // https://github.com/beego/beego/issues/3445#issuecomment-455411915
ctx.Input.CruSession.SessionReleaseIfPresent(ctx.ResponseWriter) ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
} }
func setSessionExpire(ctx *context.Context, ExpireTime int64) { func setSessionExpire(ctx *context.Context, ExpireTime int64) {
@ -135,7 +134,7 @@ func setSessionExpire(ctx *context.Context, ExpireTime int64) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
ctx.Input.CruSession.SessionReleaseIfPresent(ctx.ResponseWriter) ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
} }
func setSessionOidc(ctx *context.Context, scope string, aud string) { func setSessionOidc(ctx *context.Context, scope string, aud string) {
@ -147,7 +146,7 @@ func setSessionOidc(ctx *context.Context, scope string, aud string) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
ctx.Input.CruSession.SessionReleaseIfPresent(ctx.ResponseWriter) ctx.Input.CruSession.SessionRelease(ctx.ResponseWriter)
} }
func parseBearerToken(ctx *context.Context) string { func parseBearerToken(ctx *context.Context) string {

View File

@ -48,6 +48,10 @@ func CorsFilter(ctx *context.Context) {
originHostname := getHostname(origin) originHostname := getHostname(origin)
host := removePort(ctx.Request.Host) host := removePort(ctx.Request.Host)
if origin == "null" {
origin = ""
}
if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") || strings.Contains(origin, ".chromiumapp.org") { if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") || strings.Contains(origin, ".chromiumapp.org") {
setCorsHeaders(ctx, origin) setCorsHeaders(ctx, origin)
return return

View File

@ -290,6 +290,7 @@ func initAPI() {
beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery") beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks") beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")
beego.Router("/.well-known/webfinger", &controllers.RootController{}, "GET:GetWebFinger")
beego.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceValidate") beego.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceValidate")
beego.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasProxyValidate") beego.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasProxyValidate")

View File

@ -43,6 +43,10 @@ func getWebBuildFolder() string {
return path return path
} }
if util.FileExist(filepath.Join(frontendBaseDir, "index.html")) {
return frontendBaseDir
}
path = filepath.Join(frontendBaseDir, "web/build") path = filepath.Join(frontendBaseDir, "web/build")
return path return path
} }
@ -58,7 +62,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
redirectUri := ctx.Input.Query("redirect_uri") redirectUri := ctx.Input.Query("redirect_uri")
scope := ctx.Input.Query("scope") scope := ctx.Input.Query("scope")
state := ctx.Input.Query("state") state := ctx.Input.Query("state")
nonce := "" nonce := ctx.Input.Query("nonce")
codeChallenge := ctx.Input.Query("code_challenge") codeChallenge := ctx.Input.Query("code_challenge")
if clientId == "" || responseType != "code" || redirectUri == "" { if clientId == "" || responseType != "code" || redirectUri == "" {
return "", nil return "", nil

64
routers/timeout_filter.go Normal file
View File

@ -0,0 +1,64 @@
// Copyright 2024 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 routers
import (
"fmt"
"sync"
"time"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf"
)
var (
inactiveTimeoutMinutes int64
requestTimeMap sync.Map
)
func init() {
var err error
inactiveTimeoutMinutes, err = conf.GetConfigInt64("inactiveTimeoutMinutes")
if err != nil {
inactiveTimeoutMinutes = 0
}
}
func timeoutLogout(ctx *context.Context, sessionId string) {
requestTimeMap.Delete(sessionId)
ctx.Input.CruSession.Set("username", "")
ctx.Input.CruSession.Set("accessToken", "")
ctx.Input.CruSession.Delete("SessionData")
responseError(ctx, fmt.Sprintf(T(ctx, "auth:Timeout for inactivity of %d minutes"), inactiveTimeoutMinutes))
}
func TimeoutFilter(ctx *context.Context) {
if inactiveTimeoutMinutes <= 0 {
return
}
owner, name := getSubject(ctx)
if owner == "anonymous" || name == "anonymous" {
return
}
sessionId := ctx.Input.CruSession.SessionID()
currentTime := time.Now()
preRequestTime, has := requestTimeMap.Load(sessionId)
requestTimeMap.Store(sessionId, currentTime)
if has && preRequestTime.(time.Time).Add(time.Minute*time.Duration(inactiveTimeoutMinutes)).Before(currentTime) {
timeoutLogout(ctx, sessionId)
}
}

19
storage/casdoor.go Normal file
View File

@ -0,0 +1,19 @@
package storage
import (
"github.com/casdoor/oss"
"github.com/casdoor/oss/casdoor"
)
func NewCasdoorStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string, cert string, content string) oss.StorageInterface {
sp := casdoor.New(&casdoor.Config{
clientId,
clientSecret,
endpoint,
cert,
region,
content,
bucket,
})
return sp
}

View File

@ -16,7 +16,7 @@ package storage
import "github.com/casdoor/oss" import "github.com/casdoor/oss"
func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string) (oss.StorageInterface, error) { func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string, cert string, content string) (oss.StorageInterface, error) {
switch providerType { switch providerType {
case "Local File System": case "Local File System":
return NewLocalFileSystemStorageProvider(), nil return NewLocalFileSystemStorageProvider(), nil
@ -36,6 +36,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
return NewGoogleCloudStorageProvider(clientSecret, bucket, endpoint), nil return NewGoogleCloudStorageProvider(clientSecret, bucket, endpoint), nil
case "Synology": case "Synology":
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil
case "Casdoor":
return NewCasdoorStorageProvider(providerType, clientId, clientSecret, region, bucket, endpoint, cert, content), nil
} }
return nil, nil return nil, nil

76
util/obfuscator.go Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2024 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 util
import (
"crypto/aes"
"crypto/cipher"
"crypto/des"
"encoding/hex"
"fmt"
)
func unPaddingPkcs7(s []byte) []byte {
length := len(s)
if length == 0 {
return s
}
unPadding := int(s[length-1])
return s[:(length - unPadding)]
}
func decryptDesOrAes(passwordCipher string, block cipher.Block) (string, error) {
passwordCipherBytes, err := hex.DecodeString(passwordCipher)
if err != nil {
return "", err
}
if len(passwordCipherBytes) < block.BlockSize() {
return "", fmt.Errorf("the password ciphertext should contain a random hexadecimal string of length %d at the beginning", block.BlockSize()*2)
}
iv := passwordCipherBytes[:block.BlockSize()]
password := make([]byte, len(passwordCipherBytes)-block.BlockSize())
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(password, passwordCipherBytes[block.BlockSize():])
return string(unPaddingPkcs7(password)), nil
}
func GetUnobfuscatedPassword(passwordObfuscatorType string, passwordObfuscatorKey string, passwordCipher string) (string, error) {
if passwordObfuscatorType == "Plain" || passwordObfuscatorType == "" {
return passwordCipher, nil
} else if passwordObfuscatorType == "DES" || passwordObfuscatorType == "AES" {
key, err := hex.DecodeString(passwordObfuscatorKey)
if err != nil {
return "", err
}
var block cipher.Block
if passwordObfuscatorType == "DES" {
block, err = des.NewCipher(key)
} else {
block, err = aes.NewCipher(key)
}
if err != nil {
return "", err
}
return decryptDesOrAes(passwordCipher, block)
} else {
return "", fmt.Errorf("unsupported password obfuscator type: %s", passwordObfuscatorType)
}
}

View File

@ -131,6 +131,15 @@ func GetOwnerAndNameFromId(id string) (string, string) {
return tokens[0], tokens[1] return tokens[0], tokens[1]
} }
func GetOwnerAndNameFromIdWithError(id string) (string, string, error) {
tokens := strings.Split(id, "/")
if len(tokens) != 2 {
return "", "", errors.New("GetOwnerAndNameFromId() error, wrong token count for ID: " + id)
}
return tokens[0], tokens[1], nil
}
func GetOwnerFromId(id string) string { func GetOwnerFromId(id string) string {
tokens := strings.Split(id, "/") tokens := strings.Split(id, "/")
if len(tokens) != 2 { if len(tokens) != 2 {
@ -154,6 +163,16 @@ func GetOwnerAndNameAndOtherFromId(id string) (string, string, string) {
return tokens[0], tokens[1], tokens[2] return tokens[0], tokens[1], tokens[2]
} }
func GetSharedOrgFromApp(rawName string) (name string, organization string) {
name = rawName
splitName := strings.Split(rawName, "-org-")
if len(splitName) >= 2 {
organization = splitName[len(splitName)-1]
name = splitName[0]
}
return name, organization
}
func GenerateId() string { func GenerateId() string {
return uuid.NewString() return uuid.NewString()
} }

View File

@ -51,6 +51,9 @@ func IsPhoneValid(phone string, countryCode string) bool {
} }
func IsPhoneAllowInRegin(countryCode string, allowRegions []string) bool { func IsPhoneAllowInRegin(countryCode string, allowRegions []string) bool {
if ContainsString(allowRegions, "All") {
return true
}
return ContainsString(allowRegions, countryCode) return ContainsString(allowRegions, countryCode)
} }

View File

@ -27,6 +27,7 @@
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0", "core-js": "^3.25.0",
"craco-less": "^2.0.0", "craco-less": "^2.0.0",
"crypto-js": "^4.2.0",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"ethers": "5.6.9", "ethers": "5.6.9",
"face-api.js": "^0.22.2", "face-api.js": "^0.22.2",

View File

@ -56,9 +56,11 @@ class AdapterListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -344,7 +344,8 @@ class App extends Component {
window.location.pathname.startsWith("/cas") || window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/select-plan") || window.location.pathname.startsWith("/select-plan") ||
window.location.pathname.startsWith("/buy-plan") || window.location.pathname.startsWith("/buy-plan") ||
window.location.pathname.startsWith("/qrcode") ; window.location.pathname.startsWith("/qrcode") ||
window.location.pathname.startsWith("/captcha");
} }
onClick = ({key}) => { onClick = ({key}) => {
@ -361,7 +362,11 @@ class App extends Component {
if (this.isDoorPages()) { if (this.isDoorPages()) {
return ( return (
<ConfigProvider theme={{ <ConfigProvider theme={{
algorithm: Setting.getAlgorithm(["default"]), token: {
colorPrimary: this.state.themeData.colorPrimary,
borderRadius: this.state.themeData.borderRadius,
},
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}> }}>
<StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}> <StyleProvider hashPriority="high" transformers={[legacyLogicalPropertiesTransformer]}>
<Layout id="parent-area"> <Layout id="parent-area">
@ -371,6 +376,7 @@ class App extends Component {
<EntryPage <EntryPage
account={this.state.account} account={this.state.account}
theme={this.state.themeData} theme={this.state.themeData}
themeAlgorithm={this.state.themeAlgorithm}
updateApplication={(application) => { updateApplication={(application) => {
this.setState({ this.setState({
application: application, application: application,
@ -445,7 +451,6 @@ class App extends Component {
setLogoutState={() => { setLogoutState={() => {
this.setState({ this.setState({
account: null, account: null,
themeAlgorithm: ["default"],
}); });
}} }}
/> />

View File

@ -129,6 +129,15 @@ img {
background-attachment: fixed; background-attachment: fixed;
} }
.loginBackgroundDark {
flex: auto;
display: flex;
align-items: center;
background: #000 no-repeat;
background-size: 100% 100%;
background-attachment: fixed;
}
.ant-menu-horizontal { .ant-menu-horizontal {
border-bottom: none !important; border-bottom: none !important;
} }

View File

@ -116,7 +116,6 @@ class ApplicationEditPage extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getApplication(); this.getApplication();
this.getOrganizations(); this.getOrganizations();
this.getProviders();
} }
getApplication() { getApplication() {
@ -145,7 +144,9 @@ class ApplicationEditPage extends React.Component {
application: application, application: application,
}); });
this.getCerts(application.organization); this.getProviders(application);
this.getCerts(application);
this.getSamlMetadata(application.enableSamlPostBinding); this.getSamlMetadata(application.enableSamlPostBinding);
}); });
@ -166,7 +167,11 @@ class ApplicationEditPage extends React.Component {
}); });
} }
getCerts(owner) { getCerts(application) {
let owner = application.organization;
if (application.isShared) {
owner = this.props.owner;
}
CertBackend.getCerts(owner) CertBackend.getCerts(owner)
.then((res) => { .then((res) => {
this.setState({ this.setState({
@ -175,8 +180,12 @@ class ApplicationEditPage extends React.Component {
}); });
} }
getProviders() { getProviders(application) {
ProviderBackend.getProviders(this.state.owner) let owner = application.organization;
if (application.isShared) {
owner = this.props.account.owner;
}
ProviderBackend.getProviders(owner)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.setState({ this.setState({
@ -263,6 +272,16 @@ class ApplicationEditPage 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:Is shared"), i18next.t("general:Is shared - Tooltip"))} :
</Col>
<Col span={22} >
<Switch disabled={Setting.isAdminUser()} checked={this.state.application.isShared} onChange={checked => {
this.updateApplicationField("isShared", 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("general:Logo"), i18next.t("general:Logo - Tooltip"))} : {Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} :
@ -388,6 +407,16 @@ class ApplicationEditPage extends React.Component {
/> />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Token signing method"), i18next.t("application:Token signing method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenSigningMethod === "" ? "RS256" : this.state.application.tokenSigningMethod} onChange={(value => {this.updateApplicationField("tokenSigningMethod", value);})}
options={["RS256", "RS512", "ES256", "ES512", "ES384"].map((item) => Setting.getOption(item, item))}
/>
</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("application:Token fields"), i18next.t("application:Token fields - Tooltip"))} : {Setting.getLabel(i18next.t("application:Token fields"), i18next.t("application:Token fields - Tooltip"))} :
@ -674,6 +703,16 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Use Email as NameID"), i18next.t("application:Use Email as NameID - Tooltip"))} :
</Col>
<Col span={1}>
<Switch checked={this.state.application.useEmailAsSamlNameId} onChange={checked => {
this.updateApplicationField("useEmailAsSamlNameId", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable SAML POST binding"), i18next.t("application:Enable SAML POST binding - Tooltip"))} : {Setting.getLabel(i18next.t("application:Enable SAML POST binding"), i18next.t("application:Enable SAML POST binding - Tooltip"))} :
@ -938,6 +977,7 @@ class ApplicationEditPage extends React.Component {
<SigninTable <SigninTable
title={i18next.t("application:Signin items")} title={i18next.t("application:Signin items")}
table={this.state.application.signinItems} table={this.state.application.signinItems}
themeAlgorithm={this.state.themeAlgorithm}
onUpdateTable={(value) => { onUpdateTable={(value) => {
this.updateApplicationField("signinItems", value); this.updateApplicationField("signinItems", value);
}} }}
@ -989,7 +1029,11 @@ class ApplicationEditPage extends React.Component {
redirectUri = "\"ERROR: You must specify at least one Redirect URL in 'Redirect URLs'\""; redirectUri = "\"ERROR: You must specify at least one Redirect URL in 'Redirect URLs'\"";
} }
const signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=read&state=casdoor`; let clientId = this.state.application.clientId;
if (this.state.application.isShared) {
clientId += `-org-${this.props.account.owner}`;
}
const signInUrl = `/login/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=read&state=casdoor`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"}; const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"};
if (!Setting.isPasswordEnabled(this.state.application)) { if (!Setting.isPasswordEnabled(this.state.application)) {
signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize"); signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize");

View File

@ -97,9 +97,11 @@ class ApplicationListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
@ -123,7 +125,7 @@ class ApplicationListPage extends BaseListPage {
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/applications/${record.organization}/${text}`}> <Link to={`/applications/${record.organization}/${text}`}>
{text} {Setting.getApplicationDisplayName(record)}
</Link> </Link>
); );
}, },

116
web/src/CaptchaPage.js Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {CaptchaModal} from "./common/modal/CaptchaModal";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Setting from "./Setting";
class CaptchaPage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(this.props.location.search);
this.state = {
owner: "admin",
application: null,
clientId: params.get("client_id"),
applicationName: params.get("state"),
redirectUri: params.get("redirect_uri"),
};
}
componentDidMount() {
this.getApplication();
}
onUpdateApplication(application) {
this.setState({
application: application,
});
}
getApplication() {
if (this.state.applicationName === null) {
return null;
}
ApplicationBackend.getApplication(this.state.owner, this.state.applicationName)
.then((res) => {
if (res.status === "error") {
this.onUpdateApplication(null);
this.setState({
msg: res.msg,
});
return ;
}
this.onUpdateApplication(res.data);
});
}
getCaptchaProviderItems(application) {
const providers = application?.providers;
if (providers === undefined || providers === null) {
return null;
}
return providers.filter(providerItem => {
if (providerItem.provider === undefined || providerItem.provider === null) {
return false;
}
return providerItem.provider.category === "Captcha";
});
}
callback(values) {
Setting.goToLink(`${this.state.redirectUri}?code=${values.captchaToken}&type=${values.captchaType}&secret=${values.clientSecret}&applicationId=${values.applicationId}`);
}
renderCaptchaModal(application) {
const captchaProviderItems = this.getCaptchaProviderItems(application);
if (captchaProviderItems === null) {
return null;
}
const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always");
const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic");
const provider = alwaysProviderItems.length > 0
? alwaysProviderItems[0].provider
: dynamicProviderItems[0].provider;
return <CaptchaModal
owner={provider.owner}
name={provider.name}
visible={true}
onOk={(captchaType, captchaToken, clientSecret) => {
const values = {
captchaType: captchaType,
captchaToken: captchaToken,
clientSecret: clientSecret,
applicationId: `${provider.owner}/${provider.name}`,
};
this.callback(values);
}}
onCancel={() => this.callback({captchaType: "none", captchaToken: "", clientSecret: ""})}
isCurrentProvider={true}
/>;
}
render() {
return (
this.renderCaptchaModal(this.state.application)
);
}
}
export default CaptchaPage;

97
web/src/CasbinEditor.js Normal file
View File

@ -0,0 +1,97 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {useCallback, useEffect, useRef, useState} from "react";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import "codemirror/mode/properties/properties";
import * as Setting from "./Setting";
import IframeEditor from "./IframeEditor";
import {Tabs} from "antd";
const {TabPane} = Tabs;
const CasbinEditor = ({model, onModelTextChange}) => {
const [activeKey, setActiveKey] = useState("advanced");
const iframeRef = useRef(null);
const [localModelText, setLocalModelText] = useState(model.modelText);
const handleModelTextChange = useCallback((newModelText) => {
if (!Setting.builtInObject(model)) {
setLocalModelText(newModelText);
onModelTextChange(newModelText);
}
}, [model, onModelTextChange]);
const syncModelText = useCallback(() => {
return new Promise((resolve) => {
if (activeKey === "advanced" && iframeRef.current) {
const handleSyncMessage = (event) => {
if (event.data.type === "modelUpdate") {
window.removeEventListener("message", handleSyncMessage);
handleModelTextChange(event.data.modelText);
resolve();
}
};
window.addEventListener("message", handleSyncMessage);
iframeRef.current.getModelText();
} else {
resolve();
}
});
}, [activeKey, handleModelTextChange]);
const handleTabChange = (key) => {
syncModelText().then(() => {
setActiveKey(key);
if (key === "advanced" && iframeRef.current) {
iframeRef.current.updateModelText(localModelText);
}
});
};
useEffect(() => {
setLocalModelText(model.modelText);
}, [model.modelText]);
return (
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
<TabPane tab="Basic Editor" key="basic" />
<TabPane tab="Advanced Editor" key="advanced" />
</Tabs>
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
{activeKey === "advanced" ? (
<IframeEditor
ref={iframeRef}
initialModelText={localModelText}
onModelTextChange={handleModelTextChange}
style={{width: "100%", height: "100%"}}
/>
) : (
<CodeMirror
value={localModelText}
className="full-height-editor no-horizontal-scroll-editor"
options={{mode: "properties", theme: "default"}}
onBeforeChange={(editor, data, value) => {
handleModelTextChange(value);
}}
/>
)}
</div>
</div>
);
};
export default CasbinEditor;

View File

@ -288,14 +288,14 @@ class CertEditPage extends React.Component {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({ this.setState({
certName: this.state.cert.name, certName: this.state.cert.name,
}, () => {
if (exitAfterSave) {
this.props.history.push("/certs");
} else {
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`);
this.getCert();
}
}); });
if (exitAfterSave) {
this.props.history.push("/certs");
} else {
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`);
this.getCert();
}
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateCertField("name", this.state.certName); this.updateCertField("name", this.state.certName);

View File

@ -73,9 +73,11 @@ class CertListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -55,9 +55,11 @@ class EnforcerListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -32,6 +32,7 @@ import {authConfig} from "./auth/Auth";
import ProductBuyPage from "./ProductBuyPage"; import ProductBuyPage from "./ProductBuyPage";
import PaymentResultPage from "./PaymentResultPage"; import PaymentResultPage from "./PaymentResultPage";
import QrCodePage from "./QrCodePage"; import QrCodePage from "./QrCodePage";
import CaptchaPage from "./CaptchaPage";
import CustomHead from "./basic/CustomHead"; import CustomHead from "./basic/CustomHead";
class EntryPage extends React.Component { class EntryPage extends React.Component {
@ -93,10 +94,12 @@ class EntryPage extends React.Component {
}); });
}; };
const isDarkMode = this.props.themeAlgorithm.includes("dark");
return ( return (
<React.Fragment> <React.Fragment>
<CustomHead headerHtml={this.state.application?.headerHtml} /> <CustomHead headerHtml={this.state.application?.headerHtml} />
<div className="loginBackground" <div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}> style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")} <Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
style={{margin: "0 auto"}} /> style={{margin: "0 auto"}} />
@ -120,8 +123,10 @@ class EntryPage extends React.Component {
<Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} /> <Route exact path="/buy-plan/:owner/:pricingName" render={(props) => <ProductBuyPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
<Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} /> <Route exact path="/buy-plan/:owner/:pricingName/result" render={(props) => <PaymentResultPage {...this.props} pricing={this.state.pricing} onUpdatePricing={onUpdatePricing} {...props} />} />
<Route exact path="/qrcode/:owner/:paymentName" render={(props) => <QrCodePage {...this.props} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/qrcode/:owner/:paymentName" render={(props) => <QrCodePage {...this.props} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/captcha" render={(props) => <CaptchaPage {...props} />} />
</Switch> </Switch>
</div> </div>
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -84,9 +84,11 @@ class GroupListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

66
web/src/IframeEditor.js Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react";
const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) => {
const iframeRef = useRef(null);
const [iframeReady, setIframeReady] = useState(false);
useEffect(() => {
const handleMessage = (event) => {
if (event.origin !== "https://editor.casbin.org") {return;}
if (event.data.type === "modelUpdate") {
onModelTextChange(event.data.modelText);
} else if (event.data.type === "iframeReady") {
setIframeReady(true);
iframeRef.current?.contentWindow.postMessage({
type: "initializeModel",
modelText: initialModelText,
}, "*");
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [onModelTextChange, initialModelText]);
useImperativeHandle(ref, () => ({
getModelText: () => {
iframeRef.current?.contentWindow.postMessage({type: "getModelText"}, "*");
},
updateModelText: (newModelText) => {
if (iframeReady) {
iframeRef.current?.contentWindow.postMessage({
type: "updateModelText",
modelText: newModelText,
}, "*");
}
},
}));
return (
<iframe
ref={iframeRef}
src="https://editor.casbin.org/model-editor"
frameBorder="0"
width="100%"
height="500px"
title="Casbin Model Editor"
/>
);
});
export default IframeEditor;

View File

@ -20,6 +20,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import * as GroupBackend from "./backend/GroupBackend";
const {Option} = Select; const {Option} = Select;
@ -33,6 +34,7 @@ class InvitationEditPage extends React.Component {
invitation: null, invitation: null,
organizations: [], organizations: [],
applications: [], applications: [],
groups: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
} }
@ -41,6 +43,7 @@ class InvitationEditPage extends React.Component {
this.getInvitation(); this.getInvitation();
this.getOrganizations(); this.getOrganizations();
this.getApplicationsByOrganization(this.state.organizationName); this.getApplicationsByOrganization(this.state.organizationName);
this.getGroupsByOrganization(this.state.organizationName);
} }
getInvitation() { getInvitation() {
@ -75,6 +78,17 @@ class InvitationEditPage extends React.Component {
}); });
} }
getGroupsByOrganization(organizationName) {
GroupBackend.getGroups(organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
parseInvitationField(key, value) { parseInvitationField(key, value) {
if ([""].includes(key)) { if ([""].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
@ -120,7 +134,7 @@ class InvitationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account) || isCreatedByPlan} value={this.state.invitation.owner} onChange={(value => {this.updateInvitationField("owner", value); this.getApplicationsByOrganization(value);})}> <Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account) || isCreatedByPlan} value={this.state.invitation.owner} onChange={(value => {this.updateInvitationField("owner", value); this.getApplicationsByOrganization(value);this.getGroupsByOrganization(value);})}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
} }
@ -204,6 +218,21 @@ class InvitationEditPage extends React.Component {
]} /> ]} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Signup group"), i18next.t("provider:Signup group - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.invitation.signupGroup} onChange={(value => {this.updateInvitationField("signupGroup", value);})}>
<Option key={""} value={""}>
{i18next.t("general:Default")}
</Option>
{
this.state.groups.map((group, index) => <Option key={index} value={`${group.owner}/${group.name}`}>{group.name}</Option>)
}
</Select>
</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("signup:Username"), i18next.t("signup:Username - Tooltip"))} : {Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"))} :

View File

@ -68,9 +68,11 @@ class InvitationListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -13,12 +13,13 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd"; import {Button, Card, Col, Input, InputNumber, Row, Select, Space, Switch} from "antd";
import {EyeInvisibleOutlined, EyeTwoTone} from "@ant-design/icons"; import {EyeInvisibleOutlined, EyeTwoTone, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as LddpBackend from "./backend/LdapBackend"; import * as LddpBackend from "./backend/LdapBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as GroupBackend from "./backend/GroupBackend";
const {Option} = Select; const {Option} = Select;
@ -30,12 +31,14 @@ class LdapEditPage extends React.Component {
organizationName: props.match.params.organizationName, organizationName: props.match.params.organizationName,
ldap: null, ldap: null,
organizations: [], organizations: [],
groups: null,
}; };
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getLdap(); this.getLdap();
this.getOrganizations(); this.getOrganizations();
this.getGroups();
} }
getLdap() { getLdap() {
@ -60,6 +63,17 @@ class LdapEditPage extends React.Component {
}); });
} }
getGroups() {
GroupBackend.getGroups(this.state.organizationName)
.then((res) => {
if (res.status === "ok") {
this.setState({
groups: res.data,
});
}
});
}
updateLdapField(key, value) { updateLdapField(key, value) {
this.setState((prevState) => { this.setState((prevState) => {
prevState.ldap[key] = value; prevState.ldap[key] = value;
@ -214,6 +228,31 @@ 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("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
</Col>
<Col span={21}>
<Select virtual={false} style={{width: "100%"}} value={this.state.ldap.defaultGroup ?? []} onChange={(value => {
this.updateLdapField("defaultGroup", value);
})}
>
<Option key={""} value={""}>
<Space>
{i18next.t("general:Default")}
</Space>
</Option>
{
this.state.groups?.map((group) => <Option key={group.name} value={`${group.owner}/${group.name}`}>
<Space>
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
{group.displayName}
</Space>
</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:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} : {Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} :

View File

@ -18,11 +18,7 @@ import * as ModelBackend from "./backend/ModelBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import ModelEditor from "./CasbinEditor";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/mode/properties/properties");
const {Option} = Select; const {Option} = Select;
@ -147,16 +143,10 @@ class ModelEditPage extends React.Component {
{Setting.getLabel(i18next.t("model:Model text"), i18next.t("model:Model text - Tooltip"))} : {Setting.getLabel(i18next.t("model:Model text"), i18next.t("model:Model text - Tooltip"))} :
</Col> </Col>
<Col span={22}> <Col span={22}>
<div style={{width: "100%"}} > <div style={{position: "relative", height: "500px"}} >
<CodeMirror <ModelEditor
value={this.state.model.modelText} model={this.state.model}
options={{mode: "properties", theme: "default"}} onModelTextChange={(value) => this.updateModelField("modelText", value)}
onBeforeChange={(editor, data, value) => {
if (Setting.builtInObject(this.state.model)) {
return;
}
this.updateModelField("modelText", value);
}}
/> />
</div> </div>
</Col> </Col>

View File

@ -72,9 +72,11 @@ class ModelListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -19,6 +19,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as LdapBackend from "./backend/LdapBackend"; import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as Conf from "./Conf"; import * as Conf from "./Conf";
import * as Obfuscator from "./auth/Obfuscator";
import i18next from "i18next"; import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons"; import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./table/LdapTable"; import LdapTable from "./table/LdapTable";
@ -112,6 +113,22 @@ class OrganizationEditPage extends React.Component {
}); });
} }
updatePasswordObfuscator(key, value) {
const organization = this.state.organization;
if (organization.passwordObfuscatorType === "") {
organization.passwordObfuscatorType = "Plain";
}
if (key === "type") {
organization.passwordObfuscatorType = value;
organization.passwordObfuscatorKey = Obfuscator.getRandomKeyForObfuscator(value);
} else if (key === "key") {
organization.passwordObfuscatorKey = value;
}
this.setState({
organization: organization,
});
}
renderOrganization() { renderOrganization() {
return ( return (
<Card size="small" title={ <Card size="small" title={
@ -294,6 +311,34 @@ 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:Password obfuscator"), i18next.t("general:Password obfuscator - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}}
value={this.state.organization.passwordObfuscatorType}
onChange={(value => {this.updatePasswordObfuscator("type", value);})}>
{
[
{id: "Plain", name: "Plain"},
{id: "AES", name: "AES"},
{id: "DES", name: "DES"},
].map((obfuscatorType, index) => <Option key={index} value={obfuscatorType.id}>{obfuscatorType.name}</Option>)
}
</Select>
</Col>
</Row>
{
(this.state.organization.passwordObfuscatorType === "Plain" || this.state.organization.passwordObfuscatorType === "") ? null : (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password obf key"), i18next.t("general:Password obf key - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.organization.passwordObfuscatorKey} onChange={(e) => {this.updatePasswordObfuscator("key", e.target.value);}} />
</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("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} : {Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :
@ -305,6 +350,7 @@ class OrganizationEditPage extends React.Component {
}} }}
filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())} filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())}
> >
{Setting.getCountryCodeOption({name: i18next.t("organization:All"), code: "All", phone: 0})}
{ {
Setting.getCountryCodeData().map((country) => Setting.getCountryCodeOption(country)) Setting.getCountryCodeData().map((country) => Setting.getCountryCodeOption(country))
} }
@ -360,7 +406,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.defaultApplication} onChange={(value => {this.updateOrganizationField("defaultApplication", value);})} <Select virtual={false} style={{width: "100%"}} value={this.state.organization.defaultApplication} onChange={(value => {this.updateOrganizationField("defaultApplication", value);})}
options={this.state.applications?.map((item) => Setting.getOption(item.name, item.name)) options={this.state.applications?.map((item) => Setting.getOption(Setting.getApplicationDisplayName(item.name), item.name))
} /> } />
</Col> </Col>
</Row> </Row>
@ -528,6 +574,12 @@ class OrganizationEditPage extends React.Component {
const organization = Setting.deepCopy(this.state.organization); const organization = Setting.deepCopy(this.state.organization);
organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item"); organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item");
const passwordObfuscatorErrorMessage = Obfuscator.checkPasswordObfuscator(organization.passwordObfuscatorType, organization.passwordObfuscatorKey);
if (passwordObfuscatorErrorMessage.length > 0) {
Setting.showMessage("error", passwordObfuscatorErrorMessage);
return;
}
OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization) OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {

View File

@ -35,6 +35,8 @@ class OrganizationListPage extends BaseListPage {
passwordType: "plain", passwordType: "plain",
PasswordSalt: "", PasswordSalt: "",
passwordOptions: [], passwordOptions: [],
passwordObfuscatorType: "Plain",
passwordObfuscatorKey: "",
countryCodes: ["US"], countryCodes: ["US"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`, defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "", defaultApplication: "",
@ -115,11 +117,11 @@ class OrganizationListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i),
pagination: { pagination: {
...this.state.pagination, ...this.state.pagination,
total: this.state.pagination.total - 1}, current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
window.dispatchEvent(new Event("storageOrganizationsChanged")); window.dispatchEvent(new Event("storageOrganizationsChanged"));
} else { } else {

View File

@ -70,9 +70,11 @@ class PaymentListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -69,9 +69,11 @@ class PermissionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -63,9 +63,11 @@ class PlanListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -59,9 +59,11 @@ class PricingListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -65,9 +65,11 @@ class ProductListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -843,7 +843,7 @@ class ProviderEditPage extends React.Component {
) )
} }
{ {
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : ( this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.category !== "Storage") && this.state.provider.type !== "Okta" ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -870,7 +870,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} : {Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@ -885,7 +885,9 @@ class ProviderEditPage extends React.Component {
{["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} : {["Casdoor"].includes(this.state.provider.type) ?
Setting.getLabel(i18next.t("general:Provider"), i18next.t("provider:Provider - Tooltip"))
: Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.provider.bucket} onChange={e => { <Input value={this.state.provider.bucket} onChange={e => {
@ -906,7 +908,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Custom HTTP SMS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : ( {["Custom HTTP SMS", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -918,10 +920,24 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? ( {["Casdoor"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.content} onChange={e => {
this.updateProviderField("content", e.target.value);
}} />
</Col>
</Row>
) : null}
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor"].includes(this.state.provider.type) ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{["Casdoor"].includes(this.state.provider.type) ?
Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip")) :
Setting.getLabel(i18next.t("provider:Region ID"), i18next.t("provider:Region ID - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.provider.regionId} onChange={e => { <Input value={this.state.provider.regionId} onChange={e => {
@ -1298,7 +1314,7 @@ class ProviderEditPage extends React.Component {
) : null ) : null
} }
{ {
(this.state.provider.type === "Alipay" || this.state.provider.type === "WeChat Pay") ? ( (this.state.provider.type === "Alipay" || this.state.provider.type === "WeChat Pay" || this.state.provider.type === "Casdoor") ? (
<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("general:Cert"), i18next.t("general:Cert - Tooltip"))} : {Setting.getLabel(i18next.t("general:Cert"), i18next.t("general:Cert - Tooltip"))} :

View File

@ -76,9 +76,11 @@ class ProviderListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -40,9 +40,11 @@ class ResourceListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -61,9 +61,11 @@ class RoleListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -27,9 +27,11 @@ class SessionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -229,6 +229,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_synology.png`, logo: `${StaticBaseUrl}/img/social_synology.png`,
url: "https://www.synology.com/en-global/dsm/feature/file_sharing", url: "https://www.synology.com/en-global/dsm/feature/file_sharing",
}, },
"Casdoor": {
logo: `${StaticBaseUrl}/img/casdoor.png`,
url: "https://casdoor.org/docs/provider/storage/overview",
},
}, },
SAML: { SAML: {
"Aliyun IDaaS": { "Aliyun IDaaS": {
@ -283,6 +287,14 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`, logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha", url: "https://www.google.com/recaptcha",
}, },
"reCAPTCHA v2": {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha",
},
"reCAPTCHA v3": {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha",
},
"hCaptcha": { "hCaptcha": {
logo: `${StaticBaseUrl}/img/social_hcaptcha.png`, logo: `${StaticBaseUrl}/img/social_hcaptcha.png`,
url: "https://www.hcaptcha.com", url: "https://www.hcaptcha.com",
@ -406,6 +418,9 @@ export function getCountryCode(country) {
} }
export function getCountryCodeData(countryCodes = phoneNumber.getCountries()) { export function getCountryCodeData(countryCodes = phoneNumber.getCountries()) {
if (countryCodes?.includes("All")) {
countryCodes = phoneNumber.getCountries();
}
return countryCodes?.map((countryCode) => { return countryCodes?.map((countryCode) => {
if (phoneNumber.isSupportedCountry(countryCode)) { if (phoneNumber.isSupportedCountry(countryCode)) {
const name = initCountries().getName(countryCode, getLanguage()); const name = initCountries().getName(countryCode, getLanguage());
@ -424,10 +439,10 @@ export function getCountryCodeOption(country) {
<Option key={country.code} value={country.code} label={`+${country.phone}`} text={`${country.name}, ${country.code}, ${country.phone}`} > <Option key={country.code} value={country.code} label={`+${country.phone}`} text={`${country.name}, ${country.code}, ${country.phone}`} >
<div style={{display: "flex", justifyContent: "space-between", marginRight: "10px"}}> <div style={{display: "flex", justifyContent: "space-between", marginRight: "10px"}}>
<div> <div>
{getCountryImage(country)} {country.code === "All" ? null : getCountryImage(country)}
{`${country.name}`} {`${country.name}`}
</div> </div>
{`+${country.phone}`} {country.code === "All" ? null : `+${country.phone}`}
</div> </div>
</Option> </Option>
); );
@ -1062,6 +1077,7 @@ export function getProviderTypeOptions(category) {
{id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"}, {id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"},
{id: "Google Cloud Storage", name: "Google Cloud Storage"}, {id: "Google Cloud Storage", name: "Google Cloud Storage"},
{id: "Synology", name: "Synology"}, {id: "Synology", name: "Synology"},
{id: "Casdoor", name: "Casdoor"},
] ]
); );
} else if (category === "SAML") { } else if (category === "SAML") {
@ -1083,7 +1099,8 @@ export function getProviderTypeOptions(category) {
} else if (category === "Captcha") { } else if (category === "Captcha") {
return ([ return ([
{id: "Default", name: "Default"}, {id: "Default", name: "Default"},
{id: "reCAPTCHA", name: "reCAPTCHA"}, {id: "reCAPTCHA v2", name: "reCAPTCHA v2"},
{id: "reCAPTCHA v3", name: "reCAPTCHA v3"},
{id: "hCaptcha", name: "hCaptcha"}, {id: "hCaptcha", name: "hCaptcha"},
{id: "Aliyun Captcha", name: "Aliyun Captcha"}, {id: "Aliyun Captcha", name: "Aliyun Captcha"},
{id: "GEETEST", name: "GEETEST"}, {id: "GEETEST", name: "GEETEST"},
@ -1154,7 +1171,7 @@ export function renderLogo(application) {
function isSigninMethodEnabled(application, signinMethod) { function isSigninMethodEnabled(application, signinMethod) {
if (application && application.signinMethods) { if (application && application.signinMethods) {
return application.signinMethods.filter(item => item.name === signinMethod).length > 0; return application.signinMethods.filter(item => item.name === signinMethod && item.rule !== "Hide-Password").length > 0;
} else { } else {
return false; return false;
} }
@ -1371,6 +1388,13 @@ export function getApplicationName(application) {
return `${application?.owner}/${application?.name}`; return `${application?.owner}/${application?.name}`;
} }
export function getApplicationDisplayName(application) {
if (application.isShared) {
return `${application.name}(Shared)`;
}
return application.name;
}
export function getRandomName() { export function getRandomName() {
return Math.random().toString(36).slice(-6); return Math.random().toString(36).slice(-6);
} }

View File

@ -64,9 +64,11 @@ class SubscriptionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -434,10 +434,9 @@ class SyncerEditPage extends React.Component {
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} : {Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.syncer.table} <Input value={this.state.syncer.table} onChange={e => {
disabled={this.state.syncer.type === "Keycloak"} onChange={e => { this.updateSyncerField("table", e.target.value);
this.updateSyncerField("table", e.target.value); }} />
}} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >

View File

@ -69,9 +69,11 @@ class SyncerListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -61,9 +61,11 @@ class TokenListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -125,7 +125,7 @@ class TransactionEditPage extends React.Component {
application: application, application: application,
}); });
this.getCerts(application.organization); this.getCerts(application);
this.getSamlMetadata(application.enableSamlPostBinding); this.getSamlMetadata(application.enableSamlPostBinding);
}); });

View File

@ -54,9 +54,11 @@ class TransactionListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -1050,6 +1050,8 @@ class UserEditPage extends React.Component {
<MfaAccountTable <MfaAccountTable
title={i18next.t("user:MFA accounts")} title={i18next.t("user:MFA accounts")}
table={this.state.user.mfaAccounts} table={this.state.user.mfaAccounts}
accessToken={this.props.account?.accessToken}
icon={this.state.user.avatar}
onUpdateTable={(table) => {this.updateUserField("mfaAccounts", table);}} onUpdateTable={(table) => {this.updateUserField("mfaAccounts", table);}}
/> />
</Col> </Col>

View File

@ -110,9 +110,11 @@ class UserListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -61,9 +61,11 @@ class WebhookListPage extends BaseListPage {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted")); Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({ this.fetch({
data: Setting.deleteRow(this.state.data, i), pagination: {
pagination: {total: this.state.pagination.total - 1}, ...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
}); });
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);

View File

@ -34,25 +34,42 @@ class CasLogout extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
const params = new URLSearchParams(this.props.location.search); const params = new URLSearchParams(this.props.location.search);
const logoutInterval = 100;
const logoutTimeOut = (redirectUri) => {
setTimeout(() => {
AuthBackend.getAccount().then((accountRes) => {
if (accountRes.status === "ok") {
AuthBackend.logout().then((logoutRes) => {
if (logoutRes.status === "ok") {
logoutTimeOut(logoutRes.data2);
} else {
Setting.showMessage("error", `${i18next.t("login:Failed to log out")}: ${logoutRes.msg}`);
}
});
} else {
Setting.showMessage("success", i18next.t("application:Logged out successfully"));
this.props.onUpdateAccount(null);
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
Setting.goToLink(redirectUri);
} else if (params.has("service")) {
Setting.goToLink(params.get("service"));
} else {
Setting.goToLinkSoft(this, `/cas/${this.state.owner}/${this.state.applicationName}/login`);
}
}
});
}, logoutInterval);
};
AuthBackend.logout() AuthBackend.logout()
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", "Logged out successfully"); logoutTimeOut(res.data2);
this.props.onUpdateAccount(null);
const redirectUri = res.data2;
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
Setting.goToLink(redirectUri);
} else if (params.has("service")) {
Setting.goToLink(params.get("service"));
} else {
Setting.goToLinkSoft(this, `/cas/${this.state.owner}/${this.state.applicationName}/login`);
}
} else { } else {
Setting.showMessage("error", `Failed to log out: ${res.msg}`); Setting.showMessage("error", `${i18next.t("login:Failed to log out")}: ${res.msg}`);
} }
}); });
} }
render() { render() {

View File

@ -19,6 +19,7 @@ import {withRouter} from "react-router-dom";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend"; import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
import OrganizationSelect from "../common/select/OrganizationSelect"; import OrganizationSelect from "../common/select/OrganizationSelect";
import * as Conf from "../Conf"; import * as Conf from "../Conf";
import * as Obfuscator from "./Obfuscator";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import * as OrganizationBackend from "../backend/OrganizationBackend"; import * as OrganizationBackend from "../backend/OrganizationBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
@ -379,6 +380,14 @@ class LoginPage extends React.Component {
return; return;
} }
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") { if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
const organization = this.getApplicationObj()?.organizationObj;
const [passwordCipher, errorMessage] = Obfuscator.encryptByPasswordObfuscator(organization?.passwordObfuscatorType, organization?.passwordObfuscatorKey, values["password"]);
if (errorMessage.length > 0) {
Setting.showMessage("error", errorMessage);
return;
} else {
values["password"] = passwordCipher;
}
if (this.state.enableCaptchaModal === CaptchaRule.Always) { if (this.state.enableCaptchaModal === CaptchaRule.Always) {
this.setState({ this.setState({
openCaptchaModal: true, openCaptchaModal: true,
@ -938,7 +947,7 @@ class LoginPage extends React.Component {
signinItem.label ? Setting.renderSignupLink(application, signinItem.label) : signinItem.label ? Setting.renderSignupLink(application, signinItem.label) :
( (
<React.Fragment> <React.Fragment>
{i18next.t("login:No account?")} {i18next.t("login:No account?")}&nbsp;
{ {
Setting.renderSignupLink(application, i18next.t("login:sign up now")) Setting.renderSignupLink(application, i18next.t("login:sign up now"))
} }
@ -1125,6 +1134,9 @@ class LoginPage extends React.Component {
]); ]);
application?.signinMethods?.forEach((signinMethod) => { application?.signinMethods?.forEach((signinMethod) => {
if (signinMethod.rule === "Hide-Password") {
return;
}
const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule)); const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule));
if (item) { if (item) {
let label = signinMethod.name === signinMethod.displayName ? item.label : signinMethod.displayName; let label = signinMethod.name === signinMethod.displayName ? item.label : signinMethod.displayName;

View File

@ -0,0 +1,95 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CryptoJS from "crypto-js";
import i18next from "i18next";
export function getRandomKeyForObfuscator(obfuscatorType) {
if (obfuscatorType === "DES") {
return getRandomHexKey(16);
} else if (obfuscatorType === "AES") {
return getRandomHexKey(32);
} else {
return "";
}
}
export const passwordObfuscatorKeyRegexes = {
"DES": /^[1-9a-f]{16}$/,
"AES": /^[1-9a-f]{32}$/,
};
function encrypt(cipher, key, iv, password) {
const encrypted = cipher.encrypt(
CryptoJS.enc.Hex.parse(Buffer.from(password, "utf-8").toString("hex")),
CryptoJS.enc.Hex.parse(key),
{
iv: iv,
mode: CryptoJS.mode.CBC,
pad: CryptoJS.pad.Pkcs7,
}
);
return iv.concat(encrypted.ciphertext).toString(CryptoJS.enc.Hex);
}
export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) {
if (passwordObfuscatorType === undefined) {
return i18next.t("organization:failed to get password obfuscator");
} else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
return "";
} else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") {
if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) {
return "";
} else {
return `${i18next.t("organization:The password obfuscator key doesn't match the regex")}: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`;
}
} else {
return `${i18next.t("organization:unsupported password obfuscator type")}: ${passwordObfuscatorType}`;
}
}
export function encryptByPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey, password) {
const passwordObfuscatorErrorMessage = checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey);
if (passwordObfuscatorErrorMessage.length > 0) {
return ["", passwordObfuscatorErrorMessage];
} else {
if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
return [password, ""];
} else if (passwordObfuscatorType === "AES") {
return [encryptByAes(passwordObfuscatorKey, password), ""];
} else if (passwordObfuscatorType === "DES") {
return [encryptByDes(passwordObfuscatorKey, password), ""];
}
}
}
function encryptByDes(key, password) {
const iv = CryptoJS.lib.WordArray.random(8);
return encrypt(CryptoJS.DES, key, iv, password);
}
function encryptByAes(key, password) {
const iv = CryptoJS.lib.WordArray.random(16);
return encrypt(CryptoJS.AES, key, iv, password);
}
function getRandomHexKey(length) {
const characters = "123456789abcdef";
let key = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
key += characters[randomIndex];
}
return key;
}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Form, Input, Radio, Result, Row, message} from "antd"; import {Button, Form, Input, Radio, Result, Row, Select, message} from "antd";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import * as ProviderButton from "./ProviderButton"; import * as ProviderButton from "./ProviderButton";
@ -50,6 +50,38 @@ const formItemLayout = {
}, },
}; };
const renderFormItem = (signupItem) => {
const commonProps = {
name: signupItem.name.toLowerCase(),
label: signupItem.label || signupItem.name,
rules: [
{
required: signupItem.required,
message: i18next.t(`signup:Please input your ${signupItem.label || signupItem.name}!`),
},
],
};
if (!signupItem.type || signupItem.type === "Input") {
return (
<Form.Item {...commonProps}>
<Input placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.type === "Single Choice" || signupItem.type === "Multiple Choices") {
return (
<Form.Item {...commonProps}>
<Select
mode={signupItem.type === "Multiple Choices" ? "multiple" : "single"}
placeholder={signupItem.placeholder}
showSearch={false}
options={signupItem.options.map(option => ({label: option, value: option}))}
/>
</Form.Item>
);
}
};
export const tailFormItemLayout = { export const tailFormItemLayout = {
wrapperCol: { wrapperCol: {
xs: { xs: {
@ -198,6 +230,22 @@ class SignupPage extends React.Component {
onFinish(values) { onFinish(values) {
const application = this.getApplicationObj(); const application = this.getApplicationObj();
if (Array.isArray(values.gender)) {
values.gender = values.gender.join(", ");
}
if (Array.isArray(values.bio)) {
values.bio = values.bio.join(", ");
}
if (Array.isArray(values.tag)) {
values.tag = values.tag.join(", ");
}
if (Array.isArray(values.education)) {
values.education = values.education.join(", ");
}
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
values.plan = params.get("plan"); values.plan = params.get("plan");
values.pricing = params.get("pricing"); values.pricing = params.get("pricing");
@ -238,6 +286,7 @@ class SignupPage extends React.Component {
} }
renderFormItem(application, signupItem) { renderFormItem(application, signupItem) {
const validItems = ["Gender", "Bio", "Tag", "Education"];
if (!signupItem.visible) { if (!signupItem.visible) {
return null; return null;
} }
@ -366,7 +415,9 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<RegionSelect className="signup-region-select" onChange={(value) => {this.setState({region: value});}} /> <RegionSelect className="signup-region-select" onChange={(value) => {
this.setState({region: value});
}} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Email" || signupItem.name === "Phone" || signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") { } else if (signupItem.name === "Email" || signupItem.name === "Phone" || signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
@ -669,8 +720,9 @@ class SignupPage extends React.Component {
</span> </span>
); );
}) })
); );
} else if (validItems.includes(signupItem.name)) {
return renderFormItem(signupItem);
} }
} }

View File

@ -27,7 +27,8 @@ export const CaptchaWidget = (props) => {
useEffect(() => { useEffect(() => {
switch (captchaType) { switch (captchaType) {
case "reCAPTCHA": { case "reCAPTCHA" :
case "reCAPTCHA v2": {
const reTimer = setInterval(() => { const reTimer = setInterval(() => {
if (!window.grecaptcha) { if (!window.grecaptcha) {
loadScript("https://recaptcha.net/recaptcha/api.js"); loadScript("https://recaptcha.net/recaptcha/api.js");
@ -42,6 +43,32 @@ export const CaptchaWidget = (props) => {
}, 300); }, 300);
break; break;
} }
case "reCAPTCHA v3": {
const reTimer = setInterval(() => {
if (!window.grecaptcha) {
loadScript(`https://recaptcha.net/recaptcha/api.js?render=${siteKey}`);
}
if (window.grecaptcha && window.grecaptcha.render) {
const clientId = window.grecaptcha.render("captcha", {
"sitekey": siteKey,
"badge": "inline",
"size": "invisible",
"callback": onChange,
"error-callback": function() {
const logoWidth = `${document.getElementById("captcha").offsetWidth + 40}px`;
document.getElementsByClassName("grecaptcha-logo")[0].firstChild.style.width = logoWidth;
document.getElementsByClassName("grecaptcha-badge")[0].style.width = logoWidth;
},
});
window.grecaptcha.ready(function() {
window.grecaptcha.execute(clientId, {action: "submit"});
});
clearInterval(reTimer);
}
}, 300);
break;
}
case "hCaptcha": { case "hCaptcha": {
const hTimer = setInterval(() => { const hTimer = setInterval(() => {
if (!window.hcaptcha) { if (!window.hcaptcha) {

View File

@ -115,7 +115,7 @@ export const CaptchaModal = (props) => {
} else { } else {
return ( return (
<Col> <Col>
<Row> <Row justify={"center"}>
<CaptchaWidget <CaptchaWidget
captchaType={captchaType} captchaType={captchaType}
subType={subType} subType={subType}

View File

@ -51,3 +51,19 @@ code {
.custom-link:hover { .custom-link:hover {
color: rgb(64 64 64) !important; color: rgb(64 64 64) !important;
} }
.full-height-editor {
height: 100%;
}
.full-height-editor [class*="CodeMirror"] {
height: 100%;
}
.no-horizontal-scroll-editor [class*="CodeMirror-hscrollbar"] {
display: none !important;
}
.no-horizontal-scroll-editor [class*="CodeMirror-scroll"] {
overflow-x: hidden !important;
}

View File

@ -24,6 +24,12 @@ import * as serviceWorker from "./serviceWorker";
import {BrowserRouter} from "react-router-dom"; import {BrowserRouter} from "react-router-dom";
import "./backend/FetchFilter"; import "./backend/FetchFilter";
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function(search, replace) {
return this.split(search).join(replace);
};
}
const container = document.getElementById("root"); const container = document.getElementById("root");
const app = createRoot(container); const app = createRoot(container);

View File

@ -74,6 +74,7 @@
"Left": "Left", "Left": "Left",
"Logged in successfully": "Logged in successfully", "Logged in successfully": "Logged in successfully",
"Logged out successfully": "Logged out successfully", "Logged out successfully": "Logged out successfully",
"Multiple Choices": "Multiple Choices",
"New Application": "New Application", "New Application": "New Application",
"No verification": "No verification", "No verification": "No verification",
"Normal": "Normal", "Normal": "Normal",
@ -112,6 +113,7 @@
"Signin session": "Signin session", "Signin session": "Signin session",
"Signup items": "Signup items", "Signup items": "Signup items",
"Signup items - Tooltip": "Items for users to fill in when registering new accounts", "Signup items - Tooltip": "Items for users to fill in when registering new accounts",
"Single Choice": "Single Choice",
"Small icon": "Small icon", "Small icon": "Small icon",
"Tags - Tooltip": "Only users with the tag that is listed in the application tags can login", "Tags - Tooltip": "Only users with the tag that is listed in the application tags can login",
"The application does not allow to sign up new account": "The application does not allow to sign up new account", "The application does not allow to sign up new account": "The application does not allow to sign up new account",
@ -121,6 +123,10 @@
"Token fields - Tooltip": "Token fields - Tooltip", "Token fields - Tooltip": "Token fields - Tooltip",
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "The format of access token", "Token format - Tooltip": "The format of access token",
"Token signing method": "Token signing method",
"Token signing method - Tooltip": "Signing method of JWT token, needs to be the same algorithm as the certificate",
"Use Email as NameID": "Use Email as NameID",
"Use Email as NameID - Tooltip": "Use Email as NameID - Tooltip",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page" "You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
}, },
"cert": { "cert": {
@ -177,6 +183,7 @@
"Access key - Tooltip": "Access key - Tooltip", "Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret", "Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip", "Access secret - Tooltip": "Access secret - Tooltip",
"Access token is empty": "Access token is empty",
"Action": "Action", "Action": "Action",
"Adapter": "Adapter", "Adapter": "Adapter",
"Adapter - Tooltip": "Table name of the policy store", "Adapter - Tooltip": "Table name of the policy store",
@ -234,6 +241,8 @@
"Enable": "Enable", "Enable": "Enable",
"Enable dark logo": "Enable dark logo", "Enable dark logo": "Enable dark logo",
"Enable dark logo - Tooltip": "Enable dark logo", "Enable dark logo - Tooltip": "Enable dark logo",
"Enable tour": "Enable tour",
"Enable tour - Tooltip": "Display tour for users",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers", "Enforcers": "Enforcers",
@ -265,6 +274,8 @@
"Invitations": "Invitations", "Invitations": "Invitations",
"Is enabled": "Is enabled", "Is enabled": "Is enabled",
"Is enabled - Tooltip": "Set whether it can use", "Is enabled - Tooltip": "Set whether it can use",
"Is shared": "Is shared",
"Is shared - Tooltip": "Share this application with other organizations",
"LDAPs": "LDAPs", "LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAP servers", "LDAPs - Tooltip": "LDAP servers",
"Languages": "Languages", "Languages": "Languages",
@ -328,6 +339,8 @@
"Provider - Tooltip": "Payment providers to be configured, including PayPal, Alipay, WeChat Pay, etc.", "Provider - Tooltip": "Payment providers to be configured, including PayPal, Alipay, WeChat Pay, etc.",
"Providers": "Providers", "Providers": "Providers",
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.", "Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
"QR Code": "QR Code",
"QR code is too large": "QR code is too large",
"Real name": "Real name", "Real name": "Real name",
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
@ -441,6 +454,8 @@
"Base DN": "Base DN", "Base DN": "Base DN",
"Base DN - Tooltip": "Base DN during LDAP search", "Base DN - Tooltip": "Base DN during LDAP search",
"CN": "CN", "CN": "CN",
"Default group": "Default group",
"Default group - Tooltip": "Group to which users belong after synchronization",
"Edit LDAP": "Edit LDAP", "Edit LDAP": "Edit LDAP",
"Enable SSL": "Enable SSL", "Enable SSL": "Enable SSL",
"Enable SSL - Tooltip": "Whether to enable SSL", "Enable SSL - Tooltip": "Whether to enable SSL",
@ -470,6 +485,7 @@
"Face ID": "Face ID", "Face ID": "Face ID",
"Face Recognition": "Face Recognition", "Face Recognition": "Face Recognition",
"Face recognition failed": "Face recognition failed", "Face recognition failed": "Face recognition failed",
"Failed to log out": "Failed to log out",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization", "Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Failed to obtain Web3-Onboard authorization": "Failed to obtain Web3-Onboard authorization", "Failed to obtain Web3-Onboard authorization": "Failed to obtain Web3-Onboard authorization",
"Forgot password?": "Forgot password?", "Forgot password?": "Forgot password?",
@ -551,6 +567,11 @@
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"
}, },
"mfaAccount": {
"Account Name": "Account Name",
"Issuer": "Issuer",
"Secret Key": "Secret Key"
},
"model": { "model": {
"Edit Model": "Edit Model", "Edit Model": "Edit Model",
"Model text": "Model text", "Model text": "Model text",
@ -818,6 +839,7 @@
"Project Id": "Project Id", "Project Id": "Project Id",
"Project Id - Tooltip": "Project Id - Tooltip", "Project Id - Tooltip": "Project Id - Tooltip",
"Prompted": "Prompted", "Prompted": "Prompted",
"Provider - Tooltip": "Provider - Tooltip",
"Provider URL": "Provider URL", "Provider URL": "Provider URL",
"Provider URL - Tooltip": "URL for configuring the service provider, this field is only used for reference and is not used in Casdoor", "Provider URL - Tooltip": "URL for configuring the service provider, this field is only used for reference and is not used in Casdoor",
"Public key": "Public key", "Public key": "Public key",
@ -937,6 +959,7 @@
"Have account?": "Have account?", "Have account?": "Have account?",
"Label": "Label", "Label": "Label",
"Label HTML": "Label HTML", "Label HTML": "Label HTML",
"Options": "Options",
"Placeholder": "Placeholder", "Placeholder": "Placeholder",
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
@ -1136,6 +1159,7 @@
"Link": "Link", "Link": "Link",
"Location": "Location", "Location": "Location",
"Location - Tooltip": "City of residence", "Location - Tooltip": "City of residence",
"MFA accounts": "MFA accounts",
"Managed accounts": "Managed accounts", "Managed accounts": "Managed accounts",
"Modify password...": "Modify password...", "Modify password...": "Modify password...",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Multi-factor authentication",

View File

@ -74,6 +74,7 @@
"Left": "Vlevo", "Left": "Vlevo",
"Logged in successfully": "Úspěšně přihlášen", "Logged in successfully": "Úspěšně přihlášen",
"Logged out successfully": "Úspěšně odhlášen", "Logged out successfully": "Úspěšně odhlášen",
"Multiple Choices": "Multiple Choices",
"New Application": "Nová aplikace", "New Application": "Nová aplikace",
"No verification": "Bez ověření", "No verification": "Bez ověření",
"Normal": "Normální", "Normal": "Normální",
@ -112,6 +113,7 @@
"Signin session": "Přihlašovací relace", "Signin session": "Přihlašovací relace",
"Signup items": "Položky registrace", "Signup items": "Položky registrace",
"Signup items - Tooltip": "Položky, které uživatelé vyplňují při registraci nových účtů", "Signup items - Tooltip": "Položky, které uživatelé vyplňují při registraci nových účtů",
"Single Choice": "Single Choice",
"Small icon": "Malá ikona", "Small icon": "Malá ikona",
"Tags - Tooltip": "Pouze uživatelé s tagem uvedeným v tazích aplikace se mohou přihlásit", "Tags - Tooltip": "Pouze uživatelé s tagem uvedeným v tazích aplikace se mohou přihlásit",
"The application does not allow to sign up new account": "Aplikace neumožňuje registraci nového účtu", "The application does not allow to sign up new account": "Aplikace neumožňuje registraci nového účtu",
@ -121,6 +123,10 @@
"Token fields - Tooltip": "Uživatelská pole zahrnutá v tokenu", "Token fields - Tooltip": "Uživatelská pole zahrnutá v tokenu",
"Token format": "Formát tokenu", "Token format": "Formát tokenu",
"Token format - Tooltip": "Formát přístupového tokenu", "Token format - Tooltip": "Formát přístupového tokenu",
"Token signing method": "Token signing method",
"Token signing method - Tooltip": "Signing method of JWT token, needs to be the same algorithm as the certificate",
"Use Email as NameID": "Use Email as NameID",
"Use Email as NameID - Tooltip": "Use Email as NameID - Tooltip",
"You are unexpected to see this prompt page": "Nečekali jste, že uvidíte tuto výzvu" "You are unexpected to see this prompt page": "Nečekali jste, že uvidíte tuto výzvu"
}, },
"cert": { "cert": {
@ -177,6 +183,7 @@
"Access key - Tooltip": "Přístupový klíč - Tooltip", "Access key - Tooltip": "Přístupový klíč - Tooltip",
"Access secret": "Přístupové tajemství", "Access secret": "Přístupové tajemství",
"Access secret - Tooltip": "Přístupové tajemství - Tooltip", "Access secret - Tooltip": "Přístupové tajemství - Tooltip",
"Access token is empty": "Access token is empty",
"Action": "Akce", "Action": "Akce",
"Adapter": "Adaptér", "Adapter": "Adaptér",
"Adapter - Tooltip": "Název tabulky úložiště politiky", "Adapter - Tooltip": "Název tabulky úložiště politiky",
@ -234,6 +241,8 @@
"Enable": "Povolit", "Enable": "Povolit",
"Enable dark logo": "Povolit tmavé logo", "Enable dark logo": "Povolit tmavé logo",
"Enable dark logo - Tooltip": "Povolit tmavé logo", "Enable dark logo - Tooltip": "Povolit tmavé logo",
"Enable tour": "Enable tour",
"Enable tour - Tooltip": "Display tour for users",
"Enabled": "Povoleno", "Enabled": "Povoleno",
"Enabled successfully": "Úspěšně povoleno", "Enabled successfully": "Úspěšně povoleno",
"Enforcers": "Enforcers", "Enforcers": "Enforcers",
@ -265,6 +274,8 @@
"Invitations": "Pozvánky", "Invitations": "Pozvánky",
"Is enabled": "Je povoleno", "Is enabled": "Je povoleno",
"Is enabled - Tooltip": "Nastavit, zda může být použito", "Is enabled - Tooltip": "Nastavit, zda může být použito",
"Is shared": "Is shared",
"Is shared - Tooltip": "Share this application with other organizations",
"LDAPs": "LDAPy", "LDAPs": "LDAPy",
"LDAPs - Tooltip": "LDAP servery", "LDAPs - Tooltip": "LDAP servery",
"Languages": "Jazyky", "Languages": "Jazyky",
@ -328,6 +339,8 @@
"Provider - Tooltip": "Poskytovatelé plateb, které mají být nakonfigurovány, včetně PayPal, Alipay, WeChat Pay, atd.", "Provider - Tooltip": "Poskytovatelé plateb, které mají být nakonfigurovány, včetně PayPal, Alipay, WeChat Pay, atd.",
"Providers": "Poskytovatelé", "Providers": "Poskytovatelé",
"Providers - Tooltip": "Poskytovatelé, kteří mají být nakonfigurováni, včetně přihlášení třetích stran, objektového úložiště, ověřovacího kódu, atd.", "Providers - Tooltip": "Poskytovatelé, kteří mají být nakonfigurováni, včetně přihlášení třetích stran, objektového úložiště, ověřovacího kódu, atd.",
"QR Code": "QR Code",
"QR code is too large": "QR code is too large",
"Real name": "Skutečné jméno", "Real name": "Skutečné jméno",
"Records": "Záznamy", "Records": "Záznamy",
"Request URI": "Požadavek URI", "Request URI": "Požadavek URI",
@ -441,6 +454,8 @@
"Base DN": "Základní DN", "Base DN": "Základní DN",
"Base DN - Tooltip": "Základní DN při vyhledávání v LDAP", "Base DN - Tooltip": "Základní DN při vyhledávání v LDAP",
"CN": "CN", "CN": "CN",
"Default group": "Default group",
"Default group - Tooltip": "Group to which users belong after synchronization",
"Edit LDAP": "Upravit LDAP", "Edit LDAP": "Upravit LDAP",
"Enable SSL": "Povolit SSL", "Enable SSL": "Povolit SSL",
"Enable SSL - Tooltip": "Zda povolit SSL", "Enable SSL - Tooltip": "Zda povolit SSL",
@ -470,6 +485,7 @@
"Face ID": "Face ID", "Face ID": "Face ID",
"Face Recognition": "Rozpoznávání obličeje", "Face Recognition": "Rozpoznávání obličeje",
"Face recognition failed": "Rozpoznávání obličeje selhalo", "Face recognition failed": "Rozpoznávání obličeje selhalo",
"Failed to log out": "Failed to log out",
"Failed to obtain MetaMask authorization": "Nepodařilo se získat autorizaci MetaMask", "Failed to obtain MetaMask authorization": "Nepodařilo se získat autorizaci MetaMask",
"Failed to obtain Web3-Onboard authorization": "Nepodařilo se získat autorizaci Web3-Onboard", "Failed to obtain Web3-Onboard authorization": "Nepodařilo se získat autorizaci Web3-Onboard",
"Forgot password?": "Zapomněli jste heslo?", "Forgot password?": "Zapomněli jste heslo?",
@ -521,7 +537,7 @@
"Failed to initiate MFA": "Nepodařilo se zahájit MFA", "Failed to initiate MFA": "Nepodařilo se zahájit MFA",
"Have problems?": "Máte problémy?", "Have problems?": "Máte problémy?",
"Multi-factor authentication": "Vícefaktorové ověřování", "Multi-factor authentication": "Vícefaktorové ověřování",
"Multi-factor authentication - Tooltip": "Dvoufaktorové ověřování - Tooltip", "Multi-factor authentication - Tooltip ": "Dvoufaktorové ověřování - Tooltip",
"Multi-factor authentication description": "Popis dvoufaktorového ověřování", "Multi-factor authentication description": "Popis dvoufaktorového ověřování",
"Multi-factor methods": "Metody dvoufaktorového ověřování", "Multi-factor methods": "Metody dvoufaktorového ověřování",
"Multi-factor recover": "Obnovení dvoufaktorového ověřování", "Multi-factor recover": "Obnovení dvoufaktorového ověřování",
@ -551,6 +567,11 @@
"Your phone is": "Váš telefon je", "Your phone is": "Váš telefon je",
"preferred": "preferované" "preferred": "preferované"
}, },
"mfaAccount": {
"Account Name": "Account Name",
"Issuer": "Issuer",
"Secret Key": "Secret Key"
},
"model": { "model": {
"Edit Model": "Upravit model", "Edit Model": "Upravit model",
"Model text": "Text modelu", "Model text": "Text modelu",
@ -818,6 +839,7 @@
"Project Id": "ID projektu", "Project Id": "ID projektu",
"Project Id - Tooltip": "Nápověda k ID projektu", "Project Id - Tooltip": "Nápověda k ID projektu",
"Prompted": "Vyzván", "Prompted": "Vyzván",
"Provider - Tooltip": "Provider - Tooltip",
"Provider URL": "URL poskytovatele", "Provider URL": "URL poskytovatele",
"Provider URL - Tooltip": "URL pro konfiguraci poskytovatele služby, toto pole je pouze pro referenci a není použito v Casdoor", "Provider URL - Tooltip": "URL pro konfiguraci poskytovatele služby, toto pole je pouze pro referenci a není použito v Casdoor",
"Public key": "Veřejný klíč", "Public key": "Veřejný klíč",
@ -937,6 +959,7 @@
"Have account?": "Máte účet?", "Have account?": "Máte účet?",
"Label": "Štítek", "Label": "Štítek",
"Label HTML": "HTML štítek", "Label HTML": "HTML štítek",
"Options": "Options",
"Placeholder": "Zástupný text", "Placeholder": "Zástupný text",
"Please accept the agreement!": "Prosím přijměte smlouvu!", "Please accept the agreement!": "Prosím přijměte smlouvu!",
"Please click the below button to sign in": "Prosím klikněte na tlačítko níže pro přihlášení", "Please click the below button to sign in": "Prosím klikněte na tlačítko níže pro přihlášení",
@ -1136,6 +1159,7 @@
"Link": "Odkaz", "Link": "Odkaz",
"Location": "Místo", "Location": "Místo",
"Location - Tooltip": "Město bydliště", "Location - Tooltip": "Město bydliště",
"MFA accounts": "MFA accounts",
"Managed accounts": "Spravované účty", "Managed accounts": "Spravované účty",
"Modify password...": "Změnit heslo...", "Modify password...": "Změnit heslo...",
"Multi-factor authentication": "Vícefaktorové ověřování", "Multi-factor authentication": "Vícefaktorové ověřování",

View File

@ -74,6 +74,7 @@
"Left": "Links", "Left": "Links",
"Logged in successfully": "Erfolgreich eingeloggt", "Logged in successfully": "Erfolgreich eingeloggt",
"Logged out successfully": "Erfolgreich ausgeloggt", "Logged out successfully": "Erfolgreich ausgeloggt",
"Multiple Choices": "Multiple Choices",
"New Application": "Neue Anwendung", "New Application": "Neue Anwendung",
"No verification": "No verification", "No verification": "No verification",
"Normal": "Normal", "Normal": "Normal",
@ -112,6 +113,7 @@
"Signin session": "Anmeldesession", "Signin session": "Anmeldesession",
"Signup items": "Registrierungs Items", "Signup items": "Registrierungs Items",
"Signup items - Tooltip": "Items, die Benutzer ausfüllen müssen, wenn sie neue Konten registrieren", "Signup items - Tooltip": "Items, die Benutzer ausfüllen müssen, wenn sie neue Konten registrieren",
"Single Choice": "Single Choice",
"Small icon": "Small icon", "Small icon": "Small icon",
"Tags - Tooltip": "Only users with the tag that is listed in the application tags can login", "Tags - Tooltip": "Only users with the tag that is listed in the application tags can login",
"The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, ein neues Konto zu registrieren", "The application does not allow to sign up new account": "Die Anwendung erlaubt es nicht, ein neues Konto zu registrieren",
@ -121,6 +123,10 @@
"Token fields - Tooltip": "Token fields - Tooltip", "Token fields - Tooltip": "Token fields - Tooltip",
"Token format": "Token-Format", "Token format": "Token-Format",
"Token format - Tooltip": "Das Format des Access-Tokens", "Token format - Tooltip": "Das Format des Access-Tokens",
"Token signing method": "Token signing method",
"Token signing method - Tooltip": "Signing method of JWT token, needs to be the same algorithm as the certificate",
"Use Email as NameID": "Use Email as NameID",
"Use Email as NameID - Tooltip": "Use Email as NameID - Tooltip",
"You are unexpected to see this prompt page": "Sie sind unerwartet auf diese Aufforderungsseite gelangt" "You are unexpected to see this prompt page": "Sie sind unerwartet auf diese Aufforderungsseite gelangt"
}, },
"cert": { "cert": {
@ -177,6 +183,7 @@
"Access key - Tooltip": "Access key - Tooltip", "Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret", "Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip", "Access secret - Tooltip": "Access secret - Tooltip",
"Access token is empty": "Access token is empty",
"Action": "Aktion", "Action": "Aktion",
"Adapter": "Adapter", "Adapter": "Adapter",
"Adapter - Tooltip": "Tabellenname des Policy Stores", "Adapter - Tooltip": "Tabellenname des Policy Stores",
@ -234,6 +241,8 @@
"Enable": "Enable", "Enable": "Enable",
"Enable dark logo": "Enable dark logo", "Enable dark logo": "Enable dark logo",
"Enable dark logo - Tooltip": "Enable dark logo", "Enable dark logo - Tooltip": "Enable dark logo",
"Enable tour": "Enable tour",
"Enable tour - Tooltip": "Display tour for users",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers", "Enforcers": "Enforcers",
@ -265,6 +274,8 @@
"Invitations": "Invitations", "Invitations": "Invitations",
"Is enabled": "Ist aktiviert", "Is enabled": "Ist aktiviert",
"Is enabled - Tooltip": "Festlegen, ob es verwendet werden kann", "Is enabled - Tooltip": "Festlegen, ob es verwendet werden kann",
"Is shared": "Is shared",
"Is shared - Tooltip": "Share this application with other organizations",
"LDAPs": "LDAPs", "LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAP-Server", "LDAPs - Tooltip": "LDAP-Server",
"Languages": "Sprachen", "Languages": "Sprachen",
@ -328,6 +339,8 @@
"Provider - Tooltip": "Zahlungsprovider, die konfiguriert werden müssen, inkl. PayPal, Alipay, WeChat Pay usw.", "Provider - Tooltip": "Zahlungsprovider, die konfiguriert werden müssen, inkl. PayPal, Alipay, WeChat Pay usw.",
"Providers": "Provider", "Providers": "Provider",
"Providers - Tooltip": "Provider, die konfiguriert werden müssen, einschließlich Drittanbieter-Logins, Objektspeicherung, Verifizierungscode usw.", "Providers - Tooltip": "Provider, die konfiguriert werden müssen, einschließlich Drittanbieter-Logins, Objektspeicherung, Verifizierungscode usw.",
"QR Code": "QR Code",
"QR code is too large": "QR code is too large",
"Real name": "Echter Name", "Real name": "Echter Name",
"Records": "Datensätze", "Records": "Datensätze",
"Request URI": "Request URI", "Request URI": "Request URI",
@ -441,6 +454,8 @@
"Base DN": "Basis-DN", "Base DN": "Basis-DN",
"Base DN - Tooltip": "Basis-DN während der LDAP-Suche", "Base DN - Tooltip": "Basis-DN während der LDAP-Suche",
"CN": "KN", "CN": "KN",
"Default group": "Default group",
"Default group - Tooltip": "Group to which users belong after synchronization",
"Edit LDAP": "LDAP bearbeiten", "Edit LDAP": "LDAP bearbeiten",
"Enable SSL": "Aktivieren Sie SSL", "Enable SSL": "Aktivieren Sie SSL",
"Enable SSL - Tooltip": "Ob SSL aktiviert werden soll", "Enable SSL - Tooltip": "Ob SSL aktiviert werden soll",
@ -470,6 +485,7 @@
"Face ID": "Face ID", "Face ID": "Face ID",
"Face Recognition": "Face Recognition", "Face Recognition": "Face Recognition",
"Face recognition failed": "Face recognition failed", "Face recognition failed": "Face recognition failed",
"Failed to log out": "Failed to log out",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization", "Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Failed to obtain Web3-Onboard authorization": "Failed to obtain Web3-Onboard authorization", "Failed to obtain Web3-Onboard authorization": "Failed to obtain Web3-Onboard authorization",
"Forgot password?": "Passwort vergessen?", "Forgot password?": "Passwort vergessen?",
@ -551,6 +567,11 @@
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"
}, },
"mfaAccount": {
"Account Name": "Account Name",
"Issuer": "Issuer",
"Secret Key": "Secret Key"
},
"model": { "model": {
"Edit Model": "Modell bearbeiten", "Edit Model": "Modell bearbeiten",
"Model text": "Modelltext", "Model text": "Modelltext",
@ -818,6 +839,7 @@
"Project Id": "Project Id", "Project Id": "Project Id",
"Project Id - Tooltip": "Project Id - Tooltip", "Project Id - Tooltip": "Project Id - Tooltip",
"Prompted": "ausgelöst", "Prompted": "ausgelöst",
"Provider - Tooltip": "Provider - Tooltip",
"Provider URL": "Anbieter-URL", "Provider URL": "Anbieter-URL",
"Provider URL - Tooltip": "URL zur Konfiguration des Dienstanbieters, dieses Feld dient nur als Referenz und wird in Casdoor nicht verwendet", "Provider URL - Tooltip": "URL zur Konfiguration des Dienstanbieters, dieses Feld dient nur als Referenz und wird in Casdoor nicht verwendet",
"Public key": "Public key", "Public key": "Public key",
@ -937,6 +959,7 @@
"Have account?": "Haben Sie ein Konto?", "Have account?": "Haben Sie ein Konto?",
"Label": "Label", "Label": "Label",
"Label HTML": "Label HTML", "Label HTML": "Label HTML",
"Options": "Options",
"Placeholder": "Placeholder", "Placeholder": "Placeholder",
"Please accept the agreement!": "Bitte akzeptieren Sie die Vereinbarung!", "Please accept the agreement!": "Bitte akzeptieren Sie die Vereinbarung!",
"Please click the below button to sign in": "Bitte klicken Sie auf den untenstehenden Button, um sich anzumelden", "Please click the below button to sign in": "Bitte klicken Sie auf den untenstehenden Button, um sich anzumelden",
@ -1136,6 +1159,7 @@
"Link": "Link", "Link": "Link",
"Location": "Ort", "Location": "Ort",
"Location - Tooltip": "Stadt des Wohnsitzes", "Location - Tooltip": "Stadt des Wohnsitzes",
"MFA accounts": "MFA accounts",
"Managed accounts": "Verwaltete Konten", "Managed accounts": "Verwaltete Konten",
"Modify password...": "Passwort ändern...", "Modify password...": "Passwort ändern...",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Multi-factor authentication",

View File

@ -74,6 +74,7 @@
"Left": "Left", "Left": "Left",
"Logged in successfully": "Logged in successfully", "Logged in successfully": "Logged in successfully",
"Logged out successfully": "Logged out successfully", "Logged out successfully": "Logged out successfully",
"Multiple Choices": "Multiple Choices",
"New Application": "New Application", "New Application": "New Application",
"No verification": "No verification", "No verification": "No verification",
"Normal": "Normal", "Normal": "Normal",
@ -112,6 +113,7 @@
"Signin session": "Signin session", "Signin session": "Signin session",
"Signup items": "Signup items", "Signup items": "Signup items",
"Signup items - Tooltip": "Items for users to fill in when registering new accounts", "Signup items - Tooltip": "Items for users to fill in when registering new accounts",
"Single Choice": "Single Choice",
"Small icon": "Small icon", "Small icon": "Small icon",
"Tags - Tooltip": "Only users with the tag that is listed in the application tags can login", "Tags - Tooltip": "Only users with the tag that is listed in the application tags can login",
"The application does not allow to sign up new account": "The application does not allow to sign up new account", "The application does not allow to sign up new account": "The application does not allow to sign up new account",
@ -121,6 +123,10 @@
"Token fields - Tooltip": "The user fields included in the token", "Token fields - Tooltip": "The user fields included in the token",
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "The format of access token", "Token format - Tooltip": "The format of access token",
"Token signing method": "Token signing method",
"Token signing method - Tooltip": "Signing method of JWT token, needs to be the same algorithm as the certificate",
"Use Email as NameID": "Use Email as NameID",
"Use Email as NameID - Tooltip": "Use Email as NameID - Tooltip",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page" "You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
}, },
"cert": { "cert": {
@ -177,6 +183,7 @@
"Access key - Tooltip": "Access key - Tooltip", "Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret", "Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip", "Access secret - Tooltip": "Access secret - Tooltip",
"Access token is empty": "Access token is empty",
"Action": "Action", "Action": "Action",
"Adapter": "Adapter", "Adapter": "Adapter",
"Adapter - Tooltip": "Table name of the policy store", "Adapter - Tooltip": "Table name of the policy store",
@ -234,6 +241,8 @@
"Enable": "Enable", "Enable": "Enable",
"Enable dark logo": "Enable dark logo", "Enable dark logo": "Enable dark logo",
"Enable dark logo - Tooltip": "Enable dark logo", "Enable dark logo - Tooltip": "Enable dark logo",
"Enable tour": "Enable tour",
"Enable tour - Tooltip": "Display tour for users",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers", "Enforcers": "Enforcers",
@ -265,6 +274,8 @@
"Invitations": "Invitations", "Invitations": "Invitations",
"Is enabled": "Is enabled", "Is enabled": "Is enabled",
"Is enabled - Tooltip": "Set whether it can use", "Is enabled - Tooltip": "Set whether it can use",
"Is shared": "Is shared",
"Is shared - Tooltip": "Share this application with other organizations",
"LDAPs": "LDAPs", "LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAP servers", "LDAPs - Tooltip": "LDAP servers",
"Languages": "Languages", "Languages": "Languages",
@ -328,6 +339,8 @@
"Provider - Tooltip": "Payment providers to be configured, including PayPal, Alipay, WeChat Pay, etc.", "Provider - Tooltip": "Payment providers to be configured, including PayPal, Alipay, WeChat Pay, etc.",
"Providers": "Providers", "Providers": "Providers",
"Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.", "Providers - Tooltip": "Providers to be configured, including 3rd-party login, object storage, verification code, etc.",
"QR Code": "QR Code",
"QR code is too large": "QR code is too large",
"Real name": "Real name", "Real name": "Real name",
"Records": "Records", "Records": "Records",
"Request URI": "Request URI", "Request URI": "Request URI",
@ -441,6 +454,8 @@
"Base DN": "Base DN", "Base DN": "Base DN",
"Base DN - Tooltip": "Base DN during LDAP search", "Base DN - Tooltip": "Base DN during LDAP search",
"CN": "CN", "CN": "CN",
"Default group": "Default group",
"Default group - Tooltip": "Group to which users belong after synchronization",
"Edit LDAP": "Edit LDAP", "Edit LDAP": "Edit LDAP",
"Enable SSL": "Enable SSL", "Enable SSL": "Enable SSL",
"Enable SSL - Tooltip": "Whether to enable SSL", "Enable SSL - Tooltip": "Whether to enable SSL",
@ -470,6 +485,7 @@
"Face ID": "Face ID", "Face ID": "Face ID",
"Face Recognition": "Face Recognition", "Face Recognition": "Face Recognition",
"Face recognition failed": "Face recognition failed", "Face recognition failed": "Face recognition failed",
"Failed to log out": "Failed to log out",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization", "Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Failed to obtain Web3-Onboard authorization": "Failed to obtain Web3-Onboard authorization", "Failed to obtain Web3-Onboard authorization": "Failed to obtain Web3-Onboard authorization",
"Forgot password?": "Forgot password?", "Forgot password?": "Forgot password?",
@ -551,6 +567,11 @@
"Your phone is": "Your phone is", "Your phone is": "Your phone is",
"preferred": "preferred" "preferred": "preferred"
}, },
"mfaAccount": {
"Account Name": "Account Name",
"Issuer": "Issuer",
"Secret Key": "Secret Key"
},
"model": { "model": {
"Edit Model": "Edit Model", "Edit Model": "Edit Model",
"Model text": "Model text", "Model text": "Model text",
@ -818,6 +839,7 @@
"Project Id": "Project Id", "Project Id": "Project Id",
"Project Id - Tooltip": "Project Id - Tooltip", "Project Id - Tooltip": "Project Id - Tooltip",
"Prompted": "Prompted", "Prompted": "Prompted",
"Provider - Tooltip": "Provider - Tooltip",
"Provider URL": "Provider URL", "Provider URL": "Provider URL",
"Provider URL - Tooltip": "URL for configuring the service provider, this field is only used for reference and is not used in Casdoor", "Provider URL - Tooltip": "URL for configuring the service provider, this field is only used for reference and is not used in Casdoor",
"Public key": "Public key", "Public key": "Public key",
@ -937,6 +959,7 @@
"Have account?": "Have account?", "Have account?": "Have account?",
"Label": "Label", "Label": "Label",
"Label HTML": "Label HTML", "Label HTML": "Label HTML",
"Options": "Options",
"Placeholder": "Placeholder", "Placeholder": "Placeholder",
"Please accept the agreement!": "Please accept the agreement!", "Please accept the agreement!": "Please accept the agreement!",
"Please click the below button to sign in": "Please click the below button to sign in", "Please click the below button to sign in": "Please click the below button to sign in",
@ -1136,6 +1159,7 @@
"Link": "Link", "Link": "Link",
"Location": "Location", "Location": "Location",
"Location - Tooltip": "City of residence", "Location - Tooltip": "City of residence",
"MFA accounts": "MFA accounts",
"Managed accounts": "Managed accounts", "Managed accounts": "Managed accounts",
"Modify password...": "Modify password...", "Modify password...": "Modify password...",
"Multi-factor authentication": "Multi-factor authentication", "Multi-factor authentication": "Multi-factor authentication",

Some files were not shown because too many files have changed in this diff Show More