Compare commits

...

27 Commits

Author SHA1 Message Date
2da597b26f feat: add support for per-account MFA validity period in org setting to reduce repeated prompts (#3917) 2025-07-11 00:24:33 +08:00
ef14c84edc feat: show the popover on the top when window's width too small and close popover when password options is empty (#3952) 2025-07-10 19:56:05 +08:00
cb5c7667b5 feat: change Subscription's StartTime and EndTime to string 2025-07-10 14:11:40 +08:00
920ed87f75 fix: refactor the code in CheckPassword() 2025-07-10 00:49:13 +08:00
6598f0ccdf feat: use token's client ID instead in IntrospectToken() API (#3948) 2025-07-09 22:07:44 +08:00
8e71e23d75 feat: improve error message for GetConfigInt64() 2025-07-09 00:32:00 +08:00
146a369f80 feat: improve error handling in AutoSigninFilter 2025-07-08 23:47:14 +08:00
9bbe5afb7c feat: use only one salt arg in CredManager.IsPasswordCorrect() (#3936) 2025-07-07 17:56:25 +08:00
b42391c6ce feat: move needUpdatePassword to response's Data3 field to avoid refresh token conflict (#3931) 2025-07-05 22:48:44 +08:00
fb035a5353 feat: CredManager.GetHashedPassword() only contains one salt arg now (#3928) 2025-07-05 18:41:37 +08:00
b1f68a60a4 feat: set createDatabase to false in TestDumpToFile() (#3924) 2025-07-03 22:50:23 +08:00
201d704a31 feat: improve TikTok username generation logic (#3923) 2025-07-03 20:53:15 +08:00
bf91ad6c97 feat: add Internet-Only captcha rule (#3919) 2025-07-03 02:39:06 +08:00
3ccc0339c7 feat: improve CheckToEnableCaptcha() logic 2025-07-03 02:32:07 +08:00
1f2b0a3587 feat: add user's MFA items (#3921) 2025-07-02 23:05:07 +08:00
0b3feb0d5f feat: use Input.OTP to input totp code (#3922) 2025-07-02 18:22:59 +08:00
568c0e2c3d feat: show Organization.PasswordOptions in login UI (#3913) 2025-06-28 22:13:00 +08:00
f4ad2b4034 feat: remove "@" from name's forbidden chars 2025-06-27 18:41:50 +08:00
c9f8727890 feat: fix bug in InitCleanupTokens() (#3910) 2025-06-27 02:08:18 +08:00
e2e3c1fbb8 feat: support Product.SuccessUrl (#3908) 2025-06-26 22:52:07 +08:00
73915ac0a0 feat: fix issue that LDAP user address was not syncing (#3905) 2025-06-26 09:38:16 +08:00
bf9d55ff40 feat: add InitCleanupTokens() (#3903) 2025-06-26 09:31:59 +08:00
b36fb50239 feat: fix check bug to allow logged-in users to buy product (#3897) 2025-06-25 10:49:20 +08:00
4307baa759 feat: fix Tumblr OAuth's wrong scope (#3898) 2025-06-25 09:55:02 +08:00
3964bae1df feat: fix org's LDAP table wrong link (#3900) 2025-06-25 09:51:40 +08:00
d9b97d70be feat: change CRLF to LF for some files 2025-06-24 09:55:00 +08:00
ca224fdd4c feat: add group xlsx upload button (#3885) 2025-06-17 23:43:38 +08:00
129 changed files with 2103 additions and 1412 deletions

8
.gitattributes vendored
View File

@ -1,5 +1,5 @@
*.go linguist-detectable=true
*.js linguist-detectable=false
# Declare files that will always have LF line endings on checkout.
# Git will always convert line endings to LF on checkout. You should use this for files that must keep LF endings, even on Windows.
*.go linguist-detectable=true
*.js linguist-detectable=false
# Declare files that will always have LF line endings on checkout.
# Git will always convert line endings to LF on checkout. You should use this for files that must keep LF endings, even on Windows.
*.sh text eol=lf

204
README.md
View File

@ -1,102 +1,102 @@
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
<p align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a>
<a href="https://hub.docker.com/r/casbin/casdoor">
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
</a>
<a href="https://github.com/casdoor/casdoor/actions/workflows/build.yml">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casdoor/casdoor/workflows/Build/badge.svg?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casdoor/casdoor.svg">
</a>
<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">
</a>
</p>
<p align="center">
<a href="https://goreportcard.com/report/github.com/casdoor/casdoor">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casdoor/casdoor?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/casdoor/casdoor?style=flat-square" alt="license">
</a>
<a href="https://github.com/casdoor/casdoor/issues">
<img alt="GitHub issues" src="https://img.shields.io/github/issues/casdoor/casdoor?style=flat-square">
</a>
<a href="#">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/casdoor/casdoor?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/network">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/casdoor/casdoor?style=flat-square">
</a>
<a href="https://crowdin.com/project/casdoor-site">
<img alt="Crowdin" src="https://badges.crowdin.net/casdoor-site/localized.svg">
</a>
<a href="https://discord.gg/5rPsrAzK7S">
<img alt="Discord" src="https://img.shields.io/discord/1022748306096537660?style=flat-square&logo=discord&label=discord&color=5865F2">
</a>
</p>
<p align="center">
<sup>Sponsored by</sup>
<br>
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.casbin.org/img/stytch-white.png">
<source media="(prefers-color-scheme: light)" srcset="https://cdn.casbin.org/img/stytch-charcoal.png">
<img src="https://cdn.casbin.org/img/stytch-charcoal.png" width="275">
</picture>
</a><br/>
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin"><b>Build auth with fraud prevention, faster.</b><br/> Try Stytch for API-first authentication, user & org management, multi-tenant SSO, MFA, device fingerprinting, and more.</a>
<br>
</p>
## Online demo
- Read-only site: https://door.casdoor.com (any modification operation will fail)
- Writable site: https://demo.casdoor.com (original data will be restored for every 5 minutes)
## Documentation
https://casdoor.org
## Install
- By source code: https://casdoor.org/docs/basic/server-installation
- By Docker: https://casdoor.org/docs/basic/try-with-docker
- By Kubernetes Helm: https://casdoor.org/docs/basic/try-with-helm
## How to connect to Casdoor?
https://casdoor.org/docs/how-to-connect/overview
## Casdoor Public API
- Docs: https://casdoor.org/docs/basic/public-api
- Swagger: https://door.casdoor.com/swagger
## Integrations
https://casdoor.org/docs/category/integrations
## How to contact?
- Discord: https://discord.gg/5rPsrAzK7S
- Contact: https://casdoor.org/help
## Contribute
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
### I18n translation
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the `web/` directory, please remember to add what you have added to the `web/src/locales/en/data.json` file.
## License
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
<p align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a>
<a href="https://hub.docker.com/r/casbin/casdoor">
<img alt="docker pull casbin/casdoor" src="https://img.shields.io/docker/pulls/casbin/casdoor.svg">
</a>
<a href="https://github.com/casdoor/casdoor/actions/workflows/build.yml">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casdoor/casdoor/workflows/Build/badge.svg?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casdoor/casdoor.svg">
</a>
<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">
</a>
</p>
<p align="center">
<a href="https://goreportcard.com/report/github.com/casdoor/casdoor">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casdoor/casdoor?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/casdoor/casdoor?style=flat-square" alt="license">
</a>
<a href="https://github.com/casdoor/casdoor/issues">
<img alt="GitHub issues" src="https://img.shields.io/github/issues/casdoor/casdoor?style=flat-square">
</a>
<a href="#">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/casdoor/casdoor?style=flat-square">
</a>
<a href="https://github.com/casdoor/casdoor/network">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/casdoor/casdoor?style=flat-square">
</a>
<a href="https://crowdin.com/project/casdoor-site">
<img alt="Crowdin" src="https://badges.crowdin.net/casdoor-site/localized.svg">
</a>
<a href="https://discord.gg/5rPsrAzK7S">
<img alt="Discord" src="https://img.shields.io/discord/1022748306096537660?style=flat-square&logo=discord&label=discord&color=5865F2">
</a>
</p>
<p align="center">
<sup>Sponsored by</sup>
<br>
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.casbin.org/img/stytch-white.png">
<source media="(prefers-color-scheme: light)" srcset="https://cdn.casbin.org/img/stytch-charcoal.png">
<img src="https://cdn.casbin.org/img/stytch-charcoal.png" width="275">
</picture>
</a><br/>
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin"><b>Build auth with fraud prevention, faster.</b><br/> Try Stytch for API-first authentication, user & org management, multi-tenant SSO, MFA, device fingerprinting, and more.</a>
<br>
</p>
## Online demo
- Read-only site: https://door.casdoor.com (any modification operation will fail)
- Writable site: https://demo.casdoor.com (original data will be restored for every 5 minutes)
## Documentation
https://casdoor.org
## Install
- By source code: https://casdoor.org/docs/basic/server-installation
- By Docker: https://casdoor.org/docs/basic/try-with-docker
- By Kubernetes Helm: https://casdoor.org/docs/basic/try-with-helm
## How to connect to Casdoor?
https://casdoor.org/docs/how-to-connect/overview
## Casdoor Public API
- Docs: https://casdoor.org/docs/basic/public-api
- Swagger: https://door.casdoor.com/swagger
## Integrations
https://casdoor.org/docs/category/integrations
## How to contact?
- Discord: https://discord.gg/5rPsrAzK7S
- Contact: https://casdoor.org/help
## Contribute
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
### I18n translation
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-site) as translating platform and i18next as translating tool. When you add some words using i18next in the `web/` directory, please remember to add what you have added to the `web/src/locales/en/data.json` file.
## License
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)

View File

@ -66,7 +66,11 @@ func GetConfigBool(key string) bool {
func GetConfigInt64(key string) (int64, error) {
value := GetConfigString(key)
num, err := strconv.ParseInt(value, 10, 64)
return num, err
if err != nil {
return 0, fmt.Errorf("GetConfigInt64(%s) error, %s", key, err.Error())
}
return num, nil
}
func GetConfigDataSourceName() string {

View File

@ -42,6 +42,7 @@ type Response struct {
Name string `json:"name"`
Data interface{} `json:"data"`
Data2 interface{} `json:"data2"`
Data3 interface{} `json:"data3"`
}
type Captcha struct {

View File

@ -132,7 +132,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
resp = &Response{Status: "ok", Msg: "", Data: userId, Data3: user.NeedUpdatePassword}
} else if form.Type == ResponseTypeCode {
clientId := c.Input().Get("clientId")
responseType := c.Input().Get("responseType")
@ -154,7 +154,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
}
resp = codeToResponse(code)
resp.Data2 = user.NeedUpdatePassword
resp.Data3 = user.NeedUpdatePassword
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
@ -168,7 +168,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
token, _ := object.GetTokenByUser(application, user, scope, nonce, c.Ctx.Request.Host)
resp = tokenToResponse(token)
resp.Data2 = user.NeedUpdatePassword
resp.Data3 = user.NeedUpdatePassword
}
} else if form.Type == ResponseTypeDevice {
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
@ -195,14 +195,14 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
object.DeviceAuthMap.Store(authCacheCast.UserName, deviceAuthCacheDeviceCodeCast)
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
resp = &Response{Status: "ok", Msg: "", Data: userId, Data3: user.NeedUpdatePassword}
} else if form.Type == ResponseTypeSaml { // saml flow
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error(), nil)
return
}
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]interface{}{"redirectUrl": redirectUrl, "method": method, "needUpdatePassword": user.NeedUpdatePassword}}
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]interface{}{"redirectUrl": redirectUrl, "method": method}, Data3: user.NeedUpdatePassword}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
@ -355,20 +355,27 @@ func isProxyProviderType(providerType string) bool {
func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
if object.IsNeedPromptMfa(organization, user) {
// The prompt page needs the user to be srigned in
// The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId())
c.ResponseOk(object.RequiredMfa)
return true
}
if user.IsMfaEnabled() {
currentTime := util.String2Time(util.GetCurrentTime())
mfaRememberDeadline := util.String2Time(user.MfaRememberDeadline)
if user.MfaRememberDeadline != "" && mfaRememberDeadline.After(currentTime) {
return false
}
c.setMfaUserSession(user.GetId())
mfaList := object.GetAllMfaProps(user, true)
mfaAllowList := []*object.MfaProps{}
mfaRememberInHours := organization.MfaRememberInHours
for _, prop := range mfaList {
if prop.MfaType == verificationType || !prop.Enabled {
continue
}
prop.MfaRememberInHours = mfaRememberInHours
mfaAllowList = append(mfaAllowList, prop)
}
if len(mfaAllowList) >= 1 {
@ -555,8 +562,11 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application"))
return
}
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
var enableCaptcha bool
if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); err != nil {
if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username, clientIp); err != nil {
c.ResponseError(err.Error())
return
} else if enableCaptcha {
@ -970,6 +980,28 @@ func (c *ApiController) Login() {
return
}
var application *object.Application
if authForm.ClientId == "" {
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
} else {
application, err = object.GetApplicationByClientId(authForm.ClientId)
}
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
var organization *object.Organization
organization, err = object.GetOrganization(util.GetId("admin", application.Organization))
if err != nil {
c.ResponseError(c.T(err.Error()))
}
if authForm.Passcode != "" {
if authForm.MfaType == c.GetSession("verificationCodeType") {
c.ResponseError("Invalid multi-factor authentication type")
@ -996,6 +1028,17 @@ func (c *ApiController) Login() {
}
}
if authForm.EnableMfaRemember {
mfaRememberInSeconds := organization.MfaRememberInHours * 3600
currentTime := util.String2Time(util.GetCurrentTime())
duration := time.Duration(mfaRememberInSeconds) * time.Second
user.MfaRememberDeadline = util.Time2String(currentTime.Add(duration))
_, err = object.UpdateUser(user.GetId(), user, []string{"mfa_remember_deadline"}, user.IsAdmin)
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.SetSession("verificationCodeType", "")
} else if authForm.RecoveryCode != "" {
err = object.MfaRecover(user, authForm.RecoveryCode)
@ -1008,22 +1051,6 @@ func (c *ApiController) Login() {
return
}
var application *object.Application
if authForm.ClientId == "" {
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
} else {
application, err = object.GetApplicationByClientId(authForm.ClientId)
}
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
c.setMfaUserSession("")
@ -1222,27 +1249,26 @@ func (c *ApiController) GetQRCode() {
func (c *ApiController) GetCaptchaStatus() {
organization := c.Input().Get("organization")
userId := c.Input().Get("userId")
user, err := object.GetUserByFields(organization, userId)
applicationName := c.Input().Get("application")
application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName))
if err != nil {
c.ResponseError(err.Error())
return
}
captchaEnabled := false
if user != nil {
var failedSigninLimit int
failedSigninLimit, _, err = object.GetFailedSigninConfigByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
if user.SigninWrongTimes >= failedSigninLimit {
captchaEnabled = true
}
if application == nil {
c.ResponseError("application not found")
return
}
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
captchaEnabled, err := object.CheckToEnableCaptcha(application, organization, userId, clientIp)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(captchaEnabled)
return
}
// Callback

View File

@ -0,0 +1,56 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"fmt"
"os"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
func (c *ApiController) UploadGroups() {
userId := c.GetSessionUsername()
owner, user := util.GetOwnerAndNameFromId(userId)
file, header, err := c.Ctx.Request.FormFile("file")
if err != nil {
c.ResponseError(err.Error())
return
}
fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
path := util.GetUploadXlsxPath(fileId)
defer os.Remove(path)
err = saveFile(path, &file)
if err != nil {
c.ResponseError(err.Error())
return
}
affected, err := object.UploadGroups(owner, path)
if err != nil {
c.ResponseError(err.Error())
return
}
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("general:Failed to import groups"))
}
}

View File

@ -58,6 +58,12 @@ func (c *ApiController) MfaSetupInitiate() {
return
}
organization, err := object.GetOrganizationByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
mfaProps, err := MfaUtil.Initiate(user.GetId())
if err != nil {
c.ResponseError(err.Error())
@ -66,6 +72,7 @@ func (c *ApiController) MfaSetupInitiate() {
recoveryCode := uuid.NewString()
mfaProps.RecoveryCodes = []string{recoveryCode}
mfaProps.MfaRememberInHours = organization.MfaRememberInHours
resp := mfaProps
c.ResponseOk(resp)

View File

@ -98,6 +98,10 @@ func (c *ApiController) GetOrganization() {
return
}
if organization != nil && organization.MfaRememberInHours == 0 {
organization.MfaRememberInHours = 12
}
c.ResponseOk(organization)
}

View File

@ -49,6 +49,6 @@ func (c *ApiController) UploadPermissions() {
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("user_upload:Failed to import users"))
c.ResponseError(c.T("general:Failed to import users"))
}
}

View File

@ -182,7 +182,7 @@ func (c *ApiController) BuyProduct() {
paidUserName := c.Input().Get("userName")
owner, _ := util.GetOwnerAndNameFromId(id)
userId := util.GetId(owner, paidUserName)
if paidUserName != "" && !c.IsAdmin() {
if paidUserName != "" && paidUserName != c.GetSessionUsername() && !c.IsAdmin() {
c.ResponseError(c.T("general:Only admin user can specify user"))
return
}

View File

@ -49,6 +49,6 @@ func (c *ApiController) UploadRoles() {
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("user_upload:Failed to import users"))
c.ResponseError(c.T("general:Failed to import users"))
}
}

View File

@ -16,6 +16,7 @@ package controllers
import (
"encoding/json"
"fmt"
"time"
"github.com/beego/beego/utils/pagination"
@ -460,7 +461,18 @@ func (c *ApiController) IntrospectToken() {
}
if token != nil {
application, err = object.GetApplication(fmt.Sprintf("%s/%s", token.Owner, token.Application))
if err != nil {
c.ResponseTokenError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), token.Application))
return
}
introspectionResponse.TokenType = token.TokenType
introspectionResponse.ClientId = application.ClientId
}
c.Data["json"] = introspectionResponse

View File

@ -574,7 +574,7 @@ func (c *ApiController) SetPassword() {
targetUser.LastChangePasswordTime = util.GetCurrentTime()
if user.Ldap == "" {
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
_, err = object.UpdateUser(userId, targetUser, []string{"password", "password_salt", "need_update_password", "password_type", "last_change_password_time"}, false)
} else {
if isAdmin {
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())

View File

@ -67,6 +67,6 @@ func (c *ApiController) UploadUsers() {
if affected {
c.ResponseOk()
} else {
c.ResponseError(c.T("user_upload:Failed to import users"))
c.ResponseError(c.T("general:Failed to import users"))
}
}

View File

@ -23,7 +23,7 @@ func NewArgon2idCredManager() *Argon2idCredManager {
return cm
}
func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *Argon2idCredManager) GetHashedPassword(password string, salt string) string {
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
return ""
@ -31,7 +31,7 @@ func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt strin
return hash
}
func (cm *Argon2idCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
func (cm *Argon2idCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
match, _ := argon2id.ComparePasswordAndHash(plainPwd, hashedPwd)
return match
}

View File

@ -9,7 +9,7 @@ func NewBcryptCredManager() *BcryptCredManager {
return cm
}
func (cm *BcryptCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *BcryptCredManager) GetHashedPassword(password string, salt string) string {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return ""
@ -17,7 +17,7 @@ func (cm *BcryptCredManager) GetHashedPassword(password string, userSalt string,
return string(bytes)
}
func (cm *BcryptCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
func (cm *BcryptCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(plainPwd))
return err == nil
}

View File

@ -15,8 +15,8 @@
package cred
type CredManager interface {
GetHashedPassword(password string, userSalt string, organizationSalt string) string
IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool
GetHashedPassword(password string, salt string) string
IsPasswordCorrect(password string, passwordHash string, salt string) bool
}
func GetCredManager(passwordType string) CredManager {

View File

@ -37,14 +37,10 @@ func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
return cm
}
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
res := getMd5HexDigest(password)
if userSalt != "" {
res = getMd5HexDigest(res + userSalt)
}
return res
func (cm *Md5UserSaltCredManager) GetHashedPassword(password string, salt string) string {
return getMd5HexDigest(getMd5HexDigest(password) + salt)
}
func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
func (cm *Md5UserSaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
}

View File

@ -28,13 +28,13 @@ func NewPbkdf2SaltCredManager() *Pbkdf2SaltCredManager {
return cm
}
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, salt string) string {
// https://www.keycloak.org/docs/latest/server_admin/index.html#password-database-compromised
decodedSalt, _ := base64.StdEncoding.DecodeString(userSalt)
decodedSalt, _ := base64.StdEncoding.DecodeString(salt)
res := pbkdf2.Key([]byte(password), decodedSalt, 27500, 64, sha256.New)
return base64.StdEncoding.EncodeToString(res)
}
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
}

View File

@ -32,12 +32,8 @@ func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
return cm
}
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, salt string) string {
iterations := 260000
salt := userSalt
if salt == "" {
salt = organizationSalt
}
saltBytes := []byte(salt)
passwordBytes := []byte(password)
@ -46,7 +42,7 @@ func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt st
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
}
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool {
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, _salt string) bool {
parts := strings.Split(passwordHash, "$")
if len(parts) != 4 {
return false

View File

@ -21,10 +21,10 @@ func NewPlainCredManager() *PlainCredManager {
return cm
}
func (cm *PlainCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
func (cm *PlainCredManager) GetHashedPassword(password string, salt string) string {
return password
}
func (cm *PlainCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
func (cm *PlainCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == plainPwd
}

View File

@ -37,14 +37,10 @@ func NewSha256SaltCredManager() *Sha256SaltCredManager {
return cm
}
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
res := getSha256HexDigest(password)
if organizationSalt != "" {
res = getSha256HexDigest(res + organizationSalt)
}
return res
func (cm *Sha256SaltCredManager) GetHashedPassword(password string, salt string) string {
return getSha256HexDigest(getSha256HexDigest(password) + salt)
}
func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
func (cm *Sha256SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
}

View File

@ -23,12 +23,12 @@ func TestGetSaltedPassword(t *testing.T) {
password := "123456"
salt := "123"
cm := NewSha256SaltCredManager()
fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, "", salt))
fmt.Printf("%s -> %s\n", password, cm.GetHashedPassword(password, salt))
}
func TestGetPassword(t *testing.T) {
password := "123456"
cm := NewSha256SaltCredManager()
// https://passwordsgenerator.net/sha256-hash-generator/
fmt.Printf("%s -> %s\n", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", cm.GetHashedPassword(password, "", ""))
fmt.Printf("%s -> %s\n", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", cm.GetHashedPassword(password, ""))
}

View File

@ -37,14 +37,10 @@ func NewSha512SaltCredManager() *Sha512SaltCredManager {
return cm
}
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
res := getSha512HexDigest(password)
if organizationSalt != "" {
res = getSha512HexDigest(res + organizationSalt)
}
return res
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, salt string) string {
return getSha512HexDigest(getSha512HexDigest(password) + salt)
}
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, salt string) bool {
return hashedPwd == cm.GetHashedPassword(plainPwd, salt)
}

View File

@ -61,9 +61,10 @@ type AuthForm struct {
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
MfaType string `json:"mfaType"`
Passcode string `json:"passcode"`
RecoveryCode string `json:"recoveryCode"`
MfaType string `json:"mfaType"`
Passcode string `json:"passcode"`
RecoveryCode string `json:"recoveryCode"`
EnableMfaRemember bool `json:"enableMfaRemember"`
Plan string `json:"plan"`
Pricing string `json:"pricing"`

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Nepodařilo se importovat uživatele",
"Missing parameter": "Chybějící parametr",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Prosím, přihlaste se nejprve",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Nové heslo nemůže obsahovat prázdné místo.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Nepodařilo se importovat uživatele"
},
"util": {
"No application is found for userId: %s": "Pro userId: %s nebyla nalezena žádná aplikace",
"No provider for category: %s is found for application: %s": "Pro kategorii: %s nebyl nalezen žádný poskytovatel pro aplikaci: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Fehler beim Importieren von Benutzern",
"Missing parameter": "Fehlender Parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Bitte zuerst einloggen",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Fehler beim Importieren von Benutzern"
},
"util": {
"No application is found for userId: %s": "Es wurde keine Anwendung für die Benutzer-ID gefunden: %s",
"No provider for category: %s is found for application: %s": "Kein Anbieter für die Kategorie %s gefunden für die Anwendung: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Error al importar usuarios",
"Missing parameter": "Parámetro faltante",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Por favor, inicia sesión primero",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Error al importar usuarios"
},
"util": {
"No application is found for userId: %s": "No se encuentra ninguna aplicación para el Id de usuario: %s",
"No provider for category: %s is found for application: %s": "No se encuentra un proveedor para la categoría: %s para la aplicación: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "عدم موفقیت در وارد کردن کاربران",
"Missing parameter": "پارامتر گمشده",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "لطفاً ابتدا وارد شوید",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "رمز عبور جدید نمی‌تواند حاوی فاصله خالی باشد.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "عدم موفقیت در وارد کردن کاربران"
},
"util": {
"No application is found for userId: %s": "هیچ برنامه‌ای برای userId: %s یافت نشد",
"No provider for category: %s is found for application: %s": "هیچ ارائه‌دهنده‌ای برای دسته‌بندی: %s برای برنامه: %s یافت نشد",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Échec de l'importation des utilisateurs",
"Missing parameter": "Paramètre manquant",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Veuillez d'abord vous connecter",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Échec de l'importation des utilisateurs"
},
"util": {
"No application is found for userId: %s": "Aucune application n'a été trouvée pour l'identifiant d'utilisateur : %s",
"No provider for category: %s is found for application: %s": "Aucun fournisseur pour la catégorie: %s n'est trouvé pour l'application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Gagal mengimpor pengguna",
"Missing parameter": "Parameter hilang",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Silahkan login terlebih dahulu",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Sandi baru tidak boleh mengandung spasi kosong.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Gagal mengimpor pengguna"
},
"util": {
"No application is found for userId: %s": "Tidak ditemukan aplikasi untuk userId: %s",
"No provider for category: %s is found for application: %s": "Tidak ditemukan penyedia untuk kategori: %s untuk aplikasi: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "ユーザーのインポートに失敗しました",
"Missing parameter": "不足しているパラメーター",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "最初にログインしてください",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "新しいパスワードにはスペースを含めることはできません。",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "ユーザーのインポートに失敗しました"
},
"util": {
"No application is found for userId: %s": "ユーザーIDに対するアプリケーションが見つかりません %s",
"No provider for category: %s is found for application: %s": "アプリケーション:%sのカテゴリ%sのプロバイダが見つかりません",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "사용자 가져오기를 실패했습니다",
"Missing parameter": "누락된 매개변수",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "먼저 로그인 하십시오",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "새 비밀번호에는 공백이 포함될 수 없습니다.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "사용자 가져오기를 실패했습니다"
},
"util": {
"No application is found for userId: %s": "어플리케이션을 찾을 수 없습니다. userId: %s",
"No provider for category: %s is found for application: %s": "어플리케이션 %s에서 %s 카테고리를 위한 공급자가 찾을 수 없습니다",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Falha ao importar usuários",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Falha ao importar usuários"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Не удалось импортировать пользователей",
"Missing parameter": "Отсутствующий параметр",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Пожалуйста, сначала войдите в систему",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Новый пароль не может содержать пробелы.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Не удалось импортировать пользователей"
},
"util": {
"No application is found for userId: %s": "Не найдено заявки для пользователя с идентификатором: %s",
"No provider for category: %s is found for application: %s": "Нет провайдера для категории: %s для приложения: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Nepodarilo sa importovať používateľov",
"Missing parameter": "Chýbajúci parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Najskôr sa prosím prihláste",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Nové heslo nemôže obsahovať medzery.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Nepodarilo sa importovať používateľov"
},
"util": {
"No application is found for userId: %s": "Nebola nájdená žiadna aplikácia pre userId: %s",
"No provider for category: %s is found for application: %s": "Pre aplikáciu: %s nebol nájdený žiadny poskytovateľ pre kategóriu: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Yeni şifreniz boşluk karakteri içeremez.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Failed to import users",
"Missing parameter": "Missing parameter",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Please login first",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "New password cannot contain blank space.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "the adapter: %s is not found"
},
"general": {
"Failed to import groups": "Failed to import groups",
"Failed to import users": "Không thể nhập người dùng",
"Missing parameter": "Thiếu tham số",
"Only admin user can specify user": "Only admin user can specify user",
"Please login first": "Vui lòng đăng nhập trước",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng.",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"user_upload": {
"Failed to import users": "Không thể nhập người dùng"
},
"util": {
"No application is found for userId: %s": "Không tìm thấy ứng dụng cho ID người dùng: %s",
"No provider for category: %s is found for application: %s": "Không tìm thấy nhà cung cấp cho danh mục: %s cho ứng dụng: %s",

View File

@ -92,6 +92,8 @@
"the adapter: %s is not found": "适配器: %s 未找到"
},
"general": {
"Failed to import groups": "导入群组失败",
"Failed to import users": "导入用户失败",
"Missing parameter": "缺少参数",
"Only admin user can specify user": "仅管理员用户可以指定用户",
"Please login first": "请先登录",
@ -162,9 +164,6 @@
"New password cannot contain blank space.": "新密码不可以包含空格",
"the user's owner and name should not be empty": "用户的组织和名称不能为空"
},
"user_upload": {
"Failed to import users": "导入用户失败"
},
"util": {
"No application is found for userId: %s": "未找到用户: %s的应用",
"No provider for category: %s is found for application: %s": "未找到类别为: %s的提供商来满足应用: %s",

View File

@ -190,7 +190,7 @@ func (idp *DouyinIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
userInfo := UserInfo{
Id: douyinUserInfo.Data.OpenId,
Username: douyinUserInfo.Data.Nickname,
Username: douyinUserInfo.Data.OpenId,
DisplayName: douyinUserInfo.Data.Nickname,
AvatarUrl: douyinUserInfo.Data.Avatar,
}

View File

@ -45,6 +45,7 @@ func main() {
object.InitUserManager()
object.InitFromFile()
object.InitCasvisorConfig()
object.InitCleanupTokens()
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })

View File

@ -220,10 +220,15 @@ func checkSigninErrorTimes(user *User, lang string) error {
}
func CheckPassword(user *User, password string, lang string, options ...bool) error {
if password == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
}
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// check the login error times
if !enableCaptcha {
err := checkSigninErrorTimes(user, lang)
@ -236,35 +241,31 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
if err != nil {
return err
}
if organization == nil {
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
}
if password == "" {
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
}
passwordType := user.PasswordType
if passwordType == "" {
passwordType = organization.PasswordType
}
credManager := cred.GetCredManager(passwordType)
if credManager != nil {
if organization.MasterPassword != "" {
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
return resetUserSigninErrorTimes(user)
}
}
if credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt, organization.PasswordSalt) {
return resetUserSigninErrorTimes(user)
}
return recordSigninErrorInfo(user, lang, enableCaptcha)
} else {
if credManager == nil {
return fmt.Errorf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
}
if organization.MasterPassword != "" {
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, organization.PasswordSalt) {
return resetUserSigninErrorTimes(user)
}
}
if !credManager.IsPasswordCorrect(password, user.Password, organization.PasswordSalt) && !credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt) {
return recordSigninErrorInfo(user, lang, enableCaptcha)
}
return resetUserSigninErrorTimes(user)
}
func CheckPasswordComplexityByOrg(organization *Organization, password string) string {
@ -593,31 +594,41 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
return ""
}
func CheckToEnableCaptcha(application *Application, organization, username string) (bool, error) {
func CheckToEnableCaptcha(application *Application, organization, username string, clientIp string) (bool, error) {
if len(application.Providers) == 0 {
return false, nil
}
for _, providerItem := range application.Providers {
if providerItem.Provider == nil {
if providerItem.Provider == nil || providerItem.Provider.Category != "Captcha" {
continue
}
if providerItem.Provider.Category == "Captcha" {
if providerItem.Rule == "Dynamic" {
user, err := GetUserByFields(organization, username)
if providerItem.Rule == "Internet-Only" {
if util.IsInternetIp(clientIp) {
return true, nil
}
}
if providerItem.Rule == "Dynamic" {
user, err := GetUserByFields(organization, username)
if err != nil {
return false, err
}
if user != nil {
failedSigninLimit, _, err := GetFailedSigninConfigByUser(user)
if err != nil {
return false, err
}
failedSigninLimit := application.FailedSigninLimit
if failedSigninLimit == 0 {
failedSigninLimit = DefaultFailedSigninLimit
}
return user != nil && user.SigninWrongTimes >= failedSigninLimit, nil
return user.SigninWrongTimes >= failedSigninLimit, nil
}
return providerItem.Rule == "Always", nil
return false, nil
}
return providerItem.Rule == "Always", nil
}
return false, nil

View File

@ -103,7 +103,7 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
func countCreatedBefore(dashboardMapItem DashboardMapItem, before time.Time) int64 {
count := dashboardMapItem.itemCount
for _, e := range dashboardMapItem.dashboardDateItems {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", e.CreatedTime)
createdTime, _ := time.Parse(time.RFC3339, e.CreatedTime)
if createdTime.Before(before) {
count++
}

View File

@ -181,6 +181,41 @@ func AddGroups(groups []*Group) (bool, error) {
return affected != 0, nil
}
func AddGroupsInBatch(groups []*Group) (bool, error) {
if len(groups) == 0 {
return false, nil
}
session := ormer.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return false, err
}
for _, group := range groups {
err = checkGroupName(group.Name)
if err != nil {
return false, err
}
affected, err := session.Insert(group)
if err != nil {
return false, err
}
if affected == 0 {
return false, nil
}
}
err = session.Commit()
if err != nil {
return false, err
}
return true, nil
}
func deleteGroup(group *Group) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil {

61
object/group_upload.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casdoor/casdoor/xlsx"
)
func getGroupMap(owner string) (map[string]*Group, error) {
m := map[string]*Group{}
groups, err := GetGroups(owner)
if err != nil {
return m, err
}
for _, group := range groups {
m[group.GetId()] = group
}
return m, nil
}
func UploadGroups(owner string, path string) (bool, error) {
table := xlsx.ReadXlsxFile(path)
oldGroupMap, err := getGroupMap(owner)
if err != nil {
return false, err
}
transGroups, err := StringArrayToStruct[Group](table)
if err != nil {
return false, err
}
newGroups := []*Group{}
for _, group := range transGroups {
if _, ok := oldGroupMap[group.GetId()]; !ok {
newGroups = append(newGroups, group)
}
}
if len(newGroups) == 0 {
return false, nil
}
return AddGroupsInBatch(newGroups)
}

View File

@ -20,6 +20,7 @@ package object
import "testing"
func TestDumpToFile(t *testing.T) {
createDatabase = false
InitConfig()
err := DumpToFile("./init_data_dump.json")

View File

@ -260,15 +260,15 @@ func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
res := make([]LdapUser, len(users))
for i, user := range users {
res[i] = LdapUser{
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
Uuid: user.GetLdapUuid(),
DisplayName: user.DisplayName,
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
RegisteredAddress: util.ReturnAnyNotEmpty(user.PostalAddress, user.RegisteredAddress),
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
Uuid: user.GetLdapUuid(),
DisplayName: user.DisplayName,
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
Address: util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
}
}
return res

View File

@ -21,13 +21,14 @@ import (
)
type MfaProps struct {
Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"`
MfaType string `json:"mfaType" form:"mfaType"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
RecoveryCodes []string `json:"recoveryCodes,omitempty"`
Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"`
MfaType string `json:"mfaType" form:"mfaType"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
RecoveryCodes []string `json:"recoveryCodes,omitempty"`
MfaRememberInHours int `json:"mfaRememberInHours"`
}
type MfaInterface interface {

View File

@ -84,8 +84,9 @@ type Organization struct {
NavItems []string `xorm:"varchar(1000)" json:"navItems"`
WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberInHours int `json:"mfaRememberInHours"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
}
func GetOrganizationCount(owner, name, field, value string) (int64, error) {
@ -222,7 +223,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
if organization.MasterPassword != "" && organization.MasterPassword != "***" {
credManager := cred.GetCredManager(organization.PasswordType)
if credManager != nil {
hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, "", organization.PasswordSalt)
hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, organization.PasswordSalt)
organization.MasterPassword = hashedPassword
}
}
@ -536,7 +537,13 @@ func IsNeedPromptMfa(org *Organization, user *User) bool {
if org == nil || user == nil {
return false
}
for _, item := range org.MfaItems {
mfaItems := org.MfaItems
if len(user.MfaItems) > 0 {
mfaItems = user.MfaItems
}
for _, item := range mfaItems {
if item.Rule == "Required" {
if item.Name == EmailType && !user.MfaEmailEnabled {
return true

View File

@ -49,17 +49,21 @@ func (plan *Plan) GetId() string {
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
}
func GetDuration(period string) (startTime time.Time, endTime time.Time) {
func getDuration(period string) (string, string, error) {
startTime := time.Now()
var endTime time.Time
if period == PeriodYearly {
startTime = time.Now()
endTime = startTime.AddDate(1, 0, 0)
} else if period == PeriodMonthly {
startTime = time.Now()
endTime = startTime.AddDate(0, 1, 0)
} else {
panic(fmt.Sprintf("invalid period: %s", period))
return "", "", fmt.Errorf("invalid period: %s", period)
}
return
startTimeString := startTime.Format(time.RFC3339)
endTimeString := endTime.Format(time.RFC3339)
return startTimeString, endTimeString, nil
}
func GetPlanCount(owner, field, value string) (int64, error) {

View File

@ -42,6 +42,7 @@ type Product struct {
IsRecharge bool `json:"isRecharge"`
Providers []string `xorm:"varchar(255)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
SuccessUrl string `xorm:"varchar(1000)" json:"successUrl"`
State string `xorm:"varchar(100)" json:"state"`
@ -205,14 +206,24 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if plan == nil {
return nil, nil, fmt.Errorf("the plan: %s does not exist", planName)
}
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
if err != nil {
return nil, nil, err
}
_, err = AddSubscription(sub)
if err != nil {
return nil, nil, err
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
}
if product.SuccessUrl != "" {
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", product.SuccessUrl, owner, paymentName)
}
// Create an order
payReq := &pp.PayReq{
ProviderName: providerName,

View File

@ -48,8 +48,8 @@ type Subscription struct {
Plan string `xorm:"varchar(100)" json:"plan"`
Payment string `xorm:"varchar(100)" json:"payment"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
StartTime string `xorm:"varchar(100)" json:"startTime"`
EndTime string `xorm:"varchar(100)" json:"endTime"`
Period string `xorm:"varchar(100)" json:"period"`
State SubscriptionState `xorm:"varchar(100)" json:"state"`
}
@ -84,9 +84,19 @@ func (sub *Subscription) UpdateState() error {
}
if sub.State == SubStateActive || sub.State == SubStateUpcoming || sub.State == SubStateExpired {
if sub.EndTime.Before(time.Now()) {
startTime, err := time.Parse(time.RFC3339, sub.StartTime)
if err != nil {
return err
}
endTime, err := time.Parse(time.RFC3339, sub.EndTime)
if err != nil {
return err
}
if endTime.Before(time.Now()) {
sub.State = SubStateExpired
} else if sub.StartTime.After(time.Now()) {
} else if startTime.After(time.Now()) {
sub.State = SubStateUpcoming
} else {
sub.State = SubStateActive
@ -103,10 +113,15 @@ func (sub *Subscription) UpdateState() error {
return nil
}
func NewSubscription(owner, userName, planName, paymentName, period string) *Subscription {
startTime, endTime := GetDuration(period)
func NewSubscription(owner, userName, planName, paymentName, period string) (*Subscription, error) {
startTime, endTime, err := getDuration(period)
if err != nil {
return nil, err
}
id := util.GenerateId()[:6]
return &Subscription{
res := &Subscription{
Owner: owner,
Name: "sub_" + id,
DisplayName: "New Subscription - " + id,
@ -121,6 +136,7 @@ func NewSubscription(owner, userName, planName, paymentName, period string) *Sub
Period: period,
State: SubStatePending, // waiting for payment complete
}
return res, nil
}
func GetSubscriptionCount(owner, field, value string) (int64, error) {

93
object/token_cleanup.go Normal file
View File

@ -0,0 +1,93 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/robfig/cron/v3"
)
func CleanupTokens(tokenRetentionIntervalAfterExpiry int) error {
var sessions []*Token
err := ormer.Engine.Find(&sessions)
if err != nil {
return fmt.Errorf("failed to query expired tokens: %w", err)
}
currentTime := time.Now()
deletedCount := 0
for _, session := range sessions {
tokenString := session.AccessToken
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
fmt.Printf("Failed to parse token %s: %v\n", session.Name, err)
continue
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
exp, ok := claims["exp"].(float64)
if !ok {
fmt.Printf("Token %s does not have an 'exp' claim\n", session.Name)
continue
}
expireTime := time.Unix(int64(exp), 0)
tokenAfterExpiry := currentTime.Sub(expireTime).Seconds()
if tokenAfterExpiry > float64(tokenRetentionIntervalAfterExpiry) {
_, err = ormer.Engine.Delete(session)
if err != nil {
return fmt.Errorf("failed to delete expired token %s: %w", session.Name, err)
}
fmt.Printf("[%d] Deleted expired token: %s | Created: %s | Org: %s | App: %s | User: %s\n",
deletedCount, session.Name, session.CreatedTime, session.Organization, session.Application, session.User)
deletedCount++
}
} else {
fmt.Printf("Token %s is not valid\n", session.Name)
}
}
return nil
}
func getTokenRetentionInterval(days int) int {
if days <= 0 {
days = 30
}
return days * 24 * 3600
}
func InitCleanupTokens() {
schedule := "0 0 * * *"
interval := getTokenRetentionInterval(30)
if err := CleanupTokens(interval); err != nil {
fmt.Printf("Error cleaning up tokens at startup: %v\n", err)
}
cronJob := cron.New()
_, err := cronJob.AddFunc(schedule, func() {
if err := CleanupTokens(interval); err != nil {
fmt.Printf("Error cleaning up tokens: %v\n", err)
}
})
if err != nil {
fmt.Printf("Error scheduling token cleanup: %v\n", err)
return
}
cronJob.Start()
}

View File

@ -210,10 +210,12 @@ type User struct {
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
MfaRememberDeadline string `xorm:"varchar(100)" json:"mfaRememberDeadline"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
}
type Userinfo struct {
@ -791,11 +793,11 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist",
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_items", "mfa_remember_deadline",
}
}
if isAdmin {
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance")
columns = append(columns, "name", "id", "email", "phone", "country_code", "type", "balance", "mfa_items")
}
columns = append(columns, "updated_time")

View File

@ -42,8 +42,9 @@ func (user *User) UpdateUserHash() error {
func (user *User) UpdateUserPassword(organization *Organization) {
credManager := cred.GetCredManager(organization.PasswordType)
if credManager != nil {
hashedPassword := credManager.GetHashedPassword(user.Password, user.PasswordSalt, organization.PasswordSalt)
hashedPassword := credManager.GetHashedPassword(user.Password, organization.PasswordSalt)
user.Password = hashedPassword
user.PasswordType = organization.PasswordType
user.PasswordSalt = organization.PasswordSalt
}
}

View File

@ -81,7 +81,7 @@ func UploadUsers(owner string, path string) (bool, error) {
return false, err
}
transUsers, err := StringArrayToUser(table)
transUsers, err := StringArrayToStruct[User](table)
if err != nil {
return false, err
}

View File

@ -724,14 +724,14 @@ func setReflectAttr[T any](fieldValue *reflect.Value, fieldString string) error
return nil
}
func StringArrayToUser(stringArray [][]string) ([]*User, error) {
func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
fieldNames := stringArray[0]
excelMap := []map[string]string{}
userFieldMap := map[string]int{}
structFieldMap := map[string]int{}
reflectedUser := reflect.TypeOf(User{})
for i := 0; i < reflectedUser.NumField(); i++ {
userFieldMap[strings.ToLower(reflectedUser.Field(i).Name)] = i
reflectedStruct := reflect.TypeOf(*new(T))
for i := 0; i < reflectedStruct.NumField(); i++ {
structFieldMap[strings.ToLower(reflectedStruct.Field(i).Name)] = i
}
for idx, field := range stringArray {
@ -746,22 +746,23 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) {
excelMap = append(excelMap, tempMap)
}
users := []*User{}
instances := []*T{}
var err error
for _, u := range excelMap {
user := User{}
reflectedUser := reflect.ValueOf(&user).Elem()
for k, v := range u {
for _, m := range excelMap {
instance := new(T)
reflectedInstance := reflect.ValueOf(instance).Elem()
for k, v := range m {
if v == "" || v == "null" || v == "[]" || v == "{}" {
continue
}
fName := strings.ToLower(strings.ReplaceAll(k, "_", ""))
fieldIdx, ok := userFieldMap[fName]
fieldIdx, ok := structFieldMap[fName]
if !ok {
continue
}
fv := reflectedUser.Field(fieldIdx)
fv := reflectedInstance.Field(fieldIdx)
if !fv.IsValid() {
continue
}
@ -806,8 +807,8 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) {
return nil, err
}
}
users = append(users, &user)
instances = append(instances, instance)
}
return users, nil
return instances, nil
}

View File

@ -66,6 +66,10 @@ func AutoSigninFilter(ctx *context.Context) {
responseError(ctx, err.Error())
return
}
if application == nil {
responseError(ctx, fmt.Sprintf("No application is found for userId: app/%s", token.Application))
return
}
setSessionUser(ctx, userId)
setSessionOidc(ctx, token.Scope, application.ClientId)

View File

@ -185,17 +185,3 @@ func removePort(s string) string {
}
return ipStr
}
func isHostIntranet(s string) bool {
ipStr, _, err := net.SplitHostPort(s)
if err != nil {
ipStr = s
}
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

View File

@ -83,7 +83,7 @@ func CorsFilter(ctx *context.Context) {
setCorsHeaders(ctx, origin)
} else if originHostname == host {
setCorsHeaders(ctx, origin)
} else if isHostIntranet(host) {
} else if util.IsHostIntranet(host) {
setCorsHeaders(ctx, origin)
} else {
ok, err := object.IsOriginAllowed(origin)

View File

@ -23,7 +23,7 @@ import (
"github.com/beego/beego/context"
)
var forbiddenChars = `/?:@#&%=+;`
var forbiddenChars = `/?:#&%=+;`
func FieldValidationFilter(ctx *context.Context) {
if ctx.Input.Method() != "POST" {

View File

@ -81,6 +81,7 @@ func initAPI() {
beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup")
beego.Router("/api/upload-groups", &controllers.ApiController{}, "POST:UploadGroups")
beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")

47
util/network.go Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"net"
)
func IsInternetIp(ip string) bool {
ipStr, _, err := net.SplitHostPort(ip)
if err != nil {
ipStr = ip
}
parsedIP := net.ParseIP(ipStr)
if parsedIP == nil {
return false
}
return !parsedIP.IsPrivate() && !parsedIP.IsLoopback() && !parsedIP.IsMulticast() && !parsedIP.IsUnspecified()
}
func IsHostIntranet(ip string) bool {
ipStr, _, err := net.SplitHostPort(ip)
if err != nil {
ipStr = ip
}
parsedIP := net.ParseIP(ipStr)
if parsedIP == nil {
return false
}
return parsedIP.IsPrivate() || parsedIP.IsLoopback() || parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast()
}

View File

@ -1,97 +1,97 @@
const CracoLessPlugin = require("craco-less");
const path = require("path");
module.exports = {
devServer: {
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/swagger": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/files": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/.well-known/openid-configuration": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/serviceValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/proxyValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/proxy": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/validate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/scim": {
target: "http://localhost:8000",
changeOrigin: true,
}
},
},
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {"@primary-color": "rgb(89,54,213)", "@border-radius-base": "5px"},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig, { env, paths }) => {
paths.appBuild = path.resolve(__dirname, "build-temp");
webpackConfig.output.path = path.resolve(__dirname, "build-temp");
// ignore webpack warnings by source-map-loader
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
webpackConfig.ignoreWarnings = [
function ignoreSourcemapsloaderWarnings(warning) {
return (
warning.module &&
warning.module.resource.includes("node_modules") &&
warning.details &&
warning.details.includes("source-map-loader")
);
},
];
// use polyfill Buffer with Webpack 5
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
// https://craco.js.org/docs/configuration/webpack/
webpackConfig.resolve.fallback = {
buffer: require.resolve("buffer/"),
process: false,
util: false,
url: false,
zlib: false,
stream: false,
http: false,
https: false,
assert: false,
crypto: false,
os: false,
fs: false,
};
return webpackConfig;
},
},
};
const CracoLessPlugin = require("craco-less");
const path = require("path");
module.exports = {
devServer: {
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/swagger": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/files": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/.well-known/openid-configuration": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/serviceValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/proxyValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/proxy": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/validate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/scim": {
target: "http://localhost:8000",
changeOrigin: true,
}
},
},
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {"@primary-color": "rgb(89,54,213)", "@border-radius-base": "5px"},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig, { env, paths }) => {
paths.appBuild = path.resolve(__dirname, "build-temp");
webpackConfig.output.path = path.resolve(__dirname, "build-temp");
// ignore webpack warnings by source-map-loader
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
webpackConfig.ignoreWarnings = [
function ignoreSourcemapsloaderWarnings(warning) {
return (
warning.module &&
warning.module.resource.includes("node_modules") &&
warning.details &&
warning.details.includes("source-map-loader")
);
},
];
// use polyfill Buffer with Webpack 5
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
// https://craco.js.org/docs/configuration/webpack/
webpackConfig.resolve.fallback = {
buffer: require.resolve("buffer/"),
process: false,
util: false,
url: false,
zlib: false,
stream: false,
http: false,
https: false,
assert: false,
crypto: false,
os: false,
fs: false,
};
return webpackConfig;
},
},
};

View File

@ -1,21 +1,21 @@
const fs = require("fs");
const path = require("path");
const sourceDir = path.join(__dirname, "build-temp");
const targetDir = path.join(__dirname, "build");
if (!fs.existsSync(sourceDir)) {
// eslint-disable-next-line no-console
console.error(`Source directory "${sourceDir}" does not exist.`);
process.exit(1);
}
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, {recursive: true, force: true});
// eslint-disable-next-line no-console
console.log(`Target directory "${targetDir}" has been deleted successfully.`);
}
fs.renameSync(sourceDir, targetDir);
// eslint-disable-next-line no-console
console.log(`Renamed "${sourceDir}" to "${targetDir}" successfully.`);
const fs = require("fs");
const path = require("path");
const sourceDir = path.join(__dirname, "build-temp");
const targetDir = path.join(__dirname, "build");
if (!fs.existsSync(sourceDir)) {
// eslint-disable-next-line no-console
console.error(`Source directory "${sourceDir}" does not exist.`);
process.exit(1);
}
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, {recursive: true, force: true});
// eslint-disable-next-line no-console
console.log(`Target directory "${targetDir}" has been deleted successfully.`);
}
fs.renameSync(sourceDir, targetDir);
// eslint-disable-next-line no-console
console.log(`Renamed "${sourceDir}" to "${targetDir}" successfully.`);

View File

@ -1,36 +1,36 @@
// 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.
export const DefaultApplication = "app-built-in";
export const CasvisorUrl = "";
export const ShowGithubCorner = false;
export const IsDemoMode = false;
export const ForceLanguage = "";
export const DefaultLanguage = "en";
export const InitThemeAlgorithm = true;
export const ThemeDefault = {
themeType: "default",
colorPrimary: "#5734d3",
borderRadius: 6,
isCompact: false,
};
export const CustomFooter = null;
// Blank or null to hide Ai Assistant button
export const AiAssistantUrl = "https://ai.casbin.com";
// 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.
export const DefaultApplication = "app-built-in";
export const CasvisorUrl = "";
export const ShowGithubCorner = false;
export const IsDemoMode = false;
export const ForceLanguage = "";
export const DefaultLanguage = "en";
export const InitThemeAlgorithm = true;
export const ThemeDefault = {
themeType: "default",
colorPrimary: "#5734d3",
borderRadius: 6,
isCompact: false,
};
export const CustomFooter = null;
// Blank or null to hide Ai Assistant button
export const AiAssistantUrl = "https://ai.casbin.com";

View File

@ -14,7 +14,8 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Table, Tooltip} from "antd";
import {Button, Table, Tooltip, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import moment from "moment";
import * as Setting from "./Setting";
import * as GroupBackend from "./backend/GroupBackend";
@ -87,6 +88,42 @@ class GroupListPage extends BaseListPage {
});
}
uploadFile(info) {
const {status, response: res} = info.file;
if (status === "done") {
if (res.status === "ok") {
Setting.showMessage("success", "Groups uploaded successfully, refreshing the page");
const {pagination} = this.state;
this.fetch({pagination});
} else {
Setting.showMessage("error", `Groups failed to upload: ${res.msg}`);
}
} else if (status === "error") {
Setting.showMessage("error", "File failed to upload");
}
}
renderUpload() {
const props = {
name: "file",
accept: ".xlsx",
method: "post",
action: `${Setting.ServerUrl}/api/upload-groups`,
withCredentials: true,
onChange: (info) => {
this.uploadFile(info);
},
};
return (
<Upload {...props}>
<Button icon={<UploadOutlined />} id="upload-button" type="primary" size="small">
{i18next.t("group:Upload (.xlsx)")}
</Button>
</Upload>
);
}
renderTable(data) {
const columns = [
{
@ -231,7 +268,10 @@ class GroupListPage extends BaseListPage {
title={() => (
<div>
{i18next.t("general:Groups")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
{
this.renderUpload()
}
</div>
)}
loading={this.state.loading}

View File

@ -603,6 +603,16 @@ class OrganizationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:MFA remember time"), i18next.t("application:MFA remember time - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber style={{width: "150px"}} value={this.state.organization.mfaRememberInHours} min={1} step={1} precision={0} addonAfter="Hours" onChange={value => {
this.updateOrganizationField("mfaRememberInHours", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:MFA items"), i18next.t("general:MFA items - Tooltip"))} :

View File

@ -25,6 +25,7 @@ import PopconfirmModal from "./common/modal/PopconfirmModal";
class OrganizationListPage extends BaseListPage {
newOrganization() {
const randomName = Setting.getRandomName();
const DefaultMfaRememberInHours = 12;
return {
owner: "admin", // this.props.account.organizationname,
name: `organization_${randomName}`,
@ -48,6 +49,7 @@ class OrganizationListPage extends BaseListPage {
enableSoftDeletion: false,
isProfilePublic: true,
enableTour: true,
mfaRememberInHours: DefaultMfaRememberInHours,
accountItems: [
{name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},

View File

@ -288,6 +288,16 @@ class ProductEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Success URL"), i18next.t("product:Success URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.product.successUrl} onChange={e => {
this.updateProductField("successUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :

View File

@ -1,341 +1,341 @@
// 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 {Button, Image, Table, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import copy from "copy-to-clipboard";
import * as Setting from "./Setting";
import * as ResourceBackend from "./backend/ResourceBackend";
import i18next from "i18next";
import {Link} from "react-router-dom";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ResourceListPage extends BaseListPage {
constructor(props) {
super(props);
}
componentDidMount() {
this.setState({
fileList: [],
uploading: false,
});
}
deleteResource(i) {
ResourceBackend.deleteResource(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.fetch({
pagination: {
...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
handleUpload(info) {
this.setState({uploading: true});
const filename = info.fileList[0].name;
const fullFilePath = `resource/${this.props.account.owner}/${this.props.account.name}/${filename}`;
ResourceBackend.uploadResource(this.props.account.owner, this.props.account.name, "custom", "ResourceListPage", fullFilePath, info.file)
.then(res => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("application:File uploaded successfully"));
const {pagination} = this.state;
this.fetch({pagination});
} else {
Setting.showMessage("error", res.msg);
}
}).finally(() => {
this.setState({uploading: false});
});
}
renderUpload() {
return (
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false}
beforeUpload={file => {return false;}} onChange={info => {this.handleUpload(info);}}>
<Button id="upload-button" icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
{i18next.t("resource:Upload a file...")}
</Button>
</Upload>
);
}
renderTable(resources) {
const columns = [
{
title: i18next.t("general:Provider"),
dataIndex: "provider",
key: "provider",
width: "150px",
sorter: true,
...this.getColumnSearchProps("provider"),
render: (text, record, index) => {
return (
<Link to={`/providers/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
width: "80px",
sorter: true,
...this.getColumnSearchProps("application"),
render: (text, record, index) => {
return (
<Link to={`/applications/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:User"),
dataIndex: "user",
key: "user",
width: "80px",
sorter: true,
...this.getColumnSearchProps("user"),
render: (text, record, index) => {
return (
<Link to={`/users/${record.owner}/${record.user}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("resource:Parent"),
dataIndex: "parent",
key: "parent",
width: "80px",
sorter: true,
...this.getColumnSearchProps("parent"),
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "150px",
sorter: true,
...this.getColumnSearchProps("name"),
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("user:Tag"),
dataIndex: "tag",
key: "tag",
width: "80px",
sorter: true,
...this.getColumnSearchProps("tag"),
},
// {
// title: i18next.t("resource:File name"),
// dataIndex: 'fileName',
// key: 'fileName',
// width: '120px',
// sorter: (a, b) => a.fileName.localeCompare(b.fileName),
// },
{
title: i18next.t("provider:Type"),
dataIndex: "fileType",
key: "fileType",
width: "80px",
sorter: true,
...this.getColumnSearchProps("fileType"),
},
{
title: i18next.t("resource:Format"),
dataIndex: "fileFormat",
key: "fileFormat",
width: "80px",
sorter: true,
...this.getColumnSearchProps("fileFormat"),
},
{
title: i18next.t("resource:File size"),
dataIndex: "fileSize",
key: "fileSize",
width: "100px",
sorter: true,
render: (text, record, index) => {
return Setting.getFriendlyFileSize(text);
},
},
{
title: i18next.t("general:Preview"),
dataIndex: "preview",
key: "preview",
width: "100px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
if (record.fileType === "image") {
const errorImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==";
return (
<Image
width={200}
src={record.url}
fallback={errorImage}
/>
);
} else if (record.fileType === "video") {
return (
<video width={200} controls>
<source src={record.url} type="video/mp4" />
</video>
);
}
},
},
{
title: i18next.t("general:URL"),
dataIndex: "url",
key: "url",
width: "120px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button onClick={() => {
copy(record.url);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("resource:Copy Link")}
</Button>
</div>
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "70px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteResource(index)}
okText={i18next.t("general:OK")}
cancelText={i18next.t("general:Cancel")}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={resources} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Resources")}&nbsp;&nbsp;&nbsp;&nbsp;
{/* <Button type="primary" size="small" onClick={this.addResource.bind(this)}>{i18next.t("general:Add")}</Button>*/}
{
this.renderUpload()
}
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
const field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
this.setState({loading: true});
ResourceBackend.getResources(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), this.props.account.name, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
this.setState({
loading: false,
});
if (res.status === "ok") {
this.setState({
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (res.data.includes("Please login first")) {
this.setState({
loading: false,
isAuthorized: false,
});
}
}
});
};
}
export default ResourceListPage;
// 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 {Button, Image, Table, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import copy from "copy-to-clipboard";
import * as Setting from "./Setting";
import * as ResourceBackend from "./backend/ResourceBackend";
import i18next from "i18next";
import {Link} from "react-router-dom";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class ResourceListPage extends BaseListPage {
constructor(props) {
super(props);
}
componentDidMount() {
this.setState({
fileList: [],
uploading: false,
});
}
deleteResource(i) {
ResourceBackend.deleteResource(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.fetch({
pagination: {
...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
handleUpload(info) {
this.setState({uploading: true});
const filename = info.fileList[0].name;
const fullFilePath = `resource/${this.props.account.owner}/${this.props.account.name}/${filename}`;
ResourceBackend.uploadResource(this.props.account.owner, this.props.account.name, "custom", "ResourceListPage", fullFilePath, info.file)
.then(res => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("application:File uploaded successfully"));
const {pagination} = this.state;
this.fetch({pagination});
} else {
Setting.showMessage("error", res.msg);
}
}).finally(() => {
this.setState({uploading: false});
});
}
renderUpload() {
return (
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false}
beforeUpload={file => {return false;}} onChange={info => {this.handleUpload(info);}}>
<Button id="upload-button" icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
{i18next.t("resource:Upload a file...")}
</Button>
</Upload>
);
}
renderTable(resources) {
const columns = [
{
title: i18next.t("general:Provider"),
dataIndex: "provider",
key: "provider",
width: "150px",
sorter: true,
...this.getColumnSearchProps("provider"),
render: (text, record, index) => {
return (
<Link to={`/providers/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
width: "80px",
sorter: true,
...this.getColumnSearchProps("application"),
render: (text, record, index) => {
return (
<Link to={`/applications/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:User"),
dataIndex: "user",
key: "user",
width: "80px",
sorter: true,
...this.getColumnSearchProps("user"),
render: (text, record, index) => {
return (
<Link to={`/users/${record.owner}/${record.user}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("resource:Parent"),
dataIndex: "parent",
key: "parent",
width: "80px",
sorter: true,
...this.getColumnSearchProps("parent"),
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "150px",
sorter: true,
...this.getColumnSearchProps("name"),
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "150px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("user:Tag"),
dataIndex: "tag",
key: "tag",
width: "80px",
sorter: true,
...this.getColumnSearchProps("tag"),
},
// {
// title: i18next.t("resource:File name"),
// dataIndex: 'fileName',
// key: 'fileName',
// width: '120px',
// sorter: (a, b) => a.fileName.localeCompare(b.fileName),
// },
{
title: i18next.t("provider:Type"),
dataIndex: "fileType",
key: "fileType",
width: "80px",
sorter: true,
...this.getColumnSearchProps("fileType"),
},
{
title: i18next.t("resource:Format"),
dataIndex: "fileFormat",
key: "fileFormat",
width: "80px",
sorter: true,
...this.getColumnSearchProps("fileFormat"),
},
{
title: i18next.t("resource:File size"),
dataIndex: "fileSize",
key: "fileSize",
width: "100px",
sorter: true,
render: (text, record, index) => {
return Setting.getFriendlyFileSize(text);
},
},
{
title: i18next.t("general:Preview"),
dataIndex: "preview",
key: "preview",
width: "100px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
if (record.fileType === "image") {
const errorImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==";
return (
<Image
width={200}
src={record.url}
fallback={errorImage}
/>
);
} else if (record.fileType === "video") {
return (
<video width={200} controls>
<source src={record.url} type="video/mp4" />
</video>
);
}
},
},
{
title: i18next.t("general:URL"),
dataIndex: "url",
key: "url",
width: "120px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button onClick={() => {
copy(record.url);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("resource:Copy Link")}
</Button>
</div>
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "70px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteResource(index)}
okText={i18next.t("general:OK")}
cancelText={i18next.t("general:Cancel")}
>
</PopconfirmModal>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={resources} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Resources")}&nbsp;&nbsp;&nbsp;&nbsp;
{/* <Button type="primary" size="small" onClick={this.addResource.bind(this)}>{i18next.t("general:Add")}</Button>*/}
{
this.renderUpload()
}
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
const field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
this.setState({loading: true});
ResourceBackend.getResources(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), this.props.account.name, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
this.setState({
loading: false,
});
if (res.status === "ok") {
this.setState({
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (res.data.includes("Please login first")) {
this.setState({
loading: false,
isAuthorized: false,
});
}
}
});
};
}
export default ResourceListPage;

View File

@ -696,18 +696,27 @@ export const MfaRulePrompted = "Prompted";
export const MfaRuleOptional = "Optional";
export function isRequiredEnableMfa(user, organization) {
if (!user || !organization || !organization.mfaItems) {
if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
return false;
}
return getMfaItemsByRules(user, organization, [MfaRuleRequired]).length > 0;
}
export function getMfaItemsByRules(user, organization, mfaRules = []) {
if (!user || !organization || !organization.mfaItems) {
if (!user || !organization || (!organization.mfaItems && !user.mfaItems)) {
return [];
}
return organization.mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
let mfaItems = organization.mfaItems;
if (user.mfaItems && user.mfaItems.length !== 0) {
mfaItems = user.mfaItems;
}
if (mfaItems === null) {
return [];
}
return mfaItems.filter((mfaItem) => mfaRules.includes(mfaItem.rule))
.filter((mfaItem) => user.multiFactorAuths.some((mfa) => mfa.mfaType === mfaItem.name && !mfa.enabled));
}

View File

@ -42,6 +42,7 @@ import * as MfaBackend from "./backend/MfaBackend";
import AccountAvatar from "./account/AccountAvatar";
import FaceIdTable from "./table/FaceIdTable";
import MfaAccountTable from "./table/MfaAccountTable";
import MfaTable from "./table/MfaTable";
const {Option} = Select;
@ -926,6 +927,19 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "MFA items") {
return (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:MFA items"), i18next.t("general:MFA items - Tooltip"))} :
</Col>
<Col span={22} >
<MfaTable
title={i18next.t("general:MFA items")}
table={this.state.user.mfaItems ?? []}
onUpdateTable={(value) => {this.updateUserField("mfaItems", value);}}
/>
</Col>
</Row>);
} else if (accountItem.name === "Multi-factor authentication") {
return (
!this.isSelfOrAdmin() ? null : (

View File

@ -163,7 +163,7 @@ export function getWechatQRCode(providerId) {
}
export function getCaptchaStatus(values) {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}`, {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}&application=${values["application"]}`, {
method: "GET",
credentials: "include",
headers: {

View File

@ -166,7 +166,7 @@ class AuthCallback extends React.Component {
const responseType = this.getResponseType();
const handleLogin = (res) => {
if (responseType === "login") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
@ -176,7 +176,7 @@ class AuthCallback extends React.Component {
const link = Setting.getFromLink();
Setting.goToLink(link);
} else if (responseType === "code") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
@ -185,7 +185,7 @@ class AuthCallback extends React.Component {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
// Setting.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
@ -207,7 +207,7 @@ class AuthCallback extends React.Component {
relayState: oAuthParams.relayState,
});
} else {
if (res.data2.needUpdatePassword) {
if (res.data3) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Col, Form, Input, Row, Select, Steps} from "antd";
import {Button, Col, Form, Input, Popover, Row, Select, Steps} from "antd";
import * as AuthBackend from "./AuthBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Util from "./Util";
@ -385,30 +385,48 @@ class ForgetPage extends React.Component {
},
]}
/>
<Form.Item
name="newPassword"
hidden={this.state.current !== 2}
rules={[
{
required: true,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
<Popover placement={window.innerWidth >= 960 ? "right" : "top"} content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Form.Item
name="newPassword"
hidden={this.state.current !== 2}
rules={[
{
required: true,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
},
},
},
]}
hasFeedback
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
</Form.Item>
]}
hasFeedback
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
onChange={(e) => {
this.setState({
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, e.target.value),
});
}}
onFocus={() => {
this.setState({
passwordPopoverOpen: application.organizationObj.passwordOptions?.length > 0,
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("newPassword") ?? ""),
});
}}
onBlur={() => {
this.setState({
passwordPopoverOpen: false,
});
}}
/>
</Form.Item>
</Popover>
<Form.Item
name="confirm"
dependencies={["newPassword"]}

View File

@ -134,6 +134,8 @@ class LoginPage extends React.Component {
return CaptchaRule.Always;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) {
return CaptchaRule.Dynamic;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Internet-Only")) {
return CaptchaRule.InternetOnly;
} else {
return CaptchaRule.Never;
}
@ -443,6 +445,9 @@ class LoginPage extends React.Component {
} else if (captchaRule === CaptchaRule.Dynamic) {
this.checkCaptchaStatus(values);
return;
} else if (captchaRule === CaptchaRule.InternetOnly) {
this.checkCaptchaStatus(values);
return;
}
}
this.login(values);
@ -491,9 +496,9 @@ class LoginPage extends React.Component {
const responseType = values["type"];
if (responseType === "login") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
}
Setting.showMessage("success", i18next.t("application:Logged in successfully"));
this.props.onLoginSuccess();
@ -505,9 +510,9 @@ class LoginPage extends React.Component {
userCodeStatus: "success",
});
} else if (responseType === "token" || responseType === "id_token") {
if (res.data2) {
if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
}
const amendatoryResponseType = responseType === "token" ? "access_token" : responseType;
const accessToken = res.data;
@ -517,9 +522,9 @@ class LoginPage extends React.Component {
this.props.onLoginSuccess(window.location.href);
return;
}
if (res.data2.needUpdatePassword) {
if (res.data3) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
Setting.goToLinkSoft(this, `/forget/${this.state.applicationName}`);
}
if (res.data2.method === "POST") {
this.setState({
@ -961,9 +966,23 @@ class LoginPage extends React.Component {
const captchaProviderItems = this.getCaptchaProviderItems(application);
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;
const internetOnlyProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Internet-Only");
// Select provider based on the active captcha rule, not fixed priority
const captchaRule = this.getCaptchaRule(this.getApplicationObj());
let provider = null;
if (captchaRule === CaptchaRule.Always && alwaysProviderItems.length > 0) {
provider = alwaysProviderItems[0].provider;
} else if (captchaRule === CaptchaRule.Dynamic && dynamicProviderItems.length > 0) {
provider = dynamicProviderItems[0].provider;
} else if (captchaRule === CaptchaRule.InternetOnly && internetOnlyProviderItems.length > 0) {
provider = internetOnlyProviderItems[0].provider;
}
if (!provider) {
return null;
}
return <CaptchaModal
owner={provider.owner}

View File

@ -1,30 +1,30 @@
// 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 * as Setting from "../Setting";
class OdicDiscoveryPage extends React.Component {
UNSAFE_componentWillMount() {
if (Setting.isLocalhost()) {
Setting.goToLink(`${Setting.ServerUrl}/.well-known/openid-configuration`);
}
}
render() {
return null;
}
}
export default OdicDiscoveryPage;
// 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 * as Setting from "../Setting";
class OdicDiscoveryPage extends React.Component {
UNSAFE_componentWillMount() {
if (Setting.isLocalhost()) {
Setting.goToLink(`${Setting.ServerUrl}/.well-known/openid-configuration`);
}
}
render() {
return null;
}
}
export default OdicDiscoveryPage;

View File

@ -278,7 +278,7 @@ const authInfo = {
endpoint: "https://www.tiktok.com/auth/authorize/",
},
Tumblr: {
scope: "email",
scope: "basic",
endpoint: "https://www.tumblr.com/oauth2/authorize",
},
Twitch: {

View File

@ -1,31 +1,31 @@
// 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 {createButton} from "react-social-login-buttons";
function Icon({width = 24, height = 24, color}) {
return <svg xmlns="http://www.w3.org/2000/svg" height="48" width="32" viewBox="-18.15 -35.9725 157.3 215.835"><path fill="#faab07" d="M60.503 142.237c-12.533 0-24.038-4.195-31.445-10.46-3.762 1.124-8.574 2.932-11.61 5.175-2.6 1.918-2.275 3.874-1.807 4.663 2.056 3.47 35.273 2.216 44.862 1.136zm0 0c12.535 0 24.039-4.195 31.447-10.46 3.76 1.124 8.573 2.932 11.61 5.175 2.598 1.918 2.274 3.874 1.805 4.663-2.056 3.47-35.272 2.216-44.862 1.136zm0 0" /><path d="M60.576 67.119c20.698-.14 37.286-4.147 42.907-5.683 1.34-.367 2.056-1.024 2.056-1.024.005-.189.085-3.37.085-5.01C105.624 27.768 92.58.001 60.5 0 28.42.001 15.375 27.769 15.375 55.401c0 1.642.08 4.822.086 5.01 0 0 .583.615 1.65.913 5.19 1.444 22.09 5.65 43.312 5.795zm56.245 23.02c-1.283-4.129-3.034-8.944-4.808-13.568 0 0-1.02-.126-1.537.023-15.913 4.623-35.202 7.57-49.9 7.392h-.153c-14.616.175-33.774-2.737-49.634-7.315-.606-.175-1.802-.1-1.802-.1-1.774 4.624-3.525 9.44-4.808 13.568-6.119 19.69-4.136 27.838-2.627 28.02 3.239.392 12.606-14.821 12.606-14.821 0 15.459 13.957 39.195 45.918 39.413h.848c31.96-.218 45.917-23.954 45.917-39.413 0 0 9.368 15.213 12.607 14.822 1.508-.183 3.491-8.332-2.627-28.021" /><path fill="#fff" d="M49.085 40.824c-4.352.197-8.07-4.76-8.304-11.063-.236-6.305 3.098-11.576 7.45-11.773 4.347-.195 8.064 4.76 8.3 11.065.238 6.306-3.097 11.577-7.446 11.771m31.133-11.063c-.233 6.302-3.951 11.26-8.303 11.063-4.35-.195-7.684-5.465-7.446-11.77.236-6.305 3.952-11.26 8.3-11.066 4.352.197 7.686 5.468 7.449 11.773" /><path fill="#faab07" d="M87.952 49.725C86.79 47.15 75.077 44.28 60.578 44.28h-.156c-14.5 0-26.212 2.87-27.375 5.446a.863.863 0 00-.085.367.88.88 0 00.16.496c.98 1.427 13.985 8.487 27.3 8.487h.156c13.314 0 26.319-7.058 27.299-8.487a.873.873 0 00.16-.498.856.856 0 00-.085-.365" /><path d="M54.434 29.854c.199 2.49-1.167 4.702-3.046 4.943-1.883.242-3.568-1.58-3.768-4.07-.197-2.492 1.167-4.704 3.043-4.944 1.886-.244 3.574 1.58 3.771 4.07m11.956.833c.385-.689 3.004-4.312 8.427-2.993 1.425.347 2.084.857 2.223 1.057.205.296.262.718.053 1.286-.412 1.126-1.263 1.095-1.734.875-.305-.142-4.082-2.66-7.562 1.097-.24.257-.668.346-1.073.04-.407-.308-.574-.93-.334-1.362" /><path fill="#fff" d="M60.576 83.08h-.153c-9.996.12-22.116-1.204-33.854-3.518-1.004 5.818-1.61 13.132-1.09 21.853 1.316 22.043 14.407 35.9 34.614 36.1h.82c20.208-.2 33.298-14.057 34.616-36.1.52-8.723-.087-16.035-1.092-21.854-11.739 2.315-23.862 3.64-33.86 3.518" /><path fill="#eb1923" d="M32.102 81.235v21.693s9.937 2.004 19.893.616V83.535c-6.307-.357-13.109-1.152-19.893-2.3" /><path fill="#eb1923" d="M105.539 60.412s-19.33 6.102-44.963 6.275h-.153c-25.591-.172-44.896-6.255-44.962-6.275L8.987 76.57c16.193 4.882 36.261 8.028 51.436 7.845h.153c15.175.183 35.242-2.963 51.437-7.845zm0 0" /></svg>;
}
const config = {
text: "Sign in with QQ",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "rgb(94,188,249)"},
activeStyle: {background: "rgb(76,143,208)"},
};
const QqLoginButton = createButton(config);
export default QqLoginButton;
// 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 {createButton} from "react-social-login-buttons";
function Icon({width = 24, height = 24, color}) {
return <svg xmlns="http://www.w3.org/2000/svg" height="48" width="32" viewBox="-18.15 -35.9725 157.3 215.835"><path fill="#faab07" d="M60.503 142.237c-12.533 0-24.038-4.195-31.445-10.46-3.762 1.124-8.574 2.932-11.61 5.175-2.6 1.918-2.275 3.874-1.807 4.663 2.056 3.47 35.273 2.216 44.862 1.136zm0 0c12.535 0 24.039-4.195 31.447-10.46 3.76 1.124 8.573 2.932 11.61 5.175 2.598 1.918 2.274 3.874 1.805 4.663-2.056 3.47-35.272 2.216-44.862 1.136zm0 0" /><path d="M60.576 67.119c20.698-.14 37.286-4.147 42.907-5.683 1.34-.367 2.056-1.024 2.056-1.024.005-.189.085-3.37.085-5.01C105.624 27.768 92.58.001 60.5 0 28.42.001 15.375 27.769 15.375 55.401c0 1.642.08 4.822.086 5.01 0 0 .583.615 1.65.913 5.19 1.444 22.09 5.65 43.312 5.795zm56.245 23.02c-1.283-4.129-3.034-8.944-4.808-13.568 0 0-1.02-.126-1.537.023-15.913 4.623-35.202 7.57-49.9 7.392h-.153c-14.616.175-33.774-2.737-49.634-7.315-.606-.175-1.802-.1-1.802-.1-1.774 4.624-3.525 9.44-4.808 13.568-6.119 19.69-4.136 27.838-2.627 28.02 3.239.392 12.606-14.821 12.606-14.821 0 15.459 13.957 39.195 45.918 39.413h.848c31.96-.218 45.917-23.954 45.917-39.413 0 0 9.368 15.213 12.607 14.822 1.508-.183 3.491-8.332-2.627-28.021" /><path fill="#fff" d="M49.085 40.824c-4.352.197-8.07-4.76-8.304-11.063-.236-6.305 3.098-11.576 7.45-11.773 4.347-.195 8.064 4.76 8.3 11.065.238 6.306-3.097 11.577-7.446 11.771m31.133-11.063c-.233 6.302-3.951 11.26-8.303 11.063-4.35-.195-7.684-5.465-7.446-11.77.236-6.305 3.952-11.26 8.3-11.066 4.352.197 7.686 5.468 7.449 11.773" /><path fill="#faab07" d="M87.952 49.725C86.79 47.15 75.077 44.28 60.578 44.28h-.156c-14.5 0-26.212 2.87-27.375 5.446a.863.863 0 00-.085.367.88.88 0 00.16.496c.98 1.427 13.985 8.487 27.3 8.487h.156c13.314 0 26.319-7.058 27.299-8.487a.873.873 0 00.16-.498.856.856 0 00-.085-.365" /><path d="M54.434 29.854c.199 2.49-1.167 4.702-3.046 4.943-1.883.242-3.568-1.58-3.768-4.07-.197-2.492 1.167-4.704 3.043-4.944 1.886-.244 3.574 1.58 3.771 4.07m11.956.833c.385-.689 3.004-4.312 8.427-2.993 1.425.347 2.084.857 2.223 1.057.205.296.262.718.053 1.286-.412 1.126-1.263 1.095-1.734.875-.305-.142-4.082-2.66-7.562 1.097-.24.257-.668.346-1.073.04-.407-.308-.574-.93-.334-1.362" /><path fill="#fff" d="M60.576 83.08h-.153c-9.996.12-22.116-1.204-33.854-3.518-1.004 5.818-1.61 13.132-1.09 21.853 1.316 22.043 14.407 35.9 34.614 36.1h.82c20.208-.2 33.298-14.057 34.616-36.1.52-8.723-.087-16.035-1.092-21.854-11.739 2.315-23.862 3.64-33.86 3.518" /><path fill="#eb1923" d="M32.102 81.235v21.693s9.937 2.004 19.893.616V83.535c-6.307-.357-13.109-1.152-19.893-2.3" /><path fill="#eb1923" d="M105.539 60.412s-19.33 6.102-44.963 6.275h-.153c-25.591-.172-44.896-6.255-44.962-6.275L8.987 76.57c16.193 4.882 36.261 8.028 51.436 7.845h.153c15.175.183 35.242-2.963 51.437-7.845zm0 0" /></svg>;
}
const config = {
text: "Sign in with QQ",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "rgb(94,188,249)"},
activeStyle: {background: "rgb(76,143,208)"},
};
const QqLoginButton = createButton(config);
export default QqLoginButton;

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Form, Input, Radio, Result, Row, Select, message} from "antd";
import {Button, Form, Input, Popover, Radio, Result, Row, Select, message} from "antd";
import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend";
import * as ProviderButton from "./ProviderButton";
@ -607,28 +607,45 @@ class SignupPage extends React.Component {
}
} else if (signupItem.name === "Password") {
return (
<Form.Item
name="password"
className="signup-password"
label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
rules={[
{
required: required,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
<Popover placement={window.innerWidth >= 960 ? "right" : "top"} content={this.state.passwordPopover} open={this.state.passwordPopoverOpen}>
<Form.Item
name="password"
className="signup-password"
label={signupItem.label ? signupItem.label : i18next.t("general:Password")}
rules={[
{
required: required,
validateTrigger: "onChange",
validator: (rule, value) => {
const errorMsg = PasswordChecker.checkPasswordComplexity(value, application.organizationObj.passwordOptions);
if (errorMsg === "") {
return Promise.resolve();
} else {
return Promise.reject(errorMsg);
}
},
},
},
]}
hasFeedback
>
<Input.Password className="signup-password-input" placeholder={signupItem.placeholder} />
</Form.Item>
]}
hasFeedback
>
<Input.Password className="signup-password-input" placeholder={signupItem.placeholder} onChange={(e) => {
this.setState({
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, e.target.value),
});
}}
onFocus={() => {
this.setState({
passwordPopoverOpen: application.organizationObj.passwordOptions?.length > 0,
passwordPopover: PasswordChecker.renderPasswordPopover(application.organizationObj.passwordOptions, this.form.current?.getFieldValue("password") ?? ""),
});
}}
onBlur={() => {
this.setState({
passwordPopoverOpen: false,
});
}} />
</Form.Item>
</Popover>
);
} else if (signupItem.name === "Confirm password") {
return (

View File

@ -31,9 +31,9 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
const [mfaType, setMfaType] = useState(mfaProps.mfaType);
const [recoveryCode, setRecoveryCode] = useState("");
const verify = ({passcode}) => {
const verify = ({passcode, enableMfaRemember}) => {
setLoading(true);
const values = {...formValues, passcode};
const values = {...formValues, passcode, enableMfaRemember};
values["mfaType"] = mfaProps.mfaType;
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
loginFunction(values, authParams).then((res) => {

View File

@ -1,5 +1,5 @@
import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input, Space} from "antd";
import {Button, Checkbox, Form, Input, Space} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
@ -12,6 +12,13 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
const [dest, setDest] = React.useState("");
const [form] = Form.useForm();
const handleFinish = (values) => {
onFinish({
passcode: values.passcode,
enableMfaRemember: values.enableMfaRemember,
});
};
useEffect(() => {
if (method === mfaAuth) {
setDest(mfaProps.secret);
@ -51,9 +58,10 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
onFinish={handleFinish}
initialValues={{
countryCode: mfaProps.countryCode,
enableMfaRemember: false,
}}
>
{isShowText() ?
@ -109,6 +117,14 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
application={application}
/>
</Form.Item>
<Form.Item
name="enableMfaRemember"
valuePropName="checked"
>
<Checkbox>
{i18next.t("mfa:Remember this account for {hour} hours").replace("{hour}", mfaProps?.mfaRememberInHours)}
</Checkbox>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}

View File

@ -1,5 +1,5 @@
import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {Button, Col, Form, Input, QRCode, Space} from "antd";
import {CopyOutlined} from "@ant-design/icons";
import {Button, Checkbox, Col, Form, Input, QRCode, Space} from "antd";
import copy from "copy-to-clipboard";
import i18next from "i18next";
import React from "react";
@ -8,6 +8,13 @@ import * as Setting from "../../Setting";
export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm();
const handleFinish = (values) => {
onFinish({
passcode: values.passcode,
enableMfaRemember: values.enableMfaRemember,
});
};
const renderSecret = () => {
if (!mfaProps.secret) {
return null;
@ -40,20 +47,31 @@ export const MfaVerifyTotpForm = ({mfaProps, onFinish}) => {
<Form
form={form}
style={{width: "300px"}}
onFinish={onFinish}
onFinish={handleFinish}
initialValues={{
enableMfaRemember: false,
}}
>
{renderSecret()}
<Form.Item
name="passcode"
rules={[{required: true, message: "Please input your passcode"}]}
>
<Input
<Input.OTP
style={{marginTop: 24}}
prefix={<UserOutlined />}
placeholder={i18next.t("mfa:Passcode")}
autoComplete="off"
onChange={() => {
form.submit();
}}
/>
</Form.Item>
<Form.Item
name="enableMfaRemember"
valuePropName="checked"
>
<Checkbox>
{i18next.t("mfa:Remember this account for {hour} hours").replace("{hour}", mfaProps?.mfaRememberInHours)}
</Checkbox>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}

View File

@ -1,85 +1,85 @@
// 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 * as Setting from "../Setting";
export function getResources(owner, user, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-resources?owner=${owner}&user=${user}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getResource(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-resource?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateResource(owner, name, resource) {
const newResource = Setting.deepCopy(resource);
return fetch(`${Setting.ServerUrl}/api/update-resource?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newResource),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function addResource(resource) {
const newResource = Setting.deepCopy(resource);
return fetch(`${Setting.ServerUrl}/api/add-resource`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newResource),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function deleteResource(resource, provider = "") {
const newResource = Setting.deepCopy(resource);
return fetch(`${Setting.ServerUrl}/api/delete-resource?provider=${provider}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newResource),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function uploadResource(owner, user, tag, parent, fullFilePath, file, provider = "") {
const application = "app-built-in";
const formData = new FormData();
formData.append("file", file);
return fetch(`${Setting.ServerUrl}/api/upload-resource?owner=${owner}&user=${user}&application=${application}&tag=${tag}&parent=${parent}&fullFilePath=${encodeURIComponent(fullFilePath)}&provider=${provider}`, {
body: formData,
method: "POST",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
// 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 * as Setting from "../Setting";
export function getResources(owner, user, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-resources?owner=${owner}&user=${user}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getResource(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-resource?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateResource(owner, name, resource) {
const newResource = Setting.deepCopy(resource);
return fetch(`${Setting.ServerUrl}/api/update-resource?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newResource),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function addResource(resource) {
const newResource = Setting.deepCopy(resource);
return fetch(`${Setting.ServerUrl}/api/add-resource`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newResource),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function deleteResource(resource, provider = "") {
const newResource = Setting.deepCopy(resource);
return fetch(`${Setting.ServerUrl}/api/delete-resource?provider=${provider}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newResource),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function uploadResource(owner, user, tag, parent, fullFilePath, file, provider = "") {
const application = "app-built-in";
const formData = new FormData();
formData.append("file", file);
return fetch(`${Setting.ServerUrl}/api/upload-resource?owner=${owner}&user=${user}&application=${application}&tag=${tag}&parent=${parent}&fullFilePath=${encodeURIComponent(fullFilePath)}&provider=${provider}`, {
body: formData,
method: "POST",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@ -1,229 +1,229 @@
// 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 {Button, Col, Row} from "antd";
import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend";
import * as Setting from "../Setting";
import * as Provider from "../auth/Provider";
import * as AuthBackend from "../auth/AuthBackend";
import {goToWeb3Url} from "../auth/ProviderButton";
import AccountAvatar from "../account/AccountAvatar";
import {WechatOfficialAccountModal} from "../auth/Util";
class OAuthWidget extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
addressOptions: [],
affiliationOptions: [],
};
}
UNSAFE_componentWillMount() {
this.getAddressOptions(this.props.application);
this.getAffiliationOptions(this.props.application, this.props.user);
}
getAddressOptions(application) {
if (application.affiliationUrl === "") {
return;
}
const addressUrl = application.affiliationUrl.split("|")[0];
UserBackend.getAddressOptions(addressUrl)
.then((addressOptions) => {
this.setState({
addressOptions: addressOptions,
});
});
}
getAffiliationOptions(application, user) {
if (application.affiliationUrl === "") {
return;
}
const affiliationUrl = application.affiliationUrl.split("|")[1];
const code = user.address[user.address.length - 1];
UserBackend.getAffiliationOptions(affiliationUrl, code)
.then((affiliationOptions) => {
this.setState({
affiliationOptions: affiliationOptions,
});
});
}
updateUserField(key, value) {
this.props.onUpdateUserField(key, value);
}
unlinked() {
this.props.onUnlinked();
}
getProviderLink(user, provider) {
if (provider.type === "GitHub") {
return `https://github.com/${this.getUserProperty(user, provider.type, "username")}`;
} else if (provider.type === "Google") {
return "https://mail.google.com";
} else {
return "";
}
}
getUserProperty(user, providerType, propertyName) {
const key = `oauth_${providerType}_${propertyName}`;
if (user.properties === null) {return "";}
return user.properties[key];
}
unlinkUser(providerType, linkedValue) {
const body = {
providerType: providerType,
// should add the unlink user's info, cause the user may not be logged in, but a admin want to unlink the user.
user: this.props.user,
};
if (providerType === "MetaMask" || providerType === "Web3Onboard") {
import("../auth/Web3Auth")
.then(module => {
const delWeb3AuthToken = module.delWeb3AuthToken;
delWeb3AuthToken(linkedValue);
AuthBackend.unlink(body)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", "Unlinked successfully");
this.unlinked();
} else {
Setting.showMessage("error", `Failed to unlink: ${res.msg}`);
}
});
});
return;
}
AuthBackend.unlink(body)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", "Unlinked successfully");
this.unlinked();
} else {
Setting.showMessage("error", `Failed to unlink: ${res.msg}`);
}
});
}
renderIdp(user, application, providerItem) {
const provider = providerItem.provider;
const linkedValue = user[provider.type.toLowerCase()];
const profileUrl = this.getProviderLink(user, provider);
const id = this.getUserProperty(user, provider.type, "id");
const username = this.getUserProperty(user, provider.type, "username");
const displayName = this.getUserProperty(user, provider.type, "displayName");
const email = this.getUserProperty(user, provider.type, "email");
let avatarUrl = this.getUserProperty(user, provider.type, "avatarUrl");
// the account user
const account = this.props.account;
if (avatarUrl === "" || avatarUrl === undefined) {
avatarUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAQAAACROWYpAAAAHElEQVR42mNkoAAwjmoe1TyqeVTzqOZRzcNZMwB18wAfEFQkPQAAAABJRU5ErkJggg==";
}
let name = (username === undefined) ? displayName : `${displayName} (${username})`;
if (name === undefined) {
if (id !== undefined) {
name = id;
} else if (email !== undefined) {
name = email;
} else {
name = linkedValue;
}
}
let linkButtonWidth = "110px";
if (Setting.getLanguage() === "id") {
linkButtonWidth = "160px";
}
return (
<Row key={provider.name} style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={this.props.labelSpan}>
{
Setting.getProviderLogo(provider)
}
<span style={{marginLeft: "5px"}}>
{
`${provider.type}:`
}
</span>
</Col>
<Col span={24 - this.props.labelSpan} >
<AccountAvatar style={{marginRight: "10px"}} size={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
<span style={{
width: this.props.labelSpan === 3 ? "300px" : "200px",
display: (Setting.isMobile()) ? "inline" : "inline-block",
overflow: "hidden",
textOverflow: "ellipsis",
}} title={name}>
{
linkedValue === "" ? (
`(${i18next.t("general:empty")})`
) : (
profileUrl === "" ? name : (
<a target="_blank" rel="noreferrer" href={profileUrl}>
{
name
}
</a>
)
)
}
</span>
{
linkedValue === "" ? (
provider.category === "Web3" ? (
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={() => goToWeb3Url(application, provider, "link")}>{i18next.t("user:Link")}</Button>
) : (
provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger") ? (
<a key={provider.displayName}>
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={
() => {
WechatOfficialAccountModal(application, provider, "link");
}
}>{i18next.t("user:Link")}</Button>
</a>
) : (
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
</a>
)
)
) : (
<Button disabled={!providerItem.canUnlink && !Setting.isAdminUser(account)} style={{marginLeft: "20px", width: linkButtonWidth}} onClick={() => this.unlinkUser(provider.type, linkedValue)}>{i18next.t("user:Unlink")}</Button>
)
}
</Col>
</Row>
);
}
render() {
return this.renderIdp(this.props.user, this.props.application, this.props.providerItem);
}
}
export default OAuthWidget;
// 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 {Button, Col, Row} from "antd";
import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend";
import * as Setting from "../Setting";
import * as Provider from "../auth/Provider";
import * as AuthBackend from "../auth/AuthBackend";
import {goToWeb3Url} from "../auth/ProviderButton";
import AccountAvatar from "../account/AccountAvatar";
import {WechatOfficialAccountModal} from "../auth/Util";
class OAuthWidget extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
addressOptions: [],
affiliationOptions: [],
};
}
UNSAFE_componentWillMount() {
this.getAddressOptions(this.props.application);
this.getAffiliationOptions(this.props.application, this.props.user);
}
getAddressOptions(application) {
if (application.affiliationUrl === "") {
return;
}
const addressUrl = application.affiliationUrl.split("|")[0];
UserBackend.getAddressOptions(addressUrl)
.then((addressOptions) => {
this.setState({
addressOptions: addressOptions,
});
});
}
getAffiliationOptions(application, user) {
if (application.affiliationUrl === "") {
return;
}
const affiliationUrl = application.affiliationUrl.split("|")[1];
const code = user.address[user.address.length - 1];
UserBackend.getAffiliationOptions(affiliationUrl, code)
.then((affiliationOptions) => {
this.setState({
affiliationOptions: affiliationOptions,
});
});
}
updateUserField(key, value) {
this.props.onUpdateUserField(key, value);
}
unlinked() {
this.props.onUnlinked();
}
getProviderLink(user, provider) {
if (provider.type === "GitHub") {
return `https://github.com/${this.getUserProperty(user, provider.type, "username")}`;
} else if (provider.type === "Google") {
return "https://mail.google.com";
} else {
return "";
}
}
getUserProperty(user, providerType, propertyName) {
const key = `oauth_${providerType}_${propertyName}`;
if (user.properties === null) {return "";}
return user.properties[key];
}
unlinkUser(providerType, linkedValue) {
const body = {
providerType: providerType,
// should add the unlink user's info, cause the user may not be logged in, but a admin want to unlink the user.
user: this.props.user,
};
if (providerType === "MetaMask" || providerType === "Web3Onboard") {
import("../auth/Web3Auth")
.then(module => {
const delWeb3AuthToken = module.delWeb3AuthToken;
delWeb3AuthToken(linkedValue);
AuthBackend.unlink(body)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", "Unlinked successfully");
this.unlinked();
} else {
Setting.showMessage("error", `Failed to unlink: ${res.msg}`);
}
});
});
return;
}
AuthBackend.unlink(body)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", "Unlinked successfully");
this.unlinked();
} else {
Setting.showMessage("error", `Failed to unlink: ${res.msg}`);
}
});
}
renderIdp(user, application, providerItem) {
const provider = providerItem.provider;
const linkedValue = user[provider.type.toLowerCase()];
const profileUrl = this.getProviderLink(user, provider);
const id = this.getUserProperty(user, provider.type, "id");
const username = this.getUserProperty(user, provider.type, "username");
const displayName = this.getUserProperty(user, provider.type, "displayName");
const email = this.getUserProperty(user, provider.type, "email");
let avatarUrl = this.getUserProperty(user, provider.type, "avatarUrl");
// the account user
const account = this.props.account;
if (avatarUrl === "" || avatarUrl === undefined) {
avatarUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAQAAACROWYpAAAAHElEQVR42mNkoAAwjmoe1TyqeVTzqOZRzcNZMwB18wAfEFQkPQAAAABJRU5ErkJggg==";
}
let name = (username === undefined) ? displayName : `${displayName} (${username})`;
if (name === undefined) {
if (id !== undefined) {
name = id;
} else if (email !== undefined) {
name = email;
} else {
name = linkedValue;
}
}
let linkButtonWidth = "110px";
if (Setting.getLanguage() === "id") {
linkButtonWidth = "160px";
}
return (
<Row key={provider.name} style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={this.props.labelSpan}>
{
Setting.getProviderLogo(provider)
}
<span style={{marginLeft: "5px"}}>
{
`${provider.type}:`
}
</span>
</Col>
<Col span={24 - this.props.labelSpan} >
<AccountAvatar style={{marginRight: "10px"}} size={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
<span style={{
width: this.props.labelSpan === 3 ? "300px" : "200px",
display: (Setting.isMobile()) ? "inline" : "inline-block",
overflow: "hidden",
textOverflow: "ellipsis",
}} title={name}>
{
linkedValue === "" ? (
`(${i18next.t("general:empty")})`
) : (
profileUrl === "" ? name : (
<a target="_blank" rel="noreferrer" href={profileUrl}>
{
name
}
</a>
)
)
}
</span>
{
linkedValue === "" ? (
provider.category === "Web3" ? (
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={() => goToWeb3Url(application, provider, "link")}>{i18next.t("user:Link")}</Button>
) : (
provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger") ? (
<a key={provider.displayName}>
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={
() => {
WechatOfficialAccountModal(application, provider, "link");
}
}>{i18next.t("user:Link")}</Button>
</a>
) : (
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
</a>
)
)
) : (
<Button disabled={!providerItem.canUnlink && !Setting.isAdminUser(account)} style={{marginLeft: "20px", width: linkButtonWidth}} onClick={() => this.unlinkUser(provider.type, linkedValue)}>{i18next.t("user:Unlink")}</Button>
)
}
</Col>
</Row>
);
}
render() {
return this.renderIdp(this.props.user, this.props.application, this.props.providerItem);
}
}
export default OAuthWidget;

View File

@ -13,6 +13,8 @@
// limitations under the License.
import i18next from "i18next";
import React from "react";
import {CheckCircleTwoTone, CloseCircleTwoTone} from "@ant-design/icons";
function isValidOption_AtLeast6(password) {
if (password.length < 6) {
@ -52,6 +54,33 @@ function isValidOption_NoRepeat(password) {
return "";
}
const checkers = {
AtLeast6: isValidOption_AtLeast6,
AtLeast8: isValidOption_AtLeast8,
Aa123: isValidOption_Aa123,
SpecialChar: isValidOption_SpecialChar,
NoRepeat: isValidOption_NoRepeat,
};
function getOptionDescription(option, password) {
switch (option) {
case "AtLeast6": return i18next.t("user:The password must have at least 6 characters");
case "AtLeast8": return i18next.t("user:The password must have at least 8 characters");
case "Aa123": return i18next.t("user:The password must contain at least one uppercase letter, one lowercase letter and one digit");
case "SpecialChar": return i18next.t("user:The password must contain at least one special character");
case "NoRepeat": return i18next.t("user:The password must not contain any repeated characters");
}
}
export function renderPasswordPopover(options, password) {
return <div style={{width: 240}} >
{options.map((option, idx) => {
return <div key={idx}>{checkers[option](password) === "" ? <CheckCircleTwoTone twoToneColor={"#52c41a"} /> :
<CloseCircleTwoTone twoToneColor={"#ff4d4f"} />} {getOptionDescription(option, password)}</div>;
})}
</div>;
}
export function checkPasswordComplexity(password, options) {
if (password.length === 0) {
return i18next.t("login:Please input your password!");
@ -61,14 +90,6 @@ export function checkPasswordComplexity(password, options) {
return "";
}
const checkers = {
AtLeast6: isValidOption_AtLeast6,
AtLeast8: isValidOption_AtLeast8,
Aa123: isValidOption_Aa123,
SpecialChar: isValidOption_SpecialChar,
NoRepeat: isValidOption_NoRepeat,
};
for (const option of options) {
const checkerFunc = checkers[option];
if (checkerFunc) {

View File

@ -181,4 +181,5 @@ export const CaptchaRule = {
Always: "Always",
Never: "Never",
Dynamic: "Dynamic",
InternetOnly: "Internet-Only",
};

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Input, Modal, Row} from "antd";
import {Button, Col, Input, Modal, Popover, Row} from "antd";
import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
@ -35,6 +35,8 @@ export const PasswordModal = (props) => {
const [rePasswordValid, setRePasswordValid] = React.useState(false);
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = React.useState("");
const [rePasswordErrorMessage, setRePasswordErrorMessage] = React.useState("");
const [passwordPopoverOpen, setPasswordPopoverOpen] = React.useState(false);
const [passwordPopover, setPasswordPopover] = React.useState();
React.useEffect(() => {
if (organization) {
@ -130,12 +132,26 @@ export const PasswordModal = (props) => {
</Row>
) : null}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password
addonBefore={i18next.t("user:New Password")}
placeholder={i18next.t("user:input password")}
onChange={(e) => {handleNewPassword(e.target.value);}}
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
/>
<Popover placement={window.innerWidth >= 960 ? "right" : "top"} content={passwordPopover} open={passwordPopoverOpen}>
<Input.Password
addonBefore={i18next.t("user:New Password")}
placeholder={i18next.t("user:input password")}
onChange={(e) => {
handleNewPassword(e.target.value);
setPasswordPopoverOpen(passwordOptions?.length > 0);
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, e.target.value));
}}
onFocus={() => {
setPasswordPopoverOpen(passwordOptions?.length > 0);
setPasswordPopover(PasswordChecker.renderPasswordPopover(passwordOptions, newPassword));
}}
onBlur={() => {
setPasswordPopoverOpen(false);
}}
status={(!newPasswordValid && newPasswordErrorMessage) ? "error" : undefined}
/>
</Popover>
</Row>
{!newPasswordValid && newPasswordErrorMessage && <div style={{color: "red", marginTop: "-20px"}}>{newPasswordErrorMessage}</div>}
<Row style={{width: "100%", marginBottom: "20px"}}>

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