mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-16 07:13:49 +08:00
Compare commits
99 Commits
Author | SHA1 | Date | |
---|---|---|---|
3d12ac8dc2 | |||
f01839123f | |||
e1b3b0ac6a | |||
4b0a2fdbfc | |||
db551eb24a | |||
18b49bb731 | |||
17653888a3 | |||
ee16616df4 | |||
ea450005e0 | |||
4c5ad14f6b | |||
49dda2aea5 | |||
a74a004540 | |||
2b89f6b37b | |||
c699e35e6b | |||
e28d90d0aa | |||
4fc7600865 | |||
19f62a461b | |||
7ddc2778c0 | |||
b96fa2a995 | |||
fcfb73af6e | |||
43bebc03b9 | |||
c5f25cbc7d | |||
3feb6ce84d | |||
08d6b45fc5 | |||
56d0de64dc | |||
1813e8e8c7 | |||
e27c764a55 | |||
e5a2057382 | |||
8457ff7433 | |||
888a6f2feb | |||
b57b64fc36 | |||
0d239ba1cf | |||
8927e08217 | |||
0636069584 | |||
4d0f73c84e | |||
74a2478e10 | |||
acc6f3e887 | |||
185ab9750a | |||
48adc050d6 | |||
b0e318c9db | |||
f9a6efc00f | |||
bd4a6775dd | |||
e3a43d0062 | |||
0cf281cac0 | |||
7322f67ae0 | |||
b927c6d7b4 | |||
01212cd1f3 | |||
bf55f94d41 | |||
f14711d315 | |||
58e1c28f7c | |||
922b19c64b | |||
1d21c3fa90 | |||
6175fd6764 | |||
2ceb54f058 | |||
aaeaa7fefa | |||
d522247552 | |||
79dbdab6c9 | |||
fe40910e3b | |||
2d1736f13a | |||
12b4d1c7cd | |||
a45d2b87c1 | |||
8484465d09 | |||
dff65eee20 | |||
596016456c | |||
673261c258 | |||
3c5985a3c0 | |||
4f3d62520a | |||
96f8b3d937 | |||
7ab5a5ade1 | |||
5cbd0a96ca | |||
7ccd8c4d4f | |||
b0fa3fc484 | |||
af01c4226a | |||
7a3d85a29a | |||
fd5ccd8d41 | |||
a439c5195d | |||
ba2e997d54 | |||
0818de85d1 | |||
457c6098a4 | |||
60f979fbb5 | |||
ff53e44fa6 | |||
1832de47db | |||
535eb0c465 | |||
c190634cf3 | |||
f7559aa040 | |||
1e0b709c73 | |||
c0800b7fb3 | |||
6fcdad2100 | |||
69d26d5c21 | |||
94e6b5ecb8 | |||
95e8bdcd36 | |||
6f1f93725e | |||
7ae067e369 | |||
dde936e935 | |||
fb561a98c8 | |||
7cd8f030ee | |||
a3f8ded10c | |||
e3d135bc6e | |||
fc864b0de4 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -114,12 +114,12 @@ jobs:
|
|||||||
wait-on-timeout: 210
|
wait-on-timeout: 210
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: cypress-screenshots
|
name: cypress-screenshots
|
||||||
path: ./web/cypress/screenshots
|
path: ./web/cypress/screenshots
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: cypress-videos
|
name: cypress-videos
|
||||||
@ -147,7 +147,7 @@ jobs:
|
|||||||
- name: Release
|
- name: Release
|
||||||
run: yarn global add semantic-release@17.4.4 && semantic-release
|
run: yarn global add semantic-release@17.4.4 && semantic-release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Fetch Current version
|
- name: Fetch Current version
|
||||||
id: get-current-tag
|
id: get-current-tag
|
||||||
|
@ -98,6 +98,7 @@ p, *, *, GET, /api/get-organization-names, *, *
|
|||||||
p, *, *, GET, /api/get-all-objects, *, *
|
p, *, *, GET, /api/get-all-objects, *, *
|
||||||
p, *, *, GET, /api/get-all-actions, *, *
|
p, *, *, GET, /api/get-all-actions, *, *
|
||||||
p, *, *, GET, /api/get-all-roles, *, *
|
p, *, *, GET, /api/get-all-roles, *, *
|
||||||
|
p, *, *, GET, /api/run-casbin-command, *, *
|
||||||
p, *, *, GET, /api/get-invitation-info, *, *
|
p, *, *, GET, /api/get-invitation-info, *, *
|
||||||
p, *, *, GET, /api/faceid-signin-begin, *, *
|
p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||||
`
|
`
|
||||||
|
@ -25,9 +25,13 @@ enableErrorMask = false
|
|||||||
enableGzip = true
|
enableGzip = true
|
||||||
inactiveTimeoutMinutes =
|
inactiveTimeoutMinutes =
|
||||||
ldapServerPort = 389
|
ldapServerPort = 389
|
||||||
|
ldapsCertId = ""
|
||||||
|
ldapsServerPort = 636
|
||||||
radiusServerPort = 1812
|
radiusServerPort = 1812
|
||||||
|
radiusDefaultOrganization = "built-in"
|
||||||
radiusSecret = "secret"
|
radiusSecret = "secret"
|
||||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||||
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||||
|
initDataNewOnly = false
|
||||||
initDataFile = "./init_data.json"
|
initDataFile = "./init_data.json"
|
||||||
frontendBaseDir = "../casdoor"
|
frontendBaseDir = "../cc_0"
|
@ -116,6 +116,13 @@ func (c *ApiController) Signup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
|
err = object.CheckEntryIp(clientIp, nil, application, organization, c.GetAcceptLanguage())
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
|
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
c.ResponseError(msg)
|
c.ResponseError(msg)
|
||||||
|
@ -110,6 +110,9 @@ func (c *ApiController) GetApplication() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
|
object.CheckEntryIp(clientIp, nil, application, nil, c.GetAcceptLanguage())
|
||||||
|
|
||||||
c.ResponseOk(object.GetMaskedApplication(application, userId))
|
c.ResponseOk(object.GetMaskedApplication(application, userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,6 +232,11 @@ func (c *ApiController) UpdateApplication() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = object.CheckIpWhitelist(application.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application))
|
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
@ -259,6 +267,11 @@ func (c *ApiController) AddApplication() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = object.CheckIpWhitelist(application.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.AddApplication(&application))
|
c.Data["json"] = wrapActionResponse(object.AddApplication(&application))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -55,6 +56,13 @@ func tokenToResponse(token *object.Token) *Response {
|
|||||||
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
||||||
userId := user.GetId()
|
userId := user.GetId()
|
||||||
|
|
||||||
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
|
err := object.CheckEntryIp(clientIp, user, application, application.OrganizationObj, c.GetAcceptLanguage())
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
allowed, err := object.CheckLoginPermission(userId, application)
|
allowed, err := object.CheckLoginPermission(userId, application)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
@ -256,6 +264,9 @@ func (c *ApiController) GetApplicationLogin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
|
object.CheckEntryIp(clientIp, nil, application, nil, c.GetAcceptLanguage())
|
||||||
|
|
||||||
application = object.GetMaskedApplication(application, "")
|
application = object.GetMaskedApplication(application, "")
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
c.ResponseError(msg, application)
|
c.ResponseError(msg, application)
|
||||||
@ -320,6 +331,8 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verificationType := ""
|
||||||
|
|
||||||
if authForm.Username != "" {
|
if authForm.Username != "" {
|
||||||
if authForm.Type == ResponseTypeLogin {
|
if authForm.Type == ResponseTypeLogin {
|
||||||
if c.GetSessionUsername() != "" {
|
if c.GetSessionUsername() != "" {
|
||||||
@ -414,6 +427,12 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if verificationCodeType == object.VerifyTypePhone {
|
||||||
|
verificationType = "sms"
|
||||||
|
} else {
|
||||||
|
verificationType = "email"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||||
@ -513,8 +532,19 @@ func (c *ApiController) Login() {
|
|||||||
|
|
||||||
if user.IsMfaEnabled() {
|
if user.IsMfaEnabled() {
|
||||||
c.setMfaUserSession(user.GetId())
|
c.setMfaUserSession(user.GetId())
|
||||||
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
|
mfaList := object.GetAllMfaProps(user, true)
|
||||||
return
|
mfaAllowList := []*object.MfaProps{}
|
||||||
|
for _, prop := range mfaList {
|
||||||
|
if prop.MfaType == verificationType || !prop.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mfaAllowList = append(mfaAllowList, prop)
|
||||||
|
}
|
||||||
|
if len(mfaAllowList) >= 1 {
|
||||||
|
c.SetSession("verificationCodeType", verificationType)
|
||||||
|
c.ResponseOk(object.NextMfa, mfaAllowList)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||||
@ -607,6 +637,17 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
|
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if provider.EmailRegex != "" {
|
||||||
|
reg, err := regexp.Compile(provider.EmailRegex)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reg.MatchString(userInfo.Email) {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("check:Email is invalid")))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if authForm.Method == "signup" {
|
if authForm.Method == "signup" {
|
||||||
@ -844,7 +885,12 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if authForm.Passcode != "" {
|
if authForm.Passcode != "" {
|
||||||
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
|
if authForm.MfaType == c.GetSession("verificationCodeType") {
|
||||||
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.CountryCode = user.GetCountryCode(user.CountryCode)
|
||||||
|
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetMfaProps(authForm.MfaType, false))
|
||||||
if mfaUtil == nil {
|
if mfaUtil == nil {
|
||||||
c.ResponseError("Invalid multi-factor authentication type")
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
return
|
return
|
||||||
@ -855,6 +901,7 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.SetSession("verificationCodeType", "")
|
||||||
} else if authForm.RecoveryCode != "" {
|
} else if authForm.RecoveryCode != "" {
|
||||||
err = object.MfaRecover(user, authForm.RecoveryCode)
|
err = object.MfaRecover(user, authForm.RecoveryCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
114
controllers/casbin_cli_api.go
Normal file
114
controllers/casbin_cli_api.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
||||||
|
tempFiles := []string{}
|
||||||
|
newArgs := []string{}
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
if (args[i] == "-m" || args[i] == "-p") && i+1 < len(args) {
|
||||||
|
pattern := fmt.Sprintf("casbin_temp_%s_*.conf", args[i])
|
||||||
|
tempFile, err := os.CreateTemp("", pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tempFile.WriteString(args[i+1])
|
||||||
|
if err != nil {
|
||||||
|
tempFile.Close()
|
||||||
|
return nil, nil, fmt.Errorf("failed to write to temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.Close()
|
||||||
|
tempFiles = append(tempFiles, tempFile.Name())
|
||||||
|
newArgs = append(newArgs, args[i], tempFile.Name())
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
newArgs = append(newArgs, args[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tempFiles, newArgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCasbinCommand
|
||||||
|
// @Title RunCasbinCommand
|
||||||
|
// @Tag Enforcer API
|
||||||
|
// @Description Call Casbin CLI commands
|
||||||
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
|
// @router /run-casbin-command [get]
|
||||||
|
func (c *ApiController) RunCasbinCommand() {
|
||||||
|
language := c.Input().Get("language")
|
||||||
|
argString := c.Input().Get("args")
|
||||||
|
|
||||||
|
if language == "" {
|
||||||
|
language = "go"
|
||||||
|
}
|
||||||
|
// use "casbin-go-cli" by default, can be also "casbin-java-cli", "casbin-node-cli", etc.
|
||||||
|
// the pre-built binary of "casbin-go-cli" can be found at: https://github.com/casbin/casbin-go-cli/releases
|
||||||
|
binaryName := fmt.Sprintf("casbin-%s-cli", language)
|
||||||
|
|
||||||
|
_, err := exec.LookPath(binaryName)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(fmt.Sprintf("executable file: %s not found in PATH", binaryName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RBAC model & policy example:
|
||||||
|
// https://door.casdoor.com/api/run-casbin-command?language=go&args=["enforce", "-m", "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) %26%26 r.obj == p.obj %26%26 r.act == p.act", "-p", "p, alice, data1, read\np, bob, data2, write\np, data2_admin, data2, read\np, data2_admin, data2, write\ng, alice, data2_admin", "alice", "data1", "read"]
|
||||||
|
// Casbin CLI usage:
|
||||||
|
// https://github.com/jcasbin/casbin-java-cli?tab=readme-ov-file#get-started
|
||||||
|
var args []string
|
||||||
|
err = json.Unmarshal([]byte(argString), &args)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFiles, processedArgs, err := processArgsToTempFiles(args)
|
||||||
|
defer func() {
|
||||||
|
for _, file := range tempFiles {
|
||||||
|
os.Remove(file)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command := exec.Command(binaryName, processedArgs...)
|
||||||
|
outputBytes, err := command.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
errorString := err.Error()
|
||||||
|
if outputBytes != nil {
|
||||||
|
output := string(outputBytes)
|
||||||
|
errorString = fmt.Sprintf("%s, error: %s", output, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ResponseError(errorString)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output := string(outputBytes)
|
||||||
|
output = strings.TrimSuffix(output, "\n")
|
||||||
|
c.ResponseOk(output)
|
||||||
|
}
|
@ -22,13 +22,6 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
MfaRecoveryCodesSession = "mfa_recovery_codes"
|
|
||||||
MfaCountryCodeSession = "mfa_country_code"
|
|
||||||
MfaDestSession = "mfa_dest"
|
|
||||||
MfaTotpSecretSession = "mfa_totp_secret"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MfaSetupInitiate
|
// MfaSetupInitiate
|
||||||
// @Title MfaSetupInitiate
|
// @Title MfaSetupInitiate
|
||||||
// @Tag MFA API
|
// @Tag MFA API
|
||||||
@ -72,11 +65,6 @@ func (c *ApiController) MfaSetupInitiate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recoveryCode := uuid.NewString()
|
recoveryCode := uuid.NewString()
|
||||||
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
|
|
||||||
if mfaType == object.TotpType {
|
|
||||||
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
mfaProps.RecoveryCodes = []string{recoveryCode}
|
mfaProps.RecoveryCodes = []string{recoveryCode}
|
||||||
|
|
||||||
resp := mfaProps
|
resp := mfaProps
|
||||||
@ -94,6 +82,9 @@ func (c *ApiController) MfaSetupInitiate() {
|
|||||||
func (c *ApiController) MfaSetupVerify() {
|
func (c *ApiController) MfaSetupVerify() {
|
||||||
mfaType := c.Ctx.Request.Form.Get("mfaType")
|
mfaType := c.Ctx.Request.Form.Get("mfaType")
|
||||||
passcode := c.Ctx.Request.Form.Get("passcode")
|
passcode := c.Ctx.Request.Form.Get("passcode")
|
||||||
|
secret := c.Ctx.Request.Form.Get("secret")
|
||||||
|
dest := c.Ctx.Request.Form.Get("dest")
|
||||||
|
countryCode := c.Ctx.Request.Form.Get("countryCode")
|
||||||
|
|
||||||
if mfaType == "" || passcode == "" {
|
if mfaType == "" || passcode == "" {
|
||||||
c.ResponseError("missing auth type or passcode")
|
c.ResponseError("missing auth type or passcode")
|
||||||
@ -104,32 +95,28 @@ func (c *ApiController) MfaSetupVerify() {
|
|||||||
MfaType: mfaType,
|
MfaType: mfaType,
|
||||||
}
|
}
|
||||||
if mfaType == object.TotpType {
|
if mfaType == object.TotpType {
|
||||||
secret := c.GetSession(MfaTotpSecretSession)
|
if secret == "" {
|
||||||
if secret == nil {
|
|
||||||
c.ResponseError("totp secret is missing")
|
c.ResponseError("totp secret is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.Secret = secret.(string)
|
config.Secret = secret
|
||||||
} else if mfaType == object.SmsType {
|
} else if mfaType == object.SmsType {
|
||||||
dest := c.GetSession(MfaDestSession)
|
if dest == "" {
|
||||||
if dest == nil {
|
|
||||||
c.ResponseError("destination is missing")
|
c.ResponseError("destination is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.Secret = dest.(string)
|
config.Secret = dest
|
||||||
countryCode := c.GetSession(MfaCountryCodeSession)
|
if countryCode == "" {
|
||||||
if countryCode == nil {
|
|
||||||
c.ResponseError("country code is missing")
|
c.ResponseError("country code is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.CountryCode = countryCode.(string)
|
config.CountryCode = countryCode
|
||||||
} else if mfaType == object.EmailType {
|
} else if mfaType == object.EmailType {
|
||||||
dest := c.GetSession(MfaDestSession)
|
if dest == "" {
|
||||||
if dest == nil {
|
|
||||||
c.ResponseError("destination is missing")
|
c.ResponseError("destination is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.Secret = dest.(string)
|
config.Secret = dest
|
||||||
}
|
}
|
||||||
|
|
||||||
mfaUtil := object.GetMfaUtil(mfaType, config)
|
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||||
@ -159,6 +146,10 @@ func (c *ApiController) MfaSetupEnable() {
|
|||||||
owner := c.Ctx.Request.Form.Get("owner")
|
owner := c.Ctx.Request.Form.Get("owner")
|
||||||
name := c.Ctx.Request.Form.Get("name")
|
name := c.Ctx.Request.Form.Get("name")
|
||||||
mfaType := c.Ctx.Request.Form.Get("mfaType")
|
mfaType := c.Ctx.Request.Form.Get("mfaType")
|
||||||
|
secret := c.Ctx.Request.Form.Get("secret")
|
||||||
|
dest := c.Ctx.Request.Form.Get("dest")
|
||||||
|
countryCode := c.Ctx.Request.Form.Get("secret")
|
||||||
|
recoveryCodes := c.Ctx.Request.Form.Get("recoveryCodes")
|
||||||
|
|
||||||
user, err := object.GetUser(util.GetId(owner, name))
|
user, err := object.GetUser(util.GetId(owner, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -176,43 +167,39 @@ func (c *ApiController) MfaSetupEnable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mfaType == object.TotpType {
|
if mfaType == object.TotpType {
|
||||||
secret := c.GetSession(MfaTotpSecretSession)
|
if secret == "" {
|
||||||
if secret == nil {
|
|
||||||
c.ResponseError("totp secret is missing")
|
c.ResponseError("totp secret is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.Secret = secret.(string)
|
config.Secret = secret
|
||||||
} else if mfaType == object.EmailType {
|
} else if mfaType == object.EmailType {
|
||||||
if user.Email == "" {
|
if user.Email == "" {
|
||||||
dest := c.GetSession(MfaDestSession)
|
if dest == "" {
|
||||||
if dest == nil {
|
|
||||||
c.ResponseError("destination is missing")
|
c.ResponseError("destination is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Email = dest.(string)
|
user.Email = dest
|
||||||
}
|
}
|
||||||
} else if mfaType == object.SmsType {
|
} else if mfaType == object.SmsType {
|
||||||
if user.Phone == "" {
|
if user.Phone == "" {
|
||||||
dest := c.GetSession(MfaDestSession)
|
if dest == "" {
|
||||||
if dest == nil {
|
|
||||||
c.ResponseError("destination is missing")
|
c.ResponseError("destination is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Phone = dest.(string)
|
user.Phone = dest
|
||||||
countryCode := c.GetSession(MfaCountryCodeSession)
|
if countryCode == "" {
|
||||||
if countryCode == nil {
|
|
||||||
c.ResponseError("country code is missing")
|
c.ResponseError("country code is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.CountryCode = countryCode.(string)
|
user.CountryCode = countryCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
|
|
||||||
if recoveryCodes == nil {
|
if recoveryCodes == "" {
|
||||||
c.ResponseError("recovery codes is missing")
|
c.ResponseError("recovery codes is missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.RecoveryCodes = []string{recoveryCodes.(string)}
|
config.RecoveryCodes = []string{recoveryCodes}
|
||||||
|
|
||||||
mfaUtil := object.GetMfaUtil(mfaType, config)
|
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||||
if mfaUtil == nil {
|
if mfaUtil == nil {
|
||||||
@ -226,14 +213,6 @@ func (c *ApiController) MfaSetupEnable() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.DelSession(MfaRecoveryCodesSession)
|
|
||||||
if mfaType == object.TotpType {
|
|
||||||
c.DelSession(MfaTotpSecretSession)
|
|
||||||
} else {
|
|
||||||
c.DelSession(MfaCountryCodeSession)
|
|
||||||
c.DelSession(MfaDestSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.ResponseOk(http.StatusText(http.StatusOK))
|
c.ResponseOk(http.StatusText(http.StatusOK))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +119,11 @@ func (c *ApiController) UpdateOrganization() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = object.CheckIpWhitelist(organization.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
@ -149,6 +154,11 @@ func (c *ApiController) AddOrganization() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = object.CheckIpWhitelist(organization.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
|
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -182,6 +182,10 @@ func (c *ApiController) BuyProduct() {
|
|||||||
paidUserName := c.Input().Get("userName")
|
paidUserName := c.Input().Get("userName")
|
||||||
owner, _ := util.GetOwnerAndNameFromId(id)
|
owner, _ := util.GetOwnerAndNameFromId(id)
|
||||||
userId := util.GetId(owner, paidUserName)
|
userId := util.GetId(owner, paidUserName)
|
||||||
|
if paidUserName != "" && !c.IsAdmin() {
|
||||||
|
c.ResponseError(c.T("general:Only admin user can specify user"))
|
||||||
|
return
|
||||||
|
}
|
||||||
if paidUserName == "" {
|
if paidUserName == "" {
|
||||||
userId = c.GetSessionUsername()
|
userId = c.GetSessionUsername()
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *RootController) HandleScim() {
|
func (c *RootController) HandleScim() {
|
||||||
|
_, ok := c.RequireAdmin()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path := c.Ctx.Request.URL.Path
|
path := c.Ctx.Request.URL.Path
|
||||||
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
||||||
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||||
|
@ -93,7 +93,7 @@ func (c *ApiController) SendEmail() {
|
|||||||
|
|
||||||
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
||||||
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
||||||
err = object.DailSmtpServer(provider)
|
err = object.TestSmtpServer(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -322,17 +322,22 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||||
token, err := object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
var token *object.Token
|
||||||
if err != nil {
|
if tokenTypeHint != "" {
|
||||||
c.ResponseTokenError(err.Error())
|
token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
||||||
return
|
if err != nil {
|
||||||
}
|
c.ResponseTokenError(err.Error())
|
||||||
if token == nil {
|
return
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
}
|
||||||
c.ServeJSON()
|
if token == nil {
|
||||||
return
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var introspectionResponse object.IntrospectionResponse
|
||||||
|
|
||||||
if application.TokenFormat == "JWT-Standard" {
|
if application.TokenFormat == "JWT-Standard" {
|
||||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||||
if err != nil || jwtToken.Valid() != nil {
|
if err != nil || jwtToken.Valid() != nil {
|
||||||
@ -344,12 +349,37 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Data["json"] = &object.IntrospectionResponse{
|
introspectionResponse = object.IntrospectionResponse{
|
||||||
Active: true,
|
Active: true,
|
||||||
Scope: jwtToken.Scope,
|
Scope: jwtToken.Scope,
|
||||||
ClientId: clientId,
|
ClientId: clientId,
|
||||||
Username: token.User,
|
Username: jwtToken.Name,
|
||||||
TokenType: token.TokenType,
|
TokenType: jwtToken.TokenType,
|
||||||
|
Exp: jwtToken.ExpiresAt.Unix(),
|
||||||
|
Iat: jwtToken.IssuedAt.Unix(),
|
||||||
|
Nbf: jwtToken.NotBefore.Unix(),
|
||||||
|
Sub: jwtToken.Subject,
|
||||||
|
Aud: jwtToken.Audience,
|
||||||
|
Iss: jwtToken.Issuer,
|
||||||
|
Jti: jwtToken.ID,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||||
|
if err != nil || jwtToken.Valid() != nil {
|
||||||
|
// and token revoked case. but we not implement
|
||||||
|
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||||
|
// refs: https://tools.ietf.org/html/rfc7009
|
||||||
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
introspectionResponse = object.IntrospectionResponse{
|
||||||
|
Active: true,
|
||||||
|
Scope: jwtToken.Scope,
|
||||||
|
ClientId: clientId,
|
||||||
|
Username: jwtToken.Name,
|
||||||
|
TokenType: jwtToken.TokenType,
|
||||||
Exp: jwtToken.ExpiresAt.Unix(),
|
Exp: jwtToken.ExpiresAt.Unix(),
|
||||||
Iat: jwtToken.IssuedAt.Unix(),
|
Iat: jwtToken.IssuedAt.Unix(),
|
||||||
Nbf: jwtToken.NotBefore.Unix(),
|
Nbf: jwtToken.NotBefore.Unix(),
|
||||||
@ -358,33 +388,22 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
Iss: jwtToken.Issuer,
|
Iss: jwtToken.Issuer,
|
||||||
Jti: jwtToken.ID,
|
Jti: jwtToken.ID,
|
||||||
}
|
}
|
||||||
c.ServeJSON()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
if tokenTypeHint == "" {
|
||||||
if err != nil || jwtToken.Valid() != nil {
|
token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
|
||||||
// and token revoked case. but we not implement
|
if err != nil {
|
||||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
c.ResponseTokenError(err.Error())
|
||||||
// refs: https://tools.ietf.org/html/rfc7009
|
return
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
}
|
||||||
c.ServeJSON()
|
if token == nil {
|
||||||
return
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
introspectionResponse.TokenType = token.TokenType
|
||||||
|
|
||||||
c.Data["json"] = &object.IntrospectionResponse{
|
c.Data["json"] = introspectionResponse
|
||||||
Active: true,
|
|
||||||
Scope: jwtToken.Scope,
|
|
||||||
ClientId: clientId,
|
|
||||||
Username: token.User,
|
|
||||||
TokenType: token.TokenType,
|
|
||||||
Exp: jwtToken.ExpiresAt.Unix(),
|
|
||||||
Iat: jwtToken.IssuedAt.Unix(),
|
|
||||||
Nbf: jwtToken.NotBefore.Unix(),
|
|
||||||
Sub: jwtToken.Subject,
|
|
||||||
Aud: jwtToken.Audience,
|
|
||||||
Iss: jwtToken.Issuer,
|
|
||||||
Jti: jwtToken.ID,
|
|
||||||
}
|
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -353,18 +353,13 @@ func (c *ApiController) AddUser() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := object.GetUserCount("", "", "", "")
|
if err := checkQuotaForUser(); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkQuotaForUser(int(count)); err != nil {
|
emptyUser := object.User{}
|
||||||
c.ResponseError(err.Error())
|
msg := object.CheckUpdateUser(&emptyUser, &user, c.GetAcceptLanguage())
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := object.CheckUsername(user.Name, c.GetAcceptLanguage())
|
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
c.ResponseError(msg)
|
c.ResponseError(msg)
|
||||||
return
|
return
|
||||||
@ -474,6 +469,16 @@ func (c *ApiController) SetPassword() {
|
|||||||
|
|
||||||
userId := util.GetId(userOwner, userName)
|
userId := util.GetId(userOwner, userName)
|
||||||
|
|
||||||
|
user, err := object.GetUser(userId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
requestUserId := c.GetSessionUsername()
|
requestUserId := c.GetSessionUsername()
|
||||||
if requestUserId == "" && code == "" {
|
if requestUserId == "" && code == "" {
|
||||||
c.ResponseError(c.T("general:Please login first"), "Please login first")
|
c.ResponseError(c.T("general:Please login first"), "Please login first")
|
||||||
@ -489,7 +494,12 @@ func (c *ApiController) SetPassword() {
|
|||||||
c.ResponseError(c.T("general:Missing parameter"))
|
c.ResponseError(c.T("general:Missing parameter"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if userId != c.GetSession("verifiedUserId") {
|
||||||
|
c.ResponseError(c.T("general:Wrong userId"))
|
||||||
|
return
|
||||||
|
}
|
||||||
c.SetSession("verifiedCode", "")
|
c.SetSession("verifiedCode", "")
|
||||||
|
c.SetSession("verifiedUserId", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
targetUser, err := object.GetUser(userId)
|
targetUser, err := object.GetUser(userId)
|
||||||
@ -512,7 +522,11 @@ func (c *ApiController) SetPassword() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if code == "" {
|
} else if code == "" {
|
||||||
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
|
if user.Ldap == "" {
|
||||||
|
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
|
||||||
|
} else {
|
||||||
|
err = object.CheckLdapUserPassword(targetUser, oldPassword, c.GetAcceptLanguage())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
@ -535,11 +549,38 @@ func (c *ApiController) SetPassword() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application, err := object.GetApplicationByUser(targetUser)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if application == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("auth:the application for user %s is not found"), userId))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
|
err = object.CheckEntryIp(clientIp, targetUser, application, organization, c.GetAcceptLanguage())
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
targetUser.Password = newPassword
|
targetUser.Password = newPassword
|
||||||
targetUser.UpdateUserPassword(organization)
|
targetUser.UpdateUserPassword(organization)
|
||||||
targetUser.NeedUpdatePassword = false
|
targetUser.NeedUpdatePassword = false
|
||||||
|
targetUser.LastChangePasswordTime = util.GetCurrentTime()
|
||||||
|
|
||||||
|
if user.Ldap == "" {
|
||||||
|
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
||||||
|
} else {
|
||||||
|
if isAdmin {
|
||||||
|
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())
|
||||||
|
} else {
|
||||||
|
err = object.ResetLdapPassword(targetUser, oldPassword, newPassword, c.GetAcceptLanguage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -294,12 +294,18 @@ func checkQuotaForProvider(count int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkQuotaForUser(count int) error {
|
func checkQuotaForUser() error {
|
||||||
quota := conf.GetConfigQuota().User
|
quota := conf.GetConfigQuota().User
|
||||||
if quota == -1 {
|
if quota == -1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if count >= quota {
|
|
||||||
|
count, err := object.GetUserCount("", "", "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(count) >= quota {
|
||||||
return fmt.Errorf("user quota is exceeded")
|
return fmt.Errorf("user quota is exceeded")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -132,7 +132,8 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
|
|
||||||
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
|
|
||||||
if msg := vform.CheckParameter(form.SendVerifyCode, c.GetAcceptLanguage()); msg != "" {
|
if msg := vform.CheckParameter(form.SendVerifyCode, c.GetAcceptLanguage()); msg != "" {
|
||||||
c.ResponseError(msg)
|
c.ResponseError(msg)
|
||||||
@ -245,8 +246,6 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||||
vform.Dest = mfaProps.Secret
|
vform.Dest = mfaProps.Secret
|
||||||
}
|
}
|
||||||
} else if vform.Method == MfaSetupVerification {
|
|
||||||
c.SetSession(MfaDestSession, vform.Dest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err = application.GetEmailProvider(vform.Method)
|
provider, err = application.GetEmailProvider(vform.Method)
|
||||||
@ -259,7 +258,7 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, vform.Dest)
|
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, clientIp, vform.Dest)
|
||||||
case object.VerifyTypePhone:
|
case object.VerifyTypePhone:
|
||||||
if vform.Method == LoginVerification || vform.Method == ForgetVerification {
|
if vform.Method == LoginVerification || vform.Method == ForgetVerification {
|
||||||
if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {
|
if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {
|
||||||
@ -281,11 +280,6 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if vform.Method == MfaSetupVerification {
|
|
||||||
c.SetSession(MfaCountryCodeSession, vform.CountryCode)
|
|
||||||
c.SetSession(MfaDestSession, vform.Dest)
|
|
||||||
}
|
|
||||||
} else if vform.Method == MfaAuthVerification {
|
} else if vform.Method == MfaAuthVerification {
|
||||||
mfaProps := user.GetPreferredMfaProps(false)
|
mfaProps := user.GetPreferredMfaProps(false)
|
||||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||||
@ -293,6 +287,7 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vform.CountryCode = mfaProps.CountryCode
|
vform.CountryCode = mfaProps.CountryCode
|
||||||
|
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode)
|
provider, err = application.GetSmsProvider(vform.Method, vform.CountryCode)
|
||||||
@ -309,7 +304,7 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))
|
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), vform.CountryCode))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, phone)
|
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, clientIp, phone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,5 +527,6 @@ func (c *ApiController) VerifyCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.SetSession("verifiedCode", authForm.Code)
|
c.SetSession("verifiedCode", authForm.Code)
|
||||||
|
c.SetSession("verifiedUserId", user.GetId())
|
||||||
c.ResponseOk()
|
c.ResponseOk()
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,9 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/gomail/v2"
|
"github.com/casdoor/gomail/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,6 +35,13 @@ func NewSmtpEmailProvider(userName string, password string, host string, port in
|
|||||||
|
|
||||||
dialer.SSL = !disableSsl
|
dialer.SSL = !disableSsl
|
||||||
|
|
||||||
|
if strings.HasSuffix(host, ".amazonaws.com") {
|
||||||
|
socks5Proxy := conf.GetConfigString("socks5Proxy")
|
||||||
|
if socks5Proxy != "" {
|
||||||
|
dialer.SetSocks5Proxy(socks5Proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &SmtpEmailProvider{Dialer: dialer}
|
return &SmtpEmailProvider{Dialer: dialer}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
go.mod
11
go.mod
@ -9,10 +9,10 @@ require (
|
|||||||
github.com/beego/beego v1.12.12
|
github.com/beego/beego v1.12.12
|
||||||
github.com/beevik/etree v1.1.0
|
github.com/beevik/etree v1.1.0
|
||||||
github.com/casbin/casbin/v2 v2.77.2
|
github.com/casbin/casbin/v2 v2.77.2
|
||||||
github.com/casdoor/go-sms-sender v0.24.0
|
github.com/casdoor/go-sms-sender v0.25.0
|
||||||
github.com/casdoor/gomail/v2 v2.0.1
|
github.com/casdoor/gomail/v2 v2.1.0
|
||||||
github.com/casdoor/ldapserver v1.2.0
|
github.com/casdoor/ldapserver v1.2.0
|
||||||
github.com/casdoor/notify v0.45.0
|
github.com/casdoor/notify v1.0.0
|
||||||
github.com/casdoor/oss v1.8.0
|
github.com/casdoor/oss v1.8.0
|
||||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||||
@ -60,9 +60,10 @@ require (
|
|||||||
github.com/xorm-io/core v0.7.4
|
github.com/xorm-io/core v0.7.4
|
||||||
github.com/xorm-io/xorm v1.1.6
|
github.com/xorm-io/xorm v1.1.6
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
golang.org/x/crypto v0.21.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/net v0.21.0
|
golang.org/x/net v0.34.0
|
||||||
golang.org/x/oauth2 v0.17.0
|
golang.org/x/oauth2 v0.17.0
|
||||||
|
golang.org/x/text v0.21.0
|
||||||
google.golang.org/api v0.150.0
|
google.golang.org/api v0.150.0
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
|
45
go.sum
45
go.sum
@ -1087,14 +1087,14 @@ github.com/casdoor/casdoor-go-sdk v0.50.0 h1:bUYbz/MzJuWfLKJbJM0+U0YpYewAur+THp5
|
|||||||
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
|
github.com/casdoor/casdoor-go-sdk v0.50.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
|
||||||
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
|
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
|
||||||
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
|
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
|
||||||
github.com/casdoor/go-sms-sender v0.24.0 h1:LNLsce3EG/87I3JS6UiajF3LlQmdIiCgebEu0IE4wSM=
|
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
|
||||||
github.com/casdoor/go-sms-sender v0.24.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
|
github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
|
||||||
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
|
github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWUxM=
|
||||||
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
|
github.com/casdoor/gomail/v2 v2.1.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
|
||||||
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
|
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
|
||||||
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
|
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
|
||||||
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
|
github.com/casdoor/notify v1.0.0 h1:oldsaaQFPrlufm/OA314z8DwFVE1Tc9Gt1z4ptRHhXw=
|
||||||
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
|
github.com/casdoor/notify v1.0.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
|
||||||
github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
|
github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
|
||||||
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
|
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
|
||||||
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
|
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
|
||||||
@ -2163,8 +2163,10 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
|||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@ -2230,8 +2232,10 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -2319,8 +2323,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -2375,8 +2381,11 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -2503,8 +2512,11 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
|
||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@ -2524,8 +2536,10 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
|
||||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -2546,8 +2560,10 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@ -2634,8 +2650,9 @@ golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
|||||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||||
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
@ -1,167 +1,167 @@
|
|||||||
{
|
{
|
||||||
"account": {
|
"account": {
|
||||||
"Failed to add user": "Failed to add user",
|
"Failed to add user": "عدم موفقیت در افزودن کاربر",
|
||||||
"Get init score failed, error: %w": "Get init score failed, error: %w",
|
"Get init score failed, error: %w": "عدم موفقیت در دریافت امتیاز اولیه، خطا: %w",
|
||||||
"Please sign out first": "Please sign out first",
|
"Please sign out first": "لطفاً ابتدا خارج شوید",
|
||||||
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
|
"The application does not allow to sign up new account": "برنامه اجازه ثبتنام حساب جدید را نمیدهد"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"Challenge method should be S256": "Challenge method should be S256",
|
"Challenge method should be S256": "روش چالش باید S256 باشد",
|
||||||
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
|
"Failed to create user, user information is invalid: %s": "عدم موفقیت در ایجاد کاربر، اطلاعات کاربر نامعتبر است: %s",
|
||||||
"Failed to login in: %s": "Failed to login in: %s",
|
"Failed to login in: %s": "عدم موفقیت در ورود: %s",
|
||||||
"Invalid token": "Invalid token",
|
"Invalid token": "توکن نامعتبر",
|
||||||
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
|
"State expected: %s, but got: %s": "وضعیت مورد انتظار: %s، اما دریافت شد: %s",
|
||||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
|
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "حساب برای ارائهدهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبتنام بهعنوان حساب جدید از طریق %%s نیست، لطفاً از روش دیگری برای ثبتنام استفاده کنید",
|
||||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
|
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "حساب برای ارائهدهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبتنام بهعنوان حساب جدید نیست، لطفاً با پشتیبانی IT خود تماس بگیرید",
|
||||||
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
|
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "حساب برای ارائهدهنده: %s و نام کاربری: %s (%s) در حال حاضر به حساب دیگری مرتبط است: %s (%s)",
|
||||||
"The application: %s does not exist": "The application: %s does not exist",
|
"The application: %s does not exist": "برنامه: %s وجود ندارد",
|
||||||
"The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application",
|
"The login method: login with LDAP is not enabled for the application": "روش ورود: ورود با LDAP برای برنامه فعال نیست",
|
||||||
"The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application",
|
"The login method: login with SMS is not enabled for the application": "روش ورود: ورود با پیامک برای برنامه فعال نیست",
|
||||||
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application",
|
"The login method: login with email is not enabled for the application": "روش ورود: ورود با ایمیل برای برنامه فعال نیست",
|
||||||
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application",
|
"The login method: login with face is not enabled for the application": "روش ورود: ورود با چهره برای برنامه فعال نیست",
|
||||||
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
|
"The login method: login with password is not enabled for the application": "روش ورود: ورود با رمز عبور برای برنامه فعال نیست",
|
||||||
"The organization: %s does not exist": "The organization: %s does not exist",
|
"The organization: %s does not exist": "سازمان: %s وجود ندارد",
|
||||||
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
|
"The provider: %s is not enabled for the application": "ارائهدهنده: %s برای برنامه فعال نیست",
|
||||||
"Unauthorized operation": "Unauthorized operation",
|
"Unauthorized operation": "عملیات غیرمجاز",
|
||||||
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
|
"Unknown authentication type (not password or provider), form = %s": "نوع احراز هویت ناشناخته (نه رمز عبور و نه ارائهدهنده)، فرم = %s",
|
||||||
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
|
"User's tag: %s is not listed in the application's tags": "برچسب کاربر: %s در برچسبهای برنامه فهرست نشده است",
|
||||||
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
|
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "کاربر پرداختی %s اشتراک فعال یا در انتظار ندارد و برنامه: %s قیمتگذاری پیشفرض ندارد"
|
||||||
},
|
},
|
||||||
"cas": {
|
"cas": {
|
||||||
"Service %s and %s do not match": "Service %s and %s do not match"
|
"Service %s and %s do not match": "سرویس %s و %s مطابقت ندارند"
|
||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "وابستگی نمیتواند خالی باشد",
|
||||||
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
"Default code does not match the code's matching rules": "کد پیشفرض با قوانین تطبیق کد مطابقت ندارد",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "نام نمایشی نمیتواند خالی باشد",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "نام نمایشی یک نام واقعی معتبر نیست",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "ایمیل قبلاً وجود دارد",
|
||||||
"Email cannot be empty": "Email cannot be empty",
|
"Email cannot be empty": "ایمیل نمیتواند خالی باشد",
|
||||||
"Email is invalid": "Email is invalid",
|
"Email is invalid": "ایمیل نامعتبر است",
|
||||||
"Empty username.": "Empty username.",
|
"Empty username.": "نام کاربری خالی است.",
|
||||||
"Face data does not exist, cannot log in": "Face data does not exist, cannot log in",
|
"Face data does not exist, cannot log in": "دادههای چهره وجود ندارد، نمیتوان وارد شد",
|
||||||
"Face data mismatch": "Face data mismatch",
|
"Face data mismatch": "عدم تطابق دادههای چهره",
|
||||||
"FirstName cannot be blank": "FirstName cannot be blank",
|
"FirstName cannot be blank": "نام نمیتواند خالی باشد",
|
||||||
"Invitation code cannot be blank": "Invitation code cannot be blank",
|
"Invitation code cannot be blank": "کد دعوت نمیتواند خالی باشد",
|
||||||
"Invitation code exhausted": "Invitation code exhausted",
|
"Invitation code exhausted": "کد دعوت استفاده شده است",
|
||||||
"Invitation code is invalid": "Invitation code is invalid",
|
"Invitation code is invalid": "کد دعوت نامعتبر است",
|
||||||
"Invitation code suspended": "Invitation code suspended",
|
"Invitation code suspended": "کد دعوت معلق است",
|
||||||
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
|
"LDAP user name or password incorrect": "نام کاربری یا رمز عبور LDAP نادرست است",
|
||||||
"LastName cannot be blank": "LastName cannot be blank",
|
"LastName cannot be blank": "نام خانوادگی نمیتواند خالی باشد",
|
||||||
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
|
"Multiple accounts with same uid, please check your ldap server": "چندین حساب با uid یکسان، لطفاً سرور LDAP خود را بررسی کنید",
|
||||||
"Organization does not exist": "Organization does not exist",
|
"Organization does not exist": "سازمان وجود ندارد",
|
||||||
"Phone already exists": "Phone already exists",
|
"Phone already exists": "تلفن قبلاً وجود دارد",
|
||||||
"Phone cannot be empty": "Phone cannot be empty",
|
"Phone cannot be empty": "تلفن نمیتواند خالی باشد",
|
||||||
"Phone number is invalid": "Phone number is invalid",
|
"Phone number is invalid": "شماره تلفن نامعتبر است",
|
||||||
"Please register using the email corresponding to the invitation code": "Please register using the email corresponding to the invitation code",
|
"Please register using the email corresponding to the invitation code": "لطفاً با استفاده از ایمیل مربوط به کد دعوت ثبتنام کنید",
|
||||||
"Please register using the phone corresponding to the invitation code": "Please register using the phone corresponding to the invitation code",
|
"Please register using the phone corresponding to the invitation code": "لطفاً با استفاده از تلفن مربوط به کد دعوت ثبتنام کنید",
|
||||||
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code",
|
"Please register using the username corresponding to the invitation code": "لطفاً با استفاده از نام کاربری مربوط به کد دعوت ثبتنام کنید",
|
||||||
"Session outdated, please login again": "Session outdated, please login again",
|
"Session outdated, please login again": "جلسه منقضی شده است، لطفاً دوباره وارد شوید",
|
||||||
"The invitation code has already been used": "The invitation code has already been used",
|
"The invitation code has already been used": "کد دعوت قبلاً استفاده شده است",
|
||||||
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
|
"The user is forbidden to sign in, please contact the administrator": "ورود کاربر ممنوع است، لطفاً با مدیر تماس بگیرید",
|
||||||
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
|
"The user: %s doesn't exist in LDAP server": "کاربر: %s در سرور LDAP وجود ندارد",
|
||||||
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
|
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "نام کاربری فقط میتواند حاوی کاراکترهای الفبایی عددی، زیرخط یا خط تیره باشد، نمیتواند خط تیره یا زیرخط متوالی داشته باشد، و نمیتواند با خط تیره یا زیرخط شروع یا پایان یابد.",
|
||||||
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex",
|
"The value \"%s\" for account field \"%s\" doesn't match the account item regex": "مقدار \"%s\" برای فیلد حساب \"%s\" با عبارت منظم مورد حساب مطابقت ندارد",
|
||||||
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"",
|
"The value \"%s\" for signup field \"%s\" doesn't match the signup item regex of the application \"%s\"": "مقدار \"%s\" برای فیلد ثبتنام \"%s\" با عبارت منظم مورد ثبتنام برنامه \"%s\" مطابقت ندارد",
|
||||||
"Username already exists": "Username already exists",
|
"Username already exists": "نام کاربری قبلاً وجود دارد",
|
||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "نام کاربری نمیتواند با یک رقم شروع شود",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 39 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کردهاید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
||||||
"password or code is incorrect": "password or code is incorrect",
|
"password or code is incorrect": "رمز عبور یا کد نادرست است",
|
||||||
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
|
"password or code is incorrect, you have %d remaining chances": "رمز عبور یا کد نادرست است، شما %d فرصت باقیمانده دارید",
|
||||||
"unsupported password type: %s": "unsupported password type: %s"
|
"unsupported password type: %s": "نوع رمز عبور پشتیبانی نشده: %s"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"Missing parameter": "Missing parameter",
|
"Missing parameter": "پارامتر گمشده",
|
||||||
"Please login first": "Please login first",
|
"Please login first": "لطفاً ابتدا وارد شوید",
|
||||||
"The organization: %s should have one application at least": "The organization: %s should have one application at least",
|
"The organization: %s should have one application at least": "سازمان: %s باید حداقل یک برنامه داشته باشد",
|
||||||
"The user: %s doesn't exist": "The user: %s doesn't exist",
|
"The user: %s doesn't exist": "کاربر: %s وجود ندارد",
|
||||||
"don't support captchaProvider: ": "don't support captchaProvider: ",
|
"don't support captchaProvider: ": "از captchaProvider پشتیبانی نمیشود: ",
|
||||||
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
|
"this operation is not allowed in demo mode": "این عملیات در حالت دمو مجاز نیست",
|
||||||
"this operation requires administrator to perform": "this operation requires administrator to perform"
|
"this operation requires administrator to perform": "این عملیات نیاز به مدیر برای انجام دارد"
|
||||||
},
|
},
|
||||||
"ldap": {
|
"ldap": {
|
||||||
"Ldap server exist": "Ldap server exist"
|
"Ldap server exist": "سرور LDAP وجود دارد"
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
"Please link first": "Please link first",
|
"Please link first": "لطفاً ابتدا پیوند دهید",
|
||||||
"This application has no providers": "This application has no providers",
|
"This application has no providers": "این برنامه ارائهدهندهای ندارد",
|
||||||
"This application has no providers of type": "This application has no providers of type",
|
"This application has no providers of type": "این برنامه ارائهدهندهای از نوع ندارد",
|
||||||
"This provider can't be unlinked": "This provider can't be unlinked",
|
"This provider can't be unlinked": "این ارائهدهنده نمیتواند لغو پیوند شود",
|
||||||
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
|
"You are not the global admin, you can't unlink other users": "شما مدیر جهانی نیستید، نمیتوانید کاربران دیگر را لغو پیوند کنید",
|
||||||
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
|
"You can't unlink yourself, you are not a member of any application": "شما نمیتوانید خودتان را لغو پیوند کنید، شما عضو هیچ برنامهای نیستید"
|
||||||
},
|
},
|
||||||
"organization": {
|
"organization": {
|
||||||
"Only admin can modify the %s.": "Only admin can modify the %s.",
|
"Only admin can modify the %s.": "فقط مدیر میتواند %s را تغییر دهد.",
|
||||||
"The %s is immutable.": "The %s is immutable.",
|
"The %s is immutable.": "%s غیرقابل تغییر است.",
|
||||||
"Unknown modify rule %s.": "Unknown modify rule %s."
|
"Unknown modify rule %s.": "قانون تغییر ناشناخته %s."
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
|
"The permission: \"%s\" doesn't exist": "مجوز: \"%s\" وجود ندارد"
|
||||||
},
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"Invalid application id": "Invalid application id",
|
"Invalid application id": "شناسه برنامه نامعتبر",
|
||||||
"the provider: %s does not exist": "the provider: %s does not exist"
|
"the provider: %s does not exist": "ارائهدهنده: %s وجود ندارد"
|
||||||
},
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"User is nil for tag: avatar": "User is nil for tag: avatar",
|
"User is nil for tag: avatar": "کاربر برای برچسب: آواتار تهی است",
|
||||||
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
|
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "نام کاربری یا مسیر کامل فایل خالی است: نام کاربری = %s، مسیر کامل فایل = %s"
|
||||||
},
|
},
|
||||||
"saml": {
|
"saml": {
|
||||||
"Application %s not found": "Application %s not found"
|
"Application %s not found": "برنامه %s یافت نشد"
|
||||||
},
|
},
|
||||||
"saml_sp": {
|
"saml_sp": {
|
||||||
"provider %s's category is not SAML": "provider %s's category is not SAML"
|
"provider %s's category is not SAML": "دستهبندی ارائهدهنده %s SAML نیست"
|
||||||
},
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
|
"Empty parameters for emailForm: %v": "پارامترهای خالی برای emailForm: %v",
|
||||||
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
|
"Invalid Email receivers: %s": "گیرندگان ایمیل نامعتبر: %s",
|
||||||
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
|
"Invalid phone receivers: %s": "گیرندگان تلفن نامعتبر: %s"
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
|
"The objectKey: %s is not allowed": "objectKey: %s مجاز نیست",
|
||||||
"The provider type: %s is not supported": "The provider type: %s is not supported"
|
"The provider type: %s is not supported": "نوع ارائهدهنده: %s پشتیبانی نمیشود"
|
||||||
},
|
},
|
||||||
"token": {
|
"token": {
|
||||||
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
|
"Grant_type: %s is not supported in this application": "grant_type: %s در این برنامه پشتیبانی نمیشود",
|
||||||
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
|
"Invalid application or wrong clientSecret": "برنامه نامعتبر یا clientSecret نادرست",
|
||||||
"Invalid client_id": "Invalid client_id",
|
"Invalid client_id": "client_id نامعتبر",
|
||||||
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
|
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "آدرس بازگشت: %s در لیست آدرسهای بازگشت مجاز وجود ندارد",
|
||||||
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
|
"Token not found, invalid accessToken": "توکن یافت نشد، accessToken نامعتبر"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"Display name cannot be empty": "Display name cannot be empty",
|
"Display name cannot be empty": "نام نمایشی نمیتواند خالی باشد",
|
||||||
"New password cannot contain blank space.": "New password cannot contain blank space."
|
"New password cannot contain blank space.": "رمز عبور جدید نمیتواند حاوی فاصله خالی باشد."
|
||||||
},
|
},
|
||||||
"user_upload": {
|
"user_upload": {
|
||||||
"Failed to import users": "Failed to import users"
|
"Failed to import users": "عدم موفقیت در وارد کردن کاربران"
|
||||||
},
|
},
|
||||||
"util": {
|
"util": {
|
||||||
"No application is found for userId: %s": "No application is found for userId: %s",
|
"No application is found for userId: %s": "هیچ برنامهای برای userId: %s یافت نشد",
|
||||||
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
|
"No provider for category: %s is found for application: %s": "هیچ ارائهدهندهای برای دستهبندی: %s برای برنامه: %s یافت نشد",
|
||||||
"The provider: %s is not found": "The provider: %s is not found"
|
"The provider: %s is not found": "ارائهدهنده: %s یافت نشد"
|
||||||
},
|
},
|
||||||
"verification": {
|
"verification": {
|
||||||
"Invalid captcha provider.": "Invalid captcha provider.",
|
"Invalid captcha provider.": "ارائهدهنده کپچا نامعتبر.",
|
||||||
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
|
"Phone number is invalid in your region %s": "شماره تلفن در منطقه شما نامعتبر است %s",
|
||||||
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
|
"The verification code has not been sent yet!": "کد تأیید هنوز ارسال نشده است!",
|
||||||
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
|
"The verification code has not been sent yet, or has already been used!": "کد تأیید هنوز ارسال نشده است، یا قبلاً استفاده شده است!",
|
||||||
"Turing test failed.": "Turing test failed.",
|
"Turing test failed.": "تست تورینگ ناموفق بود.",
|
||||||
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
|
"Unable to get the email modify rule.": "عدم توانایی در دریافت قانون تغییر ایمیل.",
|
||||||
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
|
"Unable to get the phone modify rule.": "عدم توانایی در دریافت قانون تغییر تلفن.",
|
||||||
"Unknown type": "Unknown type",
|
"Unknown type": "نوع ناشناخته",
|
||||||
"Wrong verification code!": "Wrong verification code!",
|
"Wrong verification code!": "کد تأیید اشتباه!",
|
||||||
"You should verify your code in %d min!": "You should verify your code in %d min!",
|
"You should verify your code in %d min!": "شما باید کد خود را در %d دقیقه تأیید کنید!",
|
||||||
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s",
|
"please add a SMS provider to the \"Providers\" list for the application: %s": "لطفاً یک ارائهدهنده پیامک به لیست \"ارائهدهندگان\" برای برنامه: %s اضافه کنید",
|
||||||
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s",
|
"please add an Email provider to the \"Providers\" list for the application: %s": "لطفاً یک ارائهدهنده ایمیل به لیست \"ارائهدهندگان\" برای برنامه: %s اضافه کنید",
|
||||||
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
|
"the user does not exist, please sign up first": "کاربر وجود ندارد، لطفاً ابتدا ثبتنام کنید"
|
||||||
},
|
},
|
||||||
"webauthn": {
|
"webauthn": {
|
||||||
"Found no credentials for this user": "Found no credentials for this user",
|
"Found no credentials for this user": "هیچ اعتباری برای این کاربر یافت نشد",
|
||||||
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
|
"Please call WebAuthnSigninBegin first": "لطفاً ابتدا WebAuthnSigninBegin را فراخوانی کنید"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Аккаунт для провайдера: %s и имя пользователя: %s (%s) не существует и не может быть зарегистрирован как новый аккаунт. Пожалуйста, обратитесь в службу поддержки IT",
|
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Аккаунт для провайдера: %s и имя пользователя: %s (%s) не существует и не может быть зарегистрирован как новый аккаунт. Пожалуйста, обратитесь в службу поддержки IT",
|
||||||
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Аккаунт поставщика: %s и имя пользователя: %s (%s) уже связаны с другим аккаунтом: %s (%s)",
|
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Аккаунт поставщика: %s и имя пользователя: %s (%s) уже связаны с другим аккаунтом: %s (%s)",
|
||||||
"The application: %s does not exist": "Приложение: %s не существует",
|
"The application: %s does not exist": "Приложение: %s не существует",
|
||||||
"The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application",
|
"The login method: login with LDAP is not enabled for the application": "Метод входа в систему: вход с помощью LDAP не включен для приложения",
|
||||||
"The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application",
|
"The login method: login with SMS is not enabled for the application": "Метод входа: вход с помощью SMS не включен для приложения",
|
||||||
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application",
|
"The login method: login with email is not enabled for the application": "Метод входа: вход с помощью электронной почты не включен для приложения",
|
||||||
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application",
|
"The login method: login with face is not enabled for the application": "Метод входа: вход с помощью лица не включен для приложения",
|
||||||
"The login method: login with password is not enabled for the application": "Метод входа: вход с паролем не включен для приложения",
|
"The login method: login with password is not enabled for the application": "Метод входа: вход с паролем не включен для приложения",
|
||||||
"The organization: %s does not exist": "The organization: %s does not exist",
|
"The organization: %s does not exist": "The organization: %s does not exist",
|
||||||
"The provider: %s is not enabled for the application": "Провайдер: %s не включен для приложения",
|
"The provider: %s is not enabled for the application": "Провайдер: %s не включен для приложения",
|
||||||
@ -53,16 +53,16 @@
|
|||||||
"Phone already exists": "Телефон уже существует",
|
"Phone already exists": "Телефон уже существует",
|
||||||
"Phone cannot be empty": "Телефон не может быть пустым",
|
"Phone cannot be empty": "Телефон не может быть пустым",
|
||||||
"Phone number is invalid": "Номер телефона является недействительным",
|
"Phone number is invalid": "Номер телефона является недействительным",
|
||||||
"Please register using the email corresponding to the invitation code": "Please register using the email corresponding to the invitation code",
|
"Please register using the email corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя электронную почту, соответствующую коду приглашения",
|
||||||
"Please register using the phone corresponding to the invitation code": "Please register using the phone corresponding to the invitation code",
|
"Please register using the phone corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь по телефону, соответствующему коду приглашения",
|
||||||
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code",
|
"Please register using the username corresponding to the invitation code": "Пожалуйста, зарегистрируйтесь, используя имя пользователя, соответствующее коду приглашения",
|
||||||
"Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова",
|
"Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова",
|
||||||
"The invitation code has already been used": "The invitation code has already been used",
|
"The invitation code has already been used": "The invitation code has already been used",
|
||||||
"The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору",
|
"The user is forbidden to sign in, please contact the administrator": "Пользователю запрещен вход, пожалуйста, обратитесь к администратору",
|
||||||
"The user: %s doesn't exist in LDAP server": "Пользователь %s не существует на LDAP сервере",
|
"The user: %s doesn't exist in LDAP server": "Пользователь %s не существует на LDAP сервере",
|
||||||
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Имя пользователя может состоять только из буквенно-цифровых символов, нижних подчеркиваний или дефисов, не может содержать последовательные дефисы или подчеркивания, а также не может начинаться или заканчиваться на дефис или подчеркивание.",
|
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Имя пользователя может состоять только из буквенно-цифровых символов, нижних подчеркиваний или дефисов, не может содержать последовательные дефисы или подчеркивания, а также не может начинаться или заканчиваться на дефис или подчеркивание.",
|
||||||
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex",
|
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Значение \\\"%s\\\" для поля аккаунта \\\"%s\\\" не соответствует регулярному значению",
|
||||||
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"",
|
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Значение \\\"%s\\\" поля регистрации \\\"%s\\\" не соответствует регулярному выражению приложения \\\"%s\\\"",
|
||||||
"Username already exists": "Имя пользователя уже существует",
|
"Username already exists": "Имя пользователя уже существует",
|
||||||
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
||||||
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
||||||
@ -78,11 +78,11 @@
|
|||||||
"general": {
|
"general": {
|
||||||
"Missing parameter": "Отсутствующий параметр",
|
"Missing parameter": "Отсутствующий параметр",
|
||||||
"Please login first": "Пожалуйста, сначала войдите в систему",
|
"Please login first": "Пожалуйста, сначала войдите в систему",
|
||||||
"The organization: %s should have one application at least": "The organization: %s should have one application at least",
|
"The organization: %s should have one application at least": "Организация: %s должна иметь хотя бы одно приложение",
|
||||||
"The user: %s doesn't exist": "Пользователь %s не существует",
|
"The user: %s doesn't exist": "Пользователь %s не существует",
|
||||||
"don't support captchaProvider: ": "неподдерживаемый captchaProvider: ",
|
"don't support captchaProvider: ": "неподдерживаемый captchaProvider: ",
|
||||||
"this operation is not allowed in demo mode": "эта операция не разрешена в демо-режиме",
|
"this operation is not allowed in demo mode": "эта операция не разрешена в демо-режиме",
|
||||||
"this operation requires administrator to perform": "this operation requires administrator to perform"
|
"this operation requires administrator to perform": "для выполнения этой операции требуется администратор"
|
||||||
},
|
},
|
||||||
"ldap": {
|
"ldap": {
|
||||||
"Ldap server exist": "LDAP-сервер существует"
|
"Ldap server exist": "LDAP-сервер существует"
|
||||||
@ -101,11 +101,11 @@
|
|||||||
"Unknown modify rule %s.": "Неизвестное изменение правила %s."
|
"Unknown modify rule %s.": "Неизвестное изменение правила %s."
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
|
"The permission: \\\"%s\\\" doesn't exist": "Разрешение: \\\"%s\\\" не существует"
|
||||||
},
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"Invalid application id": "Неверный идентификатор приложения",
|
"Invalid application id": "Неверный идентификатор приложения",
|
||||||
"the provider: %s does not exist": "провайдер: %s не существует"
|
"the provider: %s does not exist": "Провайдер: %s не существует"
|
||||||
},
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"User is nil for tag: avatar": "Пользователь равен нулю для тега: аватар",
|
"User is nil for tag: avatar": "Пользователь равен нулю для тега: аватар",
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"Application %s not found": "Приложение %s не найдено"
|
"Application %s not found": "Приложение %s не найдено"
|
||||||
},
|
},
|
||||||
"saml_sp": {
|
"saml_sp": {
|
||||||
"provider %s's category is not SAML": "категория провайдера %s не является SAML"
|
"provider %s's category is not SAML": "Категория провайдера %s не является SAML"
|
||||||
},
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"Empty parameters for emailForm: %v": "Пустые параметры для emailForm: %v",
|
"Empty parameters for emailForm: %v": "Пустые параметры для emailForm: %v",
|
||||||
@ -148,7 +148,7 @@
|
|||||||
"verification": {
|
"verification": {
|
||||||
"Invalid captcha provider.": "Недействительный поставщик CAPTCHA.",
|
"Invalid captcha provider.": "Недействительный поставщик CAPTCHA.",
|
||||||
"Phone number is invalid in your region %s": "Номер телефона недействителен в вашем регионе %s",
|
"Phone number is invalid in your region %s": "Номер телефона недействителен в вашем регионе %s",
|
||||||
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
|
"The verification code has not been sent yet!": "Код проверки еще не отправлен!",
|
||||||
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
|
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
|
||||||
"Turing test failed.": "Тест Тьюринга не удался.",
|
"Turing test failed.": "Тест Тьюринга не удался.",
|
||||||
"Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.",
|
"Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.",
|
||||||
@ -156,8 +156,8 @@
|
|||||||
"Unknown type": "Неизвестный тип",
|
"Unknown type": "Неизвестный тип",
|
||||||
"Wrong verification code!": "Неправильный код подтверждения!",
|
"Wrong verification code!": "Неправильный код подтверждения!",
|
||||||
"You should verify your code in %d min!": "Вы должны проверить свой код через %d минут!",
|
"You should verify your code in %d min!": "Вы должны проверить свой код через %d минут!",
|
||||||
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s",
|
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "Пожалуйста, добавьте поставщика SMS в список \\\"Провайдеры\\\" для приложения: %s",
|
||||||
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s",
|
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "Пожалуйста, добавьте поставщика электронной почты в список \\\"Провайдеры\\\" для приложения: %s",
|
||||||
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"
|
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"
|
||||||
},
|
},
|
||||||
"webauthn": {
|
"webauthn": {
|
||||||
|
@ -188,10 +188,23 @@ type GitHubUserInfo struct {
|
|||||||
} `json:"plan"`
|
} `json:"plan"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GitHubUserEmailInfo struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
Visibility string `json:"visibility"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitHubErrorInfo struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
DocumentationUrl string `json:"documentation_url"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||||
req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
|
req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Add("Authorization", "token "+token.AccessToken)
|
req.Header.Add("Authorization", "token "+token.AccessToken)
|
||||||
resp, err := idp.Client.Do(req)
|
resp, err := idp.Client.Do(req)
|
||||||
@ -212,6 +225,42 @@ func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if githubUserInfo.Email == "" {
|
||||||
|
reqEmail, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reqEmail.Header.Add("Authorization", "token "+token.AccessToken)
|
||||||
|
respEmail, err := idp.Client.Do(reqEmail)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer respEmail.Body.Close()
|
||||||
|
emailBody, err := io.ReadAll(respEmail.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if respEmail.StatusCode != 200 {
|
||||||
|
var errMessage GitHubErrorInfo
|
||||||
|
err = json.Unmarshal(emailBody, &errMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("GithubIdProvider:GetUserInfo() error, status code = %d, error message = %v\n", respEmail.StatusCode, errMessage)
|
||||||
|
} else {
|
||||||
|
var userEmails []GitHubUserEmailInfo
|
||||||
|
err = json.Unmarshal(emailBody, &userEmails)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
githubUserInfo.Email = idp.getEmailFromEmailsResult(userEmails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
userInfo := UserInfo{
|
userInfo := UserInfo{
|
||||||
Id: strconv.Itoa(githubUserInfo.Id),
|
Id: strconv.Itoa(githubUserInfo.Id),
|
||||||
Username: githubUserInfo.Login,
|
Username: githubUserInfo.Login,
|
||||||
@ -248,3 +297,27 @@ func (idp *GithubIdProvider) postWithBody(body interface{}, url string) ([]byte,
|
|||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (idp *GithubIdProvider) getEmailFromEmailsResult(emailInfo []GitHubUserEmailInfo) string {
|
||||||
|
primaryEmail := ""
|
||||||
|
verifiedEmail := ""
|
||||||
|
|
||||||
|
for _, addr := range emailInfo {
|
||||||
|
if !addr.Verified || strings.Contains(addr.Email, "users.noreply.github.com") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Primary {
|
||||||
|
primaryEmail = addr.Email
|
||||||
|
break
|
||||||
|
} else if verifiedEmail == "" {
|
||||||
|
verifiedEmail = addr.Email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if primaryEmail != "" {
|
||||||
|
return primaryEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifiedEmail
|
||||||
|
}
|
||||||
|
161
idp/kwai.go
Normal file
161
idp/kwai.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package idp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KwaiIdProvider struct {
|
||||||
|
Client *http.Client
|
||||||
|
Config *oauth2.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKwaiIdProvider(clientId string, clientSecret string, redirectUrl string) *KwaiIdProvider {
|
||||||
|
idp := &KwaiIdProvider{}
|
||||||
|
idp.Config = idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||||
|
return idp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idp *KwaiIdProvider) SetHttpClient(client *http.Client) {
|
||||||
|
idp.Client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idp *KwaiIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
|
||||||
|
endpoint := oauth2.Endpoint{
|
||||||
|
TokenURL: "https://open.kuaishou.com/oauth2/access_token",
|
||||||
|
AuthURL: "https://open.kuaishou.com/oauth2/authorize", // qr code: /oauth2/connect
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &oauth2.Config{
|
||||||
|
Scopes: []string{"user_info"},
|
||||||
|
Endpoint: endpoint,
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
RedirectURL: redirectUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
type KwaiTokenResp struct {
|
||||||
|
Result int `json:"result"`
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
RefreshTokenExpiresIn int `json:"refresh_token_expires_in"`
|
||||||
|
OpenId string `json:"open_id"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken use code to get access_token
|
||||||
|
func (idp *KwaiIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"app_id": idp.Config.ClientID,
|
||||||
|
"app_secret": idp.Config.ClientSecret,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
}
|
||||||
|
tokenUrl := fmt.Sprintf("%s?app_id=%s&app_secret=%s&code=%s&grant_type=authorization_code",
|
||||||
|
idp.Config.Endpoint.TokenURL, params["app_id"], params["app_secret"], params["code"])
|
||||||
|
resp, err := idp.Client.Get(tokenUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tokenResp KwaiTokenResp
|
||||||
|
err = json.Unmarshal(body, &tokenResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tokenResp.Result != 1 {
|
||||||
|
return nil, fmt.Errorf("get token error: %s", tokenResp.ErrorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &oauth2.Token{
|
||||||
|
AccessToken: tokenResp.AccessToken,
|
||||||
|
RefreshToken: tokenResp.RefreshToken,
|
||||||
|
Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := make(map[string]interface{})
|
||||||
|
raw["open_id"] = tokenResp.OpenId
|
||||||
|
token = token.WithExtra(raw)
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// More details: https://open.kuaishou.com/openapi/user_info
|
||||||
|
type KwaiUserInfo struct {
|
||||||
|
Result int `json:"result"`
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
UserInfo struct {
|
||||||
|
Head string `json:"head"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Sex string `json:"sex"`
|
||||||
|
City string `json:"city"`
|
||||||
|
} `json:"user_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo use token to get user profile
|
||||||
|
func (idp *KwaiIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||||
|
userInfoUrl := fmt.Sprintf("https://open.kuaishou.com/openapi/user_info?app_id=%s&access_token=%s",
|
||||||
|
idp.Config.ClientID, token.AccessToken)
|
||||||
|
|
||||||
|
resp, err := idp.Client.Get(userInfoUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kwaiUserInfo KwaiUserInfo
|
||||||
|
err = json.Unmarshal(body, &kwaiUserInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if kwaiUserInfo.Result != 1 {
|
||||||
|
return nil, fmt.Errorf("get user info error: %s", kwaiUserInfo.ErrorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := &UserInfo{
|
||||||
|
Id: token.Extra("open_id").(string),
|
||||||
|
Username: kwaiUserInfo.UserInfo.Name,
|
||||||
|
DisplayName: kwaiUserInfo.UserInfo.Name,
|
||||||
|
AvatarUrl: kwaiUserInfo.UserInfo.Head,
|
||||||
|
Extra: map[string]string{
|
||||||
|
"gender": kwaiUserInfo.UserInfo.Sex,
|
||||||
|
"city": kwaiUserInfo.UserInfo.City,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
@ -113,6 +113,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
|
|||||||
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
|
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
|
||||||
case "Douyin":
|
case "Douyin":
|
||||||
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||||
|
case "Kwai":
|
||||||
|
return NewKwaiIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||||
case "Bilibili":
|
case "Bilibili":
|
||||||
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||||
case "MetaMask":
|
case "MetaMask":
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"log"
|
"log"
|
||||||
@ -27,21 +28,68 @@ import (
|
|||||||
|
|
||||||
func StartLdapServer() {
|
func StartLdapServer() {
|
||||||
ldapServerPort := conf.GetConfigString("ldapServerPort")
|
ldapServerPort := conf.GetConfigString("ldapServerPort")
|
||||||
if ldapServerPort == "" || ldapServerPort == "0" {
|
ldapsServerPort := conf.GetConfigString("ldapsServerPort")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server := ldap.NewServer()
|
server := ldap.NewServer()
|
||||||
|
serverSsl := ldap.NewServer()
|
||||||
routes := ldap.NewRouteMux()
|
routes := ldap.NewRouteMux()
|
||||||
|
|
||||||
routes.Bind(handleBind)
|
routes.Bind(handleBind)
|
||||||
routes.Search(handleSearch).Label(" SEARCH****")
|
routes.Search(handleSearch).Label(" SEARCH****")
|
||||||
|
|
||||||
server.Handle(routes)
|
server.Handle(routes)
|
||||||
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort)
|
serverSsl.Handle(routes)
|
||||||
|
go func() {
|
||||||
|
if ldapServerPort == "" || ldapServerPort == "0" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("StartLdapServer() failed, err = %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if ldapsServerPort == "" || ldapsServerPort == "0" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ldapsCertId := conf.GetConfigString("ldapsCertId")
|
||||||
|
if ldapsCertId == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config, err := getTLSconfig(ldapsCertId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("StartLdapsServer() failed, err = %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
secureConn := func(s *ldap.Server) {
|
||||||
|
s.Listener = tls.NewListener(s.Listener, config)
|
||||||
|
}
|
||||||
|
err = serverSsl.ListenAndServe("0.0.0.0:"+ldapsServerPort, secureConn)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("StartLdapsServer() failed, err = %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTLSconfig(ldapsCertId string) (*tls.Config, error) {
|
||||||
|
rawCert, err := object.GetCert(ldapsCertId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("StartLdapServer() failed, err = %s", err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if rawCert == nil {
|
||||||
|
return nil, fmt.Errorf("cert is empty")
|
||||||
|
}
|
||||||
|
cert, err := tls.X509KeyPair([]byte(rawCert.Certificate), []byte(rawCert.PrivateKey))
|
||||||
|
if err != nil {
|
||||||
|
return &tls.Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS10,
|
||||||
|
MaxVersion: tls.VersionTLS13,
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
|
func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
|
||||||
@ -142,7 +190,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
|
|||||||
}
|
}
|
||||||
for _, attr := range attrs {
|
for _, attr := range attrs {
|
||||||
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
|
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
|
||||||
if string(attr) == "cn" {
|
if string(attr) == "title" {
|
||||||
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
|
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
main.go
5
main.go
@ -83,6 +83,11 @@ func main() {
|
|||||||
// logs.SetLevel(logs.LevelInformational)
|
// logs.SetLevel(logs.LevelInformational)
|
||||||
logs.SetLogFuncCall(false)
|
logs.SetLogFuncCall(false)
|
||||||
|
|
||||||
|
err = util.StopOldInstance(port)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
go ldap.StartLdapServer()
|
go ldap.StartLdapServer()
|
||||||
go radius.StartRadiusServer()
|
go radius.StartRadiusServer()
|
||||||
go object.ClearThroughputPerSecond()
|
go object.ClearThroughputPerSecond()
|
||||||
|
29
notification/cucloud.go
Normal file
29
notification/cucloud.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// 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 notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/casdoor/notify"
|
||||||
|
"github.com/casdoor/notify/service/cucloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCucloudProvider(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType string) (notify.Notifier, error) {
|
||||||
|
cucloud := cucloud.New(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType)
|
||||||
|
|
||||||
|
notifier := notify.New()
|
||||||
|
notifier.UseServices(cucloud)
|
||||||
|
|
||||||
|
return notifier, nil
|
||||||
|
}
|
@ -16,7 +16,7 @@ package notification
|
|||||||
|
|
||||||
import "github.com/casdoor/notify"
|
import "github.com/casdoor/notify"
|
||||||
|
|
||||||
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string) (notify.Notifier, error) {
|
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string, regionId string) (notify.Notifier, error) {
|
||||||
if typ == "Telegram" {
|
if typ == "Telegram" {
|
||||||
return NewTelegramProvider(clientSecret, receiver)
|
return NewTelegramProvider(clientSecret, receiver)
|
||||||
} else if typ == "Custom HTTP" {
|
} else if typ == "Custom HTTP" {
|
||||||
@ -53,6 +53,8 @@ func GetNotificationProvider(typ string, clientId string, clientSecret string, c
|
|||||||
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
|
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
|
||||||
} else if typ == "Viber" {
|
} else if typ == "Viber" {
|
||||||
return NewViberProvider(clientId, clientSecret, appId, receiver)
|
return NewViberProvider(clientId, clientSecret, appId, receiver)
|
||||||
|
} else if typ == "CUCloud" {
|
||||||
|
return NewCucloudProvider(clientId, clientSecret, appId, title, regionId, clientId2, metaData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -95,6 +95,7 @@ type Application struct {
|
|||||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||||
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
|
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
|
||||||
IsShared bool `json:"isShared"`
|
IsShared bool `json:"isShared"`
|
||||||
|
IpRestriction string `json:"ipRestriction"`
|
||||||
|
|
||||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||||
@ -108,6 +109,7 @@ type Application struct {
|
|||||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||||
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
||||||
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
||||||
|
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||||
@ -721,8 +723,15 @@ func (application *Application) GetId() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
|
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
|
||||||
redirectUris := append([]string{"http://localhost:", "https://localhost:", "http://127.0.0.1:", "http://casdoor-app", ".chromiumapp.org"}, application.RedirectUris...)
|
isValid, err := util.IsValidOrigin(redirectUri)
|
||||||
for _, targetUri := range redirectUris {
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if isValid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetUri := range application.RedirectUris {
|
||||||
targetUriRegex := regexp.MustCompile(targetUri)
|
targetUriRegex := regexp.MustCompile(targetUri)
|
||||||
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
|
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
|
||||||
return true
|
return true
|
||||||
|
@ -273,7 +273,7 @@ func CheckPasswordComplexity(user *User, password string) string {
|
|||||||
return CheckPasswordComplexityByOrg(organization, password)
|
return CheckPasswordComplexityByOrg(organization, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkLdapUserPassword(user *User, password string, lang string) error {
|
func CheckLdapUserPassword(user *User, password string, lang string) error {
|
||||||
ldaps, err := GetLdaps(user.Owner)
|
ldaps, err := GetLdaps(user.Owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -368,7 +368,7 @@ func CheckUserPassword(organization string, username string, password string, la
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only for LDAP users
|
// only for LDAP users
|
||||||
err = checkLdapUserPassword(user, password, lang)
|
err = CheckLdapUserPassword(user, password, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "user not exist" {
|
if err.Error() == "user not exist" {
|
||||||
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
|
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
|
||||||
@ -381,7 +381,13 @@ func CheckUserPassword(organization string, username string, password string, la
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = checkPasswordExpired(user, lang)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,11 +526,46 @@ func CheckUsername(username string, lang string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckUsernameWithEmail(username string, lang string) string {
|
||||||
|
if username == "" {
|
||||||
|
return i18n.Translate(lang, "check:Empty username.")
|
||||||
|
} else if len(username) > 39 {
|
||||||
|
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||||
|
|
||||||
|
if !util.ReUserNameWithEmail.MatchString(username) {
|
||||||
|
return i18n.Translate(lang, "check:Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func CheckUpdateUser(oldUser, user *User, lang string) string {
|
func CheckUpdateUser(oldUser, user *User, lang string) string {
|
||||||
if oldUser.Name != user.Name {
|
if oldUser.Name != user.Name {
|
||||||
if msg := CheckUsername(user.Name, lang); msg != "" {
|
organizationName := oldUser.Owner
|
||||||
return msg
|
if organizationName == "" {
|
||||||
|
organizationName = user.Owner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
organization, err := getOrganization("admin", organizationName)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
if organization == nil {
|
||||||
|
return fmt.Sprintf(i18n.Translate(lang, "auth:The organization: %s does not exist"), organizationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization.UseEmailAsUsername {
|
||||||
|
if msg := CheckUsernameWithEmail(user.Name, lang); msg != "" {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if msg := CheckUsername(user.Name, lang); msg != "" {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if HasUserByField(user.Owner, "name", user.Name) {
|
if HasUserByField(user.Owner, "name", user.Name) {
|
||||||
return i18n.Translate(lang, "check:Username already exists")
|
return i18n.Translate(lang, "check:Username already exists")
|
||||||
}
|
}
|
||||||
@ -539,6 +580,11 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
|
|||||||
return i18n.Translate(lang, "check:Phone already exists")
|
return i18n.Translate(lang, "check:Phone already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if oldUser.IpWhitelist != user.IpWhitelist {
|
||||||
|
if err := CheckIpWhitelist(user.IpWhitelist, lang); err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
104
object/check_ip.go
Normal file
104
object/check_ip.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckEntryIp(clientIp string, user *User, application *Application, organization *Organization, lang string) error {
|
||||||
|
entryIp := net.ParseIP(clientIp)
|
||||||
|
if entryIp == nil {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Failed to parse client IP: %s"), clientIp)
|
||||||
|
} else if entryIp.IsLoopback() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if user != nil {
|
||||||
|
err = isEntryIpAllowd(user.IpWhitelist, entryIp, lang)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(err.Error() + user.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if application != nil {
|
||||||
|
err = isEntryIpAllowd(application.IpWhitelist, entryIp, lang)
|
||||||
|
if err != nil {
|
||||||
|
application.IpRestriction = err.Error() + application.Name
|
||||||
|
return fmt.Errorf(err.Error() + application.Name)
|
||||||
|
} else {
|
||||||
|
application.IpRestriction = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization == nil && application.OrganizationObj != nil {
|
||||||
|
organization = application.OrganizationObj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization != nil {
|
||||||
|
err = isEntryIpAllowd(organization.IpWhitelist, entryIp, lang)
|
||||||
|
if err != nil {
|
||||||
|
organization.IpRestriction = err.Error() + organization.Name
|
||||||
|
return fmt.Errorf(err.Error() + organization.Name)
|
||||||
|
} else {
|
||||||
|
organization.IpRestriction = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEntryIpAllowd(ipWhitelistStr string, entryIp net.IP, lang string) error {
|
||||||
|
if ipWhitelistStr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ipWhitelist := strings.Split(ipWhitelistStr, ",")
|
||||||
|
for _, ip := range ipWhitelist {
|
||||||
|
_, ipNet, err := net.ParseCIDR(ip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ipNet == nil {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:CIDR for IP: %s should not be empty"), entryIp.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipNet.Contains(entryIp) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Your IP address: %s has been banned according to the configuration of: "), entryIp.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIpWhitelist(ipWhitelistStr string, lang string) error {
|
||||||
|
if ipWhitelistStr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ipWhiteList := strings.Split(ipWhitelistStr, ",")
|
||||||
|
for _, ip := range ipWhiteList {
|
||||||
|
if _, _, err := net.ParseCIDR(ip); err != nil {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:%s does not meet the CIDR format requirements: %s"), ip, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
53
object/check_password_expired.go
Normal file
53
object/check_password_expired.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/i18n"
|
||||||
|
"github.com/casdoor/casdoor/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkPasswordExpired(user *User, lang string) error {
|
||||||
|
organization, err := GetOrganizationByUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if organization == nil {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordExpireDays := organization.PasswordExpireDays
|
||||||
|
if passwordExpireDays <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastChangePasswordTime := user.LastChangePasswordTime
|
||||||
|
if lastChangePasswordTime == "" {
|
||||||
|
if user.CreatedTime == "" {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
|
||||||
|
}
|
||||||
|
lastChangePasswordTime = user.CreatedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTime := util.String2Time(lastChangePasswordTime)
|
||||||
|
expireTime := lastTime.AddDate(0, 0, passwordExpireDays)
|
||||||
|
if time.Now().After(expireTime) {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -16,23 +16,18 @@
|
|||||||
|
|
||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import "github.com/casdoor/casdoor/email"
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/email"
|
// TestSmtpServer Test the SMTP server
|
||||||
"github.com/casdoor/gomail/v2"
|
func TestSmtpServer(provider *Provider) error {
|
||||||
)
|
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl)
|
||||||
|
sender, err := smtpEmailProvider.Dialer.Dial()
|
||||||
func getDialer(provider *Provider) *gomail.Dialer {
|
if err != nil {
|
||||||
dialer := &gomail.Dialer{}
|
return err
|
||||||
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
|
|
||||||
if provider.Type == "SUBMAIL" {
|
|
||||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
}
|
||||||
|
defer sender.Close()
|
||||||
|
|
||||||
dialer.SSL = !provider.DisableSsl
|
return nil
|
||||||
|
|
||||||
return dialer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||||
@ -50,16 +45,3 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
|
|||||||
|
|
||||||
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DailSmtpServer Dail Smtp server
|
|
||||||
func DailSmtpServer(provider *Provider) error {
|
|
||||||
dialer := getDialer(provider)
|
|
||||||
|
|
||||||
sender, err := dialer.Dial()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sender.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -19,124 +19,90 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Dashboard struct {
|
type DashboardDateItem struct {
|
||||||
OrganizationCounts []int `json:"organizationCounts"`
|
CreatedTime string `json:"createTime"`
|
||||||
UserCounts []int `json:"userCounts"`
|
|
||||||
ProviderCounts []int `json:"providerCounts"`
|
|
||||||
ApplicationCounts []int `json:"applicationCounts"`
|
|
||||||
SubscriptionCounts []int `json:"subscriptionCounts"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDashboard(owner string) (*Dashboard, error) {
|
type DashboardMapItem struct {
|
||||||
|
dashboardDateItems []DashboardDateItem
|
||||||
|
itemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDashboard(owner string) (*map[string][]int64, error) {
|
||||||
if owner == "All" {
|
if owner == "All" {
|
||||||
owner = ""
|
owner = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboard := &Dashboard{
|
dashboard := make(map[string][]int64)
|
||||||
OrganizationCounts: make([]int, 31),
|
dashboardMap := sync.Map{}
|
||||||
UserCounts: make([]int, 31),
|
tableNames := []string{"organization", "user", "provider", "application", "subscription", "role", "group", "resource", "cert", "permission", "transaction", "model", "adapter", "enforcer"}
|
||||||
ProviderCounts: make([]int, 31),
|
|
||||||
ApplicationCounts: make([]int, 31),
|
time30day := time.Now().AddDate(0, 0, -30)
|
||||||
SubscriptionCounts: make([]int, 31),
|
var wg sync.WaitGroup
|
||||||
|
var err error
|
||||||
|
wg.Add(len(tableNames))
|
||||||
|
ch := make(chan error, len(tableNames))
|
||||||
|
for _, tableName := range tableNames {
|
||||||
|
dashboard[tableName+"Counts"] = make([]int64, 31)
|
||||||
|
tableName := tableName
|
||||||
|
go func(ch chan error) {
|
||||||
|
defer wg.Done()
|
||||||
|
dashboardDateItems := []DashboardDateItem{}
|
||||||
|
var countResult int64
|
||||||
|
|
||||||
|
dbQueryBefore := ormer.Engine.Cols("created_time")
|
||||||
|
dbQueryAfter := ormer.Engine.Cols("created_time")
|
||||||
|
|
||||||
|
if owner != "" {
|
||||||
|
dbQueryAfter = dbQueryAfter.And("owner = ?", owner)
|
||||||
|
dbQueryBefore = dbQueryBefore.And("owner = ?", owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableName).Count(); err != nil {
|
||||||
|
ch <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableName).Find(&dashboardDateItems); err != nil {
|
||||||
|
ch <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardMap.Store(tableName, DashboardMapItem{
|
||||||
|
dashboardDateItems: dashboardDateItems,
|
||||||
|
itemCount: countResult,
|
||||||
|
})
|
||||||
|
}(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
organizations := []Organization{}
|
|
||||||
users := []User{}
|
|
||||||
providers := []Provider{}
|
|
||||||
applications := []Application{}
|
|
||||||
subscriptions := []Subscription{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(5)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if err := ormer.Engine.Find(&organizations, &Organization{Owner: owner}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
if err := ormer.Engine.Find(&users, &User{Owner: owner}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
if err := ormer.Engine.Find(&providers, &Provider{Owner: owner}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
if err := ormer.Engine.Find(&applications, &Application{Owner: owner}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
if err := ormer.Engine.Find(&subscriptions, &Subscription{Owner: owner}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
|
||||||
|
for err = range ch {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nowTime := time.Now()
|
nowTime := time.Now()
|
||||||
for i := 30; i >= 0; i-- {
|
for i := 30; i >= 0; i-- {
|
||||||
cutTime := nowTime.AddDate(0, 0, -i)
|
cutTime := nowTime.AddDate(0, 0, -i)
|
||||||
dashboard.OrganizationCounts[30-i] = countCreatedBefore(organizations, cutTime)
|
for _, tableName := range tableNames {
|
||||||
dashboard.UserCounts[30-i] = countCreatedBefore(users, cutTime)
|
item, exist := dashboardMap.Load(tableName)
|
||||||
dashboard.ProviderCounts[30-i] = countCreatedBefore(providers, cutTime)
|
if !exist {
|
||||||
dashboard.ApplicationCounts[30-i] = countCreatedBefore(applications, cutTime)
|
continue
|
||||||
dashboard.SubscriptionCounts[30-i] = countCreatedBefore(subscriptions, cutTime)
|
}
|
||||||
|
dashboard[tableName+"Counts"][30-i] = countCreatedBefore(item.(DashboardMapItem), cutTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dashboard, nil
|
return &dashboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func countCreatedBefore(objects interface{}, before time.Time) int {
|
func countCreatedBefore(dashboardMapItem DashboardMapItem, before time.Time) int64 {
|
||||||
count := 0
|
count := dashboardMapItem.itemCount
|
||||||
switch obj := objects.(type) {
|
for _, e := range dashboardMapItem.dashboardDateItems {
|
||||||
case []Organization:
|
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", e.CreatedTime)
|
||||||
for _, o := range obj {
|
if createdTime.Before(before) {
|
||||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", o.CreatedTime)
|
count++
|
||||||
if createdTime.Before(before) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []User:
|
|
||||||
for _, u := range obj {
|
|
||||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", u.CreatedTime)
|
|
||||||
if createdTime.Before(before) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []Provider:
|
|
||||||
for _, p := range obj {
|
|
||||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", p.CreatedTime)
|
|
||||||
if createdTime.Before(before) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []Application:
|
|
||||||
for _, a := range obj {
|
|
||||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", a.CreatedTime)
|
|
||||||
if createdTime.Before(before) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []Subscription:
|
|
||||||
for _, s := range obj {
|
|
||||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", s.CreatedTime)
|
|
||||||
if createdTime.Before(before) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
|
@ -48,12 +48,16 @@ type InitData struct {
|
|||||||
Transactions []*Transaction `json:"transactions"`
|
Transactions []*Transaction `json:"transactions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var initDataNewOnly bool
|
||||||
|
|
||||||
func InitFromFile() {
|
func InitFromFile() {
|
||||||
initDataFile := conf.GetConfigString("initDataFile")
|
initDataFile := conf.GetConfigString("initDataFile")
|
||||||
if initDataFile == "" {
|
if initDataFile == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initDataNewOnly = conf.GetConfigBool("initDataNewOnly")
|
||||||
|
|
||||||
initData, err := readInitDataFromFile(initDataFile)
|
initData, err := readInitDataFromFile(initDataFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -269,6 +273,9 @@ func initDefinedOrganization(organization *Organization) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := deleteOrganization(organization)
|
affected, err := deleteOrganization(organization)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -295,6 +302,9 @@ func initDefinedApplication(application *Application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := deleteApplication(application)
|
affected, err := deleteApplication(application)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -316,6 +326,9 @@ func initDefinedUser(user *User) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := deleteUser(user)
|
affected, err := deleteUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -342,6 +355,9 @@ func initDefinedCert(cert *Cert) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteCert(cert)
|
affected, err := DeleteCert(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -364,6 +380,9 @@ func initDefinedLdap(ldap *Ldap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteLdap(ldap)
|
affected, err := DeleteLdap(ldap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -385,6 +404,9 @@ func initDefinedProvider(provider *Provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteProvider(provider)
|
affected, err := DeleteProvider(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -406,6 +428,9 @@ func initDefinedModel(model *Model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteModel(model)
|
affected, err := DeleteModel(model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -428,6 +453,9 @@ func initDefinedPermission(permission *Permission) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := deletePermission(permission)
|
affected, err := deletePermission(permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -450,6 +478,9 @@ func initDefinedPayment(payment *Payment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeletePayment(payment)
|
affected, err := DeletePayment(payment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -472,6 +503,9 @@ func initDefinedProduct(product *Product) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteProduct(product)
|
affected, err := DeleteProduct(product)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -494,6 +528,9 @@ func initDefinedResource(resource *Resource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteResource(resource)
|
affected, err := DeleteResource(resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -516,6 +553,9 @@ func initDefinedRole(role *Role) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := deleteRole(role)
|
affected, err := deleteRole(role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -538,6 +578,9 @@ func initDefinedSyncer(syncer *Syncer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteSyncer(syncer)
|
affected, err := DeleteSyncer(syncer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -560,6 +603,9 @@ func initDefinedToken(token *Token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteToken(token)
|
affected, err := DeleteToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -582,6 +628,9 @@ func initDefinedWebhook(webhook *Webhook) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteWebhook(webhook)
|
affected, err := DeleteWebhook(webhook)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -603,6 +652,9 @@ func initDefinedGroup(group *Group) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := deleteGroup(group)
|
affected, err := deleteGroup(group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -624,6 +676,9 @@ func initDefinedAdapter(adapter *Adapter) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteAdapter(adapter)
|
affected, err := DeleteAdapter(adapter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -645,6 +700,9 @@ func initDefinedEnforcer(enforcer *Enforcer) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteEnforcer(enforcer)
|
affected, err := DeleteEnforcer(enforcer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -666,6 +724,9 @@ func initDefinedPlan(plan *Plan) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeletePlan(plan)
|
affected, err := DeletePlan(plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -687,6 +748,9 @@ func initDefinedPricing(pricing *Pricing) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeletePricing(pricing)
|
affected, err := DeletePricing(pricing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -708,6 +772,9 @@ func initDefinedInvitation(invitation *Invitation) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteInvitation(invitation)
|
affected, err := DeleteInvitation(invitation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -743,6 +810,9 @@ func initDefinedSubscription(subscription *Subscription) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteSubscription(subscription)
|
affected, err := DeleteSubscription(subscription)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -764,6 +834,9 @@ func initDefinedTransaction(transaction *Transaction) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if existed != nil {
|
if existed != nil {
|
||||||
|
if initDataNewOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
affected, err := DeleteTransaction(transaction)
|
affected, err := DeleteTransaction(transaction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -33,6 +33,7 @@ type Ldap struct {
|
|||||||
Filter string `xorm:"varchar(200)" json:"filter"`
|
Filter string `xorm:"varchar(200)" json:"filter"`
|
||||||
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
||||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||||
|
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||||
|
|
||||||
AutoSync int `json:"autoSync"`
|
AutoSync int `json:"autoSync"`
|
||||||
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
||||||
@ -149,7 +150,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
||||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group").Update(ldap)
|
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").Update(ldap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,18 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
|
"github.com/casdoor/casdoor/i18n"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
goldap "github.com/go-ldap/ldap/v3"
|
goldap "github.com/go-ldap/ldap/v3"
|
||||||
"github.com/thanhpk/randstr"
|
"github.com/thanhpk/randstr"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LdapConn struct {
|
type LdapConn struct {
|
||||||
@ -371,6 +375,88 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
|
|||||||
return existUuids, nil
|
return existUuids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang string) error {
|
||||||
|
ldaps, err := GetLdaps(user.Owner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ldapServer := range ldaps {
|
||||||
|
conn, err := ldapServer.GetLdapConn()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
|
||||||
|
0, 0, false, ldapServer.buildAuthFilterString(user), []string{}, nil)
|
||||||
|
|
||||||
|
searchResult, err := conn.Conn.Search(searchReq)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(searchResult.Entries) == 0 {
|
||||||
|
conn.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(searchResult.Entries) > 1 {
|
||||||
|
conn.Close()
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
|
||||||
|
}
|
||||||
|
|
||||||
|
userDn := searchResult.Entries[0].DN
|
||||||
|
|
||||||
|
var pwdEncoded string
|
||||||
|
modifyPasswordRequest := goldap.NewModifyRequest(userDn, nil)
|
||||||
|
if conn.IsAD {
|
||||||
|
utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
|
||||||
|
pwdEncoded, err := utf16.NewEncoder().String("\"" + newPassword + "\"")
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
||||||
|
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
|
||||||
|
} else if oldPassword != "" {
|
||||||
|
modifyPasswordRequestWithOldPassword := goldap.NewPasswordModifyRequest(userDn, oldPassword, newPassword)
|
||||||
|
_, err = conn.Conn.PasswordModify(modifyPasswordRequestWithOldPassword)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
switch ldapServer.PasswordType {
|
||||||
|
case "SSHA":
|
||||||
|
pwdEncoded, err = generateSSHA(newPassword)
|
||||||
|
break
|
||||||
|
case "MD5":
|
||||||
|
md5Byte := md5.Sum([]byte(newPassword))
|
||||||
|
md5Password := base64.StdEncoding.EncodeToString(md5Byte[:])
|
||||||
|
pwdEncoded = "{MD5}" + md5Password
|
||||||
|
break
|
||||||
|
case "Plain":
|
||||||
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
}
|
||||||
|
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.Conn.Modify(modifyPasswordRequest)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) {
|
func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) {
|
||||||
user := User{}
|
user := User{}
|
||||||
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
|
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
|
||||||
|
36
object/ldap_password_type.go
Normal file
36
object/ldap_password_type.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSSHA(password string) (string, error) {
|
||||||
|
salt := make([]byte, 4)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := append([]byte(password), salt...)
|
||||||
|
hash := sha1.Sum(combined)
|
||||||
|
hashWithSalt := append(hash[:], salt...)
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(hashWithSalt)
|
||||||
|
|
||||||
|
return "{SSHA}" + encoded, nil
|
||||||
|
}
|
@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
|
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
|
||||||
var client notify.Notifier
|
var client notify.Notifier
|
||||||
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata)
|
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata, provider.RegionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -62,20 +62,23 @@ type Organization struct {
|
|||||||
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
|
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
|
||||||
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
||||||
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
||||||
|
PasswordExpireDays int `json:"passwordExpireDays"`
|
||||||
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
||||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||||
Languages []string `xorm:"varchar(255)" json:"languages"`
|
Languages []string `xorm:"varchar(255)" json:"languages"`
|
||||||
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
||||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
MasterPassword string `xorm:"varchar(200)" json:"masterPassword"`
|
||||||
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
|
DefaultPassword string `xorm:"varchar(200)" json:"defaultPassword"`
|
||||||
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
|
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
|
||||||
|
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||||
InitScore int `json:"initScore"`
|
InitScore int `json:"initScore"`
|
||||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||||
IsProfilePublic bool `json:"isProfilePublic"`
|
IsProfilePublic bool `json:"isProfilePublic"`
|
||||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||||
EnableTour bool `json:"enableTour"`
|
EnableTour bool `json:"enableTour"`
|
||||||
|
IpRestriction string `json:"ipRestriction"`
|
||||||
|
|
||||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||||
|
@ -364,7 +364,7 @@ func GetAllActions(userId string) ([]string, error) {
|
|||||||
|
|
||||||
res := []string{}
|
res := []string{}
|
||||||
for _, enforcer := range enforcers {
|
for _, enforcer := range enforcers {
|
||||||
items := enforcer.GetAllObjects()
|
items := enforcer.GetAllActions()
|
||||||
res = append(res, items...)
|
res = append(res, items...)
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
|
@ -16,6 +16,7 @@ package object
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/beego/beego/context"
|
"github.com/beego/beego/context"
|
||||||
@ -70,6 +71,7 @@ type Provider struct {
|
|||||||
IdP string `xorm:"mediumtext" json:"idP"`
|
IdP string `xorm:"mediumtext" json:"idP"`
|
||||||
IssuerUrl string `xorm:"varchar(100)" json:"issuerUrl"`
|
IssuerUrl string `xorm:"varchar(100)" json:"issuerUrl"`
|
||||||
EnableSignAuthnRequest bool `json:"enableSignAuthnRequest"`
|
EnableSignAuthnRequest bool `json:"enableSignAuthnRequest"`
|
||||||
|
EmailRegex string `xorm:"varchar(200)" json:"emailRegex"`
|
||||||
|
|
||||||
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
|
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
|
||||||
}
|
}
|
||||||
@ -200,6 +202,13 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if provider.EmailRegex != "" {
|
||||||
|
_, err := regexp.Compile(provider.EmailRegex)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if name != provider.Name {
|
if name != provider.Name {
|
||||||
err := providerChangeTrigger(name, provider.Name)
|
err := providerChangeTrigger(name, provider.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -234,6 +243,13 @@ func AddProvider(provider *Provider) (bool, error) {
|
|||||||
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
|
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if provider.EmailRegex != "" {
|
||||||
|
_, err := regexp.Compile(provider.EmailRegex)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.Insert(provider)
|
affected, err := ormer.Engine.Insert(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -421,7 +437,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
|
|||||||
providerInfo.ClientId = provider.ClientId2
|
providerInfo.ClientId = provider.ClientId2
|
||||||
providerInfo.ClientSecret = provider.ClientSecret2
|
providerInfo.ClientSecret = provider.ClientSecret2
|
||||||
}
|
}
|
||||||
} else if provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "ADFS" || provider.Type == "Okta" {
|
} else if provider.Type == "ADFS" || provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "Casdoor" || provider.Type == "Okta" {
|
||||||
providerInfo.HostUrl = provider.Domain
|
providerInfo.HostUrl = provider.Domain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
logPostOnly = conf.GetConfigBool("logPostOnly")
|
logPostOnly = conf.GetConfigBool("logPostOnly")
|
||||||
passwordRegex = regexp.MustCompile("\"password\":\".+\"")
|
passwordRegex = regexp.MustCompile("\"password\":\"([^\"]*?)\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Record struct {
|
type Record struct {
|
||||||
@ -50,7 +50,7 @@ func maskPassword(recordString string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
|
func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
|
||||||
ip := strings.Replace(util.GetIPFromRequest(ctx.Request), ": ", "", -1)
|
clientIp := strings.Replace(util.GetClientIpFromRequest(ctx.Request), ": ", "", -1)
|
||||||
action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1)
|
action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1)
|
||||||
requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"})
|
requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"})
|
||||||
if len(requestUri) > 1000 {
|
if len(requestUri) > 1000 {
|
||||||
@ -83,7 +83,7 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
|
|||||||
record := casvisorsdk.Record{
|
record := casvisorsdk.Record{
|
||||||
Name: util.GenerateId(),
|
Name: util.GenerateId(),
|
||||||
CreatedTime: util.GetCurrentTime(),
|
CreatedTime: util.GetCurrentTime(),
|
||||||
ClientIp: ip,
|
ClientIp: clientIp,
|
||||||
User: "",
|
User: "",
|
||||||
Method: ctx.Request.Method,
|
Method: ctx.Request.Method,
|
||||||
RequestUri: requestUri,
|
RequestUri: requestUri,
|
||||||
|
@ -338,6 +338,10 @@ func roleChangeTrigger(oldName string, newName string) error {
|
|||||||
|
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
for j, u := range role.Roles {
|
for j, u := range role.Roles {
|
||||||
|
if u == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
owner, name := util.GetOwnerAndNameFromId(u)
|
owner, name := util.GetOwnerAndNameFromId(u)
|
||||||
if name == oldName {
|
if name == oldName {
|
||||||
role.Roles[j] = util.GetId(owner, newName)
|
role.Roles[j] = util.GetId(owner, newName)
|
||||||
@ -358,6 +362,10 @@ func roleChangeTrigger(oldName string, newName string) error {
|
|||||||
for _, permission := range permissions {
|
for _, permission := range permissions {
|
||||||
for j, u := range permission.Roles {
|
for j, u := range permission.Roles {
|
||||||
// u = organization/username
|
// u = organization/username
|
||||||
|
if u == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
owner, name := util.GetOwnerAndNameFromId(u)
|
owner, name := util.GetOwnerAndNameFromId(u)
|
||||||
if name == oldName {
|
if name == oldName {
|
||||||
permission.Roles[j] = util.GetId(owner, newName)
|
permission.Roles[j] = util.GetId(owner, newName)
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
@ -222,10 +223,13 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
|
|||||||
originFrontend, originBackend := getOriginFromHost(host)
|
originFrontend, originBackend := getOriginFromHost(host)
|
||||||
|
|
||||||
idpLocation := ""
|
idpLocation := ""
|
||||||
|
idpBinding := ""
|
||||||
if enablePostBinding {
|
if enablePostBinding {
|
||||||
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
|
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
|
||||||
|
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||||
} else {
|
} else {
|
||||||
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
|
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
|
||||||
|
idpBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||||
}
|
}
|
||||||
|
|
||||||
d := IdpEntityDescriptor{
|
d := IdpEntityDescriptor{
|
||||||
@ -258,7 +262,7 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
|
|||||||
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"},
|
{Xmlns: "urn:oasis:names:tc:SAML:2.0:assertion", Name: "Name", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", FriendlyName: "Name"},
|
||||||
},
|
},
|
||||||
SingleSignOnService: SingleSignOnService{
|
SingleSignOnService: SingleSignOnService{
|
||||||
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
Binding: idpBinding,
|
||||||
Location: idpLocation,
|
Location: idpLocation,
|
||||||
},
|
},
|
||||||
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
|
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
|
||||||
@ -273,29 +277,38 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
|
|||||||
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) {
|
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) {
|
||||||
// request type
|
// request type
|
||||||
method := "GET"
|
method := "GET"
|
||||||
|
samlRequest = strings.ReplaceAll(samlRequest, " ", "+")
|
||||||
// base64 decode
|
// base64 decode
|
||||||
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
|
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// decompress
|
var requestByte []byte
|
||||||
var buffer bytes.Buffer
|
|
||||||
rdr := flate.NewReader(bytes.NewReader(defated))
|
|
||||||
|
|
||||||
for {
|
if strings.Contains(string(defated), "xmlns:") {
|
||||||
_, err = io.CopyN(&buffer, rdr, 1024)
|
requestByte = defated
|
||||||
if err != nil {
|
} else {
|
||||||
if err == io.EOF {
|
// decompress
|
||||||
break
|
var buffer bytes.Buffer
|
||||||
|
rdr := flate.NewReader(bytes.NewReader(defated))
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
_, err = io.CopyN(&buffer, rdr, 1024)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
return "", "", "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestByte = buffer.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
var authnRequest saml.AuthNRequest
|
var authnRequest saml.AuthNRequest
|
||||||
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
|
err = xml.Unmarshal(requestByte, &authnRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
|
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
|
||||||
}
|
}
|
||||||
@ -325,6 +338,9 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
|||||||
} else if authnRequest.AssertionConsumerServiceURL == "" {
|
} else if authnRequest.AssertionConsumerServiceURL == "" {
|
||||||
return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>")
|
return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>")
|
||||||
}
|
}
|
||||||
|
if authnRequest.ProtocolBinding == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" {
|
||||||
|
method = "POST"
|
||||||
|
}
|
||||||
|
|
||||||
_, originBackend := getOriginFromHost(host)
|
_, originBackend := getOriginFromHost(host)
|
||||||
|
|
||||||
|
@ -102,14 +102,6 @@ func GetTokenByAccessToken(accessToken string) (*Token, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !existed {
|
|
||||||
token = Token{AccessToken: accessToken}
|
|
||||||
existed, err = ormer.Engine.Get(&token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !existed {
|
if !existed {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -123,14 +115,6 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !existed {
|
|
||||||
token = Token{RefreshToken: refreshToken}
|
|
||||||
existed, err = ormer.Engine.Get(&token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !existed {
|
if !existed {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -140,6 +124,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
|||||||
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||||
switch tokenTypeHint {
|
switch tokenTypeHint {
|
||||||
case "access_token":
|
case "access_token":
|
||||||
|
case "access-token":
|
||||||
token, err := GetTokenByAccessToken(tokenValue)
|
token, err := GetTokenByAccessToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -148,6 +133,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
case "refresh_token":
|
case "refresh_token":
|
||||||
|
case "refresh-token":
|
||||||
token, err := GetTokenByRefreshToken(tokenValue)
|
token, err := GetTokenByRefreshToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -184,6 +185,15 @@ func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService,
|
|||||||
return proxyTicket
|
return proxyTicket
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func escapeXMLText(input string) (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
err := xml.EscapeText(&sb, []byte(input))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func GenerateCasToken(userId string, service string) (string, error) {
|
func GenerateCasToken(userId string, service string) (string, error) {
|
||||||
user, err := GetUser(userId)
|
user, err := GetUser(userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -225,6 +235,11 @@ func GenerateCasToken(userId string, service string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if value != "" {
|
if value != "" {
|
||||||
|
if escapedValue, err := escapeXMLText(value); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
value = escapedValue
|
||||||
|
}
|
||||||
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
|
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
|
||||||
Name: k,
|
Name: k,
|
||||||
Value: value,
|
Value: value,
|
||||||
|
@ -309,22 +309,29 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var oldTokenScope string
|
||||||
if application.TokenFormat == "JWT-Standard" {
|
if application.TokenFormat == "JWT-Standard" {
|
||||||
_, err = ParseStandardJwtToken(refreshToken, cert)
|
oldToken, err := ParseStandardJwtToken(refreshToken, cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &TokenError{
|
return &TokenError{
|
||||||
Error: InvalidGrant,
|
Error: InvalidGrant,
|
||||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
oldTokenScope = oldToken.Scope
|
||||||
} else {
|
} else {
|
||||||
_, err = ParseJwtToken(refreshToken, cert)
|
oldToken, err := ParseJwtToken(refreshToken, cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &TokenError{
|
return &TokenError{
|
||||||
Error: InvalidGrant,
|
Error: InvalidGrant,
|
||||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
oldTokenScope = oldToken.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope == "" {
|
||||||
|
scope = oldTokenScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate a new token
|
// generate a new token
|
||||||
@ -332,6 +339,9 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if user == nil {
|
||||||
|
return "", fmt.Errorf("The user: %s doesn't exist", util.GetId(application.Organization, token.User))
|
||||||
|
}
|
||||||
|
|
||||||
if user.IsForbidden {
|
if user.IsForbidden {
|
||||||
return &TokenError{
|
return &TokenError{
|
||||||
@ -501,7 +511,7 @@ func GetPasswordToken(application *Application, username string, password string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user.Ldap != "" {
|
if user.Ldap != "" {
|
||||||
err = checkLdapUserPassword(user, password, "en")
|
err = CheckLdapUserPassword(user, password, "en")
|
||||||
} else {
|
} else {
|
||||||
err = CheckPassword(user, password, "en")
|
err = CheckPassword(user, password, "en")
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,7 @@ type User struct {
|
|||||||
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
||||||
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
||||||
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
|
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
|
||||||
|
Kwai string `xorm:"kwai varchar(100)" json:"kwai"`
|
||||||
Line string `xorm:"line varchar(100)" json:"line"`
|
Line string `xorm:"line varchar(100)" json:"line"`
|
||||||
Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
|
Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
|
||||||
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
|
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
|
||||||
@ -200,12 +201,14 @@ type User struct {
|
|||||||
Permissions []*Permission `json:"permissions"`
|
Permissions []*Permission `json:"permissions"`
|
||||||
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
||||||
|
|
||||||
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
|
||||||
SigninWrongTimes int `json:"signinWrongTimes"`
|
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
||||||
|
SigninWrongTimes int `json:"signinWrongTimes"`
|
||||||
|
|
||||||
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
||||||
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
||||||
NeedUpdatePassword bool `json:"needUpdatePassword"`
|
NeedUpdatePassword bool `json:"needUpdatePassword"`
|
||||||
|
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Userinfo struct {
|
type Userinfo struct {
|
||||||
@ -235,6 +238,7 @@ type MfaAccount struct {
|
|||||||
AccountName string `xorm:"varchar(100)" json:"accountName"`
|
AccountName string `xorm:"varchar(100)" json:"accountName"`
|
||||||
Issuer string `xorm:"varchar(100)" json:"issuer"`
|
Issuer string `xorm:"varchar(100)" json:"issuer"`
|
||||||
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
|
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
|
||||||
|
Origin string `xorm:"varchar(100)" json:"origin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FaceId struct {
|
type FaceId struct {
|
||||||
@ -677,6 +681,10 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
|||||||
user.Password = oldUser.Password
|
user.Password = oldUser.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Id != oldUser.Id && user.Id == "" {
|
||||||
|
user.Id = oldUser.Id
|
||||||
|
}
|
||||||
|
|
||||||
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
|
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
|
||||||
user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
|
user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -689,14 +697,14 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
|||||||
"owner", "display_name", "avatar", "first_name", "last_name",
|
"owner", "display_name", "avatar", "first_name", "last_name",
|
||||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
||||||
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
|
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
|
||||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
|
||||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||||
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
|
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
|
||||||
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
|
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
|
||||||
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
|
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
|
||||||
"yammer", "yandex", "zoom", "custom", "need_update_password",
|
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
@ -815,6 +823,10 @@ func AddUser(user *User) (bool, error) {
|
|||||||
user.UpdateUserPassword(organization)
|
user.UpdateUserPassword(organization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.CreatedTime == "" {
|
||||||
|
user.CreatedTime = util.GetCurrentTime()
|
||||||
|
}
|
||||||
|
|
||||||
err = user.UpdateUserHash()
|
err = user.UpdateUserHash()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -834,11 +846,14 @@ func AddUser(user *User) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := GetUserCount(user.Owner, "", "", "")
|
rankingItem := GetAccountItemByName("Ranking", organization)
|
||||||
if err != nil {
|
if rankingItem != nil {
|
||||||
return false, err
|
count, err := GetUserCount(user.Owner, "", "", "")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
user.Ranking = int(count + 1)
|
||||||
}
|
}
|
||||||
user.Ranking = int(count + 1)
|
|
||||||
|
|
||||||
if user.Groups != nil && len(user.Groups) > 0 {
|
if user.Groups != nil && len(user.Groups) > 0 {
|
||||||
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||||
|
@ -557,6 +557,14 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
|||||||
itemsChanged = append(itemsChanged, item)
|
itemsChanged = append(itemsChanged, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if oldUser.IpWhitelist != newUser.IpWhitelist {
|
||||||
|
item := GetAccountItemByName("IP whitelist", organization)
|
||||||
|
if item == nil {
|
||||||
|
newUser.IpWhitelist = oldUser.IpWhitelist
|
||||||
|
} else {
|
||||||
|
itemsChanged = append(itemsChanged, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if oldUser.Balance != newUser.Balance {
|
if oldUser.Balance != newUser.Balance {
|
||||||
item := GetAccountItemByName("Balance", organization)
|
item := GetAccountItemByName("Balance", organization)
|
||||||
|
@ -57,7 +57,7 @@ type VerificationRecord struct {
|
|||||||
Receiver string `xorm:"varchar(100) index notnull" json:"receiver"`
|
Receiver string `xorm:"varchar(100) index notnull" json:"receiver"`
|
||||||
Code string `xorm:"varchar(10) notnull" json:"code"`
|
Code string `xorm:"varchar(10) notnull" json:"code"`
|
||||||
Time int64 `xorm:"notnull" json:"time"`
|
Time int64 `xorm:"notnull" json:"time"`
|
||||||
IsUsed bool
|
IsUsed bool `xorm:"notnull" json:"isUsed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsAllowSend(user *User, remoteAddr, recordType string) error {
|
func IsAllowSend(user *User, remoteAddr, recordType string) error {
|
||||||
|
@ -68,8 +68,10 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
|
|||||||
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
|
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
|
||||||
|
|
||||||
if organization == "" {
|
if organization == "" {
|
||||||
w.Write(r.Response(radius.CodeAccessReject))
|
organization = conf.GetConfigString("radiusDefaultOrganization")
|
||||||
return
|
if organization == "" {
|
||||||
|
organization = "built-in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var user *object.User
|
var user *object.User
|
||||||
|
@ -16,11 +16,11 @@ package routers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/beego/beego/context"
|
"github.com/beego/beego/context"
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
|
"github.com/casdoor/casdoor/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -52,7 +52,13 @@ func CorsFilter(ctx *context.Context) {
|
|||||||
origin = ""
|
origin = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") || strings.Contains(origin, ".chromiumapp.org") {
|
isValid, err := util.IsValidOrigin(origin)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
|
||||||
|
responseError(ctx, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isValid {
|
||||||
setCorsHeaders(ctx, origin)
|
setCorsHeaders(ctx, origin)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,8 @@ func initAPI() {
|
|||||||
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
|
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
|
||||||
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
|
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
|
||||||
|
|
||||||
|
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
|
||||||
|
|
||||||
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
|
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
|
||||||
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
|
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
|
||||||
beego.Router("/api/update-session", &controllers.ApiController{}, "POST:UpdateSession")
|
beego.Router("/api/update-session", &controllers.ApiController{}, "POST:UpdateSession")
|
||||||
|
@ -133,6 +133,11 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
path += urlPath
|
path += urlPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
organizationThemeCookie, err := appendThemeCookie(ctx, urlPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(path, "/../") || !util.FileExist(path) {
|
if strings.Contains(path, "/../") || !util.FileExist(path) {
|
||||||
path = webBuildFolder + "/index.html"
|
path = webBuildFolder + "/index.html"
|
||||||
}
|
}
|
||||||
@ -149,13 +154,13 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oldStaticBaseUrl == newStaticBaseUrl {
|
if oldStaticBaseUrl == newStaticBaseUrl {
|
||||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path)
|
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
} else {
|
} else {
|
||||||
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path)
|
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
f, err := os.Open(filepath.Clean(name))
|
f, err := os.Open(filepath.Clean(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -168,7 +173,13 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
oldContent := util.ReadStringFromPath(name)
|
oldContent := util.ReadStringFromPath(name)
|
||||||
newContent := strings.ReplaceAll(oldContent, oldStaticBaseUrl, newStaticBaseUrl)
|
newContent := oldContent
|
||||||
|
if organizationThemeCookie != nil {
|
||||||
|
newContent = strings.ReplaceAll(newContent, "https://cdn.casbin.org/img/favicon.png", organizationThemeCookie.Favicon)
|
||||||
|
newContent = strings.ReplaceAll(newContent, "<title>Casdoor</title>", fmt.Sprintf("<title>%s</title>", organizationThemeCookie.DisplayName))
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = strings.ReplaceAll(newContent, oldStaticBaseUrl, newStaticBaseUrl)
|
||||||
|
|
||||||
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
||||||
}
|
}
|
||||||
@ -182,14 +193,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
|||||||
return w.Writer.Write(b)
|
return w.Writer.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string) {
|
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
serveFileWithReplace(w, r, path)
|
serveFileWithReplace(w, r, path, organizationThemeCookie)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
gz := gzip.NewWriter(w)
|
gz := gzip.NewWriter(w)
|
||||||
defer gz.Close()
|
defer gz.Close()
|
||||||
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
serveFileWithReplace(gzw, r, path)
|
serveFileWithReplace(gzw, r, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
|
134
routers/theme_filter.go
Normal file
134
routers/theme_filter.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// 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 routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/beego/beego/context"
|
||||||
|
"github.com/casdoor/casdoor/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrganizationThemeCookie struct {
|
||||||
|
ThemeData *object.ThemeData
|
||||||
|
LogoUrl string
|
||||||
|
FooterHtml string
|
||||||
|
Favicon string
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendThemeCookie(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
|
||||||
|
if urlPath == "/login" {
|
||||||
|
application, err := object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
organization := application.OrganizationObj
|
||||||
|
if organization == nil {
|
||||||
|
organization, err = object.GetOrganization(fmt.Sprintf("admin/built-in"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if organization != nil {
|
||||||
|
organizationThemeCookie := &OrganizationThemeCookie{
|
||||||
|
application.ThemeData,
|
||||||
|
application.Logo,
|
||||||
|
application.FooterHtml,
|
||||||
|
organization.Favicon,
|
||||||
|
organization.DisplayName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if application.ThemeData != nil {
|
||||||
|
organizationThemeCookie.ThemeData = organization.ThemeData
|
||||||
|
}
|
||||||
|
return organizationThemeCookie, setThemeDataCookie(ctx, organizationThemeCookie)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(urlPath, "/login/oauth/authorize") {
|
||||||
|
clientId := ctx.Input.Query("client_id")
|
||||||
|
if clientId == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
application, err := object.GetApplicationByClientId(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if application != nil {
|
||||||
|
organization := application.OrganizationObj
|
||||||
|
if organization == nil {
|
||||||
|
organization, err = object.GetOrganization(fmt.Sprintf("admin/%s", application.Owner))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
organizationThemeCookie := &OrganizationThemeCookie{
|
||||||
|
application.ThemeData,
|
||||||
|
application.Logo,
|
||||||
|
application.FooterHtml,
|
||||||
|
organization.Favicon,
|
||||||
|
organization.DisplayName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if application.ThemeData != nil {
|
||||||
|
organizationThemeCookie.ThemeData = organization.ThemeData
|
||||||
|
}
|
||||||
|
return organizationThemeCookie, setThemeDataCookie(ctx, organizationThemeCookie)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(urlPath, "/login/") {
|
||||||
|
owner := strings.Replace(urlPath, "/login/", "", -1)
|
||||||
|
if owner != "undefined" && owner != "oauth/undefined" {
|
||||||
|
application, err := object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
organization := application.OrganizationObj
|
||||||
|
if organization == nil {
|
||||||
|
organization, err = object.GetOrganization(fmt.Sprintf("admin/%s", owner))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if organization != nil {
|
||||||
|
organizationThemeCookie := &OrganizationThemeCookie{
|
||||||
|
application.ThemeData,
|
||||||
|
application.Logo,
|
||||||
|
application.FooterHtml,
|
||||||
|
organization.Favicon,
|
||||||
|
organization.DisplayName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if application.ThemeData != nil {
|
||||||
|
organizationThemeCookie.ThemeData = organization.ThemeData
|
||||||
|
}
|
||||||
|
return organizationThemeCookie, setThemeDataCookie(ctx, organizationThemeCookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setThemeDataCookie(ctx *context.Context, organizationThemeCookie *OrganizationThemeCookie) error {
|
||||||
|
themeDataString, err := json.Marshal(organizationThemeCookie.ThemeData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.SetCookie("organizationTheme", string(themeDataString))
|
||||||
|
ctx.SetCookie("organizationLogo", organizationThemeCookie.LogoUrl)
|
||||||
|
ctx.SetCookie("organizationFootHtml", organizationThemeCookie.FooterHtml)
|
||||||
|
return nil
|
||||||
|
}
|
21
storage/cucloud_oss.go
Normal file
21
storage/cucloud_oss.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
awss3 "github.com/aws/aws-sdk-go/service/s3"
|
||||||
|
"github.com/casdoor/oss"
|
||||||
|
"github.com/casdoor/oss/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCUCloudOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||||
|
sp := s3.New(&s3.Config{
|
||||||
|
AccessID: clientId,
|
||||||
|
AccessKey: clientSecret,
|
||||||
|
Region: region,
|
||||||
|
Bucket: bucket,
|
||||||
|
Endpoint: endpoint,
|
||||||
|
S3Endpoint: endpoint,
|
||||||
|
ACL: awss3.BucketCannedACLPublicRead,
|
||||||
|
})
|
||||||
|
|
||||||
|
return sp
|
||||||
|
}
|
@ -23,7 +23,10 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
|
|||||||
case "AWS S3":
|
case "AWS S3":
|
||||||
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
||||||
case "MinIO":
|
case "MinIO":
|
||||||
return NewMinIOS3StorageProvider(clientId, clientSecret, "_", bucket, endpoint), nil
|
if region == "" {
|
||||||
|
region = "_"
|
||||||
|
}
|
||||||
|
return NewMinIOS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
||||||
case "Aliyun OSS":
|
case "Aliyun OSS":
|
||||||
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
||||||
case "Tencent Cloud COS":
|
case "Tencent Cloud COS":
|
||||||
@ -38,6 +41,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
|
|||||||
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil
|
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil
|
||||||
case "Casdoor":
|
case "Casdoor":
|
||||||
return NewCasdoorStorageProvider(providerType, clientId, clientSecret, region, bucket, endpoint, cert, content), nil
|
return NewCasdoorStorageProvider(providerType, clientId, clientSecret, region, bucket, endpoint, cert, content), nil
|
||||||
|
case "CUCloud OSS":
|
||||||
|
return NewCUCloudOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -7558,6 +7558,9 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
|
"kwai": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -4981,6 +4981,8 @@ definitions:
|
|||||||
karma:
|
karma:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
kwai:
|
||||||
|
type: string
|
||||||
language:
|
language:
|
||||||
type: string
|
type: string
|
||||||
lark:
|
lark:
|
||||||
|
48
util/log.go
48
util/log.go
@ -23,50 +23,50 @@ import (
|
|||||||
"github.com/beego/beego/logs"
|
"github.com/beego/beego/logs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetIPInfo(clientIP string) string {
|
func getIpInfo(clientIp string) string {
|
||||||
if clientIP == "" {
|
if clientIp == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
ips := strings.Split(clientIP, ",")
|
ips := strings.Split(clientIp, ",")
|
||||||
res := ""
|
res := strings.TrimSpace(ips[0])
|
||||||
for i := range ips {
|
//res := ""
|
||||||
ip := strings.TrimSpace(ips[i])
|
//for i := range ips {
|
||||||
// desc := GetDescFromIP(ip)
|
// ip := strings.TrimSpace(ips[i])
|
||||||
ipstr := fmt.Sprintf("%s: %s", ip, "")
|
// ipstr := fmt.Sprintf("%s: %s", ip, "")
|
||||||
if i != len(ips)-1 {
|
// if i != len(ips)-1 {
|
||||||
res += ipstr + " -> "
|
// res += ipstr + " -> "
|
||||||
} else {
|
// } else {
|
||||||
res += ipstr
|
// res += ipstr
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetIPFromRequest(req *http.Request) string {
|
func GetClientIpFromRequest(req *http.Request) string {
|
||||||
clientIP := req.Header.Get("x-forwarded-for")
|
clientIp := req.Header.Get("x-forwarded-for")
|
||||||
if clientIP == "" {
|
if clientIp == "" {
|
||||||
ipPort := strings.Split(req.RemoteAddr, ":")
|
ipPort := strings.Split(req.RemoteAddr, ":")
|
||||||
if len(ipPort) >= 1 && len(ipPort) <= 2 {
|
if len(ipPort) >= 1 && len(ipPort) <= 2 {
|
||||||
clientIP = ipPort[0]
|
clientIp = ipPort[0]
|
||||||
} else if len(ipPort) > 2 {
|
} else if len(ipPort) > 2 {
|
||||||
idx := strings.LastIndex(req.RemoteAddr, ":")
|
idx := strings.LastIndex(req.RemoteAddr, ":")
|
||||||
clientIP = req.RemoteAddr[0:idx]
|
clientIp = req.RemoteAddr[0:idx]
|
||||||
clientIP = strings.TrimLeft(clientIP, "[")
|
clientIp = strings.TrimLeft(clientIp, "[")
|
||||||
clientIP = strings.TrimRight(clientIP, "]")
|
clientIp = strings.TrimRight(clientIp, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetIPInfo(clientIP)
|
return getIpInfo(clientIp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogInfo(ctx *context.Context, f string, v ...interface{}) {
|
func LogInfo(ctx *context.Context, f string, v ...interface{}) {
|
||||||
ipString := fmt.Sprintf("(%s) ", GetIPFromRequest(ctx.Request))
|
ipString := fmt.Sprintf("(%s) ", GetClientIpFromRequest(ctx.Request))
|
||||||
logs.Info(ipString+f, v...)
|
logs.Info(ipString+f, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogWarning(ctx *context.Context, f string, v ...interface{}) {
|
func LogWarning(ctx *context.Context, f string, v ...interface{}) {
|
||||||
ipString := fmt.Sprintf("(%s) ", GetIPFromRequest(ctx.Request))
|
ipString := fmt.Sprintf("(%s) ", GetClientIpFromRequest(ctx.Request))
|
||||||
logs.Warning(ipString+f, v...)
|
logs.Warning(ipString+f, v...)
|
||||||
}
|
}
|
||||||
|
97
util/process.go
Normal file
97
util/process.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getPidByPort(port int) (int, error) {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
cmd = exec.Command("cmd", "/c", "netstat -ano | findstr :"+strconv.Itoa(port))
|
||||||
|
case "darwin", "linux":
|
||||||
|
cmd = exec.Command("lsof", "-t", "-i", ":"+strconv.Itoa(port))
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
if exitErr.ExitCode() == 1 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
if fields[1] == "0.0.0.0:"+strconv.Itoa(port) {
|
||||||
|
pid, err := strconv.Atoi(fields[len(fields)-1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pid, err := strconv.Atoi(fields[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopOldInstance(port int) error {
|
||||||
|
pid, err := getPidByPort(port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pid == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = process.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
fmt.Printf("The old instance with pid: %d has been stopped\n", pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -17,6 +17,7 @@ package util
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -24,10 +25,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
rePhone *regexp.Regexp
|
rePhone *regexp.Regexp
|
||||||
ReWhiteSpace *regexp.Regexp
|
ReWhiteSpace *regexp.Regexp
|
||||||
ReFieldWhiteList *regexp.Regexp
|
ReFieldWhiteList *regexp.Regexp
|
||||||
ReUserName *regexp.Regexp
|
ReUserName *regexp.Regexp
|
||||||
|
ReUserNameWithEmail *regexp.Regexp
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -35,6 +37,7 @@ func init() {
|
|||||||
ReWhiteSpace, _ = regexp.Compile(`\s`)
|
ReWhiteSpace, _ = regexp.Compile(`\s`)
|
||||||
ReFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
|
ReFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
|
||||||
ReUserName, _ = regexp.Compile("^[a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*$")
|
ReUserName, _ = regexp.Compile("^[a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*$")
|
||||||
|
ReUserNameWithEmail, _ = regexp.Compile(`^([a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*)|([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$`) // Add support for email formats
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsEmailValid(email string) bool {
|
func IsEmailValid(email string) bool {
|
||||||
@ -100,3 +103,21 @@ func GetCountryCode(prefix string, phone string) (string, error) {
|
|||||||
func FilterField(field string) bool {
|
func FilterField(field string) bool {
|
||||||
return ReFieldWhiteList.MatchString(field)
|
return ReFieldWhiteList.MatchString(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsValidOrigin(origin string) (bool, error) {
|
||||||
|
urlObj, err := url.Parse(origin)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if urlObj == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
originHostOnly := ""
|
||||||
|
if urlObj.Host != "" {
|
||||||
|
originHostOnly = fmt.Sprintf("%s://%s", urlObj.Scheme, urlObj.Hostname())
|
||||||
|
}
|
||||||
|
|
||||||
|
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-app" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const CracoLessPlugin = require("craco-less");
|
const CracoLessPlugin = require("craco-less");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devServer: {
|
devServer: {
|
||||||
@ -55,47 +56,42 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
webpack: {
|
webpack: {
|
||||||
configure: {
|
configure: (webpackConfig, { env, paths }) => {
|
||||||
// ignore webpack warnings by source-map-loader
|
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
|
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
|
||||||
ignoreWarnings: [
|
webpackConfig.ignoreWarnings = [
|
||||||
function ignoreSourcemapsloaderWarnings(warning) {
|
function ignoreSourcemapsloaderWarnings(warning) {
|
||||||
return (
|
return (
|
||||||
warning.module &&
|
warning.module &&
|
||||||
warning.module.resource.includes('node_modules') &&
|
warning.module.resource.includes("node_modules") &&
|
||||||
warning.details &&
|
warning.details &&
|
||||||
warning.details.includes('source-map-loader')
|
warning.details.includes("source-map-loader")
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
|
||||||
// use polyfill Buffer with Webpack 5
|
// use polyfill Buffer with Webpack 5
|
||||||
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
|
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
|
||||||
// https://craco.js.org/docs/configuration/webpack/
|
// https://craco.js.org/docs/configuration/webpack/
|
||||||
resolve: {
|
webpackConfig.resolve.fallback = {
|
||||||
fallback: {
|
buffer: require.resolve("buffer/"),
|
||||||
// "process": require.resolve('process/browser'),
|
process: false,
|
||||||
// "util": require.resolve("util/"),
|
util: false,
|
||||||
// "url": require.resolve("url/"),
|
url: false,
|
||||||
// "zlib": require.resolve("browserify-zlib"),
|
zlib: false,
|
||||||
// "stream": require.resolve("stream-browserify"),
|
stream: false,
|
||||||
// "http": require.resolve("stream-http"),
|
http: false,
|
||||||
// "https": require.resolve("https-browserify"),
|
https: false,
|
||||||
// "assert": require.resolve("assert/"),
|
assert: false,
|
||||||
"buffer": require.resolve('buffer/'),
|
crypto: false,
|
||||||
"process": false,
|
os: false,
|
||||||
"util": false,
|
fs: false,
|
||||||
"url": false,
|
};
|
||||||
"zlib": false,
|
|
||||||
"stream": false,
|
return webpackConfig;
|
||||||
"http": false,
|
|
||||||
"https": false,
|
|
||||||
"assert": false,
|
|
||||||
"buffer": false,
|
|
||||||
"crypto": false,
|
|
||||||
"os": false,
|
|
||||||
"fs": false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
21
web/mv.js
Normal file
21
web/mv.js
Normal file
@ -0,0 +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.`);
|
@ -57,6 +57,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env PORT=7001 craco start",
|
"start": "cross-env PORT=7001 craco start",
|
||||||
"build": "craco build",
|
"build": "craco build",
|
||||||
|
"postbuild": "node mv.js",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "craco eject",
|
"eject": "craco eject",
|
||||||
"crowdin:sync": "crowdin upload && crowdin download",
|
"crowdin:sync": "crowdin upload && crowdin download",
|
||||||
|
@ -36,6 +36,7 @@ const {Footer, Content} = Layout;
|
|||||||
|
|
||||||
import {setTwoToneColor} from "@ant-design/icons";
|
import {setTwoToneColor} from "@ant-design/icons";
|
||||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||||
|
import * as Cookie from "cookie";
|
||||||
|
|
||||||
setTwoToneColor("rgb(87,52,211)");
|
setTwoToneColor("rgb(87,52,211)");
|
||||||
|
|
||||||
@ -269,7 +270,9 @@ class App extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooter() {
|
renderFooter(logo, footerHtml) {
|
||||||
|
logo = logo ?? this.state.logo;
|
||||||
|
footerHtml = footerHtml ?? this.state.application?.footerHtml;
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
|
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
|
||||||
@ -280,14 +283,14 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
{
|
{
|
||||||
this.state.application?.footerHtml && this.state.application.footerHtml !== "" ?
|
footerHtml && footerHtml !== "" ?
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div dangerouslySetInnerHTML={{__html: this.state.application.footerHtml}} />
|
<div dangerouslySetInnerHTML={{__html: footerHtml}} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
: (
|
: (
|
||||||
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
|
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -308,7 +311,7 @@ class App extends Component {
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://ai.casbin.com"}>
|
<a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={`${Conf.AiAssistantUrl}`}>
|
||||||
<ShareAltOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
|
<ShareAltOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
|
||||||
</a>
|
</a>
|
||||||
<a className="custom-link" style={{float: "right", marginRight: "30px", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://github.com/casibase/casibase"}>
|
<a className="custom-link" style={{float: "right", marginRight: "30px", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://github.com/casibase/casibase"}>
|
||||||
@ -326,7 +329,7 @@ class App extends Component {
|
|||||||
}}
|
}}
|
||||||
visible={this.state.isAiAssistantOpen}
|
visible={this.state.isAiAssistantOpen}
|
||||||
>
|
>
|
||||||
<iframe id="iframeHelper" title={"iframeHelper"} src={"https://ai.casbin.com/?isRaw=1"} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
<iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -360,11 +363,27 @@ class App extends Component {
|
|||||||
|
|
||||||
renderPage() {
|
renderPage() {
|
||||||
if (this.isDoorPages()) {
|
if (this.isDoorPages()) {
|
||||||
|
let themeData = this.state.themeData;
|
||||||
|
let logo = this.state.logo;
|
||||||
|
let footerHtml = null;
|
||||||
|
if (this.state.organization === undefined) {
|
||||||
|
const curCookie = Cookie.parse(document.cookie);
|
||||||
|
if (curCookie["organizationTheme"] && curCookie["organizationTheme"] !== "null") {
|
||||||
|
themeData = JSON.parse(curCookie["organizationTheme"]);
|
||||||
|
}
|
||||||
|
if (curCookie["organizationLogo"] && curCookie["organizationLogo"] !== "") {
|
||||||
|
logo = curCookie["organizationLogo"];
|
||||||
|
}
|
||||||
|
if (curCookie["organizationFootHtml"] && curCookie["organizationFootHtml"] !== "") {
|
||||||
|
footerHtml = curCookie["organizationFootHtml"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={{
|
<ConfigProvider theme={{
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: this.state.themeData.colorPrimary,
|
colorPrimary: themeData.colorPrimary,
|
||||||
borderRadius: this.state.themeData.borderRadius,
|
borderRadius: themeData.borderRadius,
|
||||||
},
|
},
|
||||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||||
}}>
|
}}>
|
||||||
@ -401,7 +420,7 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
</Content>
|
</Content>
|
||||||
{
|
{
|
||||||
this.renderFooter()
|
this.renderFooter(logo, footerHtml)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.renderAiAssistant()
|
this.renderAiAssistant()
|
||||||
|
@ -46,12 +46,18 @@ require("codemirror/mode/css/css");
|
|||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
const template = `<style>
|
const template = `<style>
|
||||||
.login-panel{
|
.login-panel {
|
||||||
padding: 40px 70px 0 70px;
|
padding: 40px 70px 0 70px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 0 30px 20px rgba(0, 0, 0, 0.20);
|
box-shadow: 0 0 30px 20px rgba(0, 0, 0, 0.20);
|
||||||
}
|
}
|
||||||
|
.login-panel-dark {
|
||||||
|
padding: 40px 70px 0 70px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #333333;
|
||||||
|
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
|
||||||
|
}
|
||||||
</style>`;
|
</style>`;
|
||||||
|
|
||||||
const previewGrid = Setting.isMobile() ? 22 : 11;
|
const previewGrid = Setting.isMobile() ? 22 : 11;
|
||||||
@ -592,6 +598,16 @@ class ApplicationEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input placeholder = {this.state.application.organizationObj?.ipWhitelist} value={this.state.application.ipWhitelist} onChange={e => {
|
||||||
|
this.updateApplicationField("ipWhitelist", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("signup:Terms of Use"), i18next.t("signup:Terms of Use - Tooltip"))} :
|
{Setting.getLabel(i18next.t("signup:Terms of Use"), i18next.t("signup:Terms of Use - Tooltip"))} :
|
||||||
@ -749,7 +765,7 @@ class ApplicationEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
||||||
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`);
|
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -19,6 +19,7 @@ import "codemirror/mode/properties/properties";
|
|||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import IframeEditor from "./IframeEditor";
|
import IframeEditor from "./IframeEditor";
|
||||||
import {Tabs} from "antd";
|
import {Tabs} from "antd";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
const {TabPane} = Tabs;
|
const {TabPane} = Tabs;
|
||||||
|
|
||||||
@ -68,8 +69,8 @@ const CasbinEditor = ({model, onModelTextChange}) => {
|
|||||||
return (
|
return (
|
||||||
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
|
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
|
||||||
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
|
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
|
||||||
<TabPane tab="Basic Editor" key="basic" />
|
<TabPane tab={i18next.t("model:Basic Editor")} key="basic" />
|
||||||
<TabPane tab="Advanced Editor" key="advanced" />
|
<TabPane tab={i18next.t("model:Advanced Editor")} key="advanced" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
|
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
|
||||||
{activeKey === "advanced" ? (
|
{activeKey === "advanced" ? (
|
||||||
|
@ -31,3 +31,6 @@ export const ThemeDefault = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CustomFooter = null;
|
export const CustomFooter = null;
|
||||||
|
|
||||||
|
// Blank or null to hide Ai Assistant button
|
||||||
|
export const AiAssistantUrl = "https://ai.casbin.com";
|
||||||
|
@ -34,6 +34,7 @@ import PaymentResultPage from "./PaymentResultPage";
|
|||||||
import QrCodePage from "./QrCodePage";
|
import QrCodePage from "./QrCodePage";
|
||||||
import CaptchaPage from "./CaptchaPage";
|
import CaptchaPage from "./CaptchaPage";
|
||||||
import CustomHead from "./basic/CustomHead";
|
import CustomHead from "./basic/CustomHead";
|
||||||
|
import * as Util from "./auth/Util";
|
||||||
|
|
||||||
class EntryPage extends React.Component {
|
class EntryPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -94,6 +95,14 @@ class EntryPage extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.state.application?.ipRestriction) {
|
||||||
|
return Util.renderMessageLarge(this, this.state.application.ipRestriction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.application?.organizationObj?.ipRestriction) {
|
||||||
|
return Util.renderMessageLarge(this, this.state.application.organizationObj.ipRestriction);
|
||||||
|
}
|
||||||
|
|
||||||
const isDarkMode = this.props.themeAlgorithm.includes("dark");
|
const isDarkMode = this.props.themeAlgorithm.includes("dark");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -102,7 +111,7 @@ class EntryPage extends React.Component {
|
|||||||
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
||||||
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
||||||
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
||||||
style={{margin: "0 auto"}} />
|
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||||
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||||
|
@ -17,6 +17,7 @@ import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} fro
|
|||||||
const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) => {
|
const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) => {
|
||||||
const iframeRef = useRef(null);
|
const iframeRef = useRef(null);
|
||||||
const [iframeReady, setIframeReady] = useState(false);
|
const [iframeReady, setIframeReady] = useState(false);
|
||||||
|
const currentLang = localStorage.getItem("language") || "en";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMessage = (event) => {
|
const handleMessage = (event) => {
|
||||||
@ -26,24 +27,31 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
|
|||||||
onModelTextChange(event.data.modelText);
|
onModelTextChange(event.data.modelText);
|
||||||
} else if (event.data.type === "iframeReady") {
|
} else if (event.data.type === "iframeReady") {
|
||||||
setIframeReady(true);
|
setIframeReady(true);
|
||||||
iframeRef.current?.contentWindow.postMessage({
|
if (initialModelText && iframeRef.current?.contentWindow) {
|
||||||
type: "initializeModel",
|
iframeRef.current.contentWindow.postMessage({
|
||||||
modelText: initialModelText,
|
type: "initializeModel",
|
||||||
}, "*");
|
modelText: initialModelText,
|
||||||
|
lang: currentLang,
|
||||||
|
}, "*");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage);
|
window.addEventListener("message", handleMessage);
|
||||||
return () => window.removeEventListener("message", handleMessage);
|
return () => window.removeEventListener("message", handleMessage);
|
||||||
}, [onModelTextChange, initialModelText]);
|
}, [onModelTextChange, initialModelText, currentLang]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
getModelText: () => {
|
getModelText: () => {
|
||||||
iframeRef.current?.contentWindow.postMessage({type: "getModelText"}, "*");
|
if (iframeRef.current?.contentWindow) {
|
||||||
|
iframeRef.current.contentWindow.postMessage({
|
||||||
|
type: "getModelText",
|
||||||
|
}, "*");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateModelText: (newModelText) => {
|
updateModelText: (newModelText) => {
|
||||||
if (iframeReady) {
|
if (iframeReady && iframeRef.current?.contentWindow) {
|
||||||
iframeRef.current?.contentWindow.postMessage({
|
iframeRef.current.contentWindow.postMessage({
|
||||||
type: "updateModelText",
|
type: "updateModelText",
|
||||||
modelText: newModelText,
|
modelText: newModelText,
|
||||||
}, "*");
|
}, "*");
|
||||||
@ -54,7 +62,7 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
|
|||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
src="https://editor.casbin.org/model-editor"
|
src={`https://editor.casbin.org/model-editor?lang=${currentLang}`}
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="500px"
|
height="500px"
|
||||||
|
@ -106,6 +106,22 @@ class InvitationEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copySignupLink() {
|
||||||
|
let defaultApplication;
|
||||||
|
if (this.state.invitation.owner === "built-in") {
|
||||||
|
defaultApplication = "app-built-in";
|
||||||
|
} else {
|
||||||
|
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
||||||
|
defaultApplication = selectedOrganization.defaultApplication;
|
||||||
|
if (!defaultApplication) {
|
||||||
|
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
||||||
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
|
}
|
||||||
|
|
||||||
renderInvitation() {
|
renderInvitation() {
|
||||||
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
|
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
|
||||||
return (
|
return (
|
||||||
@ -114,16 +130,7 @@ class InvitationEditPage extends React.Component {
|
|||||||
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}
|
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}
|
||||||
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} onClick={() => {
|
<Button style={{marginLeft: "20px"}} onClick={_ => this.copySignupLink()}>
|
||||||
let defaultApplication;
|
|
||||||
if (this.state.invitation.owner === "built-in") {
|
|
||||||
defaultApplication = "app-built-in";
|
|
||||||
} else {
|
|
||||||
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
|
||||||
}
|
|
||||||
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
|
||||||
}}>
|
|
||||||
{i18next.t("application:Copy signup page URL")}
|
{i18next.t("application:Copy signup page URL")}
|
||||||
</Button>
|
</Button>
|
||||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||||
@ -330,16 +337,7 @@ class InvitationEditPage extends React.Component {
|
|||||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||||
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} size="large" onClick={() => {
|
<Button style={{marginLeft: "20px"}} size="large" onClick={_ => this.copySignupLink()}>
|
||||||
let defaultApplication;
|
|
||||||
if (this.state.invitation.owner === "built-in") {
|
|
||||||
defaultApplication = "app-built-in";
|
|
||||||
} else {
|
|
||||||
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
|
||||||
}
|
|
||||||
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
|
||||||
}}>
|
|
||||||
{i18next.t("application:Copy signup page URL")}
|
{i18next.t("application:Copy signup page URL")}
|
||||||
</Button>
|
</Button>
|
||||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||||
|
@ -228,6 +228,21 @@ class LdapEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Password type"), i18next.t("general:Password type - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={21}>
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.ldap.passwordType ?? []} onChange={(value => {
|
||||||
|
this.updateLdapField("passwordType", value);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Option key={"Plain"} value={"Plain"}>{i18next.t("general:Plain")}</Option>
|
||||||
|
<Option key={"SSHA"} value={"SSHA"} >SSHA</Option>
|
||||||
|
<Option key={"MD5"} value={"MD5"} >MD5</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||||
|
@ -192,17 +192,21 @@ function ManagementPage(props) {
|
|||||||
themeAlgorithm={props.themeAlgorithm}
|
themeAlgorithm={props.themeAlgorithm}
|
||||||
onChange={props.setLogoAndThemeAlgorithm} />
|
onChange={props.setLogoAndThemeAlgorithm} />
|
||||||
<LanguageSelect languages={props.account.organization.languages} />
|
<LanguageSelect languages={props.account.organization.languages} />
|
||||||
<Tooltip title="Click to open AI assitant">
|
{
|
||||||
<div className="select-box" onClick={props.openAiAssistant}>
|
Conf.AiAssistantUrl?.trim() && (
|
||||||
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
<Tooltip title="Click to open AI assistant">
|
||||||
</div>
|
<div className="select-box" onClick={props.openAiAssistant}>
|
||||||
</Tooltip>
|
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
<OpenTour />
|
<OpenTour />
|
||||||
{Setting.isAdminUser(props.account) && !Setting.isMobile() && (props.uri.indexOf("/trees") === -1) &&
|
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
||||||
<OrganizationSelect
|
<OrganizationSelect
|
||||||
initValue={Setting.getOrganization()}
|
initValue={Setting.getOrganization()}
|
||||||
withAll={true}
|
withAll={true}
|
||||||
style={{marginRight: "20px", width: "180px", display: "flex"}}
|
style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
Setting.setOrganization(value);
|
Setting.setOrganization(value);
|
||||||
}}
|
}}
|
||||||
|
@ -339,6 +339,16 @@ class OrganizationEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>)
|
</Row>)
|
||||||
}
|
}
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("organization:Password expire days"), i18next.t("organization:Password expire days - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={4} >
|
||||||
|
<InputNumber value={this.state.organization.passwordExpireDays} onChange={value => {
|
||||||
|
this.updateOrganizationField("passwordExpireDays", value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :
|
||||||
@ -452,6 +462,16 @@ class OrganizationEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input value={this.state.organization.ipWhitelist} onChange={e => {
|
||||||
|
this.updateOrganizationField("ipWhitelist", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||||
{Setting.getLabel(i18next.t("organization:Init score"), i18next.t("organization:Init score - Tooltip"))} :
|
{Setting.getLabel(i18next.t("organization:Init score"), i18next.t("organization:Init score - Tooltip"))} :
|
||||||
|
@ -37,6 +37,7 @@ class OrganizationListPage extends BaseListPage {
|
|||||||
passwordOptions: [],
|
passwordOptions: [],
|
||||||
passwordObfuscatorType: "Plain",
|
passwordObfuscatorType: "Plain",
|
||||||
passwordObfuscatorKey: "",
|
passwordObfuscatorKey: "",
|
||||||
|
passwordExpireDays: 0,
|
||||||
countryCodes: ["US"],
|
countryCodes: ["US"],
|
||||||
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
|
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
|
||||||
defaultApplication: "",
|
defaultApplication: "",
|
||||||
|
@ -123,6 +123,22 @@ class ProductBuyPage extends React.Component {
|
|||||||
return "$";
|
return "$";
|
||||||
} else if (product?.currency === "CNY") {
|
} else if (product?.currency === "CNY") {
|
||||||
return "¥";
|
return "¥";
|
||||||
|
} else if (product?.currency === "EUR") {
|
||||||
|
return "€";
|
||||||
|
} else if (product?.currency === "JPY") {
|
||||||
|
return "¥";
|
||||||
|
} else if (product?.currency === "GBP") {
|
||||||
|
return "£";
|
||||||
|
} else if (product?.currency === "AUD") {
|
||||||
|
return "A$";
|
||||||
|
} else if (product?.currency === "CAD") {
|
||||||
|
return "C$";
|
||||||
|
} else if (product?.currency === "CHF") {
|
||||||
|
return "CHF";
|
||||||
|
} else if (product?.currency === "HKD") {
|
||||||
|
return "HK$";
|
||||||
|
} else if (product?.currency === "SGD") {
|
||||||
|
return "S$";
|
||||||
} else {
|
} else {
|
||||||
return "(Unknown currency)";
|
return "(Unknown currency)";
|
||||||
}
|
}
|
||||||
|
@ -209,6 +209,14 @@ class ProductEditPage extends React.Component {
|
|||||||
[
|
[
|
||||||
{id: "USD", name: "USD"},
|
{id: "USD", name: "USD"},
|
||||||
{id: "CNY", name: "CNY"},
|
{id: "CNY", name: "CNY"},
|
||||||
|
{id: "EUR", name: "EUR"},
|
||||||
|
{id: "JPY", name: "JPY"},
|
||||||
|
{id: "GBP", name: "GBP"},
|
||||||
|
{id: "AUD", name: "AUD"},
|
||||||
|
{id: "CAD", name: "CAD"},
|
||||||
|
{id: "CHF", name: "CHF"},
|
||||||
|
{id: "HKD", name: "HKD"},
|
||||||
|
{id: "SGD", name: "SGD"},
|
||||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||||
}
|
}
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -297,6 +297,8 @@ class ProviderEditPage extends React.Component {
|
|||||||
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
||||||
} else if (provider.type === "WeChat Pay") {
|
} else if (provider.type === "WeChat Pay") {
|
||||||
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
||||||
|
} else if (provider.type === "CUCloud") {
|
||||||
|
return Setting.getLabel(i18next.t("provider:Account ID"), i18next.t("provider:Account ID - Tooltip"));
|
||||||
} else {
|
} else {
|
||||||
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
||||||
}
|
}
|
||||||
@ -393,6 +395,9 @@ class ProviderEditPage extends React.Component {
|
|||||||
} else if (provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") {
|
} else if (provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") {
|
||||||
text = i18next.t("provider:App Key");
|
text = i18next.t("provider:App Key");
|
||||||
tooltip = i18next.t("provider:App Key - Tooltip");
|
tooltip = i18next.t("provider:App Key - Tooltip");
|
||||||
|
} else if (provider.type === "CUCloud") {
|
||||||
|
text = i18next.t("provider:Topic name");
|
||||||
|
tooltip = i18next.t("provider:Topic name - Tooltip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,6 +638,20 @@ class ProviderEditPage extends React.Component {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
this.state.provider.category === "OAuth" ? (
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("provider:Email regex"), i18next.t("provider:Email regex - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22}>
|
||||||
|
<TextArea rows={4} value={this.state.provider.emailRegex} onChange={e => {
|
||||||
|
this.updateProviderField("emailRegex", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
{
|
{
|
||||||
this.state.provider.type === "Custom" ? (
|
this.state.provider.type === "Custom" ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@ -757,7 +776,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Apple" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" ? null : (
|
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Apple" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" && this.state.provider.type !== "CUCloud" ? null : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
@ -770,7 +789,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{
|
{
|
||||||
(this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
|
(this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{this.getClientSecret2Label(this.state.provider)} :
|
{this.getClientSecret2Label(this.state.provider)} :
|
||||||
@ -856,9 +875,9 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email"].includes(this.state.provider.type) ? (
|
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<div>
|
<div>
|
||||||
{["Local File System"].includes(this.state.provider.type) ? null : (
|
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
|
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
|
||||||
@ -870,7 +889,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
||||||
@ -882,7 +901,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{["Casdoor"].includes(this.state.provider.type) ?
|
{["Casdoor"].includes(this.state.provider.type) ?
|
||||||
@ -896,7 +915,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
||||||
@ -908,7 +927,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||||
@ -932,7 +951,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor"].includes(this.state.provider.type) ? (
|
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{["Casdoor"].includes(this.state.provider.type) ?
|
{["Casdoor"].includes(this.state.provider.type) ?
|
||||||
@ -971,7 +990,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
{["Custom HTTP"].includes(this.state.provider.type) ? (
|
{["Custom HTTP", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
|
||||||
@ -983,7 +1002,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
{["Google Chat"].includes(this.state.provider.type) ? (
|
{["Google Chat", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
||||||
|
@ -187,7 +187,7 @@ class RoleEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
|
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.role.users}
|
<Select virtual={true} mode="multiple" style={{width: "100%"}} value={this.state.role.users}
|
||||||
onChange={(value => {this.updateRoleField("users", value);})}
|
onChange={(value => {this.updateRoleField("users", value);})}
|
||||||
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
|
options={this.state.users.map((user) => Setting.getOption(`${user.owner}/${user.name}`, `${user.owner}/${user.name}`))}
|
||||||
/>
|
/>
|
||||||
|
@ -233,6 +233,10 @@ export const OtherProviderInfo = {
|
|||||||
logo: `${StaticBaseUrl}/img/casdoor.png`,
|
logo: `${StaticBaseUrl}/img/casdoor.png`,
|
||||||
url: "https://casdoor.org/docs/provider/storage/overview",
|
url: "https://casdoor.org/docs/provider/storage/overview",
|
||||||
},
|
},
|
||||||
|
"CUCloud OSS": {
|
||||||
|
logo: `${StaticBaseUrl}/img/social_cucloud.png`,
|
||||||
|
url: "https://www.cucloud.cn/product/oss.html",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
SAML: {
|
SAML: {
|
||||||
"Aliyun IDaaS": {
|
"Aliyun IDaaS": {
|
||||||
@ -401,6 +405,10 @@ export const OtherProviderInfo = {
|
|||||||
logo: `${StaticBaseUrl}/img/social_viber.png`,
|
logo: `${StaticBaseUrl}/img/social_viber.png`,
|
||||||
url: "https://www.viber.com/",
|
url: "https://www.viber.com/",
|
||||||
},
|
},
|
||||||
|
"CUCloud": {
|
||||||
|
logo: `${StaticBaseUrl}/img/cucloud.png`,
|
||||||
|
url: "https://www.cucloud.cn/",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -920,7 +928,7 @@ export function getClickable(text) {
|
|||||||
return (
|
return (
|
||||||
<a onClick={() => {
|
<a onClick={() => {
|
||||||
copy(text);
|
copy(text);
|
||||||
showMessage("success", "Copied to clipboard");
|
showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
}}>
|
}}>
|
||||||
{text}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
@ -981,6 +989,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Bilibili", name: "Bilibili"},
|
{id: "Bilibili", name: "Bilibili"},
|
||||||
{id: "Okta", name: "Okta"},
|
{id: "Okta", name: "Okta"},
|
||||||
{id: "Douyin", name: "Douyin"},
|
{id: "Douyin", name: "Douyin"},
|
||||||
|
{id: "Kwai", name: "Kwai"},
|
||||||
{id: "Line", name: "Line"},
|
{id: "Line", name: "Line"},
|
||||||
{id: "Amazon", name: "Amazon"},
|
{id: "Amazon", name: "Amazon"},
|
||||||
{id: "Auth0", name: "Auth0"},
|
{id: "Auth0", name: "Auth0"},
|
||||||
@ -1078,6 +1087,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Google Cloud Storage", name: "Google Cloud Storage"},
|
{id: "Google Cloud Storage", name: "Google Cloud Storage"},
|
||||||
{id: "Synology", name: "Synology"},
|
{id: "Synology", name: "Synology"},
|
||||||
{id: "Casdoor", name: "Casdoor"},
|
{id: "Casdoor", name: "Casdoor"},
|
||||||
|
{id: "CUCloud OSS", name: "CUCloud OSS"},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else if (category === "SAML") {
|
} else if (category === "SAML") {
|
||||||
@ -1131,6 +1141,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Reddit", name: "Reddit"},
|
{id: "Reddit", name: "Reddit"},
|
||||||
{id: "Rocket Chat", name: "Rocket Chat"},
|
{id: "Rocket Chat", name: "Rocket Chat"},
|
||||||
{id: "Viber", name: "Viber"},
|
{id: "Viber", name: "Viber"},
|
||||||
|
{id: "CUCloud", name: "CUCloud"},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
@ -1171,7 +1182,7 @@ export function renderLogo(application) {
|
|||||||
|
|
||||||
function isSigninMethodEnabled(application, signinMethod) {
|
function isSigninMethodEnabled(application, signinMethod) {
|
||||||
if (application && application.signinMethods) {
|
if (application && application.signinMethods) {
|
||||||
return application.signinMethods.filter(item => item.name === signinMethod && item.rule !== "Hide-Password").length > 0;
|
return application.signinMethods.filter(item => item.name === signinMethod && item.rule !== "Hide password").length > 0;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1550,10 +1561,30 @@ export function getDefaultHtmlEmailContent() {
|
|||||||
|
|
||||||
export function getCurrencyText(product) {
|
export function getCurrencyText(product) {
|
||||||
if (product?.currency === "USD") {
|
if (product?.currency === "USD") {
|
||||||
return i18next.t("product:USD");
|
return i18next.t("currency:USD");
|
||||||
} else if (product?.currency === "CNY") {
|
} else if (product?.currency === "CNY") {
|
||||||
return i18next.t("product:CNY");
|
return i18next.t("currency:CNY");
|
||||||
|
} else if (product?.currency === "EUR") {
|
||||||
|
return i18next.t("currency:EUR");
|
||||||
|
} else if (product?.currency === "JPY") {
|
||||||
|
return i18next.t("currency:JPY");
|
||||||
|
} else if (product?.currency === "GBP") {
|
||||||
|
return i18next.t("currency:GBP");
|
||||||
|
} else if (product?.currency === "AUD") {
|
||||||
|
return i18next.t("currency:AUD");
|
||||||
|
} else if (product?.currency === "CAD") {
|
||||||
|
return i18next.t("currency:CAD");
|
||||||
|
} else if (product?.currency === "CHF") {
|
||||||
|
return i18next.t("currency:CHF");
|
||||||
|
} else if (product?.currency === "HKD") {
|
||||||
|
return i18next.t("currency:HKD");
|
||||||
|
} else if (product?.currency === "SGD") {
|
||||||
|
return i18next.t("currency:SGD");
|
||||||
} else {
|
} else {
|
||||||
return "(Unknown currency)";
|
return "(Unknown currency)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDarkTheme(themeAlgorithm) {
|
||||||
|
return themeAlgorithm && themeAlgorithm.includes("dark");
|
||||||
|
}
|
||||||
|
@ -1009,6 +1009,19 @@ class UserEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
} else if (accountItem.name === "Last change password time") {
|
||||||
|
return (
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("user:Last change password time"), i18next.t("user:Last change password time"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22}>
|
||||||
|
<Input value={this.state.user.lastChangePasswordTime} onChange={e => {
|
||||||
|
this.updateUserField("lastChangePasswordTime", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
} else if (accountItem.name === "Managed accounts") {
|
} else if (accountItem.name === "Managed accounts") {
|
||||||
return (
|
return (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
@ -1070,6 +1083,19 @@ class UserEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
} else if (accountItem.name === "IP whitelist") {
|
||||||
|
return (
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:IP whitelist"), i18next.t("general:IP whitelist - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22}>
|
||||||
|
<Input value={this.state.user.ipWhitelist} onChange={e => {
|
||||||
|
this.updateUserField("ipWhitelist", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ class AuthCallback extends React.Component {
|
|||||||
}
|
}
|
||||||
const SAMLResponse = res.data;
|
const SAMLResponse = res.data;
|
||||||
const redirectUri = res.data2.redirectUrl;
|
const redirectUri = res.data2.redirectUrl;
|
||||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
31
web/src/auth/KwaiLoginButton.js
Normal file
31
web/src/auth/KwaiLoginButton.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import {createButton} from "react-social-login-buttons";
|
||||||
|
import {StaticBaseUrl} from "../Setting";
|
||||||
|
|
||||||
|
function Icon({width = 24, height = 24}) {
|
||||||
|
return <img src={`${StaticBaseUrl}/buttons/kwai.svg`} alt="Sign in with Kwai" style={{width: width, height: height}} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
text: "Sign in with Kwai",
|
||||||
|
icon: Icon,
|
||||||
|
style: {background: "#ffffff", color: "#000000"},
|
||||||
|
activeStyle: {background: "#ededee"},
|
||||||
|
};
|
||||||
|
|
||||||
|
const KwaiLoginButton = createButton(config);
|
||||||
|
|
||||||
|
export default KwaiLoginButton;
|
@ -37,6 +37,7 @@ import RedirectForm from "../common/RedirectForm";
|
|||||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||||
import * as ProviderButton from "./ProviderButton";
|
import * as ProviderButton from "./ProviderButton";
|
||||||
|
import {EmailMfaType, SmsMfaType, TotpMfaType} from "./MfaSetupPage";
|
||||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||||
|
|
||||||
class LoginPage extends React.Component {
|
class LoginPage extends React.Component {
|
||||||
@ -227,7 +228,26 @@ class LoginPage extends React.Component {
|
|||||||
return "password";
|
return "password";
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaceholder() {
|
getCurrentLoginMethod() {
|
||||||
|
if (this.state.loginMethod === "password") {
|
||||||
|
return "Password";
|
||||||
|
} else if (this.state.loginMethod?.includes("verificationCode")) {
|
||||||
|
return "Verification code";
|
||||||
|
} else if (this.state.loginMethod === "webAuthn") {
|
||||||
|
return "WebAuthn";
|
||||||
|
} else if (this.state.loginMethod === "ldap") {
|
||||||
|
return "LDAP";
|
||||||
|
} else if (this.state.loginMethod === "faceId") {
|
||||||
|
return "Face ID";
|
||||||
|
} else {
|
||||||
|
return "Password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaceholder(defaultPlaceholder = null) {
|
||||||
|
if (defaultPlaceholder) {
|
||||||
|
return defaultPlaceholder;
|
||||||
|
}
|
||||||
switch (this.state.loginMethod) {
|
switch (this.state.loginMethod) {
|
||||||
case "verificationCode": return i18next.t("login:Email or phone");
|
case "verificationCode": return i18next.t("login:Email or phone");
|
||||||
case "verificationCodeEmail": return i18next.t("login:Email");
|
case "verificationCodeEmail": return i18next.t("login:Email");
|
||||||
@ -262,17 +282,7 @@ class LoginPage extends React.Component {
|
|||||||
values["organization"] = this.getApplicationObj().organization;
|
values["organization"] = this.getApplicationObj().organization;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.loginMethod === "password") {
|
values["signinMethod"] = this.getCurrentLoginMethod();
|
||||||
values["signinMethod"] = "Password";
|
|
||||||
} else if (this.state.loginMethod?.includes("verificationCode")) {
|
|
||||||
values["signinMethod"] = "Verification code";
|
|
||||||
} else if (this.state.loginMethod === "webAuthn") {
|
|
||||||
values["signinMethod"] = "WebAuthn";
|
|
||||||
} else if (this.state.loginMethod === "ldap") {
|
|
||||||
values["signinMethod"] = "LDAP";
|
|
||||||
} else if (this.state.loginMethod === "faceId") {
|
|
||||||
values["signinMethod"] = "Face ID";
|
|
||||||
}
|
|
||||||
const oAuthParams = Util.getOAuthGetParameters();
|
const oAuthParams = Util.getOAuthGetParameters();
|
||||||
|
|
||||||
values["type"] = oAuthParams?.responseType ?? this.state.type;
|
values["type"] = oAuthParams?.responseType ?? this.state.type;
|
||||||
@ -409,6 +419,7 @@ class LoginPage extends React.Component {
|
|||||||
if (this.state.type === "cas") {
|
if (this.state.type === "cas") {
|
||||||
// CAS
|
// CAS
|
||||||
const casParams = Util.getCasParameters();
|
const casParams = Util.getCasParameters();
|
||||||
|
values["signinMethod"] = this.getCurrentLoginMethod();
|
||||||
values["type"] = this.state.type;
|
values["type"] = this.state.type;
|
||||||
AuthBackend.loginCas(values, casParams).then((res) => {
|
AuthBackend.loginCas(values, casParams).then((res) => {
|
||||||
const loginHandler = (res) => {
|
const loginHandler = (res) => {
|
||||||
@ -430,19 +441,12 @@ class LoginPage extends React.Component {
|
|||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
if (res.data === NextMfa) {
|
if (res.data === NextMfa) {
|
||||||
this.setState({
|
this.setState({
|
||||||
getVerifyTotp: () => {
|
mfaProps: res.data2,
|
||||||
return (
|
selectedMfaProp: this.getPreferredMfaProp(res.data2),
|
||||||
<MfaAuthVerifyForm
|
}, () => {
|
||||||
mfaProps={res.data2}
|
this.setState({
|
||||||
formValues={values}
|
getVerifyTotp: () => this.renderMfaAuthVerifyForm(values, casParams, loginHandler),
|
||||||
authParams={casParams}
|
});
|
||||||
application={this.getApplicationObj()}
|
|
||||||
onFail={() => {
|
|
||||||
Setting.showMessage("error", i18next.t("mfa:Verification failed"));
|
|
||||||
}}
|
|
||||||
onSuccess={(res) => loginHandler(res)}
|
|
||||||
/>);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
loginHandler(res);
|
loginHandler(res);
|
||||||
@ -478,6 +482,10 @@ class LoginPage extends React.Component {
|
|||||||
const accessToken = res.data;
|
const accessToken = res.data;
|
||||||
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
|
Setting.goToLink(`${oAuthParams.redirectUri}#${amendatoryResponseType}=${accessToken}&state=${oAuthParams.state}&token_type=bearer`);
|
||||||
} else if (responseType === "saml") {
|
} else if (responseType === "saml") {
|
||||||
|
if (res.data === RequiredMfa) {
|
||||||
|
this.props.onLoginSuccess(window.location.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res.data2.needUpdatePassword) {
|
if (res.data2.needUpdatePassword) {
|
||||||
sessionStorage.setItem("signinUrl", window.location.href);
|
sessionStorage.setItem("signinUrl", window.location.href);
|
||||||
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
|
Setting.goToLink(this, `/forget/${this.state.applicationName}`);
|
||||||
@ -491,7 +499,7 @@ class LoginPage extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
const SAMLResponse = res.data;
|
const SAMLResponse = res.data;
|
||||||
const redirectUri = res.data2.redirectUrl;
|
const redirectUri = res.data2.redirectUrl;
|
||||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -499,19 +507,12 @@ class LoginPage extends React.Component {
|
|||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
if (res.data === NextMfa) {
|
if (res.data === NextMfa) {
|
||||||
this.setState({
|
this.setState({
|
||||||
getVerifyTotp: () => {
|
mfaProps: res.data2,
|
||||||
return (
|
selectedMfaProp: this.getPreferredMfaProp(res.data2),
|
||||||
<MfaAuthVerifyForm
|
}, () => {
|
||||||
mfaProps={res.data2}
|
this.setState({
|
||||||
formValues={values}
|
getVerifyTotp: () => this.renderMfaAuthVerifyForm(values, oAuthParams, loginHandler),
|
||||||
authParams={oAuthParams}
|
});
|
||||||
application={this.getApplicationObj()}
|
|
||||||
onFail={() => {
|
|
||||||
Setting.showMessage("error", i18next.t("mfa:Verification failed"));
|
|
||||||
}}
|
|
||||||
onSuccess={(res) => loginHandler(res)}
|
|
||||||
/>);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} else if (res.data === "SelectPlan") {
|
} else if (res.data === "SelectPlan") {
|
||||||
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
|
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
|
||||||
@ -531,6 +532,49 @@ class LoginPage extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderMfaAuthVerifyForm(values, authParams, onSuccess) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MfaAuthVerifyForm
|
||||||
|
mfaProps={this.state.selectedMfaProp}
|
||||||
|
formValues={values}
|
||||||
|
authParams={authParams}
|
||||||
|
application={this.getApplicationObj()}
|
||||||
|
onFail={(errorMessage) => {
|
||||||
|
Setting.showMessage("error", errorMessage);
|
||||||
|
}}
|
||||||
|
onSuccess={(res) => onSuccess(res)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
this.state.mfaProps.map((mfa) => {
|
||||||
|
if (this.state.selectedMfaProp.mfaType === mfa.mfaType) {return null;}
|
||||||
|
let mfaI18n = "";
|
||||||
|
switch (mfa.mfaType) {
|
||||||
|
case SmsMfaType: mfaI18n = i18next.t("mfa:Use SMS"); break;
|
||||||
|
case TotpMfaType: mfaI18n = i18next.t("mfa:Use Authenticator App"); break ;
|
||||||
|
case EmailMfaType: mfaI18n = i18next.t("mfa:Use Email") ;break;
|
||||||
|
}
|
||||||
|
return <div key={mfa.mfaType}><Button type={"link"} onClick={() => {
|
||||||
|
this.setState({
|
||||||
|
selectedMfaProp: mfa,
|
||||||
|
});
|
||||||
|
}}>{mfaI18n}</Button></div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreferredMfaProp(mfaProps) {
|
||||||
|
for (const i in mfaProps) {
|
||||||
|
if (mfaProps[i].isPreffered) {
|
||||||
|
return mfaProps[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mfaProps[0];
|
||||||
|
}
|
||||||
|
|
||||||
isProviderVisible(providerItem) {
|
isProviderVisible(providerItem) {
|
||||||
if (this.state.mode === "signup") {
|
if (this.state.mode === "signup") {
|
||||||
return Setting.isProviderVisibleForSignUp(providerItem);
|
return Setting.isProviderVisibleForSignUp(providerItem);
|
||||||
@ -672,7 +716,7 @@ class LoginPage extends React.Component {
|
|||||||
id="input"
|
id="input"
|
||||||
className="login-username-input"
|
className="login-username-input"
|
||||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||||
placeholder={this.getPlaceholder()}
|
placeholder={this.getPlaceholder(signinItem.placeholder)}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
this.setState({
|
this.setState({
|
||||||
username: e.target.value,
|
username: e.target.value,
|
||||||
@ -1086,7 +1130,7 @@ class LoginPage extends React.Component {
|
|||||||
className="login-password-input"
|
className="login-password-input"
|
||||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={i18next.t("general:Password")}
|
placeholder={signinItem.placeholder ? signinItem.placeholder : i18next.t("general:Password")}
|
||||||
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
|
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -1136,7 +1180,7 @@ class LoginPage extends React.Component {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
application?.signinMethods?.forEach((signinMethod) => {
|
application?.signinMethods?.forEach((signinMethod) => {
|
||||||
if (signinMethod.rule === "Hide-Password") {
|
if (signinMethod.rule === "Hide password") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule));
|
const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule));
|
||||||
@ -1293,7 +1337,7 @@ class LoginPage extends React.Component {
|
|||||||
<div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}>
|
<div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}>
|
||||||
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
||||||
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
||||||
<div className="login-panel">
|
<div className={Setting.isDarkTheme(this.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
|
||||||
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
|
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
|
||||||
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
|
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +37,7 @@ class MfaSetupPage extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
account: props.account,
|
account: props.account,
|
||||||
application: null,
|
application: null,
|
||||||
applicationName: props.account.signupApplication ?? "",
|
applicationName: props.account.signupApplication ?? localStorage.getItem("applicationName") ?? "",
|
||||||
current: location.state?.from !== undefined ? 1 : 0,
|
current: location.state?.from !== undefined ? 1 : 0,
|
||||||
mfaProps: null,
|
mfaProps: null,
|
||||||
mfaType: params.get("mfaType") ?? SmsMfaType,
|
mfaType: params.get("mfaType") ?? SmsMfaType,
|
||||||
@ -179,8 +179,10 @@ class MfaSetupPage extends React.Component {
|
|||||||
mfaProps={this.state.mfaProps}
|
mfaProps={this.state.mfaProps}
|
||||||
application={this.state.application}
|
application={this.state.application}
|
||||||
user={this.props.account}
|
user={this.props.account}
|
||||||
onSuccess={() => {
|
onSuccess={(res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
dest: res.dest,
|
||||||
|
countryCode: res.countryCode,
|
||||||
current: this.state.current + 1,
|
current: this.state.current + 1,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -195,7 +197,7 @@ class MfaSetupPage extends React.Component {
|
|||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return (
|
return (
|
||||||
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
|
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} secret={this.state.mfaProps.secret} recoveryCodes={this.state.mfaProps.recoveryCodes} dest={this.state.dest} countryCode={this.state.countryCode}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
|
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
|
||||||
this.props.onfinish();
|
this.props.onfinish();
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import CryptoJS from "crypto-js";
|
import CryptoJS from "crypto-js";
|
||||||
import i18next from "i18next";
|
import {Buffer} from "buffer";
|
||||||
|
|
||||||
export function getRandomKeyForObfuscator(obfuscatorType) {
|
export function getRandomKeyForObfuscator(obfuscatorType) {
|
||||||
if (obfuscatorType === "DES") {
|
if (obfuscatorType === "DES") {
|
||||||
@ -45,17 +45,17 @@ function encrypt(cipher, key, iv, password) {
|
|||||||
|
|
||||||
export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) {
|
export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) {
|
||||||
if (passwordObfuscatorType === undefined) {
|
if (passwordObfuscatorType === undefined) {
|
||||||
return i18next.t("organization:failed to get password obfuscator");
|
return "passwordObfuscatorType should not be undefined";
|
||||||
} else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
|
} else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
|
||||||
return "";
|
return "";
|
||||||
} else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") {
|
} else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") {
|
||||||
if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) {
|
if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) {
|
||||||
return "";
|
return "";
|
||||||
} else {
|
} else {
|
||||||
return `${i18next.t("organization:The password obfuscator key doesn't match the regex")}: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`;
|
return `The password obfuscator key doesn't match the regex: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return `${i18next.t("organization:unsupported password obfuscator type")}: ${passwordObfuscatorType}`;
|
return `unsupported password obfuscator type: ${passwordObfuscatorType}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +119,10 @@ const authInfo = {
|
|||||||
scope: "user_info",
|
scope: "user_info",
|
||||||
endpoint: "https://open.douyin.com/platform/oauth/connect",
|
endpoint: "https://open.douyin.com/platform/oauth/connect",
|
||||||
},
|
},
|
||||||
|
Kwai: {
|
||||||
|
scope: "user_info",
|
||||||
|
endpoint: "https://open.kuaishou.com/oauth2/connect",
|
||||||
|
},
|
||||||
Custom: {
|
Custom: {
|
||||||
endpoint: "https://example.com/",
|
endpoint: "https://example.com/",
|
||||||
},
|
},
|
||||||
@ -470,6 +474,8 @@ export function getAuthUrl(application, provider, method, code) {
|
|||||||
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||||
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
|
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
|
||||||
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||||
|
} else if (provider.type === "Kwai") {
|
||||||
|
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||||
} else if (provider.type === "Custom") {
|
} else if (provider.type === "Custom") {
|
||||||
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
|
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
|
||||||
} else if (provider.type === "Bilibili") {
|
} else if (provider.type === "Bilibili") {
|
||||||
|
@ -40,6 +40,7 @@ import SteamLoginButton from "./SteamLoginButton";
|
|||||||
import BilibiliLoginButton from "./BilibiliLoginButton";
|
import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||||
import OktaLoginButton from "./OktaLoginButton";
|
import OktaLoginButton from "./OktaLoginButton";
|
||||||
import DouyinLoginButton from "./DouyinLoginButton";
|
import DouyinLoginButton from "./DouyinLoginButton";
|
||||||
|
import KwaiLoginButton from "./KwaiLoginButton";
|
||||||
import LoginButton from "./LoginButton";
|
import LoginButton from "./LoginButton";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
import {WechatOfficialAccountModal} from "./Util";
|
import {WechatOfficialAccountModal} from "./Util";
|
||||||
@ -96,6 +97,8 @@ function getSigninButton(provider) {
|
|||||||
return <OktaLoginButton text={text} align={"center"} />;
|
return <OktaLoginButton text={text} align={"center"} />;
|
||||||
} else if (provider.type === "Douyin") {
|
} else if (provider.type === "Douyin") {
|
||||||
return <DouyinLoginButton text={text} align={"center"} />;
|
return <DouyinLoginButton text={text} align={"center"} />;
|
||||||
|
} else if (provider.type === "Kwai") {
|
||||||
|
return <KwaiLoginButton text={text} align={"center"} />;
|
||||||
} else {
|
} else {
|
||||||
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
|
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
|
||||||
}
|
}
|
||||||
|
@ -842,7 +842,7 @@ class SignupPage extends React.Component {
|
|||||||
<div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}>
|
<div className="login-content" style={{margin: this.props.preview ?? this.parseOffset(application.formOffset)}}>
|
||||||
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
||||||
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
{Setting.inIframe() || !Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
||||||
<div className="login-panel" >
|
<div className={Setting.isDarkTheme(this.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
|
||||||
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
|
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
|
||||||
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
|
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,6 +113,9 @@ export function getCasLoginParameters(owner, name) {
|
|||||||
|
|
||||||
export function getOAuthGetParameters(params) {
|
export function getOAuthGetParameters(params) {
|
||||||
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
|
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
|
||||||
|
const lowercaseQueries = {};
|
||||||
|
queries.forEach((val, key) => {lowercaseQueries[key.toLowerCase()] = val;});
|
||||||
|
|
||||||
const clientId = getRefinedValue(queries.get("client_id"));
|
const clientId = getRefinedValue(queries.get("client_id"));
|
||||||
const responseType = getRefinedValue(queries.get("response_type"));
|
const responseType = getRefinedValue(queries.get("response_type"));
|
||||||
|
|
||||||
@ -138,9 +141,9 @@ export function getOAuthGetParameters(params) {
|
|||||||
const nonce = getRefinedValue(queries.get("nonce"));
|
const nonce = getRefinedValue(queries.get("nonce"));
|
||||||
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
|
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
|
||||||
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
|
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
|
||||||
const samlRequest = getRefinedValue(queries.get("SAMLRequest"));
|
const samlRequest = getRefinedValue(lowercaseQueries["samlRequest".toLowerCase()]);
|
||||||
const relayState = getRefinedValue(queries.get("RelayState"));
|
const relayState = getRefinedValue(lowercaseQueries["RelayState".toLowerCase()]);
|
||||||
const noRedirect = getRefinedValue(queries.get("noRedirect"));
|
const noRedirect = getRefinedValue(lowercaseQueries["noRedirect".toLowerCase()]);
|
||||||
|
|
||||||
if (clientId === "" && samlRequest === "") {
|
if (clientId === "" && samlRequest === "") {
|
||||||
// login
|
// login
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React, {useState} from "react";
|
import React, {Fragment, useState} from "react";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import {Button, Input} from "antd";
|
import {Button, Input} from "antd";
|
||||||
import * as AuthBackend from "../AuthBackend";
|
import * as AuthBackend from "../AuthBackend";
|
||||||
@ -33,7 +33,8 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
|||||||
|
|
||||||
const verify = ({passcode}) => {
|
const verify = ({passcode}) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const values = {...formValues, passcode, mfaType};
|
const values = {...formValues, passcode};
|
||||||
|
values["mfaType"] = mfaProps.mfaType;
|
||||||
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
|
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
|
||||||
loginFunction(values, authParams).then((res) => {
|
loginFunction(values, authParams).then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
@ -67,24 +68,32 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
|||||||
|
|
||||||
if (mfaType !== RecoveryMfaType) {
|
if (mfaType !== RecoveryMfaType) {
|
||||||
return (
|
return (
|
||||||
<div style={{width: 300, height: 350}}>
|
<div style={{width: 320, height: 350}}>
|
||||||
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
||||||
{i18next.t("mfa:Multi-factor authentication")}
|
{i18next.t("mfa:Multi-factor authentication")}
|
||||||
</div>
|
</div>
|
||||||
<div style={{marginBottom: 24}}>
|
{mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType ? (
|
||||||
{i18next.t("mfa:You have enabled multi-factor authentication, please enter the authentication code")}
|
<Fragment>
|
||||||
</div>
|
<div style={{marginBottom: 24}}>
|
||||||
{mfaType === SmsMfaType || mfaType === EmailMfaType ? (
|
{i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")}
|
||||||
<MfaVerifySmsForm
|
</div>
|
||||||
mfaProps={mfaProps}
|
<MfaVerifySmsForm
|
||||||
method={mfaAuth}
|
mfaProps={mfaProps}
|
||||||
onFinish={verify}
|
method={mfaAuth}
|
||||||
application={application}
|
onFinish={verify}
|
||||||
/>) : (
|
application={application}
|
||||||
<MfaVerifyTotpForm
|
/>
|
||||||
mfaProps={mfaProps}
|
</Fragment>
|
||||||
onFinish={verify}
|
) : (
|
||||||
/>
|
<Fragment>
|
||||||
|
<div style={{marginBottom: 24}}>
|
||||||
|
{i18next.t("mfa:You have enabled Multi-Factor Authentication, please enter the TOTP code")}
|
||||||
|
</div>
|
||||||
|
<MfaVerifyTotpForm
|
||||||
|
mfaProps={mfaProps}
|
||||||
|
onFinish={verify}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
<span style={{float: "right"}}>
|
<span style={{float: "right"}}>
|
||||||
{i18next.t("mfa:Have problems?")}
|
{i18next.t("mfa:Have problems?")}
|
||||||
|
@ -3,13 +3,17 @@ import i18next from "i18next";
|
|||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import * as MfaBackend from "../../backend/MfaBackend";
|
import * as MfaBackend from "../../backend/MfaBackend";
|
||||||
|
|
||||||
export function MfaEnableForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
|
export function MfaEnableForm({user, mfaType, secret, recoveryCodes, dest, countryCode, onSuccess, onFail}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const requestEnableMfa = () => {
|
const requestEnableMfa = () => {
|
||||||
const data = {
|
const data = {
|
||||||
mfaType,
|
mfaType,
|
||||||
|
secret,
|
||||||
|
dest,
|
||||||
|
countryCode,
|
||||||
...user,
|
...user,
|
||||||
};
|
};
|
||||||
|
data["recoveryCodes"] = recoveryCodes[0];
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
MfaBackend.MfaSetupEnable(data).then(res => {
|
MfaBackend.MfaSetupEnable(data).then(res => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
|
@ -26,11 +26,13 @@ export const mfaSetup = "mfaSetup";
|
|||||||
|
|
||||||
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
|
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const onFinish = ({passcode}) => {
|
const onFinish = ({passcode, countryCode, dest}) => {
|
||||||
const data = {passcode, mfaType: mfaProps.mfaType, ...user};
|
const data = {passcode, mfaType: mfaProps.mfaType, secret: mfaProps.secret, dest: dest, countryCode: countryCode, ...user};
|
||||||
MfaBackend.MfaSetupVerify(data)
|
MfaBackend.MfaSetupVerify(data)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
|
res.dest = dest;
|
||||||
|
res.countryCode = countryCode;
|
||||||
onSuccess(res);
|
onSuccess(res);
|
||||||
} else {
|
} else {
|
||||||
onFail(res);
|
onFail(res);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {UserOutlined} from "@ant-design/icons";
|
import {UserOutlined} from "@ant-design/icons";
|
||||||
import {Button, Form, Input} from "antd";
|
import {Button, Form, Input, Space} from "antd";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import React, {useEffect} from "react";
|
import React, {useEffect} from "react";
|
||||||
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
|
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
|
||||||
@ -15,15 +15,18 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (method === mfaAuth) {
|
if (method === mfaAuth) {
|
||||||
setDest(mfaProps.secret);
|
setDest(mfaProps.secret);
|
||||||
|
form.setFieldValue("dest", mfaProps.secret);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mfaProps.mfaType === SmsMfaType) {
|
if (mfaProps.mfaType === SmsMfaType) {
|
||||||
setDest(user.phone);
|
setDest(user.phone);
|
||||||
|
form.setFieldValue("dest", user.phone);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mfaProps.mfaType === EmailMfaType) {
|
if (mfaProps.mfaType === EmailMfaType) {
|
||||||
setDest(user.email);
|
setDest(user.email);
|
||||||
|
form.setFieldValue("dest", user.email);
|
||||||
}
|
}
|
||||||
}, [mfaProps.mfaType]);
|
}, [mfaProps.mfaType]);
|
||||||
|
|
||||||
@ -57,45 +60,44 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
|
|||||||
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
|
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
|
||||||
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
|
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
|
||||||
</div> :
|
</div> :
|
||||||
(<React.Fragment>
|
(
|
||||||
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
|
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
|
||||||
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
|
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
|
||||||
</p>
|
</p>
|
||||||
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
|
|
||||||
{isEmail() ? null :
|
|
||||||
<Form.Item
|
|
||||||
name="countryCode"
|
|
||||||
noStyle
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: false,
|
|
||||||
message: i18next.t("signup:Please select your country code!"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CountryCodeSelect
|
|
||||||
initValue={mfaProps.countryCode}
|
|
||||||
style={{width: "30%"}}
|
|
||||||
countryCodes={application.organizationObj.countryCodes}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
}
|
|
||||||
<Form.Item
|
|
||||||
name="dest"
|
|
||||||
noStyle
|
|
||||||
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
style={{width: isEmail() ? "100% " : "70%"}}
|
|
||||||
onChange={(e) => {setDest(e.target.value);}}
|
|
||||||
prefix={<UserOutlined />}
|
|
||||||
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Input.Group>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
<Space.Compact style={{width: "300Px", marginBottom: "30px", display: isShowText() ? "none" : ""}}>
|
||||||
|
{isEmail() || isShowText() ? null :
|
||||||
|
<Form.Item
|
||||||
|
name="countryCode"
|
||||||
|
noStyle
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
message: i18next.t("signup:Please select your country code!"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CountryCodeSelect
|
||||||
|
initValue={mfaProps.countryCode}
|
||||||
|
style={{width: "30%"}}
|
||||||
|
countryCodes={application.organizationObj.countryCodes}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
<Form.Item
|
||||||
|
name="dest"
|
||||||
|
noStyle
|
||||||
|
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
style={{width: isEmail() ? "100% " : "70%"}}
|
||||||
|
onChange={(e) => {setDest(e.target.value);}}
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Space.Compact>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="passcode"
|
name="passcode"
|
||||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||||
|
@ -32,6 +32,9 @@ export function MfaSetupVerify(values) {
|
|||||||
formData.append("name", values.name);
|
formData.append("name", values.name);
|
||||||
formData.append("mfaType", values.mfaType);
|
formData.append("mfaType", values.mfaType);
|
||||||
formData.append("passcode", values.passcode);
|
formData.append("passcode", values.passcode);
|
||||||
|
formData.append("secret", values.secret);
|
||||||
|
formData.append("dest", values.dest);
|
||||||
|
formData.append("countryCode", values.countryCode);
|
||||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
|
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@ -44,6 +47,10 @@ export function MfaSetupEnable(values) {
|
|||||||
formData.append("mfaType", values.mfaType);
|
formData.append("mfaType", values.mfaType);
|
||||||
formData.append("owner", values.owner);
|
formData.append("owner", values.owner);
|
||||||
formData.append("name", values.name);
|
formData.append("name", values.name);
|
||||||
|
formData.append("secret", values.secret);
|
||||||
|
formData.append("recoveryCodes", values.recoveryCodes);
|
||||||
|
formData.append("dest", values.dest);
|
||||||
|
formData.append("countryCode", values.countryCode);
|
||||||
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
|
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
@ -135,6 +135,15 @@ const Dashboard = (props) => {
|
|||||||
i18next.t("general:Applications"),
|
i18next.t("general:Applications"),
|
||||||
i18next.t("general:Organizations"),
|
i18next.t("general:Organizations"),
|
||||||
i18next.t("general:Subscriptions"),
|
i18next.t("general:Subscriptions"),
|
||||||
|
i18next.t("general:Roles"),
|
||||||
|
i18next.t("general:Groups"),
|
||||||
|
i18next.t("general:Resources"),
|
||||||
|
i18next.t("general:Certs"),
|
||||||
|
i18next.t("general:Permissions"),
|
||||||
|
i18next.t("general:Transactions"),
|
||||||
|
i18next.t("general:Models"),
|
||||||
|
i18next.t("general:Adapters"),
|
||||||
|
i18next.t("general:Enforcers"),
|
||||||
], top: "10%"},
|
], top: "10%"},
|
||||||
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
|
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
|
||||||
xAxis: {type: "category", boundaryGap: false, data: dateArray},
|
xAxis: {type: "category", boundaryGap: false, data: dateArray},
|
||||||
@ -145,6 +154,15 @@ const Dashboard = (props) => {
|
|||||||
{name: i18next.t("general:Providers"), type: "line", data: dashboardData.providerCounts},
|
{name: i18next.t("general:Providers"), type: "line", data: dashboardData.providerCounts},
|
||||||
{name: i18next.t("general:Applications"), type: "line", data: dashboardData.applicationCounts},
|
{name: i18next.t("general:Applications"), type: "line", data: dashboardData.applicationCounts},
|
||||||
{name: i18next.t("general:Subscriptions"), type: "line", data: dashboardData.subscriptionCounts},
|
{name: i18next.t("general:Subscriptions"), type: "line", data: dashboardData.subscriptionCounts},
|
||||||
|
{name: i18next.t("general:Roles"), type: "line", data: dashboardData.roleCounts},
|
||||||
|
{name: i18next.t("general:Groups"), type: "line", data: dashboardData.groupCounts},
|
||||||
|
{name: i18next.t("general:Resources"), type: "line", data: dashboardData.resourceCounts},
|
||||||
|
{name: i18next.t("general:Certs"), type: "line", data: dashboardData.certCounts},
|
||||||
|
{name: i18next.t("general:Permissions"), type: "line", data: dashboardData.permissionCounts},
|
||||||
|
{name: i18next.t("general:Transactions"), type: "line", data: dashboardData.transactionCounts},
|
||||||
|
{name: i18next.t("general:Models"), type: "line", data: dashboardData.modelCounts},
|
||||||
|
{name: i18next.t("general:Adapters"), type: "line", data: dashboardData.adapterCounts},
|
||||||
|
{name: i18next.t("general:Enforcers"), type: "line", data: dashboardData.enforcerCounts},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
myChart.setOption(option);
|
myChart.setOption(option);
|
||||||
|
113
web/src/common/CasdoorAppConnector.js
Normal file
113
web/src/common/CasdoorAppConnector.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {Alert, Button, QRCode} from "antd";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import * as Setting from "../Setting";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
export const generateCasdoorAppUrl = (accessToken, forQrCode = true) => {
|
||||||
|
let qrUrl = "";
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
error = i18next.t("general:Access token is empty");
|
||||||
|
return {qrUrl, error};
|
||||||
|
}
|
||||||
|
|
||||||
|
qrUrl = `casdoor-app://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
|
||||||
|
|
||||||
|
if (forQrCode && qrUrl.length >= 2000) {
|
||||||
|
qrUrl = "";
|
||||||
|
error = i18next.t("general:QR code is too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {qrUrl, error};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CasdoorAppQrCode = ({accessToken, icon}) => {
|
||||||
|
const {qrUrl, error} = generateCasdoorAppUrl(accessToken, true);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Alert message={error} type="error" showIcon />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QRCode
|
||||||
|
value={qrUrl}
|
||||||
|
icon={icon}
|
||||||
|
errorLevel="M"
|
||||||
|
size={230}
|
||||||
|
bordered={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CasdoorAppUrl = ({accessToken}) => {
|
||||||
|
const {qrUrl, error} = generateCasdoorAppUrl(accessToken, false);
|
||||||
|
|
||||||
|
const handleCopyUrl = async() => {
|
||||||
|
if (!window.isSecureContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(qrUrl);
|
||||||
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Alert message={error} type="error" showIcon />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}}>
|
||||||
|
{window.isSecureContext && (
|
||||||
|
<Button size="small" type="primary" onClick={handleCopyUrl} style={{marginLeft: "10px"}}>
|
||||||
|
{i18next.t("resource:Copy Link")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "10px",
|
||||||
|
maxWidth: "400px",
|
||||||
|
maxHeight: "100px",
|
||||||
|
overflow: "auto",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
cursor: "pointer",
|
||||||
|
userSelect: "all",
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(e.target);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{qrUrl}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -105,7 +105,7 @@ export const PasswordModal = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasOldPassword = user.password !== "";
|
const hasOldPassword = (user.password !== "" || user.ldap !== "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user