mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-26 01:20:29 +08:00
Compare commits
73 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
@@ -99,6 +99,7 @@ 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, *, *
|
||||
`
|
||||
@@ -156,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,7 +25,10 @@ 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"}
|
||||
|
@@ -22,6 +22,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -305,6 +306,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
|
||||
@@ -330,6 +360,8 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
verificationType := ""
|
||||
|
||||
if authForm.Username != "" {
|
||||
if authForm.Type == ResponseTypeLogin {
|
||||
if c.GetSessionUsername() != "" {
|
||||
@@ -424,6 +456,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))
|
||||
@@ -514,16 +552,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
|
||||
}
|
||||
|
||||
@@ -617,6 +646,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" {
|
||||
@@ -648,6 +688,11 @@ func (c *ApiController) Login() {
|
||||
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())
|
||||
@@ -854,18 +899,32 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
if authForm.Passcode != "" {
|
||||
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.GetPreferredMfaProps(false))
|
||||
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 {
|
||||
@@ -878,7 +937,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
|
||||
@@ -908,6 +971,10 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if authForm.Provider == "" {
|
||||
authForm.Provider = authForm.ProviderBack
|
||||
}
|
||||
|
||||
user := c.getCurrentUser()
|
||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||
|
||||
|
@@ -15,13 +15,76 @@
|
||||
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{}
|
||||
@@ -57,6 +120,11 @@ func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
||||
// @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")
|
||||
|
||||
@@ -84,6 +152,16 @@ func (c *ApiController) RunCasbinCommand() {
|
||||
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 {
|
||||
@@ -112,3 +190,58 @@ func (c *ApiController) RunCasbinCommand() {
|
||||
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())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -124,7 +124,9 @@ func (c *ApiController) UpdateOrganization() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
||||
isGlobalAdmin, _ := c.isGlobalAdmin()
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -402,6 +402,7 @@ func (c *ApiController) IntrospectToken() {
|
||||
return
|
||||
}
|
||||
}
|
||||
introspectionResponse.TokenType = token.TokenType
|
||||
|
||||
c.Data["json"] = introspectionResponse
|
||||
c.ServeJSON()
|
||||
|
@@ -353,13 +353,7 @@ func (c *ApiController) AddUser() {
|
||||
return
|
||||
}
|
||||
|
||||
count, err := object.GetUserCount("", "", "", "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkQuotaForUser(int(count)); err != nil {
|
||||
if err := checkQuotaForUser(); err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
@@ -475,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")
|
||||
@@ -518,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
|
||||
@@ -563,7 +571,16 @@ func (c *ApiController) SetPassword() {
|
||||
targetUser.NeedUpdatePassword = false
|
||||
targetUser.LastChangePasswordTime = util.GetCurrentTime()
|
||||
|
||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@@ -510,20 +510,28 @@ 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)
|
||||
|
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
|
||||
}
|
@@ -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"`
|
||||
|
9
go.mod
9
go.mod
@@ -10,9 +10,9 @@ require (
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.77.2
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.0.1
|
||||
github.com/casdoor/gomail/v2 v2.1.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
github.com/casdoor/notify v0.45.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
|
||||
@@ -60,9 +60,10 @@ require (
|
||||
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
|
||||
|
41
go.sum
41
go.sum
@@ -1089,12 +1089,12 @@ github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXy
|
||||
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
|
||||
github.com/casdoor/go-sms-sender v0.25.0 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
|
||||
github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
|
||||
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
|
||||
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
|
||||
github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWUxM=
|
||||
github.com/casdoor/gomail/v2 v2.1.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
|
||||
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
|
||||
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
|
||||
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
|
||||
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
|
||||
github.com/casdoor/notify v1.0.0 h1:oldsaaQFPrlufm/OA314z8DwFVE1Tc9Gt1z4ptRHhXw=
|
||||
github.com/casdoor/notify v1.0.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
|
||||
github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
|
||||
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
|
||||
@@ -2163,8 +2163,10 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -2230,8 +2232,10 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -2319,8 +2323,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -2375,8 +2381,11 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2503,8 +2512,11 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -2524,8 +2536,10 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2546,8 +2560,10 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -2634,8 +2650,9 @@ golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@@ -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":
|
||||
|
@@ -15,6 +15,7 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
@@ -27,21 +28,68 @@ import (
|
||||
|
||||
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) {
|
||||
|
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
|
||||
|
@@ -481,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)
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -19,234 +19,90 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
OrganizationCounts []int `json:"organizationCounts"`
|
||||
UserCounts []int `json:"userCounts"`
|
||||
ProviderCounts []int `json:"providerCounts"`
|
||||
ApplicationCounts []int `json:"applicationCounts"`
|
||||
SubscriptionCounts []int `json:"subscriptionCounts"`
|
||||
RoleCounts []int `json:"roleCounts"`
|
||||
GroupCounts []int `json:"groupCounts"`
|
||||
ResourceCounts []int `json:"resourceCounts"`
|
||||
CertCounts []int `json:"certCounts"`
|
||||
PermissionCounts []int `json:"permissionCounts"`
|
||||
TransactionCounts []int `json:"transactionCounts"`
|
||||
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),
|
||||
RoleCounts: make([]int, 31),
|
||||
GroupCounts: make([]int, 31),
|
||||
ResourceCounts: make([]int, 31),
|
||||
CertCounts: make([]int, 31),
|
||||
PermissionCounts: make([]int, 31),
|
||||
TransactionCounts: 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
|
||||
wg.Add(len(tableNames))
|
||||
ch := make(chan error, len(tableNames))
|
||||
for _, tableName := range tableNames {
|
||||
dashboard[tableName+"Counts"] = make([]int64, 31)
|
||||
tableName := tableName
|
||||
go func(ch chan error) {
|
||||
defer wg.Done()
|
||||
dashboardDateItems := []DashboardDateItem{}
|
||||
var countResult int64
|
||||
|
||||
dbQueryBefore := ormer.Engine.Cols("created_time")
|
||||
dbQueryAfter := ormer.Engine.Cols("created_time")
|
||||
|
||||
if owner != "" {
|
||||
dbQueryAfter = dbQueryAfter.And("owner = ?", owner)
|
||||
dbQueryBefore = dbQueryBefore.And("owner = ?", owner)
|
||||
}
|
||||
|
||||
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableName).Count(); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableName).Find(&dashboardDateItems); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
dashboardMap.Store(tableName, DashboardMapItem{
|
||||
dashboardDateItems: dashboardDateItems,
|
||||
itemCount: countResult,
|
||||
})
|
||||
}(ch)
|
||||
}
|
||||
|
||||
organizations := []Organization{}
|
||||
users := []User{}
|
||||
providers := []Provider{}
|
||||
applications := []Application{}
|
||||
subscriptions := []Subscription{}
|
||||
roles := []Role{}
|
||||
groups := []Group{}
|
||||
resources := []Resource{}
|
||||
certs := []Cert{}
|
||||
permissions := []Permission{}
|
||||
transactions := []Transaction{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(11)
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if err := ormer.Engine.Find(&roles, &Role{Owner: owner}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if err := ormer.Engine.Find(&groups, &Group{Owner: owner}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := ormer.Engine.Find(&resources, &Resource{Owner: owner}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := ormer.Engine.Find(&certs, &Cert{Owner: owner}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := ormer.Engine.Find(&permissions, &Permission{Owner: owner}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := ormer.Engine.Find(&transactions, &Transaction{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)
|
||||
dashboard.RoleCounts[30-i] = countCreatedBefore(roles, cutTime)
|
||||
dashboard.GroupCounts[30-i] = countCreatedBefore(groups, cutTime)
|
||||
dashboard.ResourceCounts[30-i] = countCreatedBefore(resources, cutTime)
|
||||
dashboard.CertCounts[30-i] = countCreatedBefore(certs, cutTime)
|
||||
dashboard.PermissionCounts[30-i] = countCreatedBefore(permissions, cutTime)
|
||||
dashboard.TransactionCounts[30-i] = countCreatedBefore(transactions, 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++
|
||||
}
|
||||
}
|
||||
case []Role:
|
||||
for _, r := range obj {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", r.CreatedTime)
|
||||
if createdTime.Before(before) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
case []Group:
|
||||
for _, g := range obj {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", g.CreatedTime)
|
||||
if createdTime.Before(before) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
case []Resource:
|
||||
for _, r := range obj {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", r.CreatedTime)
|
||||
if createdTime.Before(before) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
case []Cert:
|
||||
for _, c := range obj {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", c.CreatedTime)
|
||||
if createdTime.Before(before) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
case []Permission:
|
||||
for _, p := range obj {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", p.CreatedTime)
|
||||
if createdTime.Before(before) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
case []Transaction:
|
||||
for _, t := range obj {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", t.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,26 @@ 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)
|
||||
|
||||
groupIds := []string{}
|
||||
for _, group := range groups {
|
||||
groupIds = append(groupIds, group.Name)
|
||||
groupIds = append(groupIds, group.ParentId)
|
||||
}
|
||||
|
||||
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, group := range groups {
|
||||
resultMap[group.Name] = group
|
||||
}
|
||||
return resultMap, nil
|
||||
}
|
||||
|
||||
func getGroup(owner string, name string) (*Group, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
@@ -298,17 +319,11 @@ func ExtendGroupWithUsers(group *Group) error {
|
||||
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 +331,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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -69,8 +69,8 @@ type Organization struct {
|
||||
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"`
|
||||
@@ -79,6 +79,7 @@ type Organization struct {
|
||||
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"`
|
||||
@@ -151,7 +152,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)
|
||||
}
|
||||
|
||||
@@ -192,9 +196,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
|
||||
@@ -219,6 +224,10 @@ func UpdateOrganization(id string, organization *Organization) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if !isGlobalAdmin {
|
||||
organization.NavItems = org.NavItems
|
||||
}
|
||||
|
||||
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
|
||||
if organization.MasterPassword == "***" {
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
@@ -338,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)
|
||||
|
||||
|
@@ -123,8 +123,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
||||
|
||||
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||
switch tokenTypeHint {
|
||||
case "access_token":
|
||||
case "access-token":
|
||||
case "access_token", "access-token":
|
||||
token, err := GetTokenByAccessToken(tokenValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -132,8 +131,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||
if token != nil {
|
||||
return token, nil
|
||||
}
|
||||
case "refresh_token":
|
||||
case "refresh-token":
|
||||
case "refresh_token", "refresh-token":
|
||||
token, err := GetTokenByRefreshToken(tokenValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -146,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,
|
||||
|
@@ -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
|
||||
@@ -504,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")
|
||||
}
|
||||
|
@@ -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"`
|
||||
@@ -237,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 {
|
||||
@@ -679,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 {
|
||||
@@ -693,7 +699,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
||||
"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",
|
||||
@@ -840,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)
|
||||
@@ -956,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
|
||||
|
@@ -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 {
|
||||
|
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
|
||||
|
@@ -175,6 +175,7 @@ func initAPI() {
|
||||
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")
|
||||
|
@@ -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)
|
||||
}
|
||||
|
119
routers/theme_filter.go
Normal file
119
routers/theme_filter.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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" {
|
||||
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(urlPath, "/login/oauth/authorize") {
|
||||
clientId := ctx.Input.Query("client_id")
|
||||
if clientId == "" {
|
||||
return nil, nil
|
||||
}
|
||||
application, err = object.GetApplicationByClientId(clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} 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, "/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{
|
||||
application.ThemeData,
|
||||
application.Logo,
|
||||
application.FooterHtml,
|
||||
organization.Favicon,
|
||||
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"
|
||||
},
|
||||
|
@@ -4981,6 +4981,8 @@ definitions:
|
||||
karma:
|
||||
type: integer
|
||||
format: int64
|
||||
kwai:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
lark:
|
||||
|
97
util/process.go
Normal file
97
util/process.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getPidByPort(port int) (int, error) {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", "netstat -ano | findstr :"+strconv.Itoa(port))
|
||||
case "darwin", "linux":
|
||||
cmd = exec.Command("lsof", "-t", "-i", ":"+strconv.Itoa(port))
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
if exitErr.ExitCode() == 1 {
|
||||
return 0, nil
|
||||
}
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 {
|
||||
if runtime.GOOS == "windows" {
|
||||
if fields[1] == "0.0.0.0:"+strconv.Itoa(port) {
|
||||
pid, err := strconv.Atoi(fields[len(fields)-1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return pid, nil
|
||||
}
|
||||
} else {
|
||||
pid, err := strconv.Atoi(fields[0])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return pid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func StopOldInstance(port int) error {
|
||||
pid, err := getPidByPort(port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pid == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = process.Kill()
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
fmt.Printf("The old instance with pid: %d has been stopped\n", pid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -118,6 +118,6 @@ func IsValidOrigin(origin string) (bool, error) {
|
||||
originHostOnly = fmt.Sprintf("%s://%s", urlObj.Scheme, urlObj.Hostname())
|
||||
}
|
||||
|
||||
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-app" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
|
||||
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-authenticator" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
|
||||
return res, nil
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const CracoLessPlugin = require("craco-less");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
@@ -55,47 +56,42 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
webpack: {
|
||||
configure: {
|
||||
// ignore webpack warnings by source-map-loader
|
||||
configure: (webpackConfig, { env, paths }) => {
|
||||
paths.appBuild = path.resolve(__dirname, "build-temp");
|
||||
webpackConfig.output.path = path.resolve(__dirname, "build-temp");
|
||||
|
||||
// ignore webpack warnings by source-map-loader
|
||||
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
|
||||
ignoreWarnings: [
|
||||
webpackConfig.ignoreWarnings = [
|
||||
function ignoreSourcemapsloaderWarnings(warning) {
|
||||
return (
|
||||
warning.module &&
|
||||
warning.module.resource.includes('node_modules') &&
|
||||
warning.module.resource.includes("node_modules") &&
|
||||
warning.details &&
|
||||
warning.details.includes('source-map-loader')
|
||||
)
|
||||
warning.details.includes("source-map-loader")
|
||||
);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
// use polyfill Buffer with Webpack 5
|
||||
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
|
||||
// https://craco.js.org/docs/configuration/webpack/
|
||||
resolve: {
|
||||
fallback: {
|
||||
// "process": require.resolve('process/browser'),
|
||||
// "util": require.resolve("util/"),
|
||||
// "url": require.resolve("url/"),
|
||||
// "zlib": require.resolve("browserify-zlib"),
|
||||
// "stream": require.resolve("stream-browserify"),
|
||||
// "http": require.resolve("stream-http"),
|
||||
// "https": require.resolve("https-browserify"),
|
||||
// "assert": require.resolve("assert/"),
|
||||
"buffer": require.resolve('buffer/'),
|
||||
"process": false,
|
||||
"util": false,
|
||||
"url": false,
|
||||
"zlib": false,
|
||||
"stream": false,
|
||||
"http": false,
|
||||
"https": false,
|
||||
"assert": false,
|
||||
"buffer": false,
|
||||
"crypto": false,
|
||||
"os": false,
|
||||
"fs": false,
|
||||
},
|
||||
}
|
||||
webpackConfig.resolve.fallback = {
|
||||
buffer: require.resolve("buffer/"),
|
||||
process: false,
|
||||
util: false,
|
||||
url: false,
|
||||
zlib: false,
|
||||
stream: false,
|
||||
http: false,
|
||||
https: false,
|
||||
assert: false,
|
||||
crypto: false,
|
||||
os: false,
|
||||
fs: false,
|
||||
};
|
||||
|
||||
return webpackConfig;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
21
web/mv.js
Normal file
21
web/mv.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const sourceDir = path.join(__dirname, "build-temp");
|
||||
const targetDir = path.join(__dirname, "build");
|
||||
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Source directory "${sourceDir}" does not exist.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (fs.existsSync(targetDir)) {
|
||||
fs.rmSync(targetDir, {recursive: true, force: true});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Target directory "${targetDir}" has been deleted successfully.`);
|
||||
}
|
||||
|
||||
fs.renameSync(sourceDir, targetDir);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Renamed "${sourceDir}" to "${targetDir}" successfully.`);
|
@@ -57,6 +57,7 @@
|
||||
"scripts": {
|
||||
"start": "cross-env PORT=7001 craco start",
|
||||
"build": "craco build",
|
||||
"postbuild": "node mv.js",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject",
|
||||
"crowdin:sync": "crowdin upload && crowdin download",
|
||||
|
@@ -36,6 +36,7 @@ const {Footer, Content} = Layout;
|
||||
|
||||
import {setTwoToneColor} from "@ant-design/icons";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as Cookie from "cookie";
|
||||
|
||||
setTwoToneColor("rgb(87,52,211)");
|
||||
|
||||
@@ -269,7 +270,9 @@ class App extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
renderFooter() {
|
||||
renderFooter(logo, footerHtml) {
|
||||
logo = logo ?? this.state.logo;
|
||||
footerHtml = footerHtml ?? this.state.application?.footerHtml;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
|
||||
@@ -280,14 +283,14 @@ class App extends Component {
|
||||
}
|
||||
}>
|
||||
{
|
||||
this.state.application?.footerHtml && this.state.application.footerHtml !== "" ?
|
||||
footerHtml && footerHtml !== "" ?
|
||||
<React.Fragment>
|
||||
<div dangerouslySetInnerHTML={{__html: this.state.application.footerHtml}} />
|
||||
<div dangerouslySetInnerHTML={{__html: footerHtml}} />
|
||||
</React.Fragment>
|
||||
: (
|
||||
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
||||
<React.Fragment>
|
||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
|
||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
|
||||
</React.Fragment>
|
||||
)
|
||||
)
|
||||
@@ -308,7 +311,7 @@ class App extends Component {
|
||||
AI Assistant
|
||||
</a>
|
||||
</Tooltip>
|
||||
<a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://ai.casbin.com"}>
|
||||
<a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={`${Conf.AiAssistantUrl}`}>
|
||||
<ShareAltOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
|
||||
</a>
|
||||
<a className="custom-link" style={{float: "right", marginRight: "30px", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://github.com/casibase/casibase"}>
|
||||
@@ -326,7 +329,7 @@ class App extends Component {
|
||||
}}
|
||||
visible={this.state.isAiAssistantOpen}
|
||||
>
|
||||
<iframe id="iframeHelper" title={"iframeHelper"} src={"https://ai.casbin.com/?isRaw=1"} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
||||
<iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -358,13 +361,37 @@ class App extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onLoginSuccess(redirectUrl) {
|
||||
window.google?.accounts?.id?.cancel();
|
||||
if (redirectUrl) {
|
||||
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
||||
}
|
||||
this.getAccount();
|
||||
}
|
||||
|
||||
renderPage() {
|
||||
if (this.isDoorPages()) {
|
||||
let themeData = this.state.themeData;
|
||||
let logo = this.state.logo;
|
||||
let footerHtml = null;
|
||||
if (this.state.organization === undefined) {
|
||||
const curCookie = Cookie.parse(document.cookie);
|
||||
if (curCookie["organizationTheme"] && curCookie["organizationTheme"] !== "null") {
|
||||
themeData = JSON.parse(curCookie["organizationTheme"]);
|
||||
}
|
||||
if (curCookie["organizationLogo"] && curCookie["organizationLogo"] !== "") {
|
||||
logo = curCookie["organizationLogo"];
|
||||
}
|
||||
if (curCookie["organizationFootHtml"] && curCookie["organizationFootHtml"] !== "") {
|
||||
footerHtml = curCookie["organizationFootHtml"];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={{
|
||||
token: {
|
||||
colorPrimary: this.state.themeData.colorPrimary,
|
||||
borderRadius: this.state.themeData.borderRadius,
|
||||
colorPrimary: themeData.colorPrimary,
|
||||
borderRadius: themeData.borderRadius,
|
||||
},
|
||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||
}}>
|
||||
@@ -382,26 +409,20 @@ class App extends Component {
|
||||
application: application,
|
||||
});
|
||||
}}
|
||||
onLoginSuccess={(redirectUrl) => {
|
||||
window.google?.accounts?.id?.cancel();
|
||||
if (redirectUrl) {
|
||||
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
||||
}
|
||||
this.getAccount();
|
||||
}}
|
||||
onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
|
||||
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
||||
updataThemeData={this.setTheme}
|
||||
/> :
|
||||
<Switch>
|
||||
<Route exact path="/callback" component={AuthCallback} />
|
||||
<Route exact path="/callback/saml" component={SamlCallback} />
|
||||
<Route exact path="/callback" render={(props) => <AuthCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||
<Route exact path="/callback/saml" render={(props) => <SamlCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
|
||||
</Switch>
|
||||
}
|
||||
</Content>
|
||||
{
|
||||
this.renderFooter()
|
||||
this.renderFooter(logo, footerHtml)
|
||||
}
|
||||
{
|
||||
this.renderAiAssistant()
|
||||
|
@@ -19,6 +19,7 @@ import "codemirror/mode/properties/properties";
|
||||
import * as Setting from "./Setting";
|
||||
import IframeEditor from "./IframeEditor";
|
||||
import {Tabs} from "antd";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
|
||||
@@ -68,8 +69,8 @@ const CasbinEditor = ({model, onModelTextChange}) => {
|
||||
return (
|
||||
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
|
||||
<TabPane tab="Basic Editor" key="basic" />
|
||||
<TabPane tab="Advanced Editor" key="advanced" />
|
||||
<TabPane tab={i18next.t("model:Basic Editor")} key="basic" />
|
||||
<TabPane tab={i18next.t("model:Advanced Editor")} key="advanced" />
|
||||
</Tabs>
|
||||
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
|
||||
{activeKey === "advanced" ? (
|
||||
|
@@ -31,3 +31,6 @@ export const ThemeDefault = {
|
||||
};
|
||||
|
||||
export const CustomFooter = null;
|
||||
|
||||
// Blank or null to hide Ai Assistant button
|
||||
export const AiAssistantUrl = "https://ai.casbin.com";
|
||||
|
@@ -111,7 +111,7 @@ class EntryPage extends React.Component {
|
||||
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
||||
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
||||
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
||||
style={{margin: "0 auto"}} />
|
||||
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
|
||||
<Switch>
|
||||
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||
|
@@ -33,18 +33,6 @@ class GroupListPage extends BaseListPage {
|
||||
}
|
||||
UNSAFE_componentWillMount() {
|
||||
super.UNSAFE_componentWillMount();
|
||||
this.getGroups(this.state.owner);
|
||||
}
|
||||
|
||||
getGroups(organizationName) {
|
||||
GroupBackend.getGroups(organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
groups: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newGroup() {
|
||||
@@ -188,12 +176,8 @@ class GroupListPage extends BaseListPage {
|
||||
{record.parentId}
|
||||
</Link>;
|
||||
}
|
||||
const parentGroup = this.state.groups.find((group) => group.name === text);
|
||||
if (parentGroup === undefined) {
|
||||
return "";
|
||||
}
|
||||
return <Link to={`/groups/${parentGroup.owner}/${parentGroup.name}`}>
|
||||
{parentGroup?.displayName}
|
||||
return <Link to={`/groups/${record.owner}/${record.parentId}`}>
|
||||
{record?.parentName}
|
||||
</Link>;
|
||||
},
|
||||
},
|
||||
@@ -215,12 +199,11 @@ class GroupListPage extends BaseListPage {
|
||||
width: "180px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
const haveChildren = this.state.groups.find((group) => group.parentId === record.id) !== undefined;
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
disabled={haveChildren}
|
||||
disabled={record.haveChildren}
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteGroup(index)}
|
||||
>
|
||||
|
@@ -17,6 +17,7 @@ import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} fro
|
||||
const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) => {
|
||||
const iframeRef = useRef(null);
|
||||
const [iframeReady, setIframeReady] = useState(false);
|
||||
const currentLang = localStorage.getItem("language") || "en";
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event) => {
|
||||
@@ -26,24 +27,31 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
|
||||
onModelTextChange(event.data.modelText);
|
||||
} else if (event.data.type === "iframeReady") {
|
||||
setIframeReady(true);
|
||||
iframeRef.current?.contentWindow.postMessage({
|
||||
type: "initializeModel",
|
||||
modelText: initialModelText,
|
||||
}, "*");
|
||||
if (initialModelText && iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.postMessage({
|
||||
type: "initializeModel",
|
||||
modelText: initialModelText,
|
||||
lang: currentLang,
|
||||
}, "*");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [onModelTextChange, initialModelText]);
|
||||
}, [onModelTextChange, initialModelText, currentLang]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getModelText: () => {
|
||||
iframeRef.current?.contentWindow.postMessage({type: "getModelText"}, "*");
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.postMessage({
|
||||
type: "getModelText",
|
||||
}, "*");
|
||||
}
|
||||
},
|
||||
updateModelText: (newModelText) => {
|
||||
if (iframeReady) {
|
||||
iframeRef.current?.contentWindow.postMessage({
|
||||
if (iframeReady && iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.postMessage({
|
||||
type: "updateModelText",
|
||||
modelText: newModelText,
|
||||
}, "*");
|
||||
@@ -54,7 +62,7 @@ const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) =>
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src="https://editor.casbin.org/model-editor"
|
||||
src={`https://editor.casbin.org/model-editor?lang=${currentLang}`}
|
||||
frameBorder="0"
|
||||
width="100%"
|
||||
height="500px"
|
||||
|
@@ -106,6 +106,22 @@ class InvitationEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
copySignupLink() {
|
||||
let defaultApplication;
|
||||
if (this.state.invitation.owner === "built-in") {
|
||||
defaultApplication = "app-built-in";
|
||||
} else {
|
||||
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
||||
defaultApplication = selectedOrganization.defaultApplication;
|
||||
if (!defaultApplication) {
|
||||
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}
|
||||
|
||||
renderInvitation() {
|
||||
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
|
||||
return (
|
||||
@@ -114,16 +130,7 @@ class InvitationEditPage extends React.Component {
|
||||
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}
|
||||
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} onClick={() => {
|
||||
let defaultApplication;
|
||||
if (this.state.invitation.owner === "built-in") {
|
||||
defaultApplication = "app-built-in";
|
||||
} else {
|
||||
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
||||
}
|
||||
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}}>
|
||||
<Button style={{marginLeft: "20px"}} onClick={_ => this.copySignupLink()}>
|
||||
{i18next.t("application:Copy signup page URL")}
|
||||
</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
@@ -330,16 +337,7 @@ class InvitationEditPage extends React.Component {
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} size="large" onClick={() => {
|
||||
let defaultApplication;
|
||||
if (this.state.invitation.owner === "built-in") {
|
||||
defaultApplication = "app-built-in";
|
||||
} else {
|
||||
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
||||
}
|
||||
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}}>
|
||||
<Button style={{marginLeft: "20px"}} size="large" onClick={_ => this.copySignupLink()}>
|
||||
{i18next.t("application:Copy signup page URL")}
|
||||
</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
|
@@ -228,6 +228,21 @@ class LdapEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("general:Password type"), i18next.t("general:Password type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.ldap.passwordType ?? []} onChange={(value => {
|
||||
this.updateLdapField("passwordType", value);
|
||||
})}
|
||||
>
|
||||
<Option key={"Plain"} value={"Plain"}>{i18next.t("general:Plain")}</Option>
|
||||
<Option key={"SSHA"} value={"SSHA"} >SSHA</Option>
|
||||
<Option key={"MD5"} value={"MD5"} >MD5</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||
|
@@ -192,11 +192,15 @@ function ManagementPage(props) {
|
||||
themeAlgorithm={props.themeAlgorithm}
|
||||
onChange={props.setLogoAndThemeAlgorithm} />
|
||||
<LanguageSelect languages={props.account.organization.languages} />
|
||||
<Tooltip title="Click to open AI assitant">
|
||||
<div className="select-box" onClick={props.openAiAssistant}>
|
||||
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{
|
||||
Conf.AiAssistantUrl?.trim() && (
|
||||
<Tooltip title="Click to open AI assistant">
|
||||
<div className="select-box" onClick={props.openAiAssistant}>
|
||||
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
<OpenTour />
|
||||
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
||||
<OrganizationSelect
|
||||
@@ -237,7 +241,7 @@ function ManagementPage(props) {
|
||||
<Link to="/">
|
||||
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
||||
</Link>,
|
||||
disabled: true,
|
||||
disabled: true, key: "logo",
|
||||
style: {
|
||||
padding: 0,
|
||||
height: "auto",
|
||||
@@ -319,7 +323,35 @@ function ManagementPage(props) {
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
const navItems = props.account.organization.navItems;
|
||||
|
||||
if (!Array.isArray(navItems)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (navItems.includes("all")) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const resFiltered = res.map(item => {
|
||||
if (!Array.isArray(item.children)) {
|
||||
return item;
|
||||
}
|
||||
const filteredChildren = [];
|
||||
item.children.forEach(itemChild => {
|
||||
if (navItems.includes(itemChild.key)) {
|
||||
filteredChildren.push(itemChild);
|
||||
}
|
||||
});
|
||||
|
||||
item.children = filteredChildren;
|
||||
return item;
|
||||
});
|
||||
|
||||
return resFiltered.filter(item => {
|
||||
if (item.key === "#" || item.key === "logo") {return true;}
|
||||
return Array.isArray(item.children) && item.children.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function renderLoginIfNotLoggedIn(component) {
|
||||
|
@@ -26,6 +26,7 @@ import LdapTable from "./table/LdapTable";
|
||||
import AccountTable from "./table/AccountTable";
|
||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||
import MfaTable from "./table/MfaTable";
|
||||
import {NavItemTree} from "./common/NavItemTree";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@@ -522,6 +523,21 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Navbar items"), i18next.t("general:Navbar items - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<NavItemTree
|
||||
disabled={!Setting.isAdminUser(this.props.account)}
|
||||
checkedKeys={this.state.organization.navItems ?? ["all"]}
|
||||
defaultExpandedKeys={["all"]}
|
||||
onCheck={(checked, _) => {
|
||||
this.updateOrganizationField("navItems", checked);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
||||
|
@@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
|
||||
payment: payment,
|
||||
});
|
||||
if (payment.state === "Created") {
|
||||
if (["PayPal", "Stripe", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
|
||||
if (["PayPal", "Stripe", "AirWallex", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
|
||||
this.setState({
|
||||
timeout: setTimeout(async() => {
|
||||
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
||||
|
@@ -238,6 +238,8 @@ class ProductBuyPage extends React.Component {
|
||||
text = i18next.t("product:PayPal");
|
||||
} else if (provider.type === "Stripe") {
|
||||
text = i18next.t("product:Stripe");
|
||||
} else if (provider.type === "AirWallex") {
|
||||
text = i18next.t("product:AirWallex");
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -297,6 +297,8 @@ class ProviderEditPage extends React.Component {
|
||||
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
||||
} else if (provider.type === "WeChat Pay") {
|
||||
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
||||
} else if (provider.type === "CUCloud") {
|
||||
return Setting.getLabel(i18next.t("provider:Account ID"), i18next.t("provider:Account ID - Tooltip"));
|
||||
} else {
|
||||
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
||||
}
|
||||
@@ -393,6 +395,9 @@ class ProviderEditPage extends React.Component {
|
||||
} else if (provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") {
|
||||
text = i18next.t("provider:App Key");
|
||||
tooltip = i18next.t("provider:App Key - Tooltip");
|
||||
} else if (provider.type === "CUCloud") {
|
||||
text = i18next.t("provider:Topic name");
|
||||
tooltip = i18next.t("provider:Topic name - Tooltip");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +467,39 @@ class ProviderEditPage extends React.Component {
|
||||
this.updateProviderField("issuerUrl", issuerUrl);
|
||||
}
|
||||
|
||||
fetchSamlMetadata() {
|
||||
this.setState({
|
||||
metadataLoading: true,
|
||||
});
|
||||
fetch(this.state.requestUrl, {
|
||||
method: "GET",
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
return Promise.reject("error");
|
||||
}
|
||||
return res.text();
|
||||
}).then(text => {
|
||||
this.updateProviderField("metadata", text);
|
||||
this.parseSamlMetadata();
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
}).catch(err => {
|
||||
Setting.showMessage("error", err.message);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
metadataLoading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parseSamlMetadata() {
|
||||
try {
|
||||
this.loadSamlConfiguration();
|
||||
Setting.showMessage("success", i18next.t("provider:Parse metadata successfully"));
|
||||
} catch (err) {
|
||||
Setting.showMessage("error", i18next.t("provider:Can not parse metadata"));
|
||||
}
|
||||
}
|
||||
|
||||
renderProvider() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
@@ -633,6 +671,20 @@ class ProviderEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.category === "OAuth" ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Email regex"), i18next.t("provider:Email regex - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TextArea rows={4} value={this.state.provider.emailRegex} onChange={e => {
|
||||
this.updateProviderField("emailRegex", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.provider.type === "Custom" ? (
|
||||
<React.Fragment>
|
||||
@@ -757,7 +809,7 @@ class ProviderEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Apple" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" ? null : (
|
||||
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && this.state.provider.type !== "Apple" && this.state.provider.type !== "Aliyun Captcha" && this.state.provider.type !== "WeChat Pay" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" && this.state.provider.type !== "CUCloud" ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
@@ -770,7 +822,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
(this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
|
||||
(this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{this.getClientSecret2Label(this.state.provider)} :
|
||||
@@ -856,9 +908,9 @@ class ProviderEditPage extends React.Component {
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email"].includes(this.state.provider.type) ? (
|
||||
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "CUCloud"].includes(this.state.provider.type) ? (
|
||||
<div>
|
||||
{["Local File System"].includes(this.state.provider.type) ? null : (
|
||||
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
|
||||
@@ -870,7 +922,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
||||
@@ -882,7 +934,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{["Casdoor"].includes(this.state.provider.type) ?
|
||||
@@ -896,7 +948,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
||||
@@ -908,7 +960,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{["Custom HTTP SMS", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
|
||||
{["Custom HTTP SMS", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||
@@ -932,7 +984,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
) : null}
|
||||
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor"].includes(this.state.provider.type) ? (
|
||||
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO", "CUCloud"].includes(this.state.provider.type) ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{["Casdoor"].includes(this.state.provider.type) ?
|
||||
@@ -971,7 +1023,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
) : null}
|
||||
{["Custom HTTP"].includes(this.state.provider.type) ? (
|
||||
{["Custom HTTP", "CUCloud"].includes(this.state.provider.type) ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
|
||||
@@ -983,7 +1035,7 @@ class ProviderEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
) : null}
|
||||
{["Google Chat"].includes(this.state.provider.type) ? (
|
||||
{["Google Chat", "CUCloud"].includes(this.state.provider.type) ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
||||
@@ -1223,6 +1275,21 @@ class ProviderEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Metadata url"), i18next.t("provider:Metadata url - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={6} >
|
||||
<Input value={this.state.requestUrl} onChange={e => {
|
||||
this.setState({
|
||||
requestUrl: e.target.value,
|
||||
});
|
||||
}} />
|
||||
</Col>
|
||||
<Col span={16} >
|
||||
<Button type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
||||
@@ -1236,14 +1303,7 @@ class ProviderEditPage extends React.Component {
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col style={{marginTop: "5px"}} span={2} />
|
||||
<Col span={2}>
|
||||
<Button type="primary" onClick={() => {
|
||||
try {
|
||||
this.loadSamlConfiguration();
|
||||
Setting.showMessage("success", i18next.t("provider:Parse metadata successfully"));
|
||||
} catch (err) {
|
||||
Setting.showMessage("error", i18next.t("provider:Can not parse metadata"));
|
||||
}
|
||||
}}>
|
||||
<Button type="primary" onClick={() => {this.parseSamlMetadata();}}>
|
||||
{i18next.t("provider:Parse")}
|
||||
</Button>
|
||||
</Col>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Select, Tag, Tooltip, message, theme} from "antd";
|
||||
import {Button, Select, Tag, Tooltip, message, theme} from "antd";
|
||||
import {QuestionCircleTwoTone} from "@ant-design/icons";
|
||||
import {isMobile as isMobileDevice} from "react-device-detect";
|
||||
import "./i18n";
|
||||
@@ -25,6 +25,8 @@ import {Helmet} from "react-helmet";
|
||||
import * as Conf from "./Conf";
|
||||
import * as phoneNumber from "libphonenumber-js";
|
||||
import moment from "moment";
|
||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./auth/mfa/MfaAuthVerifyForm";
|
||||
import {EmailMfaType, SmsMfaType, TotpMfaType} from "./auth/MfaSetupPage";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@@ -233,6 +235,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/casdoor.png`,
|
||||
url: "https://casdoor.org/docs/provider/storage/overview",
|
||||
},
|
||||
"CUCloud OSS": {
|
||||
logo: `${StaticBaseUrl}/img/social_cucloud.png`,
|
||||
url: "https://www.cucloud.cn/product/oss.html",
|
||||
},
|
||||
},
|
||||
SAML: {
|
||||
"Aliyun IDaaS": {
|
||||
@@ -273,6 +279,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/social_stripe.png`,
|
||||
url: "https://stripe.com/",
|
||||
},
|
||||
"AirWallex": {
|
||||
logo: `${StaticBaseUrl}/img/payment_airwallex.svg`,
|
||||
url: "https://airwallex.com/",
|
||||
},
|
||||
"GC": {
|
||||
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
||||
url: "https://gc.org",
|
||||
@@ -401,6 +411,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/social_viber.png`,
|
||||
url: "https://www.viber.com/",
|
||||
},
|
||||
"CUCloud": {
|
||||
logo: `${StaticBaseUrl}/img/cucloud.png`,
|
||||
url: "https://www.cucloud.cn/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -920,7 +934,7 @@ export function getClickable(text) {
|
||||
return (
|
||||
<a onClick={() => {
|
||||
copy(text);
|
||||
showMessage("success", "Copied to clipboard");
|
||||
showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
}}>
|
||||
{text}
|
||||
</a>
|
||||
@@ -981,6 +995,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Bilibili", name: "Bilibili"},
|
||||
{id: "Okta", name: "Okta"},
|
||||
{id: "Douyin", name: "Douyin"},
|
||||
{id: "Kwai", name: "Kwai"},
|
||||
{id: "Line", name: "Line"},
|
||||
{id: "Amazon", name: "Amazon"},
|
||||
{id: "Auth0", name: "Auth0"},
|
||||
@@ -1078,6 +1093,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Google Cloud Storage", name: "Google Cloud Storage"},
|
||||
{id: "Synology", name: "Synology"},
|
||||
{id: "Casdoor", name: "Casdoor"},
|
||||
{id: "CUCloud OSS", name: "CUCloud OSS"},
|
||||
]
|
||||
);
|
||||
} else if (category === "SAML") {
|
||||
@@ -1094,6 +1110,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "WeChat Pay", name: "WeChat Pay"},
|
||||
{id: "PayPal", name: "PayPal"},
|
||||
{id: "Stripe", name: "Stripe"},
|
||||
{id: "AirWallex", name: "AirWallex"},
|
||||
{id: "GC", name: "GC"},
|
||||
]);
|
||||
} else if (category === "Captcha") {
|
||||
@@ -1131,6 +1148,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Reddit", name: "Reddit"},
|
||||
{id: "Rocket Chat", name: "Rocket Chat"},
|
||||
{id: "Viber", name: "Viber"},
|
||||
{id: "CUCloud", name: "CUCloud"},
|
||||
]);
|
||||
} else {
|
||||
return [];
|
||||
@@ -1171,7 +1189,7 @@ export function renderLogo(application) {
|
||||
|
||||
function isSigninMethodEnabled(application, signinMethod) {
|
||||
if (application && application.signinMethods) {
|
||||
return application.signinMethods.filter(item => item.name === signinMethod && item.rule !== "Hide-Password").length > 0;
|
||||
return application.signinMethods.filter(item => item.name === signinMethod && item.rule !== "Hide password").length > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -1385,7 +1403,13 @@ export function getTag(color, text, icon) {
|
||||
}
|
||||
|
||||
export function getApplicationName(application) {
|
||||
return `${application?.owner}/${application?.name}`;
|
||||
let name = `${application?.owner}/${application?.name}`;
|
||||
|
||||
if (application?.isShared && application?.organization) {
|
||||
name += `-org-${application.organization}`;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
export function getApplicationDisplayName(application) {
|
||||
@@ -1550,25 +1574,25 @@ export function getDefaultHtmlEmailContent() {
|
||||
|
||||
export function getCurrencyText(product) {
|
||||
if (product?.currency === "USD") {
|
||||
return i18next.t("product:USD");
|
||||
return i18next.t("currency:USD");
|
||||
} else if (product?.currency === "CNY") {
|
||||
return i18next.t("product:CNY");
|
||||
return i18next.t("currency:CNY");
|
||||
} else if (product?.currency === "EUR") {
|
||||
return i18next.t("product:EUR");
|
||||
return i18next.t("currency:EUR");
|
||||
} else if (product?.currency === "JPY") {
|
||||
return i18next.t("product:JPY");
|
||||
return i18next.t("currency:JPY");
|
||||
} else if (product?.currency === "GBP") {
|
||||
return i18next.t("product:GBP");
|
||||
return i18next.t("currency:GBP");
|
||||
} else if (product?.currency === "AUD") {
|
||||
return i18next.t("product:AUD");
|
||||
return i18next.t("currency:AUD");
|
||||
} else if (product?.currency === "CAD") {
|
||||
return i18next.t("product:CAD");
|
||||
return i18next.t("currency:CAD");
|
||||
} else if (product?.currency === "CHF") {
|
||||
return i18next.t("product:CHF");
|
||||
return i18next.t("currency:CHF");
|
||||
} else if (product?.currency === "HKD") {
|
||||
return i18next.t("product:HKD");
|
||||
return i18next.t("currency:HKD");
|
||||
} else if (product?.currency === "SGD") {
|
||||
return i18next.t("product:SGD");
|
||||
return i18next.t("currency:SGD");
|
||||
} else {
|
||||
return "(Unknown currency)";
|
||||
}
|
||||
@@ -1577,3 +1601,114 @@ export function getCurrencyText(product) {
|
||||
export function isDarkTheme(themeAlgorithm) {
|
||||
return themeAlgorithm && themeAlgorithm.includes("dark");
|
||||
}
|
||||
|
||||
function getPreferredMfaProp(mfaProps) {
|
||||
for (const i in mfaProps) {
|
||||
if (mfaProps[i].isPreffered) {
|
||||
return mfaProps[i];
|
||||
}
|
||||
}
|
||||
return mfaProps[0];
|
||||
}
|
||||
|
||||
export function checkLoginMfa(res, body, params, handleLogin, componentThis, requireRedirect = null) {
|
||||
if (res.data === RequiredMfa) {
|
||||
if (!requireRedirect) {
|
||||
componentThis.props.onLoginSuccess(window.location.href);
|
||||
} else {
|
||||
componentThis.props.onLoginSuccess(requireRedirect);
|
||||
}
|
||||
} else if (res.data === NextMfa) {
|
||||
componentThis.setState({
|
||||
mfaProps: res.data2,
|
||||
selectedMfaProp: getPreferredMfaProp(res.data2),
|
||||
}, () => {
|
||||
body["providerBack"] = body["provider"];
|
||||
body["provider"] = "";
|
||||
componentThis.setState({
|
||||
getVerifyTotp: () => renderMfaAuthVerifyForm(body, params, handleLogin, componentThis),
|
||||
});
|
||||
});
|
||||
} else if (res.data === "SelectPlan") {
|
||||
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
|
||||
const pricing = res.data2;
|
||||
goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${body.username}`);
|
||||
} else if (res.data === "BuyPlanResult") {
|
||||
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
|
||||
const sub = res.data2;
|
||||
goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
|
||||
} else {
|
||||
handleLogin(res);
|
||||
}
|
||||
}
|
||||
|
||||
export function getApplicationObj(componentThis) {
|
||||
return componentThis.props.application;
|
||||
}
|
||||
|
||||
export function parseOffset(offset) {
|
||||
if (offset === 2 || offset === 4 || inIframe() || isMobile()) {
|
||||
return "0 auto";
|
||||
}
|
||||
if (offset === 1) {
|
||||
return "0 10%";
|
||||
}
|
||||
if (offset === 3) {
|
||||
return "0 60%";
|
||||
}
|
||||
}
|
||||
|
||||
function renderMfaAuthVerifyForm(values, authParams, onSuccess, componentThis) {
|
||||
return (
|
||||
<div>
|
||||
<MfaAuthVerifyForm
|
||||
mfaProps={componentThis.state.selectedMfaProp}
|
||||
formValues={values}
|
||||
authParams={authParams}
|
||||
application={getApplicationObj(componentThis)}
|
||||
onFail={(errorMessage) => {
|
||||
showMessage("error", errorMessage);
|
||||
}}
|
||||
onSuccess={(res) => onSuccess(res)}
|
||||
/>
|
||||
<div>
|
||||
{
|
||||
componentThis.state.mfaProps.map((mfa) => {
|
||||
if (componentThis.state.selectedMfaProp.mfaType === mfa.mfaType) {return null;}
|
||||
let mfaI18n = "";
|
||||
switch (mfa.mfaType) {
|
||||
case SmsMfaType: mfaI18n = i18next.t("mfa:Use SMS"); break;
|
||||
case TotpMfaType: mfaI18n = i18next.t("mfa:Use Authenticator App"); break ;
|
||||
case EmailMfaType: mfaI18n = i18next.t("mfa:Use Email") ;break;
|
||||
}
|
||||
return <div key={mfa.mfaType}><Button type={"link"} onClick={() => {
|
||||
componentThis.setState({
|
||||
selectedMfaProp: mfa,
|
||||
});
|
||||
}}>{mfaI18n}</Button></div>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export function renderLoginPanel(application, getInnerComponent, componentThis) {
|
||||
return (
|
||||
<div className="login-content" style={{margin: componentThis.props.preview ?? parseOffset(application.formOffset)}}>
|
||||
{inIframe() || isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
|
||||
{inIframe() || !isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCssMobile}} />}
|
||||
<div className={isDarkTheme(componentThis.props.themeAlgorithm) ? "login-panel-dark" : "login-panel"}>
|
||||
<div className="side-image" style={{display: application.formOffset !== 4 ? "none" : null}}>
|
||||
<div dangerouslySetInnerHTML={{__html: application.formSideHtml}} />
|
||||
</div>
|
||||
<div className="login-form">
|
||||
<div>
|
||||
{
|
||||
getInnerComponent()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import {authConfig} from "./Auth";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import RedirectForm from "../common/RedirectForm";
|
||||
import {renderLoginPanel} from "../Setting";
|
||||
|
||||
class AuthCallback extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -131,19 +132,23 @@ class AuthCallback extends React.Component {
|
||||
// user is using casdoor as cas sso server, and wants the ticket to be acquired
|
||||
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
let msg = "Logged in successfully.";
|
||||
if (casService === "") {
|
||||
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
|
||||
msg += "Now you can visit apps protected by Casdoor.";
|
||||
}
|
||||
Setting.showMessage("success", msg);
|
||||
const handleCasLogin = (res) => {
|
||||
let msg = "Logged in successfully.";
|
||||
if (casService === "") {
|
||||
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
|
||||
msg += "Now you can visit apps protected by Casdoor.";
|
||||
}
|
||||
Setting.showMessage("success", msg);
|
||||
|
||||
if (casService !== "") {
|
||||
const st = res.data;
|
||||
const newUrl = new URL(casService);
|
||||
newUrl.searchParams.append("ticket", st);
|
||||
window.location.href = newUrl.toString();
|
||||
}
|
||||
if (casService !== "") {
|
||||
const st = res.data;
|
||||
const newUrl = new URL(casService);
|
||||
newUrl.searchParams.append("ticket", st);
|
||||
window.location.href = newUrl.toString();
|
||||
}
|
||||
};
|
||||
|
||||
Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
@@ -159,54 +164,58 @@ class AuthCallback extends React.Component {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const responseType = this.getResponseType();
|
||||
if (responseType === "login") {
|
||||
if (res.data2) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
Setting.showMessage("success", "Logged in successfully");
|
||||
// Setting.goToLinkSoft(this, "/");
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
} else if (responseType === "code") {
|
||||
if (res.data2) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const code = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
// Setting.showMessage("success", `Authorization code: ${res.data}`);
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
if (res.data2) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const token = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
|
||||
} else if (responseType === "link") {
|
||||
const from = innerParams.get("from");
|
||||
Setting.goToLinkSoftOrJumpSelf(this, from);
|
||||
} else if (responseType === "saml") {
|
||||
if (res.data2.method === "POST") {
|
||||
this.setState({
|
||||
samlResponse: res.data,
|
||||
redirectUrl: res.data2.redirectUrl,
|
||||
relayState: oAuthParams.relayState,
|
||||
});
|
||||
} else {
|
||||
if (res.data2.needUpdatePassword) {
|
||||
const handleLogin = (res) => {
|
||||
if (responseType === "login") {
|
||||
if (res.data2) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2.redirectUrl;
|
||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
Setting.showMessage("success", "Logged in successfully");
|
||||
// Setting.goToLinkSoft(this, "/");
|
||||
const link = Setting.getFromLink();
|
||||
Setting.goToLink(link);
|
||||
} else if (responseType === "code") {
|
||||
if (res.data2) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const code = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
|
||||
// Setting.showMessage("success", `Authorization code: ${res.data}`);
|
||||
} else if (responseType === "token" || responseType === "id_token") {
|
||||
if (res.data2) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const token = res.data;
|
||||
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
|
||||
} else if (responseType === "link") {
|
||||
const from = innerParams.get("from");
|
||||
Setting.goToLinkSoftOrJumpSelf(this, from);
|
||||
} else if (responseType === "saml") {
|
||||
if (res.data2.method === "POST") {
|
||||
this.setState({
|
||||
samlResponse: res.data,
|
||||
redirectUrl: res.data2.redirectUrl,
|
||||
relayState: oAuthParams.relayState,
|
||||
});
|
||||
} else {
|
||||
if (res.data2.needUpdatePassword) {
|
||||
sessionStorage.setItem("signinUrl", signinUrl);
|
||||
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
|
||||
return;
|
||||
}
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2.redirectUrl;
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Setting.checkLoginMfa(res, body, oAuthParams, handleLogin, this, window.location.origin);
|
||||
} else {
|
||||
this.setState({
|
||||
msg: res.msg,
|
||||
@@ -220,6 +229,11 @@ class AuthCallback extends React.Component {
|
||||
return <RedirectForm samlResponse={this.state.samlResponse} redirectUrl={this.state.redirectUrl} relayState={this.state.relayState} />;
|
||||
}
|
||||
|
||||
if (this.state.getVerifyTotp !== undefined) {
|
||||
const application = Setting.getApplicationObj(this);
|
||||
return renderLoginPanel(application, this.state.getVerifyTotp, this);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||
{
|
||||
|
@@ -264,6 +264,9 @@ class ForgetPage extends React.Component {
|
||||
)
|
||||
}
|
||||
onValuesChange={(changedValues, allValues) => {
|
||||
if (!changedValues.dest) {
|
||||
return;
|
||||
}
|
||||
const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email";
|
||||
this.setState({
|
||||
dest: changedValues.dest,
|
||||
|
31
web/src/auth/KwaiLoginButton.js
Normal file
31
web/src/auth/KwaiLoginButton.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {createButton} from "react-social-login-buttons";
|
||||
import {StaticBaseUrl} from "../Setting";
|
||||
|
||||
function Icon({width = 24, height = 24}) {
|
||||
return <img src={`${StaticBaseUrl}/buttons/kwai.svg`} alt="Sign in with Kwai" style={{width: width, height: height}} />;
|
||||
}
|
||||
|
||||
const config = {
|
||||
text: "Sign in with Kwai",
|
||||
icon: Icon,
|
||||
style: {background: "#ffffff", color: "#000000"},
|
||||
activeStyle: {background: "#ededee"},
|
||||
};
|
||||
|
||||
const KwaiLoginButton = createButton(config);
|
||||
|
||||
export default KwaiLoginButton;
|
@@ -34,7 +34,7 @@ import {SendCodeInput} from "../common/SendCodeInput";
|
||||
import LanguageSelect from "../common/select/LanguageSelect";
|
||||
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
||||
import RedirectForm from "../common/RedirectForm";
|
||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||
import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||
@@ -438,25 +438,7 @@ class LoginPage extends React.Component {
|
||||
};
|
||||
|
||||
if (res.status === "ok") {
|
||||
if (res.data === NextMfa) {
|
||||
this.setState({
|
||||
getVerifyTotp: () => {
|
||||
return (
|
||||
<MfaAuthVerifyForm
|
||||
mfaProps={res.data2}
|
||||
formValues={values}
|
||||
authParams={casParams}
|
||||
application={this.getApplicationObj()}
|
||||
onFail={(errorMessage) => {
|
||||
Setting.showMessage("error", errorMessage);
|
||||
}}
|
||||
onSuccess={(res) => loginHandler(res)}
|
||||
/>);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
loginHandler(res);
|
||||
}
|
||||
Setting.checkLoginMfa(res, values, casParams, loginHandler, this);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
@@ -505,39 +487,13 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2.redirectUrl;
|
||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (res.status === "ok") {
|
||||
if (res.data === NextMfa) {
|
||||
this.setState({
|
||||
getVerifyTotp: () => {
|
||||
return (
|
||||
<MfaAuthVerifyForm
|
||||
mfaProps={res.data2}
|
||||
formValues={values}
|
||||
authParams={oAuthParams}
|
||||
application={this.getApplicationObj()}
|
||||
onFail={(errorMessage) => {
|
||||
Setting.showMessage("error", errorMessage);
|
||||
}}
|
||||
onSuccess={(res) => loginHandler(res)}
|
||||
/>);
|
||||
},
|
||||
});
|
||||
} else if (res.data === "SelectPlan") {
|
||||
// paid-user does not have active or pending subscription, go to application default pricing page to select-plan
|
||||
const pricing = res.data2;
|
||||
Setting.goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${values.username}`);
|
||||
} else if (res.data === "BuyPlanResult") {
|
||||
// paid-user has pending subscription, go to buy-plan/result apge to notify payment result
|
||||
const sub = res.data2;
|
||||
Setting.goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
|
||||
} else {
|
||||
loginHandler(res);
|
||||
}
|
||||
Setting.checkLoginMfa(res, values, oAuthParams, loginHandler, this);
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
}
|
||||
@@ -1150,7 +1106,7 @@ class LoginPage extends React.Component {
|
||||
]);
|
||||
|
||||
application?.signinMethods?.forEach((signinMethod) => {
|
||||
if (signinMethod.rule === "Hide-Password") {
|
||||
if (signinMethod.rule === "Hide password") {
|
||||
return;
|
||||
}
|
||||
const item = itemsMap.get(generateItemKey(signinMethod.name, signinMethod.rule));
|
||||
|
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
import CryptoJS from "crypto-js";
|
||||
import i18next from "i18next";
|
||||
import {Buffer} from "buffer";
|
||||
|
||||
export function getRandomKeyForObfuscator(obfuscatorType) {
|
||||
@@ -46,17 +45,17 @@ function encrypt(cipher, key, iv, password) {
|
||||
|
||||
export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) {
|
||||
if (passwordObfuscatorType === undefined) {
|
||||
return i18next.t("organization:failed to get password obfuscator");
|
||||
return "passwordObfuscatorType should not be undefined";
|
||||
} else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
|
||||
return "";
|
||||
} else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") {
|
||||
if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) {
|
||||
return "";
|
||||
} else {
|
||||
return `${i18next.t("organization:The password obfuscator key doesn't match the regex")}: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`;
|
||||
return `The password obfuscator key doesn't match the regex: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`;
|
||||
}
|
||||
} else {
|
||||
return `${i18next.t("organization:unsupported password obfuscator type")}: ${passwordObfuscatorType}`;
|
||||
return `unsupported password obfuscator type: ${passwordObfuscatorType}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -119,6 +119,10 @@ const authInfo = {
|
||||
scope: "user_info",
|
||||
endpoint: "https://open.douyin.com/platform/oauth/connect",
|
||||
},
|
||||
Kwai: {
|
||||
scope: "user_info",
|
||||
endpoint: "https://open.kuaishou.com/oauth2/connect",
|
||||
},
|
||||
Custom: {
|
||||
endpoint: "https://example.com/",
|
||||
},
|
||||
@@ -470,6 +474,8 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
|
||||
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Kwai") {
|
||||
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Custom") {
|
||||
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
|
||||
} else if (provider.type === "Bilibili") {
|
||||
|
@@ -40,9 +40,11 @@ import SteamLoginButton from "./SteamLoginButton";
|
||||
import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||
import OktaLoginButton from "./OktaLoginButton";
|
||||
import DouyinLoginButton from "./DouyinLoginButton";
|
||||
import KwaiLoginButton from "./KwaiLoginButton";
|
||||
import LoginButton from "./LoginButton";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import {WechatOfficialAccountModal} from "./Util";
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
function getSigninButton(provider) {
|
||||
const text = i18next.t("login:Sign in with {type}").replace("{type}", provider.displayName !== "" ? provider.displayName : provider.type);
|
||||
@@ -96,6 +98,8 @@ function getSigninButton(provider) {
|
||||
return <OktaLoginButton text={text} align={"center"} />;
|
||||
} else if (provider.type === "Douyin") {
|
||||
return <DouyinLoginButton text={text} align={"center"} />;
|
||||
} else if (provider.type === "Kwai") {
|
||||
return <KwaiLoginButton text={text} align={"center"} />;
|
||||
} else {
|
||||
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
|
||||
}
|
||||
@@ -111,10 +115,14 @@ function goToSamlUrl(provider, location) {
|
||||
|
||||
const relayState = `${clientId}&${state}&${providerName}&${realRedirectUri}&${redirectUri}`;
|
||||
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`, btoa(relayState)).then((res) => {
|
||||
if (res.data2 === "POST") {
|
||||
document.write(res.data);
|
||||
if (res.status === "ok") {
|
||||
if (res.data2 === "POST") {
|
||||
document.write(res.data);
|
||||
} else {
|
||||
window.location.href = res.data;
|
||||
}
|
||||
} else {
|
||||
window.location.href = res.data;
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import * as Util from "./Util";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import {authConfig} from "./Auth";
|
||||
import {renderLoginPanel} from "../Setting";
|
||||
|
||||
class SamlCallback extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -81,13 +82,26 @@ class SamlCallback extends React.Component {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const responseType = this.getResponseType(redirectUri);
|
||||
if (responseType === "login") {
|
||||
Setting.showMessage("success", "Logged in successfully");
|
||||
Setting.goToLink("/");
|
||||
} else if (responseType === "code") {
|
||||
const code = res.data;
|
||||
Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
|
||||
}
|
||||
const handleLogin = (res2) => {
|
||||
if (responseType === "login") {
|
||||
Setting.showMessage("success", "Logged in successfully");
|
||||
Setting.goToLink("/");
|
||||
} else if (responseType === "code") {
|
||||
const code = res2.data;
|
||||
Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
|
||||
}
|
||||
};
|
||||
Setting.checkLoginMfa(res, body, {
|
||||
clientId: clientId,
|
||||
responseType: responseType,
|
||||
redirectUri: messages[3],
|
||||
state: state,
|
||||
nonce: "",
|
||||
scope: "read",
|
||||
challengeMethod: "",
|
||||
codeChallenge: "",
|
||||
type: "code",
|
||||
}, handleLogin, this);
|
||||
} else {
|
||||
this.setState({
|
||||
msg: res.msg,
|
||||
@@ -97,6 +111,11 @@ class SamlCallback extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.getVerifyTotp !== undefined) {
|
||||
const application = Setting.getApplicationObj(this);
|
||||
return renderLoginPanel(application, this.state.getVerifyTotp, this, window.location.origin);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||
{
|
||||
|
@@ -33,7 +33,8 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
||||
|
||||
const verify = ({passcode}) => {
|
||||
setLoading(true);
|
||||
const values = {...formValues, passcode, mfaType};
|
||||
const values = {...formValues, passcode};
|
||||
values["mfaType"] = mfaProps.mfaType;
|
||||
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
|
||||
loginFunction(values, authParams).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
@@ -71,7 +72,7 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
||||
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
||||
{i18next.t("mfa:Multi-factor authentication")}
|
||||
</div>
|
||||
{mfaType === SmsMfaType || mfaType === EmailMfaType ? (
|
||||
{mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType ? (
|
||||
<Fragment>
|
||||
<div style={{marginBottom: 24}}>
|
||||
{i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")}
|
||||
|
@@ -9,11 +9,11 @@ export function MfaEnableForm({user, mfaType, secret, recoveryCodes, dest, count
|
||||
const data = {
|
||||
mfaType,
|
||||
secret,
|
||||
recoveryCodes,
|
||||
dest,
|
||||
countryCode,
|
||||
...user,
|
||||
};
|
||||
data["recoveryCodes"] = recoveryCodes[0];
|
||||
setLoading(true);
|
||||
MfaBackend.MfaSetupEnable(data).then(res => {
|
||||
if (res.status === "ok") {
|
||||
|
@@ -141,6 +141,9 @@ const Dashboard = (props) => {
|
||||
i18next.t("general:Certs"),
|
||||
i18next.t("general:Permissions"),
|
||||
i18next.t("general:Transactions"),
|
||||
i18next.t("general:Models"),
|
||||
i18next.t("general:Adapters"),
|
||||
i18next.t("general:Enforcers"),
|
||||
], top: "10%"},
|
||||
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
|
||||
xAxis: {type: "category", boundaryGap: false, data: dateArray},
|
||||
@@ -157,6 +160,9 @@ const Dashboard = (props) => {
|
||||
{name: i18next.t("general:Certs"), type: "line", data: dashboardData.certCounts},
|
||||
{name: i18next.t("general:Permissions"), type: "line", data: dashboardData.permissionCounts},
|
||||
{name: i18next.t("general:Transactions"), type: "line", data: dashboardData.transactionCounts},
|
||||
{name: i18next.t("general:Models"), type: "line", data: dashboardData.modelCounts},
|
||||
{name: i18next.t("general:Adapters"), type: "line", data: dashboardData.adapterCounts},
|
||||
{name: i18next.t("general:Enforcers"), type: "line", data: dashboardData.enforcerCounts},
|
||||
],
|
||||
};
|
||||
myChart.setOption(option);
|
||||
|
@@ -14,6 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Alert, Button, QRCode} from "antd";
|
||||
import copy from "copy-to-clipboard";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
@@ -26,7 +27,7 @@ export const generateCasdoorAppUrl = (accessToken, forQrCode = true) => {
|
||||
return {qrUrl, error};
|
||||
}
|
||||
|
||||
qrUrl = `casdoor-app://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
|
||||
qrUrl = `casdoor-authenticator://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
|
||||
|
||||
if (forQrCode && qrUrl.length >= 2000) {
|
||||
qrUrl = "";
|
||||
@@ -62,12 +63,8 @@ export const CasdoorAppUrl = ({accessToken}) => {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(qrUrl);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard"));
|
||||
} catch (err) {
|
||||
Setting.showMessage("error", i18next.t("general:Failed to copy"));
|
||||
}
|
||||
copy(qrUrl);
|
||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||
};
|
||||
|
||||
if (error) {
|
||||
@@ -82,14 +79,9 @@ export const CasdoorAppUrl = ({accessToken}) => {
|
||||
alignItems: "center",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
<span>{i18next.t("general:URL String")}</span>
|
||||
{window.isSecureContext && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleCopyUrl}
|
||||
style={{marginLeft: "10px"}}
|
||||
>
|
||||
{i18next.t("general:Copy URL")}
|
||||
<Button size="small" type="primary" onClick={handleCopyUrl} style={{marginLeft: "10px"}}>
|
||||
{i18next.t("resource:Copy Link")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
97
web/src/common/NavItemTree.js
Normal file
97
web/src/common/NavItemTree.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import i18next from "i18next";
|
||||
import {Tree} from "antd";
|
||||
import React from "react";
|
||||
|
||||
export const NavItemTree = ({disable, checkedKeys, defaultExpandedKeys, onCheck}) => {
|
||||
const NavItemNodes = [
|
||||
{
|
||||
title: i18next.t("organization:All"),
|
||||
key: "all",
|
||||
children: [
|
||||
{
|
||||
title: i18next.t("general:Home"),
|
||||
key: "/home-top",
|
||||
children: [
|
||||
{title: i18next.t("general:Dashboard"), key: "/"},
|
||||
{title: i18next.t("general:Shortcuts"), key: "/shortcuts"},
|
||||
{title: i18next.t("general:Apps"), key: "/apps"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:User Management"),
|
||||
key: "/orgs-top",
|
||||
children: [
|
||||
{title: i18next.t("general:Organizations"), key: "/organizations"},
|
||||
{title: i18next.t("general:Groups"), key: "/groups"},
|
||||
{title: i18next.t("general:Users"), key: "/users"},
|
||||
{title: i18next.t("general:Invitations"), key: "/invitations"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Identity"),
|
||||
key: "/applications-top",
|
||||
children: [
|
||||
{title: i18next.t("general:Applications"), key: "/applications"},
|
||||
{title: i18next.t("general:Providers"), key: "/providers"},
|
||||
{title: i18next.t("general:Resources"), key: "/resources"},
|
||||
{title: i18next.t("general:Certs"), key: "/certs"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Authorization"),
|
||||
key: "/roles-top",
|
||||
children: [
|
||||
{title: i18next.t("general:Applications"), key: "/roles"},
|
||||
{title: i18next.t("general:Permissions"), key: "/permissions"},
|
||||
{title: i18next.t("general:Models"), key: "/models"},
|
||||
{title: i18next.t("general:Adapters"), key: "/adapters"},
|
||||
{title: i18next.t("general:Enforcers"), key: "/enforcers"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Logging & Auditing"),
|
||||
key: "/sessions-top",
|
||||
children: [
|
||||
{title: i18next.t("general:Sessions"), key: "/sessions"},
|
||||
{title: i18next.t("general:Records"), key: "/records"},
|
||||
{title: i18next.t("general:Tokens"), key: "/tokens"},
|
||||
{title: i18next.t("general:Verifications"), key: "/verifications"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Business & Payments"),
|
||||
key: "/business-top",
|
||||
children: [
|
||||
{title: i18next.t("general:Products"), key: "/products"},
|
||||
{title: i18next.t("general:Payments"), key: "/payments"},
|
||||
{title: i18next.t("general:Plans"), key: "/plans"},
|
||||
{title: i18next.t("general:Pricings"), key: "/pricings"},
|
||||
{title: i18next.t("general:Subscriptions"), key: "/subscriptions"},
|
||||
{title: i18next.t("general:Transactions"), key: "/transactions"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Admin"),
|
||||
key: "/admin-top",
|
||||
children: [
|
||||
{title: i18next.t("general:System Info"), key: "/sysinfo"},
|
||||
{title: i18next.t("general:Syncers"), key: "/syncers"},
|
||||
{title: i18next.t("general:Webhooks"), key: "/webhooks"},
|
||||
{title: i18next.t("general:Swagger"), key: "/swagger"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Tree
|
||||
disabled={disable}
|
||||
checkable
|
||||
checkedKeys={checkedKeys}
|
||||
defaultExpandedKeys={defaultExpandedKeys}
|
||||
onCheck={onCheck}
|
||||
treeData={NavItemNodes}
|
||||
/>
|
||||
);
|
||||
};
|
@@ -105,7 +105,7 @@ export const PasswordModal = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const hasOldPassword = user.password !== "";
|
||||
const hasOldPassword = (user.password !== "" || user.ldap !== "");
|
||||
|
||||
return (
|
||||
<Row>
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Sending",
|
||||
"Submit and complete": "Submit and complete"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Go to writable demo site?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Home",
|
||||
"Home - Tooltip": "Home page of the application",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Unique random string",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Is enabled",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Make sure the password is correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Different combinations of password complexity options",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Password salt",
|
||||
"Password salt - Tooltip": "Random parameter used for password encryption",
|
||||
"Password type": "Password type",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Advanced Editor",
|
||||
"Basic Editor": "Basic Editor",
|
||||
"Edit Model": "Edit Model",
|
||||
"Model text": "Model text",
|
||||
"Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Modify rule",
|
||||
"New Organization": "New Organization",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Soft deletion",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Buy",
|
||||
"Buy Product": "Buy Product",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detail",
|
||||
"Detail - Tooltip": "Detail of product",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Sold",
|
||||
"Sold - Tooltip": "Quantity sold",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag of product",
|
||||
"Test buy page..": "Test buy page..",
|
||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||
"This product is currently not in sale.": "This product is currently not in sale.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Signup HTML - Edit",
|
||||
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Site key",
|
||||
"Site key - Tooltip": "Site key",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Keys",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Link",
|
||||
"Location": "Location",
|
||||
"Location - Tooltip": "City of residence",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Odesílání",
|
||||
"Submit and complete": "Odeslat a dokončit"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Upravit Enforcer",
|
||||
"New Enforcer": "Nový Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Přejít na zapisovatelnou demo stránku?",
|
||||
"Groups": "Skupiny",
|
||||
"Groups - Tooltip": "Skupiny - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Domů",
|
||||
"Home - Tooltip": "Domovská stránka aplikace",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Unikátní náhodný řetězec",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identita",
|
||||
"Invitations": "Pozvánky",
|
||||
"Is enabled": "Je povoleno",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Ujistěte se, že heslo je správné",
|
||||
"Password complexity options": "Možnosti složitosti hesla",
|
||||
"Password complexity options - Tooltip": "Různé kombinace možností složitosti hesla",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Heslová sůl",
|
||||
"Password salt - Tooltip": "Náhodný parametr použitý pro šifrování hesla",
|
||||
"Password type": "Typ hesla",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Použít SMS",
|
||||
"Use SMS verification code": "Použít ověřovací kód SMS",
|
||||
"Use a recovery code": "Použít obnovovací kód",
|
||||
"Verification failed": "Ověření selhalo",
|
||||
"Verify Code": "Ověřit kód",
|
||||
"Verify Password": "Ověřit heslo",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Pokročilý editor",
|
||||
"Basic Editor": "Základní editor",
|
||||
"Edit Model": "Upravit model",
|
||||
"Model text": "Text modelu",
|
||||
"Model text - Tooltip": "Casbin model řízení přístupu, včetně vestavěných modelů jako ACL, RBAC, ABAC, RESTful, atd. Můžete také vytvářet vlastní modely. Pro více informací navštivte webové stránky Casbin",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Upravit pravidlo",
|
||||
"New Organization": "Nová organizace",
|
||||
"Optional": "Volitelný",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Výzva",
|
||||
"Required": "Povinné",
|
||||
"Soft deletion": "Měkké smazání",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Koupit",
|
||||
"Buy Product": "Koupit produkt",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detail",
|
||||
"Detail - Tooltip": "Detail produktu",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Prodáno",
|
||||
"Sold - Tooltip": "Prodávané množství",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Štítek produktu",
|
||||
"Test buy page..": "Testovací stránka nákupu..",
|
||||
"There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.",
|
||||
"This product is currently not in sale.": "Tento produkt není momentálně v prodeji.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Upravit HTML pro registraci",
|
||||
"Signup HTML - Tooltip": "Vlastní HTML pro nahrazení výchozího stylu registrační stránky",
|
||||
"Signup group": "Skupina pro registraci",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Tiché",
|
||||
"Site key": "Klíč stránky",
|
||||
"Site key - Tooltip": "Nápověda ke klíči stránky",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Klíče",
|
||||
"Language": "Jazyk",
|
||||
"Language - Tooltip": "Jazyk - Nápověda",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Odkaz",
|
||||
"Location": "Místo",
|
||||
"Location - Tooltip": "Město bydliště",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Sendet",
|
||||
"Submit and complete": "Einreichen und abschließen"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Zuhause",
|
||||
"Home - Tooltip": "Homepage der Anwendung",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Einzigartiger Zufallsstring",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Ist aktiviert",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Stellen Sie sicher, dass das Passwort korrekt ist",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Passwort-Salt",
|
||||
"Password salt - Tooltip": "Zufälliger Parameter, der für die Verschlüsselung von Passwörtern verwendet wird",
|
||||
"Password type": "Passworttyp",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Erweiterter Editor",
|
||||
"Basic Editor": "Basis-Editor",
|
||||
"Edit Model": "Modell bearbeiten",
|
||||
"Model text": "Modelltext",
|
||||
"Model text - Tooltip": "Casbin Zugriffskontrollmodell inklusive integrierter Modelle wie ACL, RBAC, ABAC, RESTful, usw. Sie können auch benutzerdefinierte Modelle erstellen. Weitere Informationen finden Sie auf der Casbin-Website",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Regel ändern",
|
||||
"New Organization": "Neue Organisation",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Softe Löschung",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Kaufen",
|
||||
"Buy Product": "Produkt kaufen",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detail",
|
||||
"Detail - Tooltip": "Detail des Produkts",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Verkauft",
|
||||
"Sold - Tooltip": "Menge verkauft",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag des Produkts",
|
||||
"Test buy page..": "Testkaufseite.",
|
||||
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
|
||||
"This product is currently not in sale.": "Dieses Produkt steht derzeit nicht zum Verkauf.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Registrierung HTML - Bearbeiten",
|
||||
"Signup HTML - Tooltip": "Benutzerdefiniertes HTML zur Ersetzung des Standard-Registrierungs-Seitenstils",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Site-Key",
|
||||
"Site key - Tooltip": "Seitenschlüssel",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Schlüssel",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Link",
|
||||
"Location": "Ort",
|
||||
"Location - Tooltip": "Stadt des Wohnsitzes",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Sending",
|
||||
"Submit and complete": "Submit and complete"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Go to writable demo site?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Home",
|
||||
"Home - Tooltip": "Home page of the application",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Unique random string",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Is enabled",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Make sure the password is correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Different combinations of password complexity options",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Password salt",
|
||||
"Password salt - Tooltip": "Random parameter used for password encryption",
|
||||
"Password type": "Password type",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Advanced Editor",
|
||||
"Basic Editor": "Basic Editor",
|
||||
"Edit Model": "Edit Model",
|
||||
"Model text": "Model text",
|
||||
"Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Modify rule",
|
||||
"New Organization": "New Organization",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Soft deletion",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Buy",
|
||||
"Buy Product": "Buy Product",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detail",
|
||||
"Detail - Tooltip": "Detail of product",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,20 +757,12 @@
|
||||
"Sold": "Sold",
|
||||
"Sold - Tooltip": "Quantity sold",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag of product",
|
||||
"Test buy page..": "Test buy page..",
|
||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||
"This product is currently not in sale.": "This product is currently not in sale.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay",
|
||||
"EUR": "EUR",
|
||||
"JPY": "JPY",
|
||||
"GBP": "GBP",
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"HKD": "HKD",
|
||||
"SGD": "SGD"
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
"Access key": "Access key",
|
||||
@@ -901,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Signup HTML - Edit",
|
||||
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Site key",
|
||||
"Site key - Tooltip": "Site key",
|
||||
@@ -1165,6 +1179,7 @@
|
||||
"Keys": "Keys",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Link",
|
||||
"Location": "Location",
|
||||
"Location - Tooltip": "City of residence",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Envío",
|
||||
"Submit and complete": "Enviar y completar"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "¿Ir al sitio demo editable?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Hogar",
|
||||
"Home - Tooltip": "Página de inicio de la aplicación",
|
||||
"ID": "identificación",
|
||||
"ID - Tooltip": "Cadena aleatoria única",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Está habilitado",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Asegúrate de que la contraseña sea correcta",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Sal de contraseña",
|
||||
"Password salt - Tooltip": "Parámetro aleatorio utilizado para la encriptación de contraseñas",
|
||||
"Password type": "Tipo de contraseña",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Editor avanzado",
|
||||
"Basic Editor": "Editor básico",
|
||||
"Edit Model": "Editar modelo",
|
||||
"Model text": "Texto modelo",
|
||||
"Model text - Tooltip": "Modelo de control de acceso Casbin, incluyendo modelos integrados como ACL, RBAC, ABAC, RESTful, etc. También puede crear modelos personalizados. Para obtener más información, visite el sitio web de Casbin",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Modificar regla",
|
||||
"New Organization": "Nueva organización",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Eliminación suave",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Comprar",
|
||||
"Buy Product": "Comprar producto",
|
||||
"CNY": "Año Nuevo Chino (ANC)",
|
||||
"Detail": "Detalle",
|
||||
"Detail - Tooltip": "Detalle del producto",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Vendido",
|
||||
"Sold - Tooltip": "Cantidad vendida",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Etiqueta de producto",
|
||||
"Test buy page..": "Página de compra de prueba.",
|
||||
"There is no payment channel for this product.": "No hay canal de pago para este producto.",
|
||||
"This product is currently not in sale.": "Este producto actualmente no está a la venta.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Registro HTML - Editar",
|
||||
"Signup HTML - Tooltip": "HTML personalizado para reemplazar el estilo predeterminado de la página de registro",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Clave del sitio",
|
||||
"Site key - Tooltip": "Clave del sitio",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Claves",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Enlace",
|
||||
"Location": "Ubicación",
|
||||
"Location - Tooltip": "Ciudad de residencia",
|
||||
|
@@ -81,7 +81,7 @@
|
||||
"Only signup": "فقط ثبتنام",
|
||||
"Org choice mode": "حالت انتخاب سازمان",
|
||||
"Org choice mode - Tooltip": "حالت انتخاب سازمان - راهنمای ابزار",
|
||||
"Please enable \"Signin session\" first before enabling \"Auto signin\"": "لطفاً قبل از فعالسازی \"ورود خودکار\"، ابتدا \"جلسه ورود\" را فعال کنید",
|
||||
"Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"": "Please enable \\\"Signin session\\\" first before enabling \\\"Auto signin\\\"",
|
||||
"Please input your application!": "لطفاً برنامه خود را وارد کنید!",
|
||||
"Please input your organization!": "لطفاً سازمان خود را وارد کنید!",
|
||||
"Please select a HTML file": "لطفاً یک فایل HTML انتخاب کنید",
|
||||
@@ -161,6 +161,18 @@
|
||||
"Sending": "در حال ارسال",
|
||||
"Submit and complete": "ارسال و تکمیل"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "ویرایش Enforcer",
|
||||
"New Enforcer": "Enforcer جدید"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "به سایت دمو قابل نوشتن بروید؟",
|
||||
"Groups": "گروهها",
|
||||
"Groups - Tooltip": "گروهها - راهنمای ابزار",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "خانه",
|
||||
"Home - Tooltip": "صفحه اصلی برنامه",
|
||||
"ID": "شناسه",
|
||||
"ID - Tooltip": "رشته تصادفی منحصربهفرد",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "هویت",
|
||||
"Invitations": "دعوتها",
|
||||
"Is enabled": "فعال است",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "مطمئن شوید که رمز عبور صحیح است",
|
||||
"Password complexity options": "گزینههای پیچیدگی رمز عبور",
|
||||
"Password complexity options - Tooltip": "ترکیبات مختلف گزینههای پیچیدگی رمز عبور",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "نمک رمز عبور",
|
||||
"Password salt - Tooltip": "پارامتر تصادفی مورد استفاده برای رمزگذاری رمز عبور",
|
||||
"Password type": "نوع رمز عبور",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "استفاده از پیامک",
|
||||
"Use SMS verification code": "استفاده از کد تأیید پیامک",
|
||||
"Use a recovery code": "استفاده از کد بازیابی",
|
||||
"Verification failed": "تأیید ناموفق بود",
|
||||
"Verify Code": "تأیید کد",
|
||||
"Verify Password": "تأیید رمز عبور",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "شما احراز هویت چندعاملی را فعال کردهاید، لطفاً برای ادامه روی 'ارسال کد' کلیک کنید",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "کلید مخفی"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "ویرایشگر پیشرفته",
|
||||
"Basic Editor": "ویرایشگر ابتدایی",
|
||||
"Edit Model": "ویرایش مدل",
|
||||
"Model text": "متن مدل",
|
||||
"Model text - Tooltip": "مدل کنترل دسترسی Casbin، شامل مدلهای داخلی مانند ACL، RBAC، ABAC، RESTful و غیره. همچنین میتوانید مدلهای سفارشی ایجاد کنید. برای اطلاعات بیشتر، لطفاً به وبسایت Casbin مراجعه کنید",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "قانون اصلاح",
|
||||
"New Organization": "سازمان جدید",
|
||||
"Optional": "اختیاری",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "اعلان",
|
||||
"Required": "الزامی",
|
||||
"Soft deletion": "حذف نرم",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "خرید",
|
||||
"Buy Product": "خرید محصول",
|
||||
"CNY": "یوان چین",
|
||||
"Detail": "جزئیات",
|
||||
"Detail - Tooltip": "جزئیات محصول",
|
||||
"Dummy": "فرضی",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "فروخته شده",
|
||||
"Sold - Tooltip": "تعداد فروخته شده",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "برچسب محصول",
|
||||
"Test buy page..": "صفحه تست خرید..",
|
||||
"There is no payment channel for this product.": "برای این محصول کانال پرداختی وجود ندارد.",
|
||||
"This product is currently not in sale.": "این محصول در حال حاضر در فروش نیست.",
|
||||
"USD": "دلار آمریکا",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "ویرایش HTML ثبتنام",
|
||||
"Signup HTML - Tooltip": "HTML سفارشی برای جایگزینی سبک صفحه ثبتنام پیشفرض",
|
||||
"Signup group": "گروه ثبتنام",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "بیصدا",
|
||||
"Site key": "کلید سایت",
|
||||
"Site key - Tooltip": "کلید سایت",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "کلیدها",
|
||||
"Language": "زبان",
|
||||
"Language - Tooltip": "زبان - راهنمای ابزار",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "اتصال",
|
||||
"Location": "مکان",
|
||||
"Location - Tooltip": "شهر محل سکونت",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Sending",
|
||||
"Submit and complete": "Submit and complete"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Go to writable demo site?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Home",
|
||||
"Home - Tooltip": "Home page of the application",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Unique random string",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Is enabled",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Make sure the password is correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Different combinations of password complexity options",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Password salt",
|
||||
"Password salt - Tooltip": "Random parameter used for password encryption",
|
||||
"Password type": "Password type",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Advanced Editor",
|
||||
"Basic Editor": "Basic Editor",
|
||||
"Edit Model": "Edit Model",
|
||||
"Model text": "Model text",
|
||||
"Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Modify rule",
|
||||
"New Organization": "New Organization",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Soft deletion",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Buy",
|
||||
"Buy Product": "Buy Product",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detail",
|
||||
"Detail - Tooltip": "Detail of product",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Sold",
|
||||
"Sold - Tooltip": "Quantity sold",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag of product",
|
||||
"Test buy page..": "Test buy page..",
|
||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||
"This product is currently not in sale.": "This product is currently not in sale.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Signup HTML - Edit",
|
||||
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Site key",
|
||||
"Site key - Tooltip": "Site key",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Keys",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Link",
|
||||
"Location": "Location",
|
||||
"Location - Tooltip": "City of residence",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Envoi en cours",
|
||||
"Submit and complete": "Soumettre et compléter"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Éditer l'exécuteur",
|
||||
"New Enforcer": "Ajouter un exécuteur"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Allez sur le site de démonstration modifiable ?",
|
||||
"Groups": "Groupes",
|
||||
"Groups - Tooltip": "Groupes - infobulle",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Accueil",
|
||||
"Home - Tooltip": "Page d'accueil de l'application",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Chaîne unique aléatoire",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identité",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Est activé",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Assurez-vous que le mot de passe soit correct",
|
||||
"Password complexity options": "Options de complexité du mot de passe",
|
||||
"Password complexity options - Tooltip": "Différentes combinaisons d'options de complexité de mot de passe",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Sel de mot de passe",
|
||||
"Password salt - Tooltip": "Paramètre aléatoire utilisé pour le chiffrement des mots de passe",
|
||||
"Password type": "Type de mot de passe",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Utiliser les SMS",
|
||||
"Use SMS verification code": "Utiliser la vérification par code SMS",
|
||||
"Use a recovery code": "Utiliser un code de récupération",
|
||||
"Verification failed": "Échec de la vérification",
|
||||
"Verify Code": "Vérifier le code",
|
||||
"Verify Password": "Confirmez le mot de passe",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Éditeur avancé",
|
||||
"Basic Editor": "Éditeur de base",
|
||||
"Edit Model": "Modifier le modèle",
|
||||
"Model text": "Définition du modèle",
|
||||
"Model text - Tooltip": "Modèle de contrôle d'accès Casbin, comprenant des modèles intégrés tels que ACL, RBAC, ABAC, RESTful, etc. Vous pouvez également créer des modèles personnalisés. Pour plus d'informations, veuillez visiter le site web de Casbin",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Règle de modification",
|
||||
"New Organization": "Nouvelle organisation",
|
||||
"Optional": "Optionnel",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Requis",
|
||||
"Soft deletion": "Suppression douce",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Acheter",
|
||||
"Buy Product": "Acheter un produit",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Détail",
|
||||
"Detail - Tooltip": "Détail du produit",
|
||||
"Dummy": "Exemple",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Vendu",
|
||||
"Sold - Tooltip": "Quantité vendue",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Étiquette de produit",
|
||||
"Test buy page..": "Page d'achat de test.",
|
||||
"There is no payment channel for this product.": "Il n'y a aucun canal de paiement pour ce produit.",
|
||||
"This product is currently not in sale.": "Ce produit n'est actuellement pas en vente.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "HTML de la page d'inscription - Modifier",
|
||||
"Signup HTML - Tooltip": "HTML personnalisé pour remplacer le style par défaut de la page d'inscription",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silencieux",
|
||||
"Site key": "Clé de site",
|
||||
"Site key - Tooltip": "Clé de site",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Clés",
|
||||
"Language": "Langue",
|
||||
"Language - Tooltip": "Langue - Infobulle",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Lier",
|
||||
"Location": "Localisation",
|
||||
"Location - Tooltip": "Ville de résidence",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Sending",
|
||||
"Submit and complete": "Submit and complete"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Go to writable demo site?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Home",
|
||||
"Home - Tooltip": "Home page of the application",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Unique random string",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Is enabled",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Make sure the password is correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Different combinations of password complexity options",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Password salt",
|
||||
"Password salt - Tooltip": "Random parameter used for password encryption",
|
||||
"Password type": "Password type",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Advanced Editor",
|
||||
"Basic Editor": "Basic Editor",
|
||||
"Edit Model": "Edit Model",
|
||||
"Model text": "Model text",
|
||||
"Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Modify rule",
|
||||
"New Organization": "New Organization",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Soft deletion",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Buy",
|
||||
"Buy Product": "Buy Product",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detail",
|
||||
"Detail - Tooltip": "Detail of product",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Sold",
|
||||
"Sold - Tooltip": "Quantity sold",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag of product",
|
||||
"Test buy page..": "Test buy page..",
|
||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||
"This product is currently not in sale.": "This product is currently not in sale.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Signup HTML - Edit",
|
||||
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Site key",
|
||||
"Site key - Tooltip": "Site key",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Keys",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Link",
|
||||
"Location": "Location",
|
||||
"Location - Tooltip": "City of residence",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Mengirimkan",
|
||||
"Submit and complete": "Kirim dan selesaikan"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Pergi ke situs demo yang dapat ditulis?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Rumah",
|
||||
"Home - Tooltip": "Halaman utama aplikasi",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Karakter acak unik",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Diaktifkan",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Pastikan kata sandi yang benar",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Garam sandi",
|
||||
"Password salt - Tooltip": "Parameter acak yang digunakan untuk enkripsi kata sandi",
|
||||
"Password type": "Jenis kata sandi",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Editor lanjutan",
|
||||
"Basic Editor": "Editor dasar",
|
||||
"Edit Model": "Mengedit Model",
|
||||
"Model text": "Teks Model",
|
||||
"Model text - Tooltip": "Model kontrol akses Casbin, termasuk model bawaan seperti ACL, RBAC, ABAC, RESTful, dll. Anda juga dapat membuat model kustom. Untuk informasi lebih lanjut, silakan kunjungi situs web Casbin",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Mengubah aturan",
|
||||
"New Organization": "Organisasi baru",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Penghapusan lunak",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Beli",
|
||||
"Buy Product": "Beli Produk",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Rincian",
|
||||
"Detail - Tooltip": "Detail produk",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Terjual",
|
||||
"Sold - Tooltip": "Jumlah terjual",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag produk",
|
||||
"Test buy page..": "Halaman pembelian uji coba.",
|
||||
"There is no payment channel for this product.": "Tidak ada saluran pembayaran untuk produk ini.",
|
||||
"This product is currently not in sale.": "Produk ini saat ini tidak dijual.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Pendaftaran HTML - Sunting",
|
||||
"Signup HTML - Tooltip": "HTML khusus untuk mengganti gaya halaman pendaftaran bawaan",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Kunci situs",
|
||||
"Site key - Tooltip": "Kunci situs atau kunci halaman web",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Kunci",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Tautan",
|
||||
"Location": "Lokasi",
|
||||
"Location - Tooltip": "Kota tempat tinggal",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "Sending",
|
||||
"Submit and complete": "Submit and complete"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "Go to writable demo site?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "Home",
|
||||
"Home - Tooltip": "Home page of the application",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "Unique random string",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "Is enabled",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "Make sure the password is correct",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Different combinations of password complexity options",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "Password salt",
|
||||
"Password salt - Tooltip": "Random parameter used for password encryption",
|
||||
"Password type": "Password type",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Advanced Editor",
|
||||
"Basic Editor": "Basic Editor",
|
||||
"Edit Model": "Edit Model",
|
||||
"Model text": "Model text",
|
||||
"Model text - Tooltip": "Casbin access control model, including built-in models like ACL, RBAC, ABAC, RESTful, etc. You can also create custom models. For more information, please visit the Casbin website",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "Modify rule",
|
||||
"New Organization": "New Organization",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "Soft deletion",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "Buy",
|
||||
"Buy Product": "Buy Product",
|
||||
"CNY": "CNY",
|
||||
"Detail": "Detail",
|
||||
"Detail - Tooltip": "Detail of product",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "Sold",
|
||||
"Sold - Tooltip": "Quantity sold",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "Tag of product",
|
||||
"Test buy page..": "Test buy page..",
|
||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||
"This product is currently not in sale.": "This product is currently not in sale.",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "Signup HTML - Edit",
|
||||
"Signup HTML - Tooltip": "Custom HTML for replacing the default signup page style",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "Site key",
|
||||
"Site key - Tooltip": "Site key",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "Keys",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "Link",
|
||||
"Location": "Location",
|
||||
"Location - Tooltip": "City of residence",
|
||||
|
@@ -161,6 +161,18 @@
|
||||
"Sending": "送信",
|
||||
"Submit and complete": "提出して完了してください"
|
||||
},
|
||||
"currency": {
|
||||
"AUD": "AUD",
|
||||
"CAD": "CAD",
|
||||
"CHF": "CHF",
|
||||
"CNY": "CNY",
|
||||
"EUR": "EUR",
|
||||
"GBP": "GBP",
|
||||
"HKD": "HKD",
|
||||
"JPY": "JPY",
|
||||
"SGD": "SGD",
|
||||
"USD": "USD"
|
||||
},
|
||||
"enforcer": {
|
||||
"Edit Enforcer": "Edit Enforcer",
|
||||
"New Enforcer": "New Enforcer"
|
||||
@@ -266,10 +278,13 @@
|
||||
"Go to writable demo site?": "書き込み可能なデモサイトに移動しますか?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Hide password": "Hide password",
|
||||
"Home": "ホーム",
|
||||
"Home - Tooltip": "アプリケーションのホームページ",
|
||||
"ID": "ID",
|
||||
"ID - Tooltip": "ユニークなランダム文字列",
|
||||
"IP whitelist": "IP whitelist",
|
||||
"IP whitelist - Tooltip": "IP whitelist - Tooltip",
|
||||
"Identity": "Identity",
|
||||
"Invitations": "Invitations",
|
||||
"Is enabled": "可能になっています",
|
||||
@@ -312,6 +327,10 @@
|
||||
"Password - Tooltip": "パスワードが正しいことを確認してください",
|
||||
"Password complexity options": "Password complexity options",
|
||||
"Password complexity options - Tooltip": "Password complexity options - Tooltip",
|
||||
"Password obf key": "Password obf key",
|
||||
"Password obf key - Tooltip": "Password obf key - Tooltip",
|
||||
"Password obfuscator": "Password obfuscator",
|
||||
"Password obfuscator - Tooltip": "Password obfuscator - Tooltip",
|
||||
"Password salt": "パスワードのソルト",
|
||||
"Password salt - Tooltip": "ランダムパラメーターは、パスワードの暗号化に使用されます",
|
||||
"Password type": "パスワードタイプ",
|
||||
@@ -559,7 +578,6 @@
|
||||
"Use SMS": "Use SMS",
|
||||
"Use SMS verification code": "Use SMS verification code",
|
||||
"Use a recovery code": "Use a recovery code",
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify Code": "Verify Code",
|
||||
"Verify Password": "Verify Password",
|
||||
"You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue": "You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue",
|
||||
@@ -574,6 +592,8 @@
|
||||
"Secret Key": "Secret Key"
|
||||
},
|
||||
"model": {
|
||||
"Advanced Editor": "Advanced Editor",
|
||||
"Basic Editor": "Basic Editor",
|
||||
"Edit Model": "編集モデル",
|
||||
"Model text": "モデルテキスト",
|
||||
"Model text - Tooltip": "Casbinのアクセス制御モデルには、ACL、RBAC、ABAC、RESTfulなどの組み込みモデルが含まれています。カスタムモデルも作成できます。詳細については、Casbinのウェブサイトをご覧ください",
|
||||
@@ -592,6 +612,8 @@
|
||||
"Modify rule": "ルールを変更する",
|
||||
"New Organization": "新しい組織",
|
||||
"Optional": "Optional",
|
||||
"Password expire days": "Password expire days",
|
||||
"Password expire days - Tooltip": "Password expire days - Tooltip",
|
||||
"Prompt": "Prompt",
|
||||
"Required": "Required",
|
||||
"Soft deletion": "ソフト削除",
|
||||
@@ -709,7 +731,6 @@
|
||||
"Alipay": "Alipay",
|
||||
"Buy": "購入",
|
||||
"Buy Product": "製品を購入する",
|
||||
"CNY": "CNY",
|
||||
"Detail": "詳細",
|
||||
"Detail - Tooltip": "製品の詳細",
|
||||
"Dummy": "Dummy",
|
||||
@@ -736,11 +757,11 @@
|
||||
"Sold": "売れました",
|
||||
"Sold - Tooltip": "販売数量",
|
||||
"Stripe": "Stripe",
|
||||
"AirWallex": "AirWallex",
|
||||
"Tag - Tooltip": "製品のタグ",
|
||||
"Test buy page..": "テスト購入ページ。",
|
||||
"There is no payment channel for this product.": "この製品には支払いチャネルがありません。",
|
||||
"This product is currently not in sale.": "この製品は現在販売されていません。",
|
||||
"USD": "USD",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
},
|
||||
"provider": {
|
||||
@@ -893,6 +914,7 @@
|
||||
"Signup HTML - Edit": "サインアップ HTML - 編集",
|
||||
"Signup HTML - Tooltip": "デフォルトのサインアップページスタイルを置き換えるためのカスタムHTML",
|
||||
"Signup group": "Signup group",
|
||||
"Signup group - Tooltip": "Signup group - Tooltip",
|
||||
"Silent": "Silent",
|
||||
"Site key": "サイトキー",
|
||||
"Site key - Tooltip": "サイトキー",
|
||||
@@ -1157,6 +1179,7 @@
|
||||
"Keys": "鍵",
|
||||
"Language": "Language",
|
||||
"Language - Tooltip": "Language - Tooltip",
|
||||
"Last change password time": "Last change password time",
|
||||
"Link": "リンク",
|
||||
"Location": "場所",
|
||||
"Location - Tooltip": "居住都市",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user