mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-08 00:50:28 +08:00
Compare commits
151 Commits
v1.719.0
...
revert-361
Author | SHA1 | Date | |
---|---|---|---|
a79a2e7e55 | |||
41d9422687 | |||
3297db688b | |||
cc82d292f0 | |||
f2e3037bc5 | |||
d986a4a9e0 | |||
2df3878c15 | |||
24ab8880cc | |||
f26b4853c5 | |||
d78e8e9776 | |||
d61f9a1856 | |||
aa52af02b3 | |||
2a5722e45b | |||
26718bc4a1 | |||
f8d44e2dca | |||
26eea501be | |||
63b8e857bc | |||
81b336b37a | |||
9c39179849 | |||
37d93a5eea | |||
e926a07c58 | |||
9c46344e68 | |||
c0ec73dfd3 | |||
b1b6ebe692 | |||
a0931e4597 | |||
c181006661 | |||
2e83e49492 | |||
5661942175 | |||
7f9f7c6468 | |||
b7a818e2d3 | |||
1a8cfe4ee6 | |||
b3526de675 | |||
3b9e08b70d | |||
cfc6015aca | |||
1600a6799a | |||
ca60cc3a33 | |||
df295717f0 | |||
e3001671a2 | |||
bbe2162e27 | |||
92b5ce3722 | |||
bad21fb6bb | |||
5a78dcf06d | |||
558b168477 | |||
802b6812a9 | |||
a5a627f92e | |||
9701818a6e | |||
06986fbd41 | |||
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 | |||
3211bcc777 | |||
9f4430ed04 | |||
05830b9ff6 | |||
347b25676f | |||
2417ff84e6 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -114,12 +114,12 @@ jobs:
|
||||
wait-on-timeout: 210
|
||||
working-directory: ./web
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: ./web/cypress/screenshots
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: cypress-videos
|
||||
@ -147,7 +147,7 @@ jobs:
|
||||
- name: Release
|
||||
run: yarn global add semantic-release@17.4.4 && semantic-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Fetch Current version
|
||||
id: get-current-tag
|
||||
|
@ -98,6 +98,8 @@ p, *, *, GET, /api/get-organization-names, *, *
|
||||
p, *, *, GET, /api/get-all-objects, *, *
|
||||
p, *, *, GET, /api/get-all-actions, *, *
|
||||
p, *, *, GET, /api/get-all-roles, *, *
|
||||
p, *, *, GET, /api/run-casbin-command, *, *
|
||||
p, *, *, POST, /api/refresh-engines, *, *
|
||||
p, *, *, GET, /api/get-invitation-info, *, *
|
||||
p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||
`
|
||||
@ -155,7 +157,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
|
||||
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||
if method == "POST" {
|
||||
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" {
|
||||
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" {
|
||||
return true
|
||||
} else if urlPath == "/api/update-user" {
|
||||
// Allow ordinary users to update their own information
|
||||
|
@ -25,9 +25,13 @@ enableErrorMask = false
|
||||
enableGzip = true
|
||||
inactiveTimeoutMinutes =
|
||||
ldapServerPort = 389
|
||||
ldapsCertId = ""
|
||||
ldapsServerPort = 636
|
||||
radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
frontendBaseDir = "../casdoor"
|
||||
frontendBaseDir = "../cc_0"
|
@ -116,6 +116,13 @@ func (c *ApiController) Signup() {
|
||||
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())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
@ -238,6 +245,10 @@ func (c *ApiController) Signup() {
|
||||
}
|
||||
}
|
||||
|
||||
if invitation != nil && invitation.SignupGroup != "" {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
}
|
||||
|
||||
affected, err := object.AddUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@ -447,6 +458,10 @@ func (c *ApiController) GetAccount() {
|
||||
return
|
||||
}
|
||||
|
||||
if organization != nil && len(organization.CountryCodes) == 1 && u != nil && u.CountryCode == "" {
|
||||
u.CountryCode = organization.CountryCodes[0]
|
||||
}
|
||||
|
||||
accessToken := c.GetSessionToken()
|
||||
if accessToken == "" {
|
||||
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
@ -229,6 +232,11 @@ func (c *ApiController) UpdateApplication() {
|
||||
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.ServeJSON()
|
||||
}
|
||||
@ -259,6 +267,11 @@ func (c *ApiController) AddApplication() {
|
||||
return
|
||||
}
|
||||
|
||||
if err = object.CheckIpWhitelist(application.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddApplication(&application))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -53,8 +54,20 @@ func tokenToResponse(token *object.Token) *Response {
|
||||
|
||||
// HandleLoggedIn ...
|
||||
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
||||
if user.IsForbidden {
|
||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@ -256,6 +269,9 @@ func (c *ApiController) GetApplicationLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
object.CheckEntryIp(clientIp, nil, application, nil, c.GetAcceptLanguage())
|
||||
|
||||
application = object.GetMaskedApplication(application, "")
|
||||
if msg != "" {
|
||||
c.ResponseError(msg, application)
|
||||
@ -295,6 +311,35 @@ func isProxyProviderType(providerType string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
|
||||
if object.IsNeedPromptMfa(organization, user) {
|
||||
// The prompt page needs the user to be srigned in
|
||||
c.SetSessionUsername(user.GetId())
|
||||
c.ResponseOk(object.RequiredMfa)
|
||||
return true
|
||||
}
|
||||
|
||||
if user.IsMfaEnabled() {
|
||||
c.setMfaUserSession(user.GetId())
|
||||
mfaList := object.GetAllMfaProps(user, true)
|
||||
mfaAllowList := []*object.MfaProps{}
|
||||
for _, prop := range mfaList {
|
||||
if prop.MfaType == verificationType || !prop.Enabled {
|
||||
continue
|
||||
}
|
||||
mfaAllowList = append(mfaAllowList, prop)
|
||||
}
|
||||
if len(mfaAllowList) >= 1 {
|
||||
c.SetSession("verificationCodeType", verificationType)
|
||||
c.Ctx.Input.CruSession.SessionRelease(c.Ctx.ResponseWriter)
|
||||
c.ResponseOk(object.NextMfa, mfaAllowList)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Login ...
|
||||
// @Title Login
|
||||
// @Tag Login API
|
||||
@ -320,6 +365,8 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
verificationType := ""
|
||||
|
||||
if authForm.Username != "" {
|
||||
if authForm.Type == ResponseTypeLogin {
|
||||
if c.GetSessionUsername() != "" {
|
||||
@ -414,6 +461,12 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if verificationCodeType == object.VerifyTypePhone {
|
||||
verificationType = "sms"
|
||||
} else {
|
||||
verificationType = "email"
|
||||
}
|
||||
} else {
|
||||
var application *object.Application
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||
@ -504,16 +557,7 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
|
||||
if object.IsNeedPromptMfa(organization, user) {
|
||||
// The prompt page needs the user to be signed in
|
||||
c.SetSessionUsername(user.GetId())
|
||||
c.ResponseOk(object.RequiredMfa)
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsMfaEnabled() {
|
||||
c.setMfaUserSession(user.GetId())
|
||||
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
|
||||
if checkMfaEnable(c, user, organization, verificationType) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -607,6 +651,17 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:Failed to login in: %s"), err.Error()))
|
||||
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" {
|
||||
@ -628,16 +683,17 @@ func (c *ApiController) Login() {
|
||||
|
||||
if user != nil && !user.IsDeleted {
|
||||
// Sign in via OAuth (want to sign up but already have account)
|
||||
|
||||
if user.IsForbidden {
|
||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||
}
|
||||
// sync info from 3rd-party if possible
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if checkMfaEnable(c, user, organization, verificationType) {
|
||||
return
|
||||
}
|
||||
|
||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||
|
||||
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
||||
@ -844,17 +900,32 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
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 {
|
||||
c.ResponseError("Invalid multi-factor authentication type")
|
||||
return
|
||||
}
|
||||
|
||||
err = mfaUtil.Verify(authForm.Passcode)
|
||||
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Passcode)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !passed {
|
||||
err = mfaUtil.Verify(authForm.Passcode)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.SetSession("verificationCodeType", "")
|
||||
} else if authForm.RecoveryCode != "" {
|
||||
err = object.MfaRecover(user, authForm.RecoveryCode)
|
||||
if err != nil {
|
||||
@ -867,7 +938,11 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
var application *object.Application
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||
if authForm.ClientId == "" {
|
||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||
} else {
|
||||
application, err = object.GetApplicationByClientId(authForm.ClientId)
|
||||
}
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -897,6 +972,10 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if authForm.Provider == "" {
|
||||
authForm.Provider = authForm.ProviderBack
|
||||
}
|
||||
|
||||
user := c.getCurrentUser()
|
||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||
|
||||
|
247
controllers/casbin_cli_api.go
Normal file
247
controllers/casbin_cli_api.go
Normal file
@ -0,0 +1,247 @@
|
||||
// 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 (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CLIVersionInfo struct {
|
||||
Version string
|
||||
BinaryPath string
|
||||
BinaryTime time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
cliVersionCache = make(map[string]*CLIVersionInfo)
|
||||
cliVersionMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// getCLIVersion
|
||||
// @Title getCLIVersion
|
||||
// @Description Get CLI version with cache mechanism
|
||||
// @Param language string The language of CLI (go/java/rust etc.)
|
||||
// @Return string The version string of CLI
|
||||
// @Return error Error if CLI execution fails
|
||||
func getCLIVersion(language string) (string, error) {
|
||||
binaryName := fmt.Sprintf("casbin-%s-cli", language)
|
||||
|
||||
binaryPath, err := exec.LookPath(binaryName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("executable file not found: %v", err)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(binaryPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get binary info: %v", err)
|
||||
}
|
||||
|
||||
cliVersionMutex.RLock()
|
||||
if info, exists := cliVersionCache[language]; exists {
|
||||
if info.BinaryPath == binaryPath && info.BinaryTime == fileInfo.ModTime() {
|
||||
cliVersionMutex.RUnlock()
|
||||
return info.Version, nil
|
||||
}
|
||||
}
|
||||
cliVersionMutex.RUnlock()
|
||||
|
||||
cmd := exec.Command(binaryName, "--version")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get CLI version: %v", err)
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(output))
|
||||
|
||||
cliVersionMutex.Lock()
|
||||
cliVersionCache[language] = &CLIVersionInfo{
|
||||
Version: version,
|
||||
BinaryPath: binaryPath,
|
||||
BinaryTime: fileInfo.ModTime(),
|
||||
}
|
||||
cliVersionMutex.Unlock()
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
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() {
|
||||
if err := validateIdentifier(c); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if len(args) > 0 && args[0] == "--version" {
|
||||
version, err := getCLIVersion(language)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(version)
|
||||
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)
|
||||
}
|
||||
|
||||
// validateIdentifier
|
||||
// @Title validateIdentifier
|
||||
// @Description Validate the request hash and timestamp
|
||||
// @Param hash string The SHA-256 hash string
|
||||
// @Return error Returns error if validation fails, nil if successful
|
||||
func validateIdentifier(c *ApiController) error {
|
||||
language := c.Input().Get("language")
|
||||
args := c.Input().Get("args")
|
||||
hash := c.Input().Get("m")
|
||||
timestamp := c.Input().Get("t")
|
||||
|
||||
if hash == "" || timestamp == "" || language == "" || args == "" {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
|
||||
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
timeDiff := time.Since(requestTime)
|
||||
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"language": language,
|
||||
"args": args,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var paramParts []string
|
||||
for _, k := range keys {
|
||||
paramParts = append(paramParts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||
}
|
||||
paramString := strings.Join(paramParts, "&")
|
||||
|
||||
version := "casbin-editor-v1"
|
||||
rawString := fmt.Sprintf("%s|%s|%s", version, timestamp, paramString)
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(rawString))
|
||||
|
||||
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
||||
if calculatedHash != strings.ToLower(hash) {
|
||||
return fmt.Errorf("invalid identifier")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
519
controllers/cli_downloader.go
Normal file
519
controllers/cli_downloader.go
Normal file
@ -0,0 +1,519 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
const (
|
||||
javaCliRepo = "https://api.github.com/repos/jcasbin/casbin-java-cli/releases/latest"
|
||||
goCliRepo = "https://api.github.com/repos/casbin/casbin-go-cli/releases/latest"
|
||||
rustCliRepo = "https://api.github.com/repos/casbin-rs/casbin-rust-cli/releases/latest"
|
||||
downloadFolder = "bin"
|
||||
)
|
||||
|
||||
type ReleaseInfo struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Assets []struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
// @Title getBinaryNames
|
||||
// @Description Get binary names for different platforms and architectures
|
||||
// @Success 200 {map[string]string} map[string]string "Binary names map"
|
||||
func getBinaryNames() map[string]string {
|
||||
const (
|
||||
golang = "go"
|
||||
java = "java"
|
||||
rust = "rust"
|
||||
)
|
||||
|
||||
arch := runtime.GOARCH
|
||||
archMap := map[string]struct{ goArch, rustArch string }{
|
||||
"amd64": {"x86_64", "x86_64"},
|
||||
"arm64": {"arm64", "aarch64"},
|
||||
}
|
||||
|
||||
archNames, ok := archMap[arch]
|
||||
if !ok {
|
||||
archNames = struct{ goArch, rustArch string }{arch, arch}
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return map[string]string{
|
||||
golang: fmt.Sprintf("casbin-go-cli_Windows_%s.zip", archNames.goArch),
|
||||
java: "casbin-java-cli.jar",
|
||||
rust: fmt.Sprintf("casbin-rust-cli-%s-pc-windows-gnu", archNames.rustArch),
|
||||
}
|
||||
case "darwin":
|
||||
return map[string]string{
|
||||
golang: fmt.Sprintf("casbin-go-cli_Darwin_%s.tar.gz", archNames.goArch),
|
||||
java: "casbin-java-cli.jar",
|
||||
rust: fmt.Sprintf("casbin-rust-cli-%s-apple-darwin", archNames.rustArch),
|
||||
}
|
||||
case "linux":
|
||||
return map[string]string{
|
||||
golang: fmt.Sprintf("casbin-go-cli_Linux_%s.tar.gz", archNames.goArch),
|
||||
java: "casbin-java-cli.jar",
|
||||
rust: fmt.Sprintf("casbin-rust-cli-%s-unknown-linux-gnu", archNames.rustArch),
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// @Title getFinalBinaryName
|
||||
// @Description Get final binary name for specific language
|
||||
// @Param lang string true "Language type (go/java/rust)"
|
||||
// @Success 200 {string} string "Final binary name"
|
||||
func getFinalBinaryName(lang string) string {
|
||||
switch lang {
|
||||
case "go":
|
||||
if runtime.GOOS == "windows" {
|
||||
return "casbin-go-cli.exe"
|
||||
}
|
||||
return "casbin-go-cli"
|
||||
case "java":
|
||||
return "casbin-java-cli.jar"
|
||||
case "rust":
|
||||
if runtime.GOOS == "windows" {
|
||||
return "casbin-rust-cli.exe"
|
||||
}
|
||||
return "casbin-rust-cli"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// @Title getLatestCLIURL
|
||||
// @Description Get latest CLI download URL from GitHub
|
||||
// @Param repoURL string true "GitHub repository URL"
|
||||
// @Param language string true "Language type"
|
||||
// @Success 200 {string} string "Download URL and version"
|
||||
func getLatestCLIURL(repoURL string, language string) (string, string, error) {
|
||||
client := proxy.GetHttpClient(repoURL)
|
||||
resp, err := client.Get(repoURL)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to fetch release info: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var release ReleaseInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
binaryNames := getBinaryNames()
|
||||
if binaryNames == nil {
|
||||
return "", "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
binaryName := binaryNames[language]
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == binaryName {
|
||||
return asset.URL, release.TagName, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("no suitable binary found for OS: %s, language: %s", runtime.GOOS, language)
|
||||
}
|
||||
|
||||
// @Title extractGoCliFile
|
||||
// @Description Extract the Go CLI file
|
||||
// @Param filePath string true "The file path"
|
||||
// @Success 200 {string} string "The extracted file path"
|
||||
// @router /extractGoCliFile [post]
|
||||
func extractGoCliFile(filePath string) error {
|
||||
tempDir := filepath.Join(downloadFolder, "temp")
|
||||
if err := os.MkdirAll(tempDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := unzipFile(filePath, tempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := untarFile(filePath, tempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
execName := "casbin-go-cli"
|
||||
if runtime.GOOS == "windows" {
|
||||
execName += ".exe"
|
||||
}
|
||||
|
||||
var execPath string
|
||||
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.Name() == execName {
|
||||
execPath = path
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
finalPath := filepath.Join(downloadFolder, execName)
|
||||
if err := os.Rename(execPath, finalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// @Title unzipFile
|
||||
// @Description Unzip the file
|
||||
// @Param zipPath string true "The zip file path"
|
||||
// @Param destDir string true "The destination directory"
|
||||
// @Success 200 {string} string "The extracted file path"
|
||||
// @router /unzipFile [post]
|
||||
func unzipFile(zipPath, destDir string) error {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
fpath := filepath.Join(destDir, f.Name)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title untarFile
|
||||
// @Description Untar the file
|
||||
// @Param tarPath string true "The tar file path"
|
||||
// @Param destDir string true "The destination directory"
|
||||
// @Success 200 {string} string "The extracted file path"
|
||||
// @router /untarFile [post]
|
||||
func untarFile(tarPath, destDir string) error {
|
||||
file, err := os.Open(tarPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(destDir, header.Name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(outFile, tr); err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
}
|
||||
outFile.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title createJavaCliWrapper
|
||||
// @Description Create the Java CLI wrapper
|
||||
// @Param binPath string true "The binary path"
|
||||
// @Success 200 {string} string "The created file path"
|
||||
// @router /createJavaCliWrapper [post]
|
||||
func createJavaCliWrapper(binPath string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Create a Windows CMD file
|
||||
cmdPath := filepath.Join(binPath, "casbin-java-cli.cmd")
|
||||
cmdContent := fmt.Sprintf(`@echo off
|
||||
java -jar "%s\casbin-java-cli.jar" %%*`, binPath)
|
||||
|
||||
err := os.WriteFile(cmdPath, []byte(cmdContent), 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Create Unix shell script
|
||||
shPath := filepath.Join(binPath, "casbin-java-cli")
|
||||
shContent := fmt.Sprintf(`#!/bin/sh
|
||||
java -jar "%s/casbin-java-cli.jar" "$@"`, binPath)
|
||||
|
||||
err := os.WriteFile(shPath, []byte(shContent), 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title downloadCLI
|
||||
// @Description Download and setup CLI tools
|
||||
// @Success 200 {error} error "Error if any"
|
||||
func downloadCLI() error {
|
||||
pathEnv := os.Getenv("PATH")
|
||||
binPath, err := filepath.Abs(downloadFolder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path to download directory: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(pathEnv, binPath) {
|
||||
newPath := fmt.Sprintf("%s%s%s", binPath, string(os.PathListSeparator), pathEnv)
|
||||
if err := os.Setenv("PATH", newPath); err != nil {
|
||||
return fmt.Errorf("failed to update PATH environment variable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(downloadFolder, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create download directory: %v", err)
|
||||
}
|
||||
|
||||
repos := map[string]string{
|
||||
"java": javaCliRepo,
|
||||
"go": goCliRepo,
|
||||
"rust": rustCliRepo,
|
||||
}
|
||||
|
||||
for lang, repo := range repos {
|
||||
cliURL, version, err := getLatestCLIURL(repo, lang)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to get %s CLI URL: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
|
||||
originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang])
|
||||
fmt.Printf("downloading %s CLI: %s\n", lang, cliURL)
|
||||
|
||||
client := proxy.GetHttpClient(cliURL)
|
||||
resp, err := client.Get(cliURL)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
|
||||
func() {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(originalPath), 0o755); err != nil {
|
||||
fmt.Printf("failed to create directory for %s CLI: %v\n", lang, err)
|
||||
return
|
||||
}
|
||||
|
||||
tmpFile := originalPath + ".tmp"
|
||||
out, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create or write %s CLI: %v\n", lang, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
out.Close()
|
||||
os.Remove(tmpFile)
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(out, resp.Body); err != nil ||
|
||||
out.Close() != nil ||
|
||||
os.Rename(tmpFile, originalPath) != nil {
|
||||
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if lang == "go" {
|
||||
if err := extractGoCliFile(originalPath); err != nil {
|
||||
fmt.Printf("failed to extract Go CLI: %v\n", err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
finalPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
||||
if err := os.Rename(originalPath, finalPath); err != nil {
|
||||
fmt.Printf("failed to rename %s CLI: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
execPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
||||
if err := os.Chmod(execPath, 0o755); err != nil {
|
||||
fmt.Printf("failed to set %s CLI execution permission: %v\n", lang, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("downloaded %s CLI version: %s\n", lang, version)
|
||||
|
||||
if lang == "java" {
|
||||
if err := createJavaCliWrapper(binPath); err != nil {
|
||||
fmt.Printf("failed to create Java CLI wrapper: %v\n", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Title RefreshEngines
|
||||
// @Tag CLI API
|
||||
// @Description Refresh all CLI engines
|
||||
// @Param m query string true "Hash for request validation"
|
||||
// @Param t query string true "Timestamp for request validation"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /refresh-engines [post]
|
||||
func (c *ApiController) RefreshEngines() {
|
||||
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
c.ResponseError("refresh engines is only available in demo mode")
|
||||
return
|
||||
}
|
||||
|
||||
hash := c.Input().Get("m")
|
||||
timestamp := c.Input().Get("t")
|
||||
|
||||
if hash == "" || timestamp == "" {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
||||
if err != nil {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
timeDiff := time.Since(requestTime)
|
||||
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
version := "casbin-editor-v1"
|
||||
rawString := fmt.Sprintf("%s|%s", version, timestamp)
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(rawString))
|
||||
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
||||
|
||||
if calculatedHash != strings.ToLower(hash) {
|
||||
c.ResponseError("invalid identifier")
|
||||
return
|
||||
}
|
||||
|
||||
err = downloadCLI()
|
||||
if err != nil {
|
||||
c.ResponseError(fmt.Sprintf("failed to refresh engines: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(map[string]string{
|
||||
"status": "success",
|
||||
"message": "CLI engines updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// @Title ScheduleCLIUpdater
|
||||
// @Description Start periodic CLI update scheduler
|
||||
func ScheduleCLIUpdater() {
|
||||
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
err := downloadCLI()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to update CLI: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("CLI updated successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Title DownloadCLI
|
||||
// @Description Download the CLI
|
||||
// @Success 200 {string} string "The downloaded file path"
|
||||
// @router /downloadCLI [post]
|
||||
func DownloadCLI() error {
|
||||
return downloadCLI()
|
||||
}
|
||||
|
||||
// @Title InitCLIDownloader
|
||||
// @Description Initialize CLI downloader and start update scheduler
|
||||
func InitCLIDownloader() {
|
||||
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||
return
|
||||
}
|
||||
|
||||
util.SafeGoroutine(func() {
|
||||
err := DownloadCLI()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
|
||||
}
|
||||
|
||||
ScheduleCLIUpdater()
|
||||
})
|
||||
}
|
@ -70,15 +70,33 @@ func (c *ApiController) GetGroups() {
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else {
|
||||
err = object.ExtendGroupsWithUsers(groups)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
groupsHaveChildrenMap, err := object.GetGroupsHaveChildrenMap(groups)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
_, ok := groupsHaveChildrenMap[group.Name]
|
||||
if ok {
|
||||
group.HaveChildren = true
|
||||
}
|
||||
|
||||
c.ResponseOk(groups, paginator.Nums())
|
||||
parent, ok := groupsHaveChildrenMap[group.ParentId]
|
||||
if ok {
|
||||
group.ParentName = parent.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
err = object.ExtendGroupsWithUsers(groups)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(groups, paginator.Nums())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,13 +22,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
MfaRecoveryCodesSession = "mfa_recovery_codes"
|
||||
MfaCountryCodeSession = "mfa_country_code"
|
||||
MfaDestSession = "mfa_dest"
|
||||
MfaTotpSecretSession = "mfa_totp_secret"
|
||||
)
|
||||
|
||||
// MfaSetupInitiate
|
||||
// @Title MfaSetupInitiate
|
||||
// @Tag MFA API
|
||||
@ -72,11 +65,6 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
}
|
||||
|
||||
recoveryCode := uuid.NewString()
|
||||
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
|
||||
if mfaType == object.TotpType {
|
||||
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
|
||||
}
|
||||
|
||||
mfaProps.RecoveryCodes = []string{recoveryCode}
|
||||
|
||||
resp := mfaProps
|
||||
@ -94,6 +82,9 @@ func (c *ApiController) MfaSetupInitiate() {
|
||||
func (c *ApiController) MfaSetupVerify() {
|
||||
mfaType := c.Ctx.Request.Form.Get("mfaType")
|
||||
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 == "" {
|
||||
c.ResponseError("missing auth type or passcode")
|
||||
@ -104,32 +95,28 @@ func (c *ApiController) MfaSetupVerify() {
|
||||
MfaType: mfaType,
|
||||
}
|
||||
if mfaType == object.TotpType {
|
||||
secret := c.GetSession(MfaTotpSecretSession)
|
||||
if secret == nil {
|
||||
if secret == "" {
|
||||
c.ResponseError("totp secret is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = secret.(string)
|
||||
config.Secret = secret
|
||||
} else if mfaType == object.SmsType {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest.(string)
|
||||
countryCode := c.GetSession(MfaCountryCodeSession)
|
||||
if countryCode == nil {
|
||||
config.Secret = dest
|
||||
if countryCode == "" {
|
||||
c.ResponseError("country code is missing")
|
||||
return
|
||||
}
|
||||
config.CountryCode = countryCode.(string)
|
||||
config.CountryCode = countryCode
|
||||
} else if mfaType == object.EmailType {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = dest.(string)
|
||||
config.Secret = dest
|
||||
}
|
||||
|
||||
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||
@ -159,6 +146,10 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
owner := c.Ctx.Request.Form.Get("owner")
|
||||
name := c.Ctx.Request.Form.Get("name")
|
||||
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))
|
||||
if err != nil {
|
||||
@ -176,43 +167,39 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
}
|
||||
|
||||
if mfaType == object.TotpType {
|
||||
secret := c.GetSession(MfaTotpSecretSession)
|
||||
if secret == nil {
|
||||
if secret == "" {
|
||||
c.ResponseError("totp secret is missing")
|
||||
return
|
||||
}
|
||||
config.Secret = secret.(string)
|
||||
config.Secret = secret
|
||||
} else if mfaType == object.EmailType {
|
||||
if user.Email == "" {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
user.Email = dest.(string)
|
||||
user.Email = dest
|
||||
}
|
||||
} else if mfaType == object.SmsType {
|
||||
if user.Phone == "" {
|
||||
dest := c.GetSession(MfaDestSession)
|
||||
if dest == nil {
|
||||
if dest == "" {
|
||||
c.ResponseError("destination is missing")
|
||||
return
|
||||
}
|
||||
user.Phone = dest.(string)
|
||||
countryCode := c.GetSession(MfaCountryCodeSession)
|
||||
if countryCode == nil {
|
||||
user.Phone = dest
|
||||
if countryCode == "" {
|
||||
c.ResponseError("country code is missing")
|
||||
return
|
||||
}
|
||||
user.CountryCode = countryCode.(string)
|
||||
user.CountryCode = countryCode
|
||||
}
|
||||
}
|
||||
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
|
||||
if recoveryCodes == nil {
|
||||
|
||||
if recoveryCodes == "" {
|
||||
c.ResponseError("recovery codes is missing")
|
||||
return
|
||||
}
|
||||
config.RecoveryCodes = []string{recoveryCodes.(string)}
|
||||
config.RecoveryCodes = []string{recoveryCodes}
|
||||
|
||||
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||
if mfaUtil == nil {
|
||||
@ -226,14 +213,6 @@ func (c *ApiController) MfaSetupEnable() {
|
||||
return
|
||||
}
|
||||
|
||||
c.DelSession(MfaRecoveryCodesSession)
|
||||
if mfaType == object.TotpType {
|
||||
c.DelSession(MfaTotpSecretSession)
|
||||
} else {
|
||||
c.DelSession(MfaCountryCodeSession)
|
||||
c.DelSession(MfaDestSession)
|
||||
}
|
||||
|
||||
c.ResponseOk(http.StatusText(http.StatusOK))
|
||||
}
|
||||
|
||||
|
@ -119,7 +119,14 @@ func (c *ApiController) UpdateOrganization() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
||||
if err = object.CheckIpWhitelist(organization.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
isGlobalAdmin, _ := c.isGlobalAdmin()
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@ -149,6 +156,11 @@ func (c *ApiController) AddOrganization() {
|
||||
return
|
||||
}
|
||||
|
||||
if err = object.CheckIpWhitelist(organization.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddOrganization(&organization))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -182,6 +182,10 @@ func (c *ApiController) BuyProduct() {
|
||||
paidUserName := c.Input().Get("userName")
|
||||
owner, _ := util.GetOwnerAndNameFromId(id)
|
||||
userId := util.GetId(owner, paidUserName)
|
||||
if paidUserName != "" && !c.IsAdmin() {
|
||||
c.ResponseError(c.T("general:Only admin user can specify user"))
|
||||
return
|
||||
}
|
||||
if paidUserName == "" {
|
||||
userId = c.GetSessionUsername()
|
||||
}
|
||||
|
@ -21,6 +21,11 @@ import (
|
||||
)
|
||||
|
||||
func (c *RootController) HandleScim() {
|
||||
_, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
path := c.Ctx.Request.URL.Path
|
||||
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
||||
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
|
||||
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
||||
err = object.DailSmtpServer(provider)
|
||||
err = object.TestSmtpServer(provider)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
@ -322,17 +322,22 @@ func (c *ApiController) IntrospectToken() {
|
||||
}
|
||||
|
||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||
token, err := object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
var token *object.Token
|
||||
if tokenTypeHint != "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var introspectionResponse object.IntrospectionResponse
|
||||
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
@ -344,12 +349,37 @@ func (c *ApiController) IntrospectToken() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = &object.IntrospectionResponse{
|
||||
introspectionResponse = object.IntrospectionResponse{
|
||||
Active: true,
|
||||
Scope: jwtToken.Scope,
|
||||
ClientId: clientId,
|
||||
Username: token.User,
|
||||
TokenType: token.TokenType,
|
||||
Username: jwtToken.Name,
|
||||
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(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
@ -358,33 +388,22 @@ func (c *ApiController) IntrospectToken() {
|
||||
Iss: jwtToken.Issuer,
|
||||
Jti: jwtToken.ID,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
if tokenTypeHint == "" {
|
||||
token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
|
||||
if err != nil {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
introspectionResponse.TokenType = token.TokenType
|
||||
|
||||
c.Data["json"] = &object.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.Data["json"] = introspectionResponse
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
@ -353,18 +353,13 @@ func (c *ApiController) AddUser() {
|
||||
return
|
||||
}
|
||||
|
||||
count, err := object.GetUserCount("", "", "", "")
|
||||
if err != nil {
|
||||
if err := checkQuotaForUser(); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkQuotaForUser(int(count)); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
msg := object.CheckUsername(user.Name, c.GetAcceptLanguage())
|
||||
emptyUser := object.User{}
|
||||
msg := object.CheckUpdateUser(&emptyUser, &user, c.GetAcceptLanguage())
|
||||
if msg != "" {
|
||||
c.ResponseError(msg)
|
||||
return
|
||||
@ -474,6 +469,16 @@ func (c *ApiController) SetPassword() {
|
||||
|
||||
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()
|
||||
if requestUserId == "" && code == "" {
|
||||
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"))
|
||||
return
|
||||
}
|
||||
if userId != c.GetSession("verifiedUserId") {
|
||||
c.ResponseError(c.T("general:Wrong userId"))
|
||||
return
|
||||
}
|
||||
c.SetSession("verifiedCode", "")
|
||||
c.SetSession("verifiedUserId", "")
|
||||
}
|
||||
|
||||
targetUser, err := object.GetUser(userId)
|
||||
@ -512,7 +522,11 @@ func (c *ApiController) SetPassword() {
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -535,11 +549,38 @@ func (c *ApiController) SetPassword() {
|
||||
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.UpdateUserPassword(organization)
|
||||
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 {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
@ -294,12 +294,18 @@ func checkQuotaForProvider(count int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkQuotaForUser(count int) error {
|
||||
func checkQuotaForUser() error {
|
||||
quota := conf.GetConfigQuota().User
|
||||
if quota == -1 {
|
||||
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 nil
|
||||
|
@ -132,7 +132,8 @@ func (c *ApiController) SendVerificationCode() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
|
||||
if msg := vform.CheckParameter(form.SendVerifyCode, c.GetAcceptLanguage()); msg != "" {
|
||||
c.ResponseError(msg)
|
||||
@ -245,8 +246,6 @@ func (c *ApiController) SendVerificationCode() {
|
||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
} else if vform.Method == MfaSetupVerification {
|
||||
c.SetSession(MfaDestSession, vform.Dest)
|
||||
}
|
||||
|
||||
provider, err = application.GetEmailProvider(vform.Method)
|
||||
@ -259,7 +258,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
return
|
||||
}
|
||||
|
||||
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, vform.Dest)
|
||||
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, clientIp, vform.Dest)
|
||||
case object.VerifyTypePhone:
|
||||
if vform.Method == LoginVerification || vform.Method == ForgetVerification {
|
||||
if user != nil && util.GetMaskedPhone(user.Phone) == vform.Dest {
|
||||
@ -281,11 +280,6 @@ func (c *ApiController) SendVerificationCode() {
|
||||
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 {
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||
@ -293,6 +287,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
|
||||
vform.CountryCode = mfaProps.CountryCode
|
||||
vform.CountryCode = user.GetCountryCode(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))
|
||||
return
|
||||
} else {
|
||||
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, remoteAddr, phone)
|
||||
sendResp = object.SendVerificationCodeToPhone(organization, user, provider, clientIp, phone)
|
||||
}
|
||||
}
|
||||
|
||||
@ -515,22 +510,31 @@ func (c *ApiController) VerifyCode() {
|
||||
}
|
||||
}
|
||||
|
||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Code)
|
||||
if err != nil {
|
||||
c.ResponseError(c.T(err.Error()))
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
err = object.DisableVerificationCode(checkDest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
if !passed {
|
||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if result.Code != object.VerificationSuccess {
|
||||
c.ResponseError(result.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
err = object.DisableVerificationCode(checkDest)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.SetSession("verifiedCode", authForm.Code)
|
||||
c.SetSession("verifiedUserId", user.GetId())
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
36
controllers/verification_util.go
Normal file
36
controllers/verification_util.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 controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
|
||||
func (c *ApiController) checkOrgMasterVerificationCode(user *object.User, code string) (bool, error) {
|
||||
organization, err := object.GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if organization == nil {
|
||||
return false, fmt.Errorf("The organization: %s does not exist", user.Owner)
|
||||
}
|
||||
|
||||
if organization.MasterVerificationCode != "" && organization.MasterVerificationCode == code {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
@ -24,7 +24,7 @@ func GetEmailProvider(typ string, clientId string, clientSecret string, host str
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
return NewHttpEmailProvider(endpoint, method)
|
||||
} else if typ == "SendGrid" {
|
||||
return NewSendgridEmailProvider(clientSecret)
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
|
||||
}
|
||||
|
@ -17,14 +17,16 @@ package email
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"net/http"
|
||||
|
||||
"github.com/sendgrid/sendgrid-go"
|
||||
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
||||
)
|
||||
|
||||
type SendgridEmailProvider struct {
|
||||
ApiKey string
|
||||
ApiKey string
|
||||
Host string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
type SendgridResponseBody struct {
|
||||
@ -35,23 +37,25 @@ type SendgridResponseBody struct {
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
|
||||
return &SendgridEmailProvider{ApiKey: apiKey}
|
||||
func NewSendgridEmailProvider(apiKey string, host string, endpoint string) *SendgridEmailProvider {
|
||||
return &SendgridEmailProvider{ApiKey: apiKey, Host: host, Endpoint: endpoint}
|
||||
}
|
||||
|
||||
func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress string, subject string, content string) error {
|
||||
func (s *SendgridEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||
client := s.initSendgridClient()
|
||||
|
||||
from := mail.NewEmail(fromName, fromAddress)
|
||||
to := mail.NewEmail("", toAddress)
|
||||
message := mail.NewSingleEmail(from, subject, to, "", content)
|
||||
client := sendgrid.NewSendClient(s.ApiKey)
|
||||
response, err := client.Send(message)
|
||||
|
||||
resp, err := client.Send(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.StatusCode >= 300 {
|
||||
if resp.StatusCode >= 300 {
|
||||
var responseBody SendgridResponseBody
|
||||
err = json.Unmarshal([]byte(response.Body), &responseBody)
|
||||
err = json.Unmarshal([]byte(resp.Body), &responseBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -61,8 +65,23 @@ func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress str
|
||||
messages = append(messages, sendgridError.Message)
|
||||
}
|
||||
|
||||
return fmt.Errorf("SendGrid status code: %d, error message: %s", response.StatusCode, strings.Join(messages, " | "))
|
||||
return fmt.Errorf("status code: %d, error message: %s", resp.StatusCode, messages)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SendgridEmailProvider) initSendgridClient() *sendgrid.Client {
|
||||
if s.Host == "" || s.Endpoint == "" {
|
||||
return sendgrid.NewSendClient(s.ApiKey)
|
||||
}
|
||||
|
||||
request := sendgrid.GetRequest(s.ApiKey, s.Endpoint, s.Host)
|
||||
request.Method = "POST"
|
||||
|
||||
return &sendgrid.Client{Request: request}
|
||||
}
|
||||
|
@ -16,7 +16,9 @@ package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/gomail/v2"
|
||||
)
|
||||
|
||||
@ -33,6 +35,13 @@ func NewSmtpEmailProvider(userName string, password string, host string, port in
|
||||
|
||||
dialer.SSL = !disableSsl
|
||||
|
||||
if strings.HasSuffix(host, ".amazonaws.com") {
|
||||
socks5Proxy := conf.GetConfigString("socks5Proxy")
|
||||
if socks5Proxy != "" {
|
||||
dialer.SetSocks5Proxy(socks5Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return &SmtpEmailProvider{Dialer: dialer}
|
||||
}
|
||||
|
||||
|
15
form/auth.go
15
form/auth.go
@ -37,13 +37,14 @@ type AuthForm struct {
|
||||
Region string `json:"region"`
|
||||
InvitationCode string `json:"invitationCode"`
|
||||
|
||||
Application string `json:"application"`
|
||||
ClientId string `json:"clientId"`
|
||||
Provider string `json:"provider"`
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
Method string `json:"method"`
|
||||
Application string `json:"application"`
|
||||
ClientId string `json:"clientId"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderBack string `json:"providerBack"`
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
Method string `json:"method"`
|
||||
|
||||
EmailCode string `json:"emailCode"`
|
||||
PhoneCode string `json:"phoneCode"`
|
||||
|
184
go.mod
184
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/casdoor/casdoor
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
@ -9,18 +9,17 @@ require (
|
||||
github.com/beego/beego v1.12.12
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.77.2
|
||||
github.com/casdoor/go-sms-sender v0.24.0
|
||||
github.com/casdoor/gomail/v2 v2.0.1
|
||||
github.com/casdoor/notify v0.45.0
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.1.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
github.com/casdoor/notify v1.0.0
|
||||
github.com/casdoor/oss v1.8.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/denisenkom/go-mssqldb v0.9.0
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/forestmgy/ldapserver v1.1.0
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
@ -46,7 +45,6 @@ require (
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
@ -54,19 +52,179 @@ require (
|
||||
github.com/stripe/stripe-go/v74 v74.29.0
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
github.com/thanhpk/randstr v1.0.4
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/xorm-io/builder v0.3.13
|
||||
github.com/xorm-io/core v0.7.4
|
||||
github.com/xorm-io/xorm v1.1.6
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/api v0.150.0
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
||||
maunium.net/go/mautrix v0.16.0
|
||||
modernc.org/sqlite v1.18.2
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.8 // indirect
|
||||
cloud.google.com/go/compute v1.23.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.3 // indirect
|
||||
cloud.google.com/go/storage v1.35.1 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 // indirect
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.6.1 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bwmarrin/discordgo v0.27.1 // indirect
|
||||
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||
github.com/casdoor/go-reddit/v2 v2.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||
github.com/dghubble/sling v1.4.0 // indirect
|
||||
github.com/di-wu/parser v0.2.2 // indirect
|
||||
github.com/di-wu/xsd-datetime v1.0.0 // indirect
|
||||
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-lark/lark v1.9.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-webauthn/revoke v0.1.6 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.3.3 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gregdel/pushover v1.2.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
|
||||
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
|
||||
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/common v0.30.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rs/zerolog v1.30.0 // indirect
|
||||
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
github.com/slack-go/slack v0.12.3 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
|
||||
github.com/tidwall/gjson v1.16.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
modernc.org/cc/v3 v3.37.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||
modernc.org/libc v1.18.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.3.0 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
|
||||
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
|
||||
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
|
||||
"Username is too long (maximum is 39 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 39 znaků).",
|
||||
"Username is too long (maximum is 255 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 255 znaků).",
|
||||
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
|
||||
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
|
||||
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
|
||||
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen",
|
||||
"Username is too long (maximum is 39 characters).": "Benutzername ist zu lang (das Maximum beträgt 39 Zeichen).",
|
||||
"Username is too long (maximum is 255 characters).": "Benutzername ist zu lang (das Maximum beträgt 255 Zeichen).",
|
||||
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
|
||||
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
|
||||
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
|
||||
"Username cannot start with a digit": "El nombre de usuario no puede empezar con un dígito",
|
||||
"Username is too long (maximum is 39 characters).": "El nombre de usuario es demasiado largo (el máximo es de 39 caracteres).",
|
||||
"Username is too long (maximum is 255 characters).": "El nombre de usuario es demasiado largo (el máximo es de 255 caracteres).",
|
||||
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
|
||||
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
|
||||
|
@ -1,167 +1,167 @@
|
||||
{
|
||||
"account": {
|
||||
"Failed to add user": "Failed to add user",
|
||||
"Get init score failed, error: %w": "Get init score failed, error: %w",
|
||||
"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"
|
||||
"Failed to add user": "عدم موفقیت در افزودن کاربر",
|
||||
"Get init score failed, error: %w": "عدم موفقیت در دریافت امتیاز اولیه، خطا: %w",
|
||||
"Please sign out first": "لطفاً ابتدا خارج شوید",
|
||||
"The application does not allow to sign up new account": "برنامه اجازه ثبتنام حساب جدید را نمیدهد"
|
||||
},
|
||||
"auth": {
|
||||
"Challenge method should be S256": "Challenge method should be S256",
|
||||
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
|
||||
"Failed to login in: %s": "Failed to login in: %s",
|
||||
"Invalid token": "Invalid token",
|
||||
"State expected: %s, but got: %s": "State expected: %s, but got: %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, 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) 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 application: %s does not exist": "The application: %s does not exist",
|
||||
"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 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 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 provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
|
||||
"Unauthorized operation": "Unauthorized operation",
|
||||
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %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",
|
||||
"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"
|
||||
"Challenge method should be S256": "روش چالش باید S256 باشد",
|
||||
"Failed to create user, user information is invalid: %s": "عدم موفقیت در ایجاد کاربر، اطلاعات کاربر نامعتبر است: %s",
|
||||
"Failed to login in: %s": "عدم موفقیت در ورود: %s",
|
||||
"Invalid token": "توکن نامعتبر",
|
||||
"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": "حساب برای ارائهدهنده: %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": "حساب برای ارائهدهنده: %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 application: %s does not exist": "برنامه: %s وجود ندارد",
|
||||
"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 email 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 organization: %s does not exist": "سازمان: %s وجود ندارد",
|
||||
"The provider: %s is not enabled for the application": "ارائهدهنده: %s برای برنامه فعال نیست",
|
||||
"Unauthorized operation": "عملیات غیرمجاز",
|
||||
"Unknown authentication type (not password or provider), form = %s": "نوع احراز هویت ناشناخته (نه رمز عبور و نه ارائهدهنده)، فرم = %s",
|
||||
"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": "کاربر پرداختی %s اشتراک فعال یا در انتظار ندارد و برنامه: %s قیمتگذاری پیشفرض ندارد"
|
||||
},
|
||||
"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": {
|
||||
"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",
|
||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||
"Email already exists": "Email already exists",
|
||||
"Email cannot be empty": "Email cannot be empty",
|
||||
"Email is invalid": "Email is invalid",
|
||||
"Empty username.": "Empty username.",
|
||||
"Face data does not exist, cannot log in": "Face data does not exist, cannot log in",
|
||||
"Face data mismatch": "Face data mismatch",
|
||||
"FirstName cannot be blank": "FirstName cannot be blank",
|
||||
"Invitation code cannot be blank": "Invitation code cannot be blank",
|
||||
"Invitation code exhausted": "Invitation code exhausted",
|
||||
"Invitation code is invalid": "Invitation code is invalid",
|
||||
"Invitation code suspended": "Invitation code suspended",
|
||||
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
|
||||
"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",
|
||||
"Organization does not exist": "Organization does not exist",
|
||||
"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 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",
|
||||
"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 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 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 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\\\"",
|
||||
"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",
|
||||
"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 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",
|
||||
"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, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
|
||||
"unsupported password type: %s": "unsupported password type: %s"
|
||||
"Affiliation cannot be blank": "وابستگی نمیتواند خالی باشد",
|
||||
"Default code does not match the code's matching rules": "کد پیشفرض با قوانین تطبیق کد مطابقت ندارد",
|
||||
"DisplayName cannot be blank": "نام نمایشی نمیتواند خالی باشد",
|
||||
"DisplayName is not valid real name": "نام نمایشی یک نام واقعی معتبر نیست",
|
||||
"Email already exists": "ایمیل قبلاً وجود دارد",
|
||||
"Email cannot be empty": "ایمیل نمیتواند خالی باشد",
|
||||
"Email is invalid": "ایمیل نامعتبر است",
|
||||
"Empty username.": "نام کاربری خالی است.",
|
||||
"Face data does not exist, cannot log in": "دادههای چهره وجود ندارد، نمیتوان وارد شد",
|
||||
"Face data mismatch": "عدم تطابق دادههای چهره",
|
||||
"FirstName cannot be blank": "نام نمیتواند خالی باشد",
|
||||
"Invitation code cannot be blank": "کد دعوت نمیتواند خالی باشد",
|
||||
"Invitation code exhausted": "کد دعوت استفاده شده است",
|
||||
"Invitation code is invalid": "کد دعوت نامعتبر است",
|
||||
"Invitation code suspended": "کد دعوت معلق است",
|
||||
"LDAP user name or password incorrect": "نام کاربری یا رمز عبور LDAP نادرست است",
|
||||
"LastName cannot be blank": "نام خانوادگی نمیتواند خالی باشد",
|
||||
"Multiple accounts with same uid, please check your ldap server": "چندین حساب با uid یکسان، لطفاً سرور LDAP خود را بررسی کنید",
|
||||
"Organization does not exist": "سازمان وجود ندارد",
|
||||
"Phone already exists": "تلفن قبلاً وجود دارد",
|
||||
"Phone cannot be empty": "تلفن نمیتواند خالی باشد",
|
||||
"Phone number is invalid": "شماره تلفن نامعتبر است",
|
||||
"Please register using the email 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": "لطفاً با استفاده از نام کاربری مربوط به کد دعوت ثبتنام کنید",
|
||||
"Session outdated, please login again": "جلسه منقضی شده است، لطفاً دوباره وارد شوید",
|
||||
"The invitation code has already been used": "کد دعوت قبلاً استفاده شده است",
|
||||
"The user is forbidden to sign in, please contact the administrator": "ورود کاربر ممنوع است، لطفاً با مدیر تماس بگیرید",
|
||||
"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 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\"": "مقدار \"%s\" برای فیلد ثبتنام \"%s\" با عبارت منظم مورد ثبتنام برنامه \"%s\" مطابقت ندارد",
|
||||
"Username already exists": "نام کاربری قبلاً وجود دارد",
|
||||
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
||||
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
||||
"Username cannot start with a digit": "نام کاربری نمیتواند با یک رقم شروع شود",
|
||||
"Username is too long (maximum is 255 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": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کردهاید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
|
||||
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
||||
"password or code is incorrect": "رمز عبور یا کد نادرست است",
|
||||
"password or code is incorrect, you have %d remaining chances": "رمز عبور یا کد نادرست است، شما %d فرصت باقیمانده دارید",
|
||||
"unsupported password type: %s": "نوع رمز عبور پشتیبانی نشده: %s"
|
||||
},
|
||||
"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 user: %s doesn't exist": "The user: %s doesn't exist",
|
||||
"don't support captchaProvider: ": "don't support 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"
|
||||
"Missing parameter": "پارامتر گمشده",
|
||||
"Please login first": "لطفاً ابتدا وارد شوید",
|
||||
"The organization: %s should have one application at least": "سازمان: %s باید حداقل یک برنامه داشته باشد",
|
||||
"The user: %s doesn't exist": "کاربر: %s وجود ندارد",
|
||||
"don't support captchaProvider: ": "از captchaProvider پشتیبانی نمیشود: ",
|
||||
"this operation is not allowed in demo mode": "این عملیات در حالت دمو مجاز نیست",
|
||||
"this operation requires administrator to perform": "این عملیات نیاز به مدیر برای انجام دارد"
|
||||
},
|
||||
"ldap": {
|
||||
"Ldap server exist": "Ldap server exist"
|
||||
"Ldap server exist": "سرور LDAP وجود دارد"
|
||||
},
|
||||
"link": {
|
||||
"Please link first": "Please link first",
|
||||
"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 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 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"
|
||||
"Please link first": "لطفاً ابتدا پیوند دهید",
|
||||
"This application has no providers": "این برنامه ارائهدهندهای ندارد",
|
||||
"This application has no providers of type": "این برنامه ارائهدهندهای از نوع ندارد",
|
||||
"This provider can't be unlinked": "این ارائهدهنده نمیتواند لغو پیوند شود",
|
||||
"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": "شما نمیتوانید خودتان را لغو پیوند کنید، شما عضو هیچ برنامهای نیستید"
|
||||
},
|
||||
"organization": {
|
||||
"Only admin can modify the %s.": "Only admin can modify the %s.",
|
||||
"The %s is immutable.": "The %s is immutable.",
|
||||
"Unknown modify rule %s.": "Unknown modify rule %s."
|
||||
"Only admin can modify the %s.": "فقط مدیر میتواند %s را تغییر دهد.",
|
||||
"The %s is immutable.": "%s غیرقابل تغییر است.",
|
||||
"Unknown modify rule %s.": "قانون تغییر ناشناخته %s."
|
||||
},
|
||||
"permission": {
|
||||
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
|
||||
"The permission: \"%s\" doesn't exist": "مجوز: \"%s\" وجود ندارد"
|
||||
},
|
||||
"provider": {
|
||||
"Invalid application id": "Invalid application id",
|
||||
"the provider: %s does not exist": "the provider: %s does not exist"
|
||||
"Invalid application id": "شناسه برنامه نامعتبر",
|
||||
"the provider: %s does not exist": "ارائهدهنده: %s وجود ندارد"
|
||||
},
|
||||
"resource": {
|
||||
"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"
|
||||
"User is nil for tag: avatar": "کاربر برای برچسب: آواتار تهی است",
|
||||
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "نام کاربری یا مسیر کامل فایل خالی است: نام کاربری = %s، مسیر کامل فایل = %s"
|
||||
},
|
||||
"saml": {
|
||||
"Application %s not found": "Application %s not found"
|
||||
"Application %s not found": "برنامه %s یافت نشد"
|
||||
},
|
||||
"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": {
|
||||
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
|
||||
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
|
||||
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
|
||||
"Empty parameters for emailForm: %v": "پارامترهای خالی برای emailForm: %v",
|
||||
"Invalid Email receivers: %s": "گیرندگان ایمیل نامعتبر: %s",
|
||||
"Invalid phone receivers: %s": "گیرندگان تلفن نامعتبر: %s"
|
||||
},
|
||||
"storage": {
|
||||
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
|
||||
"The provider type: %s is not supported": "The provider type: %s is not supported"
|
||||
"The objectKey: %s is not allowed": "objectKey: %s مجاز نیست",
|
||||
"The provider type: %s is not supported": "نوع ارائهدهنده: %s پشتیبانی نمیشود"
|
||||
},
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
|
||||
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
|
||||
"Invalid client_id": "Invalid 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",
|
||||
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
|
||||
"Grant_type: %s is not supported in this application": "grant_type: %s در این برنامه پشتیبانی نمیشود",
|
||||
"Invalid application or wrong clientSecret": "برنامه نامعتبر یا clientSecret نادرست",
|
||||
"Invalid client_id": "client_id نامعتبر",
|
||||
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "آدرس بازگشت: %s در لیست آدرسهای بازگشت مجاز وجود ندارد",
|
||||
"Token not found, invalid accessToken": "توکن یافت نشد، accessToken نامعتبر"
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Display name cannot be empty",
|
||||
"New password cannot contain blank space.": "New password cannot contain blank space."
|
||||
"Display name cannot be empty": "نام نمایشی نمیتواند خالی باشد",
|
||||
"New password cannot contain blank space.": "رمز عبور جدید نمیتواند حاوی فاصله خالی باشد."
|
||||
},
|
||||
"user_upload": {
|
||||
"Failed to import users": "Failed to import users"
|
||||
"Failed to import users": "عدم موفقیت در وارد کردن کاربران"
|
||||
},
|
||||
"util": {
|
||||
"No application is found for userId: %s": "No application is found for userId: %s",
|
||||
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
|
||||
"The provider: %s is not found": "The provider: %s is not found"
|
||||
"No application is found for userId: %s": "هیچ برنامهای برای userId: %s یافت نشد",
|
||||
"No provider for category: %s is found for application: %s": "هیچ ارائهدهندهای برای دستهبندی: %s برای برنامه: %s یافت نشد",
|
||||
"The provider: %s is not found": "ارائهدهنده: %s یافت نشد"
|
||||
},
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Invalid captcha provider.",
|
||||
"Phone number is invalid in your region %s": "Phone number is invalid in your region %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, 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.",
|
||||
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
|
||||
"Unknown type": "Unknown type",
|
||||
"Wrong verification code!": "Wrong verification code!",
|
||||
"You should verify your code in %d min!": "You should verify your code in %d min!",
|
||||
"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 an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s",
|
||||
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
|
||||
"Invalid captcha provider.": "ارائهدهنده کپچا نامعتبر.",
|
||||
"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, or has already been used!": "کد تأیید هنوز ارسال نشده است، یا قبلاً استفاده شده است!",
|
||||
"Turing test failed.": "تست تورینگ ناموفق بود.",
|
||||
"Unable to get the email modify rule.": "عدم توانایی در دریافت قانون تغییر ایمیل.",
|
||||
"Unable to get the phone modify rule.": "عدم توانایی در دریافت قانون تغییر تلفن.",
|
||||
"Unknown type": "نوع ناشناخته",
|
||||
"Wrong verification code!": "کد تأیید اشتباه!",
|
||||
"You should verify your code in %d min!": "شما باید کد خود را در %d دقیقه تأیید کنید!",
|
||||
"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": "لطفاً یک ارائهدهنده ایمیل به لیست \"ارائهدهندگان\" برای برنامه: %s اضافه کنید",
|
||||
"the user does not exist, please sign up first": "کاربر وجود ندارد، لطفاً ابتدا ثبتنام کنید"
|
||||
},
|
||||
"webauthn": {
|
||||
"Found no credentials for this user": "Found no credentials for this user",
|
||||
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
|
||||
"Found no credentials for this user": "هیچ اعتباری برای این کاربر یافت نشد",
|
||||
"Please call WebAuthnSigninBegin first": "لطفاً ابتدا WebAuthnSigninBegin را فراخوانی کنید"
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
|
||||
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
|
||||
"Username cannot start with a digit": "Nom d'utilisateur ne peut pas commencer par un chiffre",
|
||||
"Username is too long (maximum is 39 characters).": "Nom d'utilisateur est trop long (maximum de 39 caractères).",
|
||||
"Username is too long (maximum is 255 characters).": "Nom d'utilisateur est trop long (maximum de 255 caractères).",
|
||||
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
|
||||
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
|
||||
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
|
||||
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
|
||||
"Username is too long (maximum is 39 characters).": "Nama pengguna terlalu panjang (maksimum 39 karakter).",
|
||||
"Username is too long (maximum is 255 characters).": "Nama pengguna terlalu panjang (maksimum 255 karakter).",
|
||||
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
|
||||
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
|
||||
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
|
||||
"Username cannot start with a digit": "ユーザー名は数字で始めることはできません",
|
||||
"Username is too long (maximum is 39 characters).": "ユーザー名が長すぎます(最大39文字)。",
|
||||
"Username is too long (maximum is 255 characters).": "ユーザー名が長すぎます(最大255文字)。",
|
||||
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
|
||||
"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": "あなたの地域は電話でサインアップすることができません",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
|
||||
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
|
||||
"Username cannot start with a digit": "사용자 이름은 숫자로 시작할 수 없습니다",
|
||||
"Username is too long (maximum is 39 characters).": "사용자 이름이 너무 깁니다 (최대 39자).",
|
||||
"Username is too long (maximum is 255 characters).": "사용자 이름이 너무 깁니다 (최대 255자).",
|
||||
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
|
||||
"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": "당신의 지역은 전화로 가입할 수 없습니다",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"Username cannot start with a digit": "O nome de usuário não pode começar com um dígito",
|
||||
"Username is too long (maximum is 39 characters).": "Nome de usuário é muito longo (máximo é 39 caracteres).",
|
||||
"Username is too long (maximum is 255 characters).": "Nome de usuário é muito longo (máximo é 255 caracteres).",
|
||||
"Username must have at least 2 characters": "Nome de usuário deve ter pelo menos 2 caracteres",
|
||||
"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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -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) is already linked to another account: %s (%s)": "Аккаунт поставщика: %s и имя пользователя: %s (%s) уже связаны с другим аккаунтом: %s (%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 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 face is not enabled for the application": "The login method: login with face 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": "Метод входа: вход с помощью SMS не включен для приложения",
|
||||
"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 password is not enabled for the application": "Метод входа: вход с паролем не включен для приложения",
|
||||
"The organization: %s does not exist": "The organization: %s does not exist",
|
||||
"The provider: %s is not enabled for the application": "Провайдер: %s не включен для приложения",
|
||||
@ -53,21 +53,21 @@
|
||||
"Phone already exists": "Телефон уже существует",
|
||||
"Phone cannot be empty": "Телефон не может быть пустым",
|
||||
"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 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 email 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": "Пожалуйста, зарегистрируйтесь, используя имя пользователя, соответствующее коду приглашения",
|
||||
"Session outdated, please login again": "Сессия устарела, пожалуйста, войдите снова",
|
||||
"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: %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 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 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 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\\\"": "Значение \\\"%s\\\" поля регистрации \\\"%s\\\" не соответствует регулярному выражению приложения \\\"%s\\\"",
|
||||
"Username already exists": "Имя пользователя уже существует",
|
||||
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
||||
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
||||
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
|
||||
"Username is too long (maximum is 39 characters).": "Имя пользователя слишком длинное (максимальная длина - 39 символов).",
|
||||
"Username is too long (maximum is 255 characters).": "Имя пользователя слишком длинное (максимальная длина - 255 символов).",
|
||||
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
|
||||
"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": "Ваш регион не разрешает регистрацию по телефону",
|
||||
@ -78,11 +78,11 @@
|
||||
"general": {
|
||||
"Missing parameter": "Отсутствующий параметр",
|
||||
"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 не существует",
|
||||
"don't support captchaProvider: ": "неподдерживаемый captchaProvider: ",
|
||||
"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 server exist": "LDAP-сервер существует"
|
||||
@ -101,11 +101,11 @@
|
||||
"Unknown modify rule %s.": "Неизвестное изменение правила %s."
|
||||
},
|
||||
"permission": {
|
||||
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
|
||||
"The permission: \\\"%s\\\" doesn't exist": "Разрешение: \\\"%s\\\" не существует"
|
||||
},
|
||||
"provider": {
|
||||
"Invalid application id": "Неверный идентификатор приложения",
|
||||
"the provider: %s does not exist": "провайдер: %s не существует"
|
||||
"the provider: %s does not exist": "Провайдер: %s не существует"
|
||||
},
|
||||
"resource": {
|
||||
"User is nil for tag: avatar": "Пользователь равен нулю для тега: аватар",
|
||||
@ -115,7 +115,7 @@
|
||||
"Application %s not found": "Приложение %s не найдено"
|
||||
},
|
||||
"saml_sp": {
|
||||
"provider %s's category is not SAML": "категория провайдера %s не является SAML"
|
||||
"provider %s's category is not SAML": "Категория провайдера %s не является SAML"
|
||||
},
|
||||
"service": {
|
||||
"Empty parameters for emailForm: %v": "Пустые параметры для emailForm: %v",
|
||||
@ -148,7 +148,7 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Недействительный поставщик CAPTCHA.",
|
||||
"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!",
|
||||
"Turing test failed.": "Тест Тьюринга не удался.",
|
||||
"Unable to get the email modify rule.": "Невозможно получить правило изменения электронной почты.",
|
||||
@ -156,8 +156,8 @@
|
||||
"Unknown type": "Неизвестный тип",
|
||||
"Wrong verification code!": "Неправильный код подтверждения!",
|
||||
"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 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 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": "Пожалуйста, добавьте поставщика электронной почты в список \\\"Провайдеры\\\" для приложения: %s",
|
||||
"the user does not exist, please sign up first": "Пользователь не существует, пожалуйста, сначала зарегистрируйтесь"
|
||||
},
|
||||
"webauthn": {
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
|
||||
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
|
||||
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
|
||||
"Username is too long (maximum is 39 characters).": "Používateľské meno je príliš dlhé (maximum je 39 znakov).",
|
||||
"Username is too long (maximum is 255 characters).": "Používateľské meno je príliš dlhé (maximum je 255 znakov).",
|
||||
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
|
||||
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Kullanıcı adı bir e-mail adresi olamaz",
|
||||
"Username cannot contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
|
||||
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz",
|
||||
"Username is too long (maximum is 39 characters).": "Kullanıcı adı çok uzun (en fazla 39 karakter olmalı).",
|
||||
"Username is too long (maximum is 255 characters).": "Kullanıcı adı çok uzun (en fazla 255 karakter olmalı).",
|
||||
"Username must have at least 2 characters": "Kullanıcı adı en az iki karakterden oluşmalı",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre denemesi yaptınız. %d dakika kadar bekleyip yeniden giriş yapmayı deneyebilirsiniz.",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Username cannot be an email address",
|
||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||
"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 255 characters).": "Username is too long (maximum is 255 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",
|
||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
|
||||
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
|
||||
"Username cannot start with a digit": "Tên người dùng không thể bắt đầu bằng chữ số",
|
||||
"Username is too long (maximum is 39 characters).": "Tên đăng nhập quá dài (tối đa là 39 ký tự).",
|
||||
"Username is too long (maximum is 255 characters).": "Tên đăng nhập quá dài (tối đa là 255 ký tự).",
|
||||
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
|
||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
|
||||
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "用户名不可以是邮箱地址",
|
||||
"Username cannot contain white spaces": "用户名禁止包含空格",
|
||||
"Username cannot start with a digit": "用户名禁止使用数字开头",
|
||||
"Username is too long (maximum is 39 characters).": "用户名过长(最大允许长度为39个字符)",
|
||||
"Username is too long (maximum is 255 characters).": "用户名过长(最大允许长度为255个字符)",
|
||||
"Username must have at least 2 characters": "用户名至少要有2个字符",
|
||||
"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": "所在地区不支持手机号注册",
|
||||
|
@ -188,10 +188,23 @@ type GitHubUserInfo struct {
|
||||
} `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) {
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Authorization", "token "+token.AccessToken)
|
||||
resp, err := idp.Client.Do(req)
|
||||
@ -212,6 +225,42 @@ func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
|
||||
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{
|
||||
Id: strconv.Itoa(githubUserInfo.Id),
|
||||
Username: githubUserInfo.Login,
|
||||
@ -248,3 +297,27 @@ func (idp *GithubIdProvider) postWithBody(body interface{}, url string) ([]byte,
|
||||
|
||||
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
|
||||
case "Douyin":
|
||||
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
case "Kwai":
|
||||
return NewKwaiIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
case "Bilibili":
|
||||
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
case "MetaMask":
|
||||
|
@ -434,7 +434,7 @@
|
||||
"isTopGroup": true,
|
||||
"title": "",
|
||||
"key": "",
|
||||
"children": "",
|
||||
"children": [],
|
||||
"isEnabled": true
|
||||
}
|
||||
],
|
||||
|
@ -15,33 +15,81 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
ldap "github.com/forestmgy/ldapserver"
|
||||
ldap "github.com/casdoor/ldapserver"
|
||||
"github.com/lor00x/goldap/message"
|
||||
)
|
||||
|
||||
func StartLdapServer() {
|
||||
ldapServerPort := conf.GetConfigString("ldapServerPort")
|
||||
if ldapServerPort == "" || ldapServerPort == "0" {
|
||||
return
|
||||
}
|
||||
ldapsServerPort := conf.GetConfigString("ldapsServerPort")
|
||||
|
||||
server := ldap.NewServer()
|
||||
serverSsl := ldap.NewServer()
|
||||
routes := ldap.NewRouteMux()
|
||||
|
||||
routes.Bind(handleBind)
|
||||
routes.Search(handleSearch).Label(" SEARCH****")
|
||||
|
||||
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 {
|
||||
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) {
|
||||
@ -142,7 +190,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
|
||||
}
|
||||
for _, attr := range attrs {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
11
ldap/util.go
11
ldap/util.go
@ -23,7 +23,7 @@ import (
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/lor00x/goldap/message"
|
||||
|
||||
ldap "github.com/forestmgy/ldapserver"
|
||||
ldap "github.com/casdoor/ldapserver"
|
||||
|
||||
"github.com/xorm-io/builder"
|
||||
)
|
||||
@ -185,12 +185,9 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
|
||||
attr := string(f.AttributeDesc())
|
||||
|
||||
if attr == ldapMemberOfAttr {
|
||||
groupId := string(f.AssertionValue())
|
||||
users, err := object.GetGroupUsers(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
groupId := string(f.AssertionValue())
|
||||
users := object.GetGroupUsersWithoutError(groupId)
|
||||
for _, user := range users {
|
||||
names = append(names, user.Name)
|
||||
}
|
||||
@ -249,7 +246,7 @@ func buildSafeCondition(filter interface{}) builder.Cond {
|
||||
condition, err := buildUserFilterCondition(filter)
|
||||
if err != nil {
|
||||
log.Printf("err = %v", err.Error())
|
||||
return nil
|
||||
return builder.And(builder.Expr("1 != 1"))
|
||||
}
|
||||
return condition
|
||||
}
|
||||
|
7
main.go
7
main.go
@ -22,6 +22,7 @@ import (
|
||||
_ "github.com/beego/beego/session/redis"
|
||||
"github.com/casdoor/casdoor/authz"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/controllers"
|
||||
"github.com/casdoor/casdoor/ldap"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
@ -45,6 +46,7 @@ func main() {
|
||||
object.InitCasvisorConfig()
|
||||
|
||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
|
||||
|
||||
// beego.DelStaticPath("/static")
|
||||
// beego.SetStaticPath("/static", "web/build/static")
|
||||
@ -83,6 +85,11 @@ func main() {
|
||||
// logs.SetLevel(logs.LevelInformational)
|
||||
logs.SetLogFuncCall(false)
|
||||
|
||||
err = util.StopOldInstance(port)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go ldap.StartLdapServer()
|
||||
go radius.StartRadiusServer()
|
||||
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"
|
||||
|
||||
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" {
|
||||
return NewTelegramProvider(clientSecret, receiver)
|
||||
} else if typ == "Custom HTTP" {
|
||||
@ -53,6 +53,8 @@ func GetNotificationProvider(typ string, clientId string, clientSecret string, c
|
||||
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
|
||||
} else if typ == "Viber" {
|
||||
return NewViberProvider(clientId, clientSecret, appId, receiver)
|
||||
} else if typ == "CUCloud" {
|
||||
return NewCucloudProvider(clientId, clientSecret, appId, title, regionId, clientId2, metaData)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
@ -95,6 +95,7 @@ type Application struct {
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
|
||||
IsShared bool `json:"isShared"`
|
||||
IpRestriction string `json:"ipRestriction"`
|
||||
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
@ -108,6 +109,7 @@ type Application struct {
|
||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
||||
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||
@ -479,7 +481,10 @@ func GetApplicationByClientId(clientId string) (*Application, error) {
|
||||
}
|
||||
|
||||
func GetApplication(id string) (*Application, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getApplication(owner, name)
|
||||
}
|
||||
|
||||
@ -721,8 +726,15 @@ func (application *Application) GetId() string {
|
||||
}
|
||||
|
||||
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...)
|
||||
for _, targetUri := range redirectUris {
|
||||
isValid, err := util.IsValidOrigin(redirectUri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if isValid {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, targetUri := range application.RedirectUris {
|
||||
targetUriRegex := regexp.MustCompile(targetUri)
|
||||
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
|
||||
return true
|
||||
|
@ -146,7 +146,12 @@ func getCertByName(name string) (*Cert, error) {
|
||||
|
||||
func GetCert(id string) (*Cert, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getCert(owner, name)
|
||||
cert, err := getCert(owner, name)
|
||||
if cert == nil && owner != "admin" {
|
||||
return getCert("admin", name)
|
||||
} else {
|
||||
return cert, err
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateCert(id string, cert *Cert) (bool, error) {
|
||||
|
@ -241,6 +241,10 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
|
||||
}
|
||||
|
||||
passwordType := user.PasswordType
|
||||
if passwordType == "" {
|
||||
passwordType = organization.PasswordType
|
||||
@ -248,7 +252,7 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
||||
credManager := cred.GetCredManager(passwordType)
|
||||
if credManager != nil {
|
||||
if organization.MasterPassword != "" {
|
||||
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
|
||||
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
|
||||
return resetUserSigninErrorTimes(user)
|
||||
}
|
||||
}
|
||||
@ -273,7 +277,7 @@ func CheckPasswordComplexity(user *User, password string) string {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -368,7 +372,7 @@ func CheckUserPassword(organization string, username string, password string, la
|
||||
}
|
||||
|
||||
// only for LDAP users
|
||||
err = checkLdapUserPassword(user, password, lang)
|
||||
err = CheckLdapUserPassword(user, password, lang)
|
||||
if err != nil {
|
||||
if err.Error() == "user not exist" {
|
||||
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
|
||||
@ -381,7 +385,13 @@ func CheckUserPassword(organization string, username string, password string, la
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = checkPasswordExpired(user, lang)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@ -507,8 +517,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
|
||||
func CheckUsername(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).")
|
||||
} else if len(username) > 255 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||
@ -520,11 +530,46 @@ func CheckUsername(username string, lang string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func CheckUsernameWithEmail(username string, lang string) string {
|
||||
if username == "" {
|
||||
return i18n.Translate(lang, "check:Empty username.")
|
||||
} else if len(username) > 255 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 255 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 {
|
||||
if oldUser.Name != user.Name {
|
||||
if msg := CheckUsername(user.Name, lang); msg != "" {
|
||||
return msg
|
||||
organizationName := oldUser.Owner
|
||||
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) {
|
||||
return i18n.Translate(lang, "check:Username already exists")
|
||||
}
|
||||
@ -539,6 +584,11 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
|
||||
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 ""
|
||||
}
|
||||
|
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
|
||||
}
|
@ -74,7 +74,7 @@ func checkPasswordComplexity(password string, options []string) string {
|
||||
}
|
||||
|
||||
if len(options) == 0 {
|
||||
options = []string{"AtLeast6"}
|
||||
return ""
|
||||
}
|
||||
|
||||
checkers := map[string]ValidatorFunc{
|
||||
|
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
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
import "github.com/casdoor/casdoor/email"
|
||||
|
||||
"github.com/casdoor/casdoor/email"
|
||||
"github.com/casdoor/gomail/v2"
|
||||
)
|
||||
|
||||
func getDialer(provider *Provider) *gomail.Dialer {
|
||||
dialer := &gomail.Dialer{}
|
||||
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
|
||||
if provider.Type == "SUBMAIL" {
|
||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
// TestSmtpServer Test the SMTP server
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sender.Close()
|
||||
|
||||
dialer.SSL = !provider.DisableSsl
|
||||
|
||||
return dialer
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// DailSmtpServer Dail Smtp server
|
||||
func DailSmtpServer(provider *Provider) error {
|
||||
dialer := getDialer(provider)
|
||||
|
||||
sender, err := dialer.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sender.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -17,126 +17,95 @@ package object
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
OrganizationCounts []int `json:"organizationCounts"`
|
||||
UserCounts []int `json:"userCounts"`
|
||||
ProviderCounts []int `json:"providerCounts"`
|
||||
ApplicationCounts []int `json:"applicationCounts"`
|
||||
SubscriptionCounts []int `json:"subscriptionCounts"`
|
||||
type DashboardDateItem struct {
|
||||
CreatedTime string `json:"createTime"`
|
||||
}
|
||||
|
||||
func GetDashboard(owner string) (*Dashboard, error) {
|
||||
type DashboardMapItem struct {
|
||||
dashboardDateItems []DashboardDateItem
|
||||
itemCount int64
|
||||
}
|
||||
|
||||
func GetDashboard(owner string) (*map[string][]int64, error) {
|
||||
if owner == "All" {
|
||||
owner = ""
|
||||
}
|
||||
|
||||
dashboard := &Dashboard{
|
||||
OrganizationCounts: make([]int, 31),
|
||||
UserCounts: make([]int, 31),
|
||||
ProviderCounts: make([]int, 31),
|
||||
ApplicationCounts: make([]int, 31),
|
||||
SubscriptionCounts: make([]int, 31),
|
||||
dashboard := make(map[string][]int64)
|
||||
dashboardMap := sync.Map{}
|
||||
tableNames := []string{"organization", "user", "provider", "application", "subscription", "role", "group", "resource", "cert", "permission", "transaction", "model", "adapter", "enforcer"}
|
||||
|
||||
time30day := time.Now().AddDate(0, 0, -30)
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
||||
wg.Add(len(tableNames))
|
||||
ch := make(chan error, len(tableNames))
|
||||
for _, tableName := range tableNames {
|
||||
dashboard[tableName+"Counts"] = make([]int64, 31)
|
||||
tableFullName := tableNamePrefix + 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(tableFullName).Count(); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableFullName).Find(&dashboardDateItems); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
dashboardMap.Store(tableFullName, 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()
|
||||
close(ch)
|
||||
|
||||
for err = range ch {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
nowTime := time.Now()
|
||||
for i := 30; i >= 0; i-- {
|
||||
cutTime := nowTime.AddDate(0, 0, -i)
|
||||
dashboard.OrganizationCounts[30-i] = countCreatedBefore(organizations, cutTime)
|
||||
dashboard.UserCounts[30-i] = countCreatedBefore(users, cutTime)
|
||||
dashboard.ProviderCounts[30-i] = countCreatedBefore(providers, cutTime)
|
||||
dashboard.ApplicationCounts[30-i] = countCreatedBefore(applications, cutTime)
|
||||
dashboard.SubscriptionCounts[30-i] = countCreatedBefore(subscriptions, cutTime)
|
||||
for _, tableName := range tableNames {
|
||||
item, exist := dashboardMap.Load(tableName)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
dashboard[tableName+"Counts"][30-i] = countCreatedBefore(item.(DashboardMapItem), cutTime)
|
||||
}
|
||||
}
|
||||
return dashboard, nil
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
func countCreatedBefore(objects interface{}, before time.Time) int {
|
||||
count := 0
|
||||
switch obj := objects.(type) {
|
||||
case []Organization:
|
||||
for _, o := range obj {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", o.CreatedTime)
|
||||
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++
|
||||
}
|
||||
func countCreatedBefore(dashboardMapItem DashboardMapItem, before time.Time) int64 {
|
||||
count := dashboardMapItem.itemCount
|
||||
for _, e := range dashboardMapItem.dashboardDateItems {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", e.CreatedTime)
|
||||
if createdTime.Before(before) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
|
@ -17,7 +17,6 @@ package object
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -36,12 +35,14 @@ type Group struct {
|
||||
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
||||
ParentName string `xorm:"-" json:"parentName"`
|
||||
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
||||
Users []string `xorm:"-" json:"users"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Children []*Group `json:"children,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
HaveChildren bool `xorm:"-" json:"haveChildren"`
|
||||
Children []*Group `json:"children,omitempty"`
|
||||
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
}
|
||||
@ -79,6 +80,30 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
|
||||
groupsHaveChildren := []*Group{}
|
||||
resultMap := make(map[string]*Group)
|
||||
groupMap := map[string]*Group{}
|
||||
|
||||
groupIds := []string{}
|
||||
for _, group := range groups {
|
||||
groupMap[group.Name] = group
|
||||
groupIds = append(groupIds, group.Name)
|
||||
if !group.IsTopGroup {
|
||||
groupIds = append(groupIds, group.ParentId)
|
||||
}
|
||||
}
|
||||
|
||||
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, group := range groupsHaveChildren {
|
||||
resultMap[group.ParentId] = groupMap[group.ParentId]
|
||||
}
|
||||
return resultMap, nil
|
||||
}
|
||||
|
||||
func getGroup(owner string, name string) (*Group, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
@ -281,7 +306,10 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
|
||||
|
||||
func GetGroupUsers(groupId string) ([]*User, error) {
|
||||
users := []*User{}
|
||||
owner, _ := util.GetOwnerAndNameFromId(groupId)
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -293,22 +321,21 @@ func GetGroupUsers(groupId string) ([]*User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetGroupUsersWithoutError(groupId string) []*User {
|
||||
users, _ := GetGroupUsers(groupId)
|
||||
return users
|
||||
}
|
||||
|
||||
func ExtendGroupWithUsers(group *Group) error {
|
||||
if group == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
users, err := GetUsers(group.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groupId := group.GetId()
|
||||
userIds := []string{}
|
||||
for _, user := range users {
|
||||
if util.InSlice(user.Groups, groupId) {
|
||||
userIds = append(userIds, user.GetId())
|
||||
}
|
||||
userIds, err := userEnforcer.GetAllUsersByGroup(groupId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group.Users = userIds
|
||||
@ -316,29 +343,14 @@ func ExtendGroupWithUsers(group *Group) error {
|
||||
}
|
||||
|
||||
func ExtendGroupsWithUsers(groups []*Group) error {
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error, len(groups))
|
||||
|
||||
for _, group := range groups {
|
||||
wg.Add(1)
|
||||
go func(group *Group) {
|
||||
defer wg.Done()
|
||||
err := ExtendGroupWithUsers(group)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}(group)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
for err := range errChan {
|
||||
users, err := userEnforcer.GetAllUsersByGroup(group.GetId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
group.Users = users
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -103,6 +103,7 @@ func initBuiltInOrganization() bool {
|
||||
PasswordOptions: []string{"AtLeast6"},
|
||||
CountryCodes: []string{"US", "ES", "FR", "DE", "GB", "CN", "JP", "KR", "VN", "ID", "SG", "IN"},
|
||||
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
|
||||
UserTypes: []string{},
|
||||
Tags: []string{},
|
||||
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"},
|
||||
InitScore: 2000,
|
||||
|
@ -48,12 +48,16 @@ type InitData struct {
|
||||
Transactions []*Transaction `json:"transactions"`
|
||||
}
|
||||
|
||||
var initDataNewOnly bool
|
||||
|
||||
func InitFromFile() {
|
||||
initDataFile := conf.GetConfigString("initDataFile")
|
||||
if initDataFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
initDataNewOnly = conf.GetConfigBool("initDataNewOnly")
|
||||
|
||||
initData, err := readInitDataFromFile(initDataFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -269,6 +273,9 @@ func initDefinedOrganization(organization *Organization) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := deleteOrganization(organization)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -295,6 +302,9 @@ func initDefinedApplication(application *Application) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := deleteApplication(application)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -316,6 +326,9 @@ func initDefinedUser(user *User) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := deleteUser(user)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -342,6 +355,9 @@ func initDefinedCert(cert *Cert) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteCert(cert)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -364,6 +380,9 @@ func initDefinedLdap(ldap *Ldap) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteLdap(ldap)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -385,6 +404,9 @@ func initDefinedProvider(provider *Provider) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteProvider(provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -406,6 +428,9 @@ func initDefinedModel(model *Model) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteModel(model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -428,6 +453,9 @@ func initDefinedPermission(permission *Permission) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := deletePermission(permission)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -450,6 +478,9 @@ func initDefinedPayment(payment *Payment) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeletePayment(payment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -472,6 +503,9 @@ func initDefinedProduct(product *Product) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteProduct(product)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -494,6 +528,9 @@ func initDefinedResource(resource *Resource) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteResource(resource)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -516,6 +553,9 @@ func initDefinedRole(role *Role) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := deleteRole(role)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -538,6 +578,9 @@ func initDefinedSyncer(syncer *Syncer) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteSyncer(syncer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -560,6 +603,9 @@ func initDefinedToken(token *Token) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteToken(token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -582,6 +628,9 @@ func initDefinedWebhook(webhook *Webhook) {
|
||||
}
|
||||
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteWebhook(webhook)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -603,6 +652,9 @@ func initDefinedGroup(group *Group) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := deleteGroup(group)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -624,6 +676,9 @@ func initDefinedAdapter(adapter *Adapter) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteAdapter(adapter)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -645,6 +700,9 @@ func initDefinedEnforcer(enforcer *Enforcer) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteEnforcer(enforcer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -666,6 +724,9 @@ func initDefinedPlan(plan *Plan) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeletePlan(plan)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -687,6 +748,9 @@ func initDefinedPricing(pricing *Pricing) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeletePricing(pricing)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -708,6 +772,9 @@ func initDefinedInvitation(invitation *Invitation) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteInvitation(invitation)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -743,6 +810,9 @@ func initDefinedSubscription(subscription *Subscription) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteSubscription(subscription)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -764,6 +834,9 @@ func initDefinedTransaction(transaction *Transaction) {
|
||||
panic(err)
|
||||
}
|
||||
if existed != nil {
|
||||
if initDataNewOnly {
|
||||
return
|
||||
}
|
||||
affected, err := DeleteTransaction(transaction)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -33,6 +33,7 @@ type Ldap struct {
|
||||
Filter string `xorm:"varchar(200)" json:"filter"`
|
||||
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||
|
||||
AutoSync int `json:"autoSync"`
|
||||
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",
|
||||
"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 {
|
||||
return false, nil
|
||||
}
|
||||
|
@ -15,14 +15,18 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/thanhpk/randstr"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
type LdapConn struct {
|
||||
@ -371,6 +375,88 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
|
||||
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) {
|
||||
user := User{}
|
||||
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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
||||
return origin, origin
|
||||
}
|
||||
|
||||
isDev := conf.GetConfigString("runmode") == "dev"
|
||||
// "door.casdoor.com"
|
||||
protocol := "https://"
|
||||
if !strings.Contains(host, ".") {
|
||||
@ -87,7 +88,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
||||
protocol = "http://"
|
||||
}
|
||||
|
||||
if host == "localhost:8000" {
|
||||
if host == "localhost:8000" && isDev {
|
||||
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
|
||||
} else {
|
||||
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
|
||||
|
@ -62,20 +62,25 @@ type Organization struct {
|
||||
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
|
||||
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
||||
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
||||
PasswordExpireDays int `json:"passwordExpireDays"`
|
||||
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
Languages []string `xorm:"varchar(255)" json:"languages"`
|
||||
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
||||
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
|
||||
MasterPassword string `xorm:"varchar(200)" json:"masterPassword"`
|
||||
DefaultPassword string `xorm:"varchar(200)" json:"defaultPassword"`
|
||||
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
|
||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||
InitScore int `json:"initScore"`
|
||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||
IsProfilePublic bool `json:"isProfilePublic"`
|
||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||
EnableTour bool `json:"enableTour"`
|
||||
IpRestriction string `json:"ipRestriction"`
|
||||
NavItems []string `xorm:"varchar(500)" json:"navItems"`
|
||||
|
||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||
@ -148,7 +153,10 @@ func getOrganization(owner string, name string) (*Organization, error) {
|
||||
}
|
||||
|
||||
func GetOrganization(id string) (*Organization, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getOrganization(owner, name)
|
||||
}
|
||||
|
||||
@ -189,9 +197,10 @@ func GetMaskedOrganizations(organizations []*Organization, errs ...error) ([]*Or
|
||||
return organizations, nil
|
||||
}
|
||||
|
||||
func UpdateOrganization(id string, organization *Organization) (bool, error) {
|
||||
func UpdateOrganization(id string, organization *Organization, isGlobalAdmin bool) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if org, err := getOrganization(owner, name); err != nil {
|
||||
org, err := getOrganization(owner, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if org == nil {
|
||||
return false, nil
|
||||
@ -216,6 +225,10 @@ func UpdateOrganization(id string, organization *Organization) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if !isGlobalAdmin {
|
||||
organization.NavItems = org.NavItems
|
||||
}
|
||||
|
||||
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
|
||||
if organization.MasterPassword == "***" {
|
||||
|
@ -364,7 +364,7 @@ func GetAllActions(userId string) ([]string, error) {
|
||||
|
||||
res := []string{}
|
||||
for _, enforcer := range enforcers {
|
||||
items := enforcer.GetAllObjects()
|
||||
items := enforcer.GetAllActions()
|
||||
res = append(res, items...)
|
||||
}
|
||||
return res, nil
|
||||
|
@ -219,8 +219,11 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
||||
ProductName: product.Name,
|
||||
PayerName: payerName,
|
||||
PayerId: user.Id,
|
||||
PayerEmail: user.Email,
|
||||
PaymentName: paymentName,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
ProductDescription: product.Description,
|
||||
ProductImage: product.Image,
|
||||
Price: product.Price,
|
||||
Currency: product.Currency,
|
||||
ReturnUrl: returnUrl,
|
||||
|
@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/context"
|
||||
@ -70,6 +71,7 @@ type Provider struct {
|
||||
IdP string `xorm:"mediumtext" json:"idP"`
|
||||
IssuerUrl string `xorm:"varchar(100)" json:"issuerUrl"`
|
||||
EnableSignAuthnRequest bool `json:"enableSignAuthnRequest"`
|
||||
EmailRegex string `xorm:"varchar(200)" json:"emailRegex"`
|
||||
|
||||
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
|
||||
}
|
||||
@ -200,6 +202,13 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if provider.EmailRegex != "" {
|
||||
_, err := regexp.Compile(provider.EmailRegex)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if name != provider.Name {
|
||||
err := providerChangeTrigger(name, provider.Name)
|
||||
if err != nil {
|
||||
@ -234,6 +243,13 @@ func AddProvider(provider *Provider) (bool, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -309,6 +325,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "AirWallex" {
|
||||
pp, err := pp.NewAirwallexPaymentProvider(p.ClientId, p.ClientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "Balance" {
|
||||
pp, err := pp.NewBalancePaymentProvider()
|
||||
if err != nil {
|
||||
@ -421,7 +443,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
|
||||
providerInfo.ClientId = provider.ClientId2
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ var (
|
||||
|
||||
func init() {
|
||||
logPostOnly = conf.GetConfigBool("logPostOnly")
|
||||
passwordRegex = regexp.MustCompile("\"password\":\".+\"")
|
||||
passwordRegex = regexp.MustCompile("\"password\":\"([^\"]*?)\"")
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
@ -50,7 +50,7 @@ func maskPassword(recordString string) string {
|
||||
}
|
||||
|
||||
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)
|
||||
requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"})
|
||||
if len(requestUri) > 1000 {
|
||||
@ -83,7 +83,7 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
|
||||
record := casvisorsdk.Record{
|
||||
Name: util.GenerateId(),
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
ClientIp: ip,
|
||||
ClientIp: clientIp,
|
||||
User: "",
|
||||
Method: ctx.Request.Method,
|
||||
RequestUri: requestUri,
|
||||
|
@ -338,6 +338,10 @@ func roleChangeTrigger(oldName string, newName string) error {
|
||||
|
||||
for _, role := range roles {
|
||||
for j, u := range role.Roles {
|
||||
if u == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
owner, name := util.GetOwnerAndNameFromId(u)
|
||||
if name == oldName {
|
||||
role.Roles[j] = util.GetId(owner, newName)
|
||||
@ -358,6 +362,10 @@ func roleChangeTrigger(oldName string, newName string) error {
|
||||
for _, permission := range permissions {
|
||||
for j, u := range permission.Roles {
|
||||
// u = organization/username
|
||||
if u == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
owner, name := util.GetOwnerAndNameFromId(u)
|
||||
if name == oldName {
|
||||
permission.Roles[j] = util.GetId(owner, newName)
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
@ -222,10 +223,13 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
|
||||
originFrontend, originBackend := getOriginFromHost(host)
|
||||
|
||||
idpLocation := ""
|
||||
idpBinding := ""
|
||||
if enablePostBinding {
|
||||
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 {
|
||||
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{
|
||||
@ -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"},
|
||||
},
|
||||
SingleSignOnService: SingleSignOnService{
|
||||
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
||||
Binding: idpBinding,
|
||||
Location: idpLocation,
|
||||
},
|
||||
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) {
|
||||
// request type
|
||||
method := "GET"
|
||||
|
||||
samlRequest = strings.ReplaceAll(samlRequest, " ", "+")
|
||||
// base64 decode
|
||||
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
|
||||
}
|
||||
|
||||
// decompress
|
||||
var buffer bytes.Buffer
|
||||
rdr := flate.NewReader(bytes.NewReader(defated))
|
||||
var requestByte []byte
|
||||
|
||||
for {
|
||||
_, err = io.CopyN(&buffer, rdr, 1024)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
if strings.Contains(string(defated), "xmlns:") {
|
||||
requestByte = defated
|
||||
} else {
|
||||
// decompress
|
||||
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
|
||||
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
|
||||
err = xml.Unmarshal(requestByte, &authnRequest)
|
||||
if err != nil {
|
||||
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 == "" {
|
||||
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)
|
||||
|
||||
|
@ -102,14 +102,6 @@ func GetTokenByAccessToken(accessToken string) (*Token, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !existed {
|
||||
token = Token{AccessToken: accessToken}
|
||||
existed, err = ormer.Engine.Get(&token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !existed {
|
||||
return nil, nil
|
||||
}
|
||||
@ -123,14 +115,6 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !existed {
|
||||
token = Token{RefreshToken: refreshToken}
|
||||
existed, err = ormer.Engine.Get(&token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !existed {
|
||||
return nil, nil
|
||||
}
|
||||
@ -139,7 +123,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
||||
|
||||
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||
switch tokenTypeHint {
|
||||
case "access_token":
|
||||
case "access_token", "access-token":
|
||||
token, err := GetTokenByAccessToken(tokenValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -147,7 +131,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||
if token != nil {
|
||||
return token, nil
|
||||
}
|
||||
case "refresh_token":
|
||||
case "refresh_token", "refresh-token":
|
||||
token, err := GetTokenByRefreshToken(tokenValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -160,13 +144,13 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func updateUsedByCode(token *Token) bool {
|
||||
func updateUsedByCode(token *Token) (bool, error) {
|
||||
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func GetToken(id string) (*Token, error) {
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -184,6 +185,15 @@ func StoreCasTokenForProxyTicket(token *CasAuthenticationSuccess, targetService,
|
||||
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) {
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
@ -225,6 +235,11 @@ func GenerateCasToken(userId string, service string) (string, error) {
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
if escapedValue, err := escapeXMLText(value); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
value = escapedValue
|
||||
}
|
||||
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
|
||||
Name: k,
|
||||
Value: value,
|
||||
|
@ -30,6 +30,8 @@ type Claims struct {
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Tag string `json:"tag"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// the `azp` (Authorized Party) claim. Optional. See https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
Azp string `json:"azp,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -137,6 +139,7 @@ type ClaimsShort struct {
|
||||
TokenType string `json:"tokenType,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -155,6 +158,7 @@ type ClaimsWithoutThirdIdp struct {
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Tag string `json:"tag"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -269,6 +273,7 @@ func getShortClaims(claims Claims) ClaimsShort {
|
||||
Nonce: claims.Nonce,
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -281,6 +286,7 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
|
||||
Tag: claims.Tag,
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -301,6 +307,7 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
|
||||
res["nonce"] = claims.Nonce
|
||||
res["tag"] = claims.Tag
|
||||
res["scope"] = claims.Scope
|
||||
res["azp"] = claims.Azp
|
||||
|
||||
for _, field := range tokenField {
|
||||
userField := userValue.FieldByName(field)
|
||||
@ -357,6 +364,7 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
||||
// FIXME: A workaround for custom claim by reusing `tag` in user info
|
||||
Tag: user.Tag,
|
||||
Scope: scope,
|
||||
Azp: application.ClientId,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: originBackend,
|
||||
Subject: user.Id,
|
||||
|
@ -248,7 +248,10 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
|
||||
token.CodeIsUsed = true
|
||||
|
||||
go updateUsedByCode(token)
|
||||
_, err = updateUsedByCode(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenWrapper := &TokenWrapper{
|
||||
AccessToken: token.AccessToken,
|
||||
@ -309,22 +312,29 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
}, nil
|
||||
}
|
||||
|
||||
var oldTokenScope string
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
_, err = ParseStandardJwtToken(refreshToken, cert)
|
||||
oldToken, err := ParseStandardJwtToken(refreshToken, cert)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
oldTokenScope = oldToken.Scope
|
||||
} else {
|
||||
_, err = ParseJwtToken(refreshToken, cert)
|
||||
oldToken, err := ParseJwtToken(refreshToken, cert)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
oldTokenScope = oldToken.Scope
|
||||
}
|
||||
|
||||
if scope == "" {
|
||||
scope = oldTokenScope
|
||||
}
|
||||
|
||||
// generate a new token
|
||||
@ -332,6 +342,9 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return "", fmt.Errorf("The user: %s doesn't exist", util.GetId(application.Organization, token.User))
|
||||
}
|
||||
|
||||
if user.IsForbidden {
|
||||
return &TokenError{
|
||||
@ -501,7 +514,7 @@ func GetPasswordToken(application *Application, username string, password string
|
||||
}
|
||||
|
||||
if user.Ldap != "" {
|
||||
err = checkLdapUserPassword(user, password, "en")
|
||||
err = CheckLdapUserPassword(user, password, "en")
|
||||
} else {
|
||||
err = CheckPassword(user, password, "en")
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ type ClaimsStandard struct {
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Address OIDCAddress `json:"address,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@ -52,6 +53,7 @@ func getStandardClaims(claims Claims) ClaimsStandard {
|
||||
Nonce: claims.Nonce,
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
}
|
||||
|
||||
res.Phone = ""
|
||||
|
@ -48,7 +48,7 @@ func InitUserManager() {
|
||||
|
||||
type User struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
Name string `xorm:"varchar(255) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
|
||||
@ -129,6 +129,7 @@ type User struct {
|
||||
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
||||
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
||||
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
|
||||
Kwai string `xorm:"kwai varchar(100)" json:"kwai"`
|
||||
Line string `xorm:"line varchar(100)" json:"line"`
|
||||
Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
|
||||
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
|
||||
@ -200,12 +201,14 @@ type User struct {
|
||||
Permissions []*Permission `json:"permissions"`
|
||||
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
||||
|
||||
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
||||
SigninWrongTimes int `json:"signinWrongTimes"`
|
||||
LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
|
||||
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
||||
SigninWrongTimes int `json:"signinWrongTimes"`
|
||||
|
||||
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
||||
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
||||
NeedUpdatePassword bool `json:"needUpdatePassword"`
|
||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||
}
|
||||
|
||||
type Userinfo struct {
|
||||
@ -235,6 +238,7 @@ type MfaAccount struct {
|
||||
AccountName string `xorm:"varchar(100)" json:"accountName"`
|
||||
Issuer string `xorm:"varchar(100)" json:"issuer"`
|
||||
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
|
||||
Origin string `xorm:"varchar(100)" json:"origin"`
|
||||
}
|
||||
|
||||
type FaceId struct {
|
||||
@ -677,6 +681,10 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
user.Password = oldUser.Password
|
||||
}
|
||||
|
||||
if user.Id != oldUser.Id && user.Id == "" {
|
||||
user.Id = oldUser.Id
|
||||
}
|
||||
|
||||
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
|
||||
user.PermanentAvatar, err = getPermanentAvatarUrl(user.Owner, user.Name, user.Avatar, false)
|
||||
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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
|
||||
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
|
||||
"spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo",
|
||||
"yammer", "yandex", "zoom", "custom", "need_update_password",
|
||||
"yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist",
|
||||
}
|
||||
}
|
||||
if isAdmin {
|
||||
@ -815,6 +823,10 @@ func AddUser(user *User) (bool, error) {
|
||||
user.UpdateUserPassword(organization)
|
||||
}
|
||||
|
||||
if user.CreatedTime == "" {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
err = user.UpdateUserHash()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -834,11 +846,14 @@ func AddUser(user *User) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
count, err := GetUserCount(user.Owner, "", "", "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
rankingItem := GetAccountItemByName("Ranking", organization)
|
||||
if rankingItem != nil {
|
||||
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 {
|
||||
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||
@ -950,6 +965,11 @@ func DeleteUser(user *User) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = userEnforcer.DeleteGroupsForUser(user.GetId())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
organization, err := GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -557,6 +557,14 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
||||
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 {
|
||||
item := GetAccountItemByName("Balance", organization)
|
||||
|
@ -57,7 +57,7 @@ type VerificationRecord struct {
|
||||
Receiver string `xorm:"varchar(100) index notnull" json:"receiver"`
|
||||
Code string `xorm:"varchar(10) notnull" json:"code"`
|
||||
Time int64 `xorm:"notnull" json:"time"`
|
||||
IsUsed bool
|
||||
IsUsed bool `xorm:"notnull" json:"isUsed"`
|
||||
}
|
||||
|
||||
func IsAllowSend(user *User, remoteAddr, recordType string) error {
|
||||
@ -86,9 +86,9 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
|
||||
title := provider.Title
|
||||
|
||||
code := getRandomCode(6)
|
||||
if organization.MasterVerificationCode != "" {
|
||||
code = organization.MasterVerificationCode
|
||||
}
|
||||
// if organization.MasterVerificationCode != "" {
|
||||
// code = organization.MasterVerificationCode
|
||||
// }
|
||||
|
||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||
content := strings.Replace(provider.Content, "%s", code, 1)
|
||||
@ -124,9 +124,9 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
|
||||
}
|
||||
|
||||
code := getRandomCode(6)
|
||||
if organization.MasterVerificationCode != "" {
|
||||
code = organization.MasterVerificationCode
|
||||
}
|
||||
// if organization.MasterVerificationCode != "" {
|
||||
// code = organization.MasterVerificationCode
|
||||
// }
|
||||
|
||||
err = SendSms(provider, code, dest)
|
||||
if err != nil {
|
||||
|
289
pp/airwallex.go
Normal file
289
pp/airwallex.go
Normal file
@ -0,0 +1,289 @@
|
||||
package pp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
)
|
||||
|
||||
type AirwallexPaymentProvider struct {
|
||||
Client *AirwallexClient
|
||||
}
|
||||
|
||||
func NewAirwallexPaymentProvider(clientId string, apiKey string) (*AirwallexPaymentProvider, error) {
|
||||
isProd := conf.GetConfigString("runmode") == "prod"
|
||||
apiEndpoint := "https://api-demo.airwallex.com/api/v1"
|
||||
apiCheckout := "https://checkout-demo.airwallex.com/#/standalone/checkout?"
|
||||
if isProd {
|
||||
apiEndpoint = "https://api.airwallex.com/api/v1"
|
||||
apiCheckout = "https://checkout.airwallex.com/#/standalone/checkout?"
|
||||
}
|
||||
client := &AirwallexClient{
|
||||
ClientId: clientId,
|
||||
APIKey: apiKey,
|
||||
APIEndpoint: apiEndpoint,
|
||||
APICheckout: apiCheckout,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
pp := &AirwallexPaymentProvider{
|
||||
Client: client,
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *AirwallexPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
// Create a payment intent
|
||||
intent, err := pp.Client.CreateIntent(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payUrl, err := pp.Client.GetCheckoutUrl(intent, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PayResp{
|
||||
PayUrl: payUrl,
|
||||
OrderId: intent.MerchantOrderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *AirwallexPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||
notifyResult := &NotifyResult{}
|
||||
intent, err := pp.Client.GetIntentByOrderId(orderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check intent status
|
||||
switch intent.Status {
|
||||
case "PENDING", "REQUIRES_PAYMENT_METHOD", "REQUIRES_CUSTOMER_ACTION", "REQUIRES_CAPTURE":
|
||||
notifyResult.PaymentStatus = PaymentStateCreated
|
||||
return notifyResult, nil
|
||||
case "CANCELLED":
|
||||
notifyResult.PaymentStatus = PaymentStateCanceled
|
||||
return notifyResult, nil
|
||||
case "EXPIRED":
|
||||
notifyResult.PaymentStatus = PaymentStateTimeout
|
||||
return notifyResult, nil
|
||||
case "SUCCEEDED":
|
||||
// Skip
|
||||
default:
|
||||
notifyResult.PaymentStatus = PaymentStateError
|
||||
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout status: %v", intent.Status)
|
||||
return notifyResult, nil
|
||||
}
|
||||
// Check attempt status
|
||||
if intent.PaymentStatus != "" {
|
||||
switch intent.PaymentStatus {
|
||||
case "CANCELLED", "EXPIRED", "RECEIVED", "AUTHENTICATION_REDIRECTED", "AUTHORIZED", "CAPTURE_REQUESTED":
|
||||
notifyResult.PaymentStatus = PaymentStateCreated
|
||||
return notifyResult, nil
|
||||
case "PAID", "SETTLED":
|
||||
// Skip
|
||||
default:
|
||||
notifyResult.PaymentStatus = PaymentStateError
|
||||
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout payment status: %v", intent.PaymentStatus)
|
||||
return notifyResult, nil
|
||||
}
|
||||
}
|
||||
// The Payment has succeeded.
|
||||
var productDisplayName, productName, providerName string
|
||||
if description, ok := intent.Metadata["description"]; ok {
|
||||
productName, productDisplayName, providerName, _ = parseAttachString(description.(string))
|
||||
}
|
||||
orderId = intent.MerchantOrderId
|
||||
return &NotifyResult{
|
||||
PaymentName: orderId,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
Price: priceStringToFloat64(intent.Amount.String()),
|
||||
Currency: intent.Currency,
|
||||
OrderId: orderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *AirwallexPaymentProvider) GetInvoice(paymentName, personName, personIdCard, personEmail, personPhone, invoiceType, invoiceTitle, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *AirwallexPaymentProvider) GetResponseError(err error) string {
|
||||
if err == nil {
|
||||
return "success"
|
||||
}
|
||||
return "fail"
|
||||
}
|
||||
|
||||
/*
|
||||
* Airwallex Client implementation (to be removed upon official SDK release)
|
||||
*/
|
||||
|
||||
type AirwallexClient struct {
|
||||
ClientId string
|
||||
APIKey string
|
||||
APIEndpoint string
|
||||
APICheckout string
|
||||
client *http.Client
|
||||
tokenCache *AirWallexTokenInfo
|
||||
tokenMutex sync.RWMutex
|
||||
}
|
||||
|
||||
type AirWallexTokenInfo struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
parsedExpiresAt time.Time
|
||||
}
|
||||
|
||||
type AirWallexIntentResp struct {
|
||||
Id string `json:"id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
MerchantOrderId string `json:"merchant_order_id"`
|
||||
}
|
||||
|
||||
func (c *AirwallexClient) GetToken() (string, error) {
|
||||
c.tokenMutex.Lock()
|
||||
defer c.tokenMutex.Unlock()
|
||||
if c.tokenCache != nil && time.Now().Before(c.tokenCache.parsedExpiresAt) {
|
||||
return c.tokenCache.Token, nil
|
||||
}
|
||||
req, _ := http.NewRequest("POST", c.APIEndpoint+"/authentication/login", bytes.NewBuffer([]byte("{}")))
|
||||
req.Header.Set("x-client-id", c.ClientId)
|
||||
req.Header.Set("x-api-key", c.APIKey)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result AirWallexTokenInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result.Token == "" {
|
||||
return "", fmt.Errorf("invalid token response")
|
||||
}
|
||||
expiresAt := strings.Replace(result.ExpiresAt, "+0000", "+00:00", 1)
|
||||
result.parsedExpiresAt, _ = time.Parse(time.RFC3339, expiresAt)
|
||||
c.tokenCache = &result
|
||||
return result.Token, nil
|
||||
}
|
||||
|
||||
func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map[string]interface{}, error) {
|
||||
token, err := c.GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest(method, url, bytes.NewBuffer(b))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *AirwallexClient) CreateIntent(r *PayReq) (*AirWallexIntentResp, error) {
|
||||
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||
orderId := r.PaymentName
|
||||
intentReq := map[string]interface{}{
|
||||
"currency": r.Currency,
|
||||
"amount": r.Price,
|
||||
"merchant_order_id": orderId,
|
||||
"request_id": orderId,
|
||||
"descriptor": strings.ReplaceAll(string([]rune(description)[:32]), "\x00", ""),
|
||||
"metadata": map[string]interface{}{"description": description},
|
||||
"order": map[string]interface{}{"products": []map[string]interface{}{{"name": r.ProductDisplayName, "quantity": 1, "desc": r.ProductDescription, "image_url": r.ProductImage}}},
|
||||
"customer": map[string]interface{}{"merchant_customer_id": r.PayerId, "email": r.PayerEmail, "first_name": r.PayerName, "last_name": r.PayerName},
|
||||
}
|
||||
intentUrl := fmt.Sprintf("%s/pa/payment_intents/create", c.APIEndpoint)
|
||||
intentRes, err := c.authRequest("POST", intentUrl, intentReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create payment intent: %v", err)
|
||||
}
|
||||
return &AirWallexIntentResp{
|
||||
Id: intentRes["id"].(string),
|
||||
ClientSecret: intentRes["client_secret"].(string),
|
||||
MerchantOrderId: intentRes["merchant_order_id"].(string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AirwallexIntent struct {
|
||||
Amount json.Number `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Id string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Descriptor string `json:"descriptor"`
|
||||
MerchantOrderId string `json:"merchant_order_id"`
|
||||
LatestPaymentAttempt struct {
|
||||
Status string `json:"status"`
|
||||
} `json:"latest_payment_attempt"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
type AirwallexIntents struct {
|
||||
Items []AirwallexIntent `json:"items"`
|
||||
}
|
||||
|
||||
type AirWallexIntentInfo struct {
|
||||
Amount json.Number
|
||||
Currency string
|
||||
Id string
|
||||
Status string
|
||||
Descriptor string
|
||||
MerchantOrderId string
|
||||
PaymentStatus string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (c *AirwallexClient) GetIntentByOrderId(orderId string) (*AirWallexIntentInfo, error) {
|
||||
intentUrl := fmt.Sprintf("%s/pa/payment_intents/?merchant_order_id=%s", c.APIEndpoint, orderId)
|
||||
intentRes, err := c.authRequest("GET", intentUrl, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get payment intent: %v", err)
|
||||
}
|
||||
items := intentRes["items"].([]interface{})
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf("no payment intent found for order id: %s", orderId)
|
||||
}
|
||||
var intent AirwallexIntent
|
||||
if b, err := json.Marshal(items[0]); err == nil {
|
||||
json.Unmarshal(b, &intent)
|
||||
}
|
||||
return &AirWallexIntentInfo{
|
||||
Id: intent.Id,
|
||||
Amount: intent.Amount,
|
||||
Currency: intent.Currency,
|
||||
Status: intent.Status,
|
||||
Descriptor: intent.Descriptor,
|
||||
MerchantOrderId: intent.MerchantOrderId,
|
||||
PaymentStatus: intent.LatestPaymentAttempt.Status,
|
||||
Metadata: intent.Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *AirwallexClient) GetCheckoutUrl(intent *AirWallexIntentResp, r *PayReq) (string, error) {
|
||||
return fmt.Sprintf("%sintent_id=%s&client_secret=%s&mode=payment¤cy=%s&amount=%v&requiredBillingContactFields=%s&successUrl=%s&failUrl=%s&logoUrl=%s",
|
||||
c.APICheckout,
|
||||
intent.Id,
|
||||
intent.ClientSecret,
|
||||
r.Currency,
|
||||
r.Price,
|
||||
url.QueryEscape(`["address"]`),
|
||||
r.ReturnUrl,
|
||||
r.ReturnUrl,
|
||||
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", // replace default logo
|
||||
), nil
|
||||
}
|
@ -33,8 +33,11 @@ type PayReq struct {
|
||||
ProductName string
|
||||
PayerName string
|
||||
PayerId string
|
||||
PayerEmail string
|
||||
PaymentName string
|
||||
ProductDisplayName string
|
||||
ProductDescription string
|
||||
ProductImage string
|
||||
Price float64
|
||||
Currency string
|
||||
|
||||
|
@ -68,8 +68,10 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
|
||||
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
|
||||
|
||||
if organization == "" {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
organization = conf.GetConfigString("radiusDefaultOrganization")
|
||||
if organization == "" {
|
||||
organization = "built-in"
|
||||
}
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
|
@ -16,11 +16,11 @@ package routers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/context"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -52,7 +52,13 @@ func CorsFilter(ctx *context.Context) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -174,6 +174,9 @@ func initAPI() {
|
||||
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
|
||||
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
|
||||
|
||||
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
|
||||
beego.Router("/api/refresh-engines", &controllers.ApiController{}, "POST:RefreshEngines")
|
||||
|
||||
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
|
||||
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
|
||||
beego.Router("/api/update-session", &controllers.ApiController{}, "POST:UpdateSession")
|
||||
|
@ -80,6 +80,15 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
isAllowed, err := object.CheckLoginPermission(userId, application)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -133,6 +142,14 @@ func StaticFilter(ctx *context.Context) {
|
||||
path += urlPath
|
||||
}
|
||||
|
||||
// Preventing synchronization problems from concurrency
|
||||
ctx.Input.CruSession = nil
|
||||
|
||||
organizationThemeCookie, err := appendThemeCookie(ctx, urlPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
if strings.Contains(path, "/../") || !util.FileExist(path) {
|
||||
path = webBuildFolder + "/index.html"
|
||||
}
|
||||
@ -149,13 +166,13 @@ func StaticFilter(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if oldStaticBaseUrl == newStaticBaseUrl {
|
||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path)
|
||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||
} 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))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -168,7 +185,13 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@ -182,14 +205,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
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") {
|
||||
serveFileWithReplace(w, r, path)
|
||||
serveFileWithReplace(w, r, path, organizationThemeCookie)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(w)
|
||||
defer gz.Close()
|
||||
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||
serveFileWithReplace(gzw, r, path)
|
||||
serveFileWithReplace(gzw, r, path, organizationThemeCookie)
|
||||
}
|
||||
|
131
routers/theme_filter.go
Normal file
131
routers/theme_filter.go
Normal file
@ -0,0 +1,131 @@
|
||||
// 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) {
|
||||
organizationThemeCookie, err := getOrganizationThemeCookieFromUrlPath(ctx, urlPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if organizationThemeCookie != nil {
|
||||
return organizationThemeCookie, setThemeDataCookie(ctx, organizationThemeCookie)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
|
||||
var application *object.Application
|
||||
var organization *object.Organization
|
||||
var err error
|
||||
if urlPath == "/login" || urlPath == "/signup" {
|
||||
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasSuffix(urlPath, "/oauth/authorize") {
|
||||
clientId := ctx.Input.Query("client_id")
|
||||
if clientId == "" {
|
||||
return nil, nil
|
||||
}
|
||||
application, err = object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(urlPath, "/login/saml") {
|
||||
owner, _ := strings.CutPrefix(urlPath, "/login/saml/authorize/")
|
||||
application, err = object.GetApplication(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(urlPath, "/login/") {
|
||||
owner, _ := strings.CutPrefix(urlPath, "/login/")
|
||||
if owner == "undefined" || strings.Count(owner, "/") > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(urlPath, "/signup/") {
|
||||
owner, _ := strings.CutPrefix(urlPath, "/signup/")
|
||||
if owner == "undefined" || strings.Count(owner, "/") > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(urlPath, "/cas/") && strings.HasSuffix(urlPath, "/login") {
|
||||
owner, _ := strings.CutPrefix(urlPath, "/cas/")
|
||||
owner, _ = strings.CutSuffix(owner, "/login")
|
||||
application, err = object.GetApplication(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if application == nil {
|
||||
return nil, nil
|
||||
}
|
||||
organization = application.OrganizationObj
|
||||
if organization == nil {
|
||||
organization, err = object.GetOrganization(fmt.Sprintf("admin/%s", application.Organization))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
organizationThemeCookie := &OrganizationThemeCookie{
|
||||
ThemeData: application.ThemeData,
|
||||
LogoUrl: application.Logo,
|
||||
FooterHtml: application.FooterHtml,
|
||||
}
|
||||
|
||||
if organization != nil {
|
||||
organizationThemeCookie.Favicon = organization.Favicon
|
||||
organizationThemeCookie.DisplayName = organization.DisplayName
|
||||
}
|
||||
|
||||
return organizationThemeCookie, 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":
|
||||
return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
||||
case "MinIO":
|
||||
return NewMinIOS3StorageProvider(clientId, clientSecret, "_", bucket, endpoint), nil
|
||||
if region == "" {
|
||||
region = "_"
|
||||
}
|
||||
return NewMinIOS3StorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
||||
case "Aliyun OSS":
|
||||
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint), nil
|
||||
case "Tencent Cloud COS":
|
||||
@ -38,6 +41,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
|
||||
return NewSynologyNasStorageProvider(clientId, clientSecret, endpoint), nil
|
||||
case "Casdoor":
|
||||
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
|
||||
|
@ -7558,6 +7558,9 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"kwai": {
|
||||
"type": "string"
|
||||
},
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user