mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-17 04:03:23 +08:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -99,6 +99,7 @@ p, *, *, GET, /api/get-all-objects, *, *
|
|||||||
p, *, *, GET, /api/get-all-actions, *, *
|
p, *, *, GET, /api/get-all-actions, *, *
|
||||||
p, *, *, GET, /api/get-all-roles, *, *
|
p, *, *, GET, /api/get-all-roles, *, *
|
||||||
p, *, *, GET, /api/run-casbin-command, *, *
|
p, *, *, GET, /api/run-casbin-command, *, *
|
||||||
|
p, *, *, POST, /api/refresh-engines, *, *
|
||||||
p, *, *, GET, /api/get-invitation-info, *, *
|
p, *, *, GET, /api/get-invitation-info, *, *
|
||||||
p, *, *, GET, /api/faceid-signin-begin, *, *
|
p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||||
`
|
`
|
||||||
|
@ -306,6 +306,35 @@ func isProxyProviderType(providerType string) bool {
|
|||||||
return false
|
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 ...
|
// Login ...
|
||||||
// @Title Login
|
// @Title Login
|
||||||
// @Tag Login API
|
// @Tag Login API
|
||||||
@ -331,6 +360,8 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verificationType := ""
|
||||||
|
|
||||||
if authForm.Username != "" {
|
if authForm.Username != "" {
|
||||||
if authForm.Type == ResponseTypeLogin {
|
if authForm.Type == ResponseTypeLogin {
|
||||||
if c.GetSessionUsername() != "" {
|
if c.GetSessionUsername() != "" {
|
||||||
@ -425,6 +456,12 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if verificationCodeType == object.VerifyTypePhone {
|
||||||
|
verificationType = "sms"
|
||||||
|
} else {
|
||||||
|
verificationType = "email"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||||
@ -515,16 +552,7 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if object.IsNeedPromptMfa(organization, user) {
|
if checkMfaEnable(c, user, organization, verificationType) {
|
||||||
// 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))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,6 +688,11 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if checkMfaEnable(c, user, organization, verificationType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||||
|
|
||||||
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
||||||
@ -866,18 +899,32 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if authForm.Passcode != "" {
|
if authForm.Passcode != "" {
|
||||||
|
if authForm.MfaType == c.GetSession("verificationCodeType") {
|
||||||
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
|
return
|
||||||
|
}
|
||||||
user.CountryCode = user.GetCountryCode(user.CountryCode)
|
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 {
|
if mfaUtil == nil {
|
||||||
c.ResponseError("Invalid multi-factor authentication type")
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Passcode)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !passed {
|
||||||
err = mfaUtil.Verify(authForm.Passcode)
|
err = mfaUtil.Verify(authForm.Passcode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetSession("verificationCodeType", "")
|
||||||
} else if authForm.RecoveryCode != "" {
|
} else if authForm.RecoveryCode != "" {
|
||||||
err = object.MfaRecover(user, authForm.RecoveryCode)
|
err = object.MfaRecover(user, authForm.RecoveryCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -890,7 +937,11 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
|
if authForm.ClientId == "" {
|
||||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||||
|
} else {
|
||||||
|
application, err = object.GetApplicationByClientId(authForm.ClientId)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
@ -920,6 +971,10 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if authForm.Provider == "" {
|
||||||
|
authForm.Provider = authForm.ProviderBack
|
||||||
|
}
|
||||||
|
|
||||||
user := c.getCurrentUser()
|
user := c.getCurrentUser()
|
||||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||||
|
|
||||||
|
@ -15,11 +15,15 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
||||||
@ -57,6 +61,11 @@ func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
|||||||
// @Success 200 {object} controllers.Response The Response object
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
// @router /run-casbin-command [get]
|
// @router /run-casbin-command [get]
|
||||||
func (c *ApiController) RunCasbinCommand() {
|
func (c *ApiController) RunCasbinCommand() {
|
||||||
|
if err := validateIdentifier(c); err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
language := c.Input().Get("language")
|
language := c.Input().Get("language")
|
||||||
argString := c.Input().Get("args")
|
argString := c.Input().Get("args")
|
||||||
|
|
||||||
@ -112,3 +121,58 @@ func (c *ApiController) RunCasbinCommand() {
|
|||||||
output = strings.TrimSuffix(output, "\n")
|
output = strings.TrimSuffix(output, "\n")
|
||||||
c.ResponseOk(output)
|
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
|
||||||
|
}
|
||||||
|
502
controllers/cli_downloader.go
Normal file
502
controllers/cli_downloader.go
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beego/beego"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
resp, err := http.Get(repoURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", 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)
|
||||||
|
|
||||||
|
resp, err := http.Get(cliURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
out, err := os.Create(originalPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to create %s CLI file: %v\n", lang, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(out, resp.Body); err != nil {
|
||||||
|
fmt.Printf("failed to save %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
|
||||||
|
}
|
||||||
|
|
||||||
|
err := DownloadCLI()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go ScheduleCLIUpdater()
|
||||||
|
}
|
@ -70,7 +70,25 @@ func (c *ApiController) GetGroups() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, ok := groupsHaveChildrenMap[group.ParentId]
|
||||||
|
if ok {
|
||||||
|
group.ParentName = parent.DisplayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = object.ExtendGroupsWithUsers(groups)
|
err = object.ExtendGroupsWithUsers(groups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
@ -78,7 +96,7 @@ func (c *ApiController) GetGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.ResponseOk(groups, paginator.Nums())
|
c.ResponseOk(groups, paginator.Nums())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +124,9 @@ func (c *ApiController) UpdateOrganization() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
isGlobalAdmin, _ := c.isGlobalAdmin()
|
||||||
|
|
||||||
|
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *RootController) HandleScim() {
|
func (c *RootController) HandleScim() {
|
||||||
|
_, ok := c.RequireAdmin()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path := c.Ctx.Request.URL.Path
|
path := c.Ctx.Request.URL.Path
|
||||||
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
||||||
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||||
|
@ -93,7 +93,7 @@ func (c *ApiController) SendEmail() {
|
|||||||
|
|
||||||
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
||||||
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
||||||
err = object.DailSmtpServer(provider)
|
err = object.TestSmtpServer(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -353,13 +353,7 @@ func (c *ApiController) AddUser() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := object.GetUserCount("", "", "", "")
|
if err := checkQuotaForUser(); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkQuotaForUser(int(count)); err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -580,7 +574,11 @@ func (c *ApiController) SetPassword() {
|
|||||||
if user.Ldap == "" {
|
if user.Ldap == "" {
|
||||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
||||||
} else {
|
} else {
|
||||||
err = object.ResetLdapPassword(targetUser, newPassword, c.GetAcceptLanguage())
|
if isAdmin {
|
||||||
|
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())
|
||||||
|
} else {
|
||||||
|
err = object.ResetLdapPassword(targetUser, oldPassword, newPassword, c.GetAcceptLanguage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -294,12 +294,18 @@ func checkQuotaForProvider(count int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkQuotaForUser(count int) error {
|
func checkQuotaForUser() error {
|
||||||
quota := conf.GetConfigQuota().User
|
quota := conf.GetConfigQuota().User
|
||||||
if quota == -1 {
|
if quota == -1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if count >= quota {
|
|
||||||
|
count, err := object.GetUserCount("", "", "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(count) >= quota {
|
||||||
return fmt.Errorf("user quota is exceeded")
|
return fmt.Errorf("user quota is exceeded")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -510,11 +510,18 @@ func (c *ApiController) VerifyCode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(c.T(err.Error()))
|
c.ResponseError(c.T(err.Error()))
|
||||||
return
|
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 {
|
if result.Code != object.VerificationSuccess {
|
||||||
c.ResponseError(result.Msg)
|
c.ResponseError(result.Msg)
|
||||||
return
|
return
|
||||||
@ -525,6 +532,7 @@ func (c *ApiController) VerifyCode() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.SetSession("verifiedCode", authForm.Code)
|
c.SetSession("verifiedCode", authForm.Code)
|
||||||
c.SetSession("verifiedUserId", user.GetId())
|
c.SetSession("verifiedUserId", user.GetId())
|
||||||
|
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 (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/gomail/v2"
|
"github.com/casdoor/gomail/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,6 +35,13 @@ func NewSmtpEmailProvider(userName string, password string, host string, port in
|
|||||||
|
|
||||||
dialer.SSL = !disableSsl
|
dialer.SSL = !disableSsl
|
||||||
|
|
||||||
|
if strings.HasSuffix(host, ".amazonaws.com") {
|
||||||
|
socks5Proxy := conf.GetConfigString("socks5Proxy")
|
||||||
|
if socks5Proxy != "" {
|
||||||
|
dialer.SetSocks5Proxy(socks5Proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &SmtpEmailProvider{Dialer: dialer}
|
return &SmtpEmailProvider{Dialer: dialer}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ type AuthForm struct {
|
|||||||
Application string `json:"application"`
|
Application string `json:"application"`
|
||||||
ClientId string `json:"clientId"`
|
ClientId string `json:"clientId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
ProviderBack string `json:"providerBack"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
RedirectUri string `json:"redirectUri"`
|
RedirectUri string `json:"redirectUri"`
|
||||||
|
10
go.mod
10
go.mod
@ -10,9 +10,9 @@ require (
|
|||||||
github.com/beevik/etree v1.1.0
|
github.com/beevik/etree v1.1.0
|
||||||
github.com/casbin/casbin/v2 v2.77.2
|
github.com/casbin/casbin/v2 v2.77.2
|
||||||
github.com/casdoor/go-sms-sender v0.25.0
|
github.com/casdoor/go-sms-sender v0.25.0
|
||||||
github.com/casdoor/gomail/v2 v2.0.1
|
github.com/casdoor/gomail/v2 v2.1.0
|
||||||
github.com/casdoor/ldapserver v1.2.0
|
github.com/casdoor/ldapserver v1.2.0
|
||||||
github.com/casdoor/notify v0.45.0
|
github.com/casdoor/notify v1.0.0
|
||||||
github.com/casdoor/oss v1.8.0
|
github.com/casdoor/oss v1.8.0
|
||||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||||
@ -60,10 +60,10 @@ require (
|
|||||||
github.com/xorm-io/core v0.7.4
|
github.com/xorm-io/core v0.7.4
|
||||||
github.com/xorm-io/xorm v1.1.6
|
github.com/xorm-io/xorm v1.1.6
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
golang.org/x/crypto v0.21.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/net v0.21.0
|
golang.org/x/net v0.34.0
|
||||||
golang.org/x/oauth2 v0.17.0
|
golang.org/x/oauth2 v0.17.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.21.0
|
||||||
google.golang.org/api v0.150.0
|
google.golang.org/api v0.150.0
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
|
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-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 h1:eF4cOCSbjVg7+0uLlJQnna/FQ0BWW+Fp/x4cXhzQu1Y=
|
||||||
github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
|
github.com/casdoor/go-sms-sender v0.25.0/go.mod h1:bOm4H8/YfJmEHjBatEVQFOnAf0OOn1B0Wi5B7zDhws0=
|
||||||
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
|
github.com/casdoor/gomail/v2 v2.1.0 h1:ua97E3CARnF1Ik8ga/Drz9uGZfaElXJumFexiErWUxM=
|
||||||
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
|
github.com/casdoor/gomail/v2 v2.1.0/go.mod h1:GFzOD9RhY0nODiiPaQiOa6DfoKtmO9aTesu5qrp26OI=
|
||||||
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
|
github.com/casdoor/ldapserver v1.2.0 h1:HdSYe+ULU6z9K+2BqgTrJKQRR4//ERAXB64ttOun6Ow=
|
||||||
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
|
github.com/casdoor/ldapserver v1.2.0/go.mod h1:VwYU2vqQ2pA8sa00PRekH71R2XmgfzMKhmp1XrrDu2s=
|
||||||
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
|
github.com/casdoor/notify v1.0.0 h1:oldsaaQFPrlufm/OA314z8DwFVE1Tc9Gt1z4ptRHhXw=
|
||||||
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
|
github.com/casdoor/notify v1.0.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
|
||||||
github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
|
github.com/casdoor/oss v1.8.0 h1:uuyKhDIp7ydOtV4lpqhAY23Ban2Ln8La8+QT36CwylM=
|
||||||
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
|
github.com/casdoor/oss v1.8.0/go.mod h1:uaqO7KBI2lnZcnB8rF7O6C2bN7llIbfC5Ql8ex1yR1U=
|
||||||
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
|
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
|
||||||
@ -2163,8 +2163,10 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
|||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@ -2230,8 +2232,10 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -2319,8 +2323,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -2375,8 +2381,11 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -2503,8 +2512,11 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
|
||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@ -2524,8 +2536,10 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
|
||||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -2546,8 +2560,10 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@ -2634,8 +2650,9 @@ golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
|||||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||||
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
161
idp/kwai.go
Normal file
161
idp/kwai.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package idp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KwaiIdProvider struct {
|
||||||
|
Client *http.Client
|
||||||
|
Config *oauth2.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKwaiIdProvider(clientId string, clientSecret string, redirectUrl string) *KwaiIdProvider {
|
||||||
|
idp := &KwaiIdProvider{}
|
||||||
|
idp.Config = idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||||
|
return idp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idp *KwaiIdProvider) SetHttpClient(client *http.Client) {
|
||||||
|
idp.Client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (idp *KwaiIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
|
||||||
|
endpoint := oauth2.Endpoint{
|
||||||
|
TokenURL: "https://open.kuaishou.com/oauth2/access_token",
|
||||||
|
AuthURL: "https://open.kuaishou.com/oauth2/authorize", // qr code: /oauth2/connect
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &oauth2.Config{
|
||||||
|
Scopes: []string{"user_info"},
|
||||||
|
Endpoint: endpoint,
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
RedirectURL: redirectUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
type KwaiTokenResp struct {
|
||||||
|
Result int `json:"result"`
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
RefreshTokenExpiresIn int `json:"refresh_token_expires_in"`
|
||||||
|
OpenId string `json:"open_id"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken use code to get access_token
|
||||||
|
func (idp *KwaiIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"app_id": idp.Config.ClientID,
|
||||||
|
"app_secret": idp.Config.ClientSecret,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
}
|
||||||
|
tokenUrl := fmt.Sprintf("%s?app_id=%s&app_secret=%s&code=%s&grant_type=authorization_code",
|
||||||
|
idp.Config.Endpoint.TokenURL, params["app_id"], params["app_secret"], params["code"])
|
||||||
|
resp, err := idp.Client.Get(tokenUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tokenResp KwaiTokenResp
|
||||||
|
err = json.Unmarshal(body, &tokenResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tokenResp.Result != 1 {
|
||||||
|
return nil, fmt.Errorf("get token error: %s", tokenResp.ErrorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &oauth2.Token{
|
||||||
|
AccessToken: tokenResp.AccessToken,
|
||||||
|
RefreshToken: tokenResp.RefreshToken,
|
||||||
|
Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := make(map[string]interface{})
|
||||||
|
raw["open_id"] = tokenResp.OpenId
|
||||||
|
token = token.WithExtra(raw)
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// More details: https://open.kuaishou.com/openapi/user_info
|
||||||
|
type KwaiUserInfo struct {
|
||||||
|
Result int `json:"result"`
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
UserInfo struct {
|
||||||
|
Head string `json:"head"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Sex string `json:"sex"`
|
||||||
|
City string `json:"city"`
|
||||||
|
} `json:"user_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo use token to get user profile
|
||||||
|
func (idp *KwaiIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||||
|
userInfoUrl := fmt.Sprintf("https://open.kuaishou.com/openapi/user_info?app_id=%s&access_token=%s",
|
||||||
|
idp.Config.ClientID, token.AccessToken)
|
||||||
|
|
||||||
|
resp, err := idp.Client.Get(userInfoUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kwaiUserInfo KwaiUserInfo
|
||||||
|
err = json.Unmarshal(body, &kwaiUserInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if kwaiUserInfo.Result != 1 {
|
||||||
|
return nil, fmt.Errorf("get user info error: %s", kwaiUserInfo.ErrorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := &UserInfo{
|
||||||
|
Id: token.Extra("open_id").(string),
|
||||||
|
Username: kwaiUserInfo.UserInfo.Name,
|
||||||
|
DisplayName: kwaiUserInfo.UserInfo.Name,
|
||||||
|
AvatarUrl: kwaiUserInfo.UserInfo.Head,
|
||||||
|
Extra: map[string]string{
|
||||||
|
"gender": kwaiUserInfo.UserInfo.Sex,
|
||||||
|
"city": kwaiUserInfo.UserInfo.City,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
@ -113,6 +113,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
|
|||||||
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
|
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
|
||||||
case "Douyin":
|
case "Douyin":
|
||||||
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||||
|
case "Kwai":
|
||||||
|
return NewKwaiIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||||
case "Bilibili":
|
case "Bilibili":
|
||||||
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||||
case "MetaMask":
|
case "MetaMask":
|
||||||
|
7
main.go
7
main.go
@ -22,6 +22,7 @@ import (
|
|||||||
_ "github.com/beego/beego/session/redis"
|
_ "github.com/beego/beego/session/redis"
|
||||||
"github.com/casdoor/casdoor/authz"
|
"github.com/casdoor/casdoor/authz"
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
|
"github.com/casdoor/casdoor/controllers"
|
||||||
"github.com/casdoor/casdoor/ldap"
|
"github.com/casdoor/casdoor/ldap"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
"github.com/casdoor/casdoor/proxy"
|
"github.com/casdoor/casdoor/proxy"
|
||||||
@ -45,6 +46,7 @@ func main() {
|
|||||||
object.InitCasvisorConfig()
|
object.InitCasvisorConfig()
|
||||||
|
|
||||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||||
|
controllers.InitCLIDownloader()
|
||||||
|
|
||||||
// beego.DelStaticPath("/static")
|
// beego.DelStaticPath("/static")
|
||||||
// beego.SetStaticPath("/static", "web/build/static")
|
// beego.SetStaticPath("/static", "web/build/static")
|
||||||
@ -83,6 +85,11 @@ func main() {
|
|||||||
// logs.SetLevel(logs.LevelInformational)
|
// logs.SetLevel(logs.LevelInformational)
|
||||||
logs.SetLogFuncCall(false)
|
logs.SetLogFuncCall(false)
|
||||||
|
|
||||||
|
err = util.StopOldInstance(port)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
go ldap.StartLdapServer()
|
go ldap.StartLdapServer()
|
||||||
go radius.StartRadiusServer()
|
go radius.StartRadiusServer()
|
||||||
go object.ClearThroughputPerSecond()
|
go object.ClearThroughputPerSecond()
|
||||||
|
29
notification/cucloud.go
Normal file
29
notification/cucloud.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/casdoor/notify"
|
||||||
|
"github.com/casdoor/notify/service/cucloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCucloudProvider(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType string) (notify.Notifier, error) {
|
||||||
|
cucloud := cucloud.New(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType)
|
||||||
|
|
||||||
|
notifier := notify.New()
|
||||||
|
notifier.UseServices(cucloud)
|
||||||
|
|
||||||
|
return notifier, nil
|
||||||
|
}
|
@ -16,7 +16,7 @@ package notification
|
|||||||
|
|
||||||
import "github.com/casdoor/notify"
|
import "github.com/casdoor/notify"
|
||||||
|
|
||||||
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string) (notify.Notifier, error) {
|
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string, regionId string) (notify.Notifier, error) {
|
||||||
if typ == "Telegram" {
|
if typ == "Telegram" {
|
||||||
return NewTelegramProvider(clientSecret, receiver)
|
return NewTelegramProvider(clientSecret, receiver)
|
||||||
} else if typ == "Custom HTTP" {
|
} else if typ == "Custom HTTP" {
|
||||||
@ -53,6 +53,8 @@ func GetNotificationProvider(typ string, clientId string, clientSecret string, c
|
|||||||
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
|
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
|
||||||
} else if typ == "Viber" {
|
} else if typ == "Viber" {
|
||||||
return NewViberProvider(clientId, clientSecret, appId, receiver)
|
return NewViberProvider(clientId, clientSecret, appId, receiver)
|
||||||
|
} else if typ == "CUCloud" {
|
||||||
|
return NewCucloudProvider(clientId, clientSecret, appId, title, regionId, clientId2, metaData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -481,7 +481,10 @@ func GetApplicationByClientId(clientId string) (*Application, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetApplication(id 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)
|
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"))
|
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
|
passwordType := user.PasswordType
|
||||||
if passwordType == "" {
|
if passwordType == "" {
|
||||||
passwordType = organization.PasswordType
|
passwordType = organization.PasswordType
|
||||||
|
@ -16,23 +16,18 @@
|
|||||||
|
|
||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import "github.com/casdoor/casdoor/email"
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/email"
|
// TestSmtpServer Test the SMTP server
|
||||||
"github.com/casdoor/gomail/v2"
|
func TestSmtpServer(provider *Provider) error {
|
||||||
)
|
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl)
|
||||||
|
sender, err := smtpEmailProvider.Dialer.Dial()
|
||||||
func getDialer(provider *Provider) *gomail.Dialer {
|
if err != nil {
|
||||||
dialer := &gomail.Dialer{}
|
return err
|
||||||
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
|
|
||||||
if provider.Type == "SUBMAIL" {
|
|
||||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
}
|
||||||
|
defer sender.Close()
|
||||||
|
|
||||||
dialer.SSL = !provider.DisableSsl
|
return nil
|
||||||
|
|
||||||
return dialer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||||
@ -50,16 +45,3 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
|
|||||||
|
|
||||||
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DailSmtpServer Dail Smtp server
|
|
||||||
func DailSmtpServer(provider *Provider) error {
|
|
||||||
dialer := getDialer(provider)
|
|
||||||
|
|
||||||
sender, err := dialer.Dial()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sender.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -17,7 +17,6 @@ package object
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
@ -36,11 +35,13 @@ type Group struct {
|
|||||||
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
||||||
Type string `xorm:"varchar(100)" json:"type"`
|
Type string `xorm:"varchar(100)" json:"type"`
|
||||||
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
||||||
|
ParentName string `xorm:"-" json:"parentName"`
|
||||||
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
||||||
Users []string `xorm:"-" json:"users"`
|
Users []string `xorm:"-" json:"users"`
|
||||||
|
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty"`
|
||||||
|
HaveChildren bool `xorm:"-" json:"haveChildren"`
|
||||||
Children []*Group `json:"children,omitempty"`
|
Children []*Group `json:"children,omitempty"`
|
||||||
|
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
@ -79,6 +80,26 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
|
|||||||
return groups, nil
|
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) {
|
func getGroup(owner string, name string) (*Group, error) {
|
||||||
if owner == "" || name == "" {
|
if owner == "" || name == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -298,17 +319,11 @@ func ExtendGroupWithUsers(group *Group) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := GetUsers(group.Owner)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupId := group.GetId()
|
groupId := group.GetId()
|
||||||
userIds := []string{}
|
userIds := []string{}
|
||||||
for _, user := range users {
|
userIds, err := userEnforcer.GetAllUsersByGroup(groupId)
|
||||||
if util.InSlice(user.Groups, groupId) {
|
if err != nil {
|
||||||
userIds = append(userIds, user.GetId())
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.Users = userIds
|
group.Users = userIds
|
||||||
@ -316,29 +331,14 @@ func ExtendGroupWithUsers(group *Group) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExtendGroupsWithUsers(groups []*Group) error {
|
func ExtendGroupsWithUsers(groups []*Group) error {
|
||||||
var wg sync.WaitGroup
|
|
||||||
errChan := make(chan error, len(groups))
|
|
||||||
|
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
wg.Add(1)
|
users, err := userEnforcer.GetAllUsersByGroup(group.GetId())
|
||||||
go func(group *Group) {
|
|
||||||
defer wg.Done()
|
|
||||||
err := ExtendGroupWithUsers(group)
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(errChan)
|
|
||||||
|
|
||||||
for err := range errChan {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
group.Users = users
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ type Ldap struct {
|
|||||||
Filter string `xorm:"varchar(200)" json:"filter"`
|
Filter string `xorm:"varchar(200)" json:"filter"`
|
||||||
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
||||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||||
|
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||||
|
|
||||||
AutoSync int `json:"autoSync"`
|
AutoSync int `json:"autoSync"`
|
||||||
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
||||||
@ -149,7 +150,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
||||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group").Update(ldap)
|
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").Update(ldap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -373,7 +375,7 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
|
|||||||
return existUuids, nil
|
return existUuids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetLdapPassword(user *User, newPassword string, lang string) error {
|
func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang string) error {
|
||||||
ldaps, err := GetLdaps(user.Owner)
|
ldaps, err := GetLdaps(user.Owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -416,8 +418,32 @@ func ResetLdapPassword(user *User, newPassword string, lang string) error {
|
|||||||
}
|
}
|
||||||
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
||||||
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
|
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 {
|
} 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
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
}
|
||||||
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
object/ldap_password_type.go
Normal file
36
object/ldap_password_type.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSSHA(password string) (string, error) {
|
||||||
|
salt := make([]byte, 4)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := append([]byte(password), salt...)
|
||||||
|
hash := sha1.Sum(combined)
|
||||||
|
hashWithSalt := append(hash[:], salt...)
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(hashWithSalt)
|
||||||
|
|
||||||
|
return "{SSHA}" + encoded, nil
|
||||||
|
}
|
@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
|
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
|
||||||
var client notify.Notifier
|
var client notify.Notifier
|
||||||
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata)
|
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata, provider.RegionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -69,8 +69,8 @@ type Organization struct {
|
|||||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||||
Languages []string `xorm:"varchar(255)" json:"languages"`
|
Languages []string `xorm:"varchar(255)" json:"languages"`
|
||||||
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
||||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
MasterPassword string `xorm:"varchar(200)" json:"masterPassword"`
|
||||||
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
|
DefaultPassword string `xorm:"varchar(200)" json:"defaultPassword"`
|
||||||
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
|
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
|
||||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||||
InitScore int `json:"initScore"`
|
InitScore int `json:"initScore"`
|
||||||
@ -79,6 +79,7 @@ type Organization struct {
|
|||||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||||
EnableTour bool `json:"enableTour"`
|
EnableTour bool `json:"enableTour"`
|
||||||
IpRestriction string `json:"ipRestriction"`
|
IpRestriction string `json:"ipRestriction"`
|
||||||
|
NavItems []string `xorm:"varchar(500)" json:"navItems"`
|
||||||
|
|
||||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||||
@ -151,7 +152,10 @@ func getOrganization(owner string, name string) (*Organization, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetOrganization(id 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)
|
return getOrganization(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,9 +196,10 @@ func GetMaskedOrganizations(organizations []*Organization, errs ...error) ([]*Or
|
|||||||
return organizations, nil
|
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)
|
owner, name := util.GetOwnerAndNameFromId(id)
|
||||||
if org, err := getOrganization(owner, name); err != nil {
|
org, err := getOrganization(owner, name)
|
||||||
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if org == nil {
|
} else if org == nil {
|
||||||
return false, 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()
|
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||||
|
|
||||||
if organization.MasterPassword == "***" {
|
if organization.MasterPassword == "***" {
|
||||||
|
@ -219,8 +219,11 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
ProductName: product.Name,
|
ProductName: product.Name,
|
||||||
PayerName: payerName,
|
PayerName: payerName,
|
||||||
PayerId: user.Id,
|
PayerId: user.Id,
|
||||||
|
PayerEmail: user.Email,
|
||||||
PaymentName: paymentName,
|
PaymentName: paymentName,
|
||||||
ProductDisplayName: product.DisplayName,
|
ProductDisplayName: product.DisplayName,
|
||||||
|
ProductDescription: product.Description,
|
||||||
|
ProductImage: product.Image,
|
||||||
Price: product.Price,
|
Price: product.Price,
|
||||||
Currency: product.Currency,
|
Currency: product.Currency,
|
||||||
ReturnUrl: returnUrl,
|
ReturnUrl: returnUrl,
|
||||||
|
@ -325,6 +325,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return pp, nil
|
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" {
|
} else if typ == "Balance" {
|
||||||
pp, err := pp.NewBalancePaymentProvider()
|
pp, err := pp.NewBalancePaymentProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -338,6 +338,10 @@ func roleChangeTrigger(oldName string, newName string) error {
|
|||||||
|
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
for j, u := range role.Roles {
|
for j, u := range role.Roles {
|
||||||
|
if u == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
owner, name := util.GetOwnerAndNameFromId(u)
|
owner, name := util.GetOwnerAndNameFromId(u)
|
||||||
if name == oldName {
|
if name == oldName {
|
||||||
role.Roles[j] = util.GetId(owner, newName)
|
role.Roles[j] = util.GetId(owner, newName)
|
||||||
@ -358,6 +362,10 @@ func roleChangeTrigger(oldName string, newName string) error {
|
|||||||
for _, permission := range permissions {
|
for _, permission := range permissions {
|
||||||
for j, u := range permission.Roles {
|
for j, u := range permission.Roles {
|
||||||
// u = organization/username
|
// u = organization/username
|
||||||
|
if u == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
owner, name := util.GetOwnerAndNameFromId(u)
|
owner, name := util.GetOwnerAndNameFromId(u)
|
||||||
if name == oldName {
|
if name == oldName {
|
||||||
permission.Roles[j] = util.GetId(owner, newName)
|
permission.Roles[j] = util.GetId(owner, newName)
|
||||||
|
@ -338,6 +338,9 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
|||||||
} else if authnRequest.AssertionConsumerServiceURL == "" {
|
} else if authnRequest.AssertionConsumerServiceURL == "" {
|
||||||
return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>")
|
return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>")
|
||||||
}
|
}
|
||||||
|
if authnRequest.ProtocolBinding == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" {
|
||||||
|
method = "POST"
|
||||||
|
}
|
||||||
|
|
||||||
_, originBackend := getOriginFromHost(host)
|
_, originBackend := getOriginFromHost(host)
|
||||||
|
|
||||||
|
@ -123,8 +123,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
|||||||
|
|
||||||
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||||
switch tokenTypeHint {
|
switch tokenTypeHint {
|
||||||
case "access_token":
|
case "access_token", "access-token":
|
||||||
case "access-token":
|
|
||||||
token, err := GetTokenByAccessToken(tokenValue)
|
token, err := GetTokenByAccessToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -132,8 +131,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
|||||||
if token != nil {
|
if token != nil {
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
case "refresh_token":
|
case "refresh_token", "refresh-token":
|
||||||
case "refresh-token":
|
|
||||||
token, err := GetTokenByRefreshToken(tokenValue)
|
token, err := GetTokenByRefreshToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -146,13 +144,13 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
|||||||
return nil, nil
|
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)
|
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return affected != 0
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetToken(id string) (*Token, error) {
|
func GetToken(id string) (*Token, error) {
|
||||||
|
@ -248,7 +248,10 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
|||||||
|
|
||||||
token.CodeIsUsed = true
|
token.CodeIsUsed = true
|
||||||
|
|
||||||
go updateUsedByCode(token)
|
_, err = updateUsedByCode(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tokenWrapper := &TokenWrapper{
|
tokenWrapper := &TokenWrapper{
|
||||||
AccessToken: token.AccessToken,
|
AccessToken: token.AccessToken,
|
||||||
|
@ -129,6 +129,7 @@ type User struct {
|
|||||||
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
||||||
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
||||||
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
|
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
|
||||||
|
Kwai string `xorm:"kwai varchar(100)" json:"kwai"`
|
||||||
Line string `xorm:"line varchar(100)" json:"line"`
|
Line string `xorm:"line varchar(100)" json:"line"`
|
||||||
Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
|
Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
|
||||||
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
|
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
|
||||||
@ -698,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",
|
"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",
|
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
|
||||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
|
||||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||||
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
|
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
|
||||||
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
|
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
|
||||||
@ -845,11 +846,14 @@ func AddUser(user *User) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rankingItem := GetAccountItemByName("Ranking", organization)
|
||||||
|
if rankingItem != nil {
|
||||||
count, err := GetUserCount(user.Owner, "", "", "")
|
count, err := GetUserCount(user.Owner, "", "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
user.Ranking = int(count + 1)
|
user.Ranking = int(count + 1)
|
||||||
|
}
|
||||||
|
|
||||||
if user.Groups != nil && len(user.Groups) > 0 {
|
if user.Groups != nil && len(user.Groups) > 0 {
|
||||||
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||||
@ -961,6 +965,11 @@ func DeleteUser(user *User) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = userEnforcer.DeleteGroupsForUser(user.GetId())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
organization, err := GetOrganizationByUser(user)
|
organization, err := GetOrganizationByUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
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": string([]rune(description)[:32]), // display to the customer.
|
||||||
|
"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
|
ProductName string
|
||||||
PayerName string
|
PayerName string
|
||||||
PayerId string
|
PayerId string
|
||||||
|
PayerEmail string
|
||||||
PaymentName string
|
PaymentName string
|
||||||
ProductDisplayName string
|
ProductDisplayName string
|
||||||
|
ProductDescription string
|
||||||
|
ProductImage string
|
||||||
Price float64
|
Price float64
|
||||||
Currency string
|
Currency string
|
||||||
|
|
||||||
|
@ -175,6 +175,7 @@ func initAPI() {
|
|||||||
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
|
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
|
||||||
|
|
||||||
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
|
beego.Router("/api/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-sessions", &controllers.ApiController{}, "GET:GetSessions")
|
||||||
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
|
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
|
||||||
|
@ -80,6 +80,15 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
|||||||
return "", nil
|
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))
|
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -133,6 +142,14 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
path += urlPath
|
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) {
|
if strings.Contains(path, "/../") || !util.FileExist(path) {
|
||||||
path = webBuildFolder + "/index.html"
|
path = webBuildFolder + "/index.html"
|
||||||
}
|
}
|
||||||
@ -149,13 +166,13 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oldStaticBaseUrl == newStaticBaseUrl {
|
if oldStaticBaseUrl == newStaticBaseUrl {
|
||||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path)
|
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
} else {
|
} else {
|
||||||
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path)
|
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
f, err := os.Open(filepath.Clean(name))
|
f, err := os.Open(filepath.Clean(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -168,7 +185,13 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
oldContent := util.ReadStringFromPath(name)
|
oldContent := util.ReadStringFromPath(name)
|
||||||
newContent := strings.ReplaceAll(oldContent, oldStaticBaseUrl, newStaticBaseUrl)
|
newContent := oldContent
|
||||||
|
if organizationThemeCookie != nil {
|
||||||
|
newContent = strings.ReplaceAll(newContent, "https://cdn.casbin.org/img/favicon.png", organizationThemeCookie.Favicon)
|
||||||
|
newContent = strings.ReplaceAll(newContent, "<title>Casdoor</title>", fmt.Sprintf("<title>%s</title>", organizationThemeCookie.DisplayName))
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = strings.ReplaceAll(newContent, oldStaticBaseUrl, newStaticBaseUrl)
|
||||||
|
|
||||||
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
||||||
}
|
}
|
||||||
@ -182,14 +205,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
|||||||
return w.Writer.Write(b)
|
return w.Writer.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string) {
|
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
serveFileWithReplace(w, r, path)
|
serveFileWithReplace(w, r, path, organizationThemeCookie)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
gz := gzip.NewWriter(w)
|
gz := gzip.NewWriter(w)
|
||||||
defer gz.Close()
|
defer gz.Close()
|
||||||
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
serveFileWithReplace(gzw, r, path)
|
serveFileWithReplace(gzw, r, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
@ -7558,6 +7558,9 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
|
"kwai": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -4981,6 +4981,8 @@ definitions:
|
|||||||
karma:
|
karma:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
kwai:
|
||||||
|
type: string
|
||||||
language:
|
language:
|
||||||
type: string
|
type: string
|
||||||
lark:
|
lark:
|
||||||
|
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())
|
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
|
return res, nil
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const CracoLessPlugin = require("craco-less");
|
const CracoLessPlugin = require("craco-less");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devServer: {
|
devServer: {
|
||||||
@ -55,47 +56,42 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
webpack: {
|
webpack: {
|
||||||
configure: {
|
configure: (webpackConfig, { env, paths }) => {
|
||||||
|
paths.appBuild = path.resolve(__dirname, "build-temp");
|
||||||
|
webpackConfig.output.path = path.resolve(__dirname, "build-temp");
|
||||||
|
|
||||||
// ignore webpack warnings by source-map-loader
|
// ignore webpack warnings by source-map-loader
|
||||||
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
|
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
|
||||||
ignoreWarnings: [
|
webpackConfig.ignoreWarnings = [
|
||||||
function ignoreSourcemapsloaderWarnings(warning) {
|
function ignoreSourcemapsloaderWarnings(warning) {
|
||||||
return (
|
return (
|
||||||
warning.module &&
|
warning.module &&
|
||||||
warning.module.resource.includes('node_modules') &&
|
warning.module.resource.includes("node_modules") &&
|
||||||
warning.details &&
|
warning.details &&
|
||||||
warning.details.includes('source-map-loader')
|
warning.details.includes("source-map-loader")
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
|
||||||
// use polyfill Buffer with Webpack 5
|
// use polyfill Buffer with Webpack 5
|
||||||
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
|
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
|
||||||
// https://craco.js.org/docs/configuration/webpack/
|
// https://craco.js.org/docs/configuration/webpack/
|
||||||
resolve: {
|
webpackConfig.resolve.fallback = {
|
||||||
fallback: {
|
buffer: require.resolve("buffer/"),
|
||||||
// "process": require.resolve('process/browser'),
|
process: false,
|
||||||
// "util": require.resolve("util/"),
|
util: false,
|
||||||
// "url": require.resolve("url/"),
|
url: false,
|
||||||
// "zlib": require.resolve("browserify-zlib"),
|
zlib: false,
|
||||||
// "stream": require.resolve("stream-browserify"),
|
stream: false,
|
||||||
// "http": require.resolve("stream-http"),
|
http: false,
|
||||||
// "https": require.resolve("https-browserify"),
|
https: false,
|
||||||
// "assert": require.resolve("assert/"),
|
assert: false,
|
||||||
"buffer": require.resolve('buffer/'),
|
crypto: false,
|
||||||
"process": false,
|
os: false,
|
||||||
"util": false,
|
fs: false,
|
||||||
"url": false,
|
};
|
||||||
"zlib": false,
|
|
||||||
"stream": false,
|
return webpackConfig;
|
||||||
"http": false,
|
|
||||||
"https": false,
|
|
||||||
"assert": false,
|
|
||||||
"buffer": false,
|
|
||||||
"crypto": false,
|
|
||||||
"os": false,
|
|
||||||
"fs": false,
|
|
||||||
},
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
21
web/mv.js
Normal file
21
web/mv.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const sourceDir = path.join(__dirname, "build-temp");
|
||||||
|
const targetDir = path.join(__dirname, "build");
|
||||||
|
|
||||||
|
if (!fs.existsSync(sourceDir)) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Source directory "${sourceDir}" does not exist.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(targetDir)) {
|
||||||
|
fs.rmSync(targetDir, {recursive: true, force: true});
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`Target directory "${targetDir}" has been deleted successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(sourceDir, targetDir);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`Renamed "${sourceDir}" to "${targetDir}" successfully.`);
|
@ -57,6 +57,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env PORT=7001 craco start",
|
"start": "cross-env PORT=7001 craco start",
|
||||||
"build": "craco build",
|
"build": "craco build",
|
||||||
|
"postbuild": "node mv.js",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "craco eject",
|
"eject": "craco eject",
|
||||||
"crowdin:sync": "crowdin upload && crowdin download",
|
"crowdin:sync": "crowdin upload && crowdin download",
|
||||||
|
@ -36,6 +36,7 @@ const {Footer, Content} = Layout;
|
|||||||
|
|
||||||
import {setTwoToneColor} from "@ant-design/icons";
|
import {setTwoToneColor} from "@ant-design/icons";
|
||||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||||
|
import * as Cookie from "cookie";
|
||||||
|
|
||||||
setTwoToneColor("rgb(87,52,211)");
|
setTwoToneColor("rgb(87,52,211)");
|
||||||
|
|
||||||
@ -269,7 +270,9 @@ class App extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooter() {
|
renderFooter(logo, footerHtml) {
|
||||||
|
logo = logo ?? this.state.logo;
|
||||||
|
footerHtml = footerHtml ?? this.state.application?.footerHtml;
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
|
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
|
||||||
@ -280,14 +283,14 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
{
|
{
|
||||||
this.state.application?.footerHtml && this.state.application.footerHtml !== "" ?
|
footerHtml && footerHtml !== "" ?
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div dangerouslySetInnerHTML={{__html: this.state.application.footerHtml}} />
|
<div dangerouslySetInnerHTML={{__html: footerHtml}} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
: (
|
: (
|
||||||
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
|
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -358,13 +361,37 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onLoginSuccess(redirectUrl) {
|
||||||
|
window.google?.accounts?.id?.cancel();
|
||||||
|
if (redirectUrl) {
|
||||||
|
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
||||||
|
}
|
||||||
|
this.getAccount();
|
||||||
|
}
|
||||||
|
|
||||||
renderPage() {
|
renderPage() {
|
||||||
if (this.isDoorPages()) {
|
if (this.isDoorPages()) {
|
||||||
|
let themeData = this.state.themeData;
|
||||||
|
let logo = this.state.logo;
|
||||||
|
let footerHtml = null;
|
||||||
|
if (this.state.organization === undefined) {
|
||||||
|
const curCookie = Cookie.parse(document.cookie);
|
||||||
|
if (curCookie["organizationTheme"] && curCookie["organizationTheme"] !== "null") {
|
||||||
|
themeData = JSON.parse(curCookie["organizationTheme"]);
|
||||||
|
}
|
||||||
|
if (curCookie["organizationLogo"] && curCookie["organizationLogo"] !== "") {
|
||||||
|
logo = curCookie["organizationLogo"];
|
||||||
|
}
|
||||||
|
if (curCookie["organizationFootHtml"] && curCookie["organizationFootHtml"] !== "") {
|
||||||
|
footerHtml = curCookie["organizationFootHtml"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={{
|
<ConfigProvider theme={{
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: this.state.themeData.colorPrimary,
|
colorPrimary: themeData.colorPrimary,
|
||||||
borderRadius: this.state.themeData.borderRadius,
|
borderRadius: themeData.borderRadius,
|
||||||
},
|
},
|
||||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||||
}}>
|
}}>
|
||||||
@ -382,26 +409,20 @@ class App extends Component {
|
|||||||
application: application,
|
application: application,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onLoginSuccess={(redirectUrl) => {
|
onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
|
||||||
window.google?.accounts?.id?.cancel();
|
|
||||||
if (redirectUrl) {
|
|
||||||
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
|
||||||
}
|
|
||||||
this.getAccount();
|
|
||||||
}}
|
|
||||||
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
||||||
updataThemeData={this.setTheme}
|
updataThemeData={this.setTheme}
|
||||||
/> :
|
/> :
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/callback" component={AuthCallback} />
|
<Route exact path="/callback" render={(props) => <AuthCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||||
<Route exact path="/callback/saml" component={SamlCallback} />
|
<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.")}
|
<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>} />} />
|
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
}
|
}
|
||||||
</Content>
|
</Content>
|
||||||
{
|
{
|
||||||
this.renderFooter()
|
this.renderFooter(logo, footerHtml)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.renderAiAssistant()
|
this.renderAiAssistant()
|
||||||
|
@ -111,7 +111,7 @@ class EntryPage extends React.Component {
|
|||||||
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
||||||
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
||||||
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
||||||
style={{margin: "0 auto"}} />
|
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} applicationName={authConfig.appName} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||||
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||||
|
@ -33,18 +33,6 @@ class GroupListPage extends BaseListPage {
|
|||||||
}
|
}
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
super.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() {
|
newGroup() {
|
||||||
@ -188,12 +176,8 @@ class GroupListPage extends BaseListPage {
|
|||||||
{record.parentId}
|
{record.parentId}
|
||||||
</Link>;
|
</Link>;
|
||||||
}
|
}
|
||||||
const parentGroup = this.state.groups.find((group) => group.name === text);
|
return <Link to={`/groups/${record.owner}/${record.parentId}`}>
|
||||||
if (parentGroup === undefined) {
|
{record?.parentName}
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return <Link to={`/groups/${parentGroup.owner}/${parentGroup.name}`}>
|
|
||||||
{parentGroup?.displayName}
|
|
||||||
</Link>;
|
</Link>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -215,12 +199,11 @@ class GroupListPage extends BaseListPage {
|
|||||||
width: "180px",
|
width: "180px",
|
||||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
const haveChildren = this.state.groups.find((group) => group.parentId === record.id) !== undefined;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<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
|
<PopconfirmModal
|
||||||
disabled={haveChildren}
|
disabled={record.haveChildren}
|
||||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||||
onConfirm={() => this.deleteGroup(index)}
|
onConfirm={() => this.deleteGroup(index)}
|
||||||
>
|
>
|
||||||
|
@ -106,6 +106,22 @@ class InvitationEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copySignupLink() {
|
||||||
|
let defaultApplication;
|
||||||
|
if (this.state.invitation.owner === "built-in") {
|
||||||
|
defaultApplication = "app-built-in";
|
||||||
|
} else {
|
||||||
|
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
||||||
|
defaultApplication = selectedOrganization.defaultApplication;
|
||||||
|
if (!defaultApplication) {
|
||||||
|
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
||||||
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
|
}
|
||||||
|
|
||||||
renderInvitation() {
|
renderInvitation() {
|
||||||
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
|
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
|
||||||
return (
|
return (
|
||||||
@ -114,16 +130,7 @@ class InvitationEditPage extends React.Component {
|
|||||||
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}
|
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}
|
||||||
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} onClick={() => {
|
<Button style={{marginLeft: "20px"}} onClick={_ => this.copySignupLink()}>
|
||||||
let defaultApplication;
|
|
||||||
if (this.state.invitation.owner === "built-in") {
|
|
||||||
defaultApplication = "app-built-in";
|
|
||||||
} else {
|
|
||||||
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
|
||||||
}
|
|
||||||
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
|
||||||
}}>
|
|
||||||
{i18next.t("application:Copy signup page URL")}
|
{i18next.t("application:Copy signup page URL")}
|
||||||
</Button>
|
</Button>
|
||||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||||
@ -330,16 +337,7 @@ class InvitationEditPage extends React.Component {
|
|||||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||||
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} size="large" onClick={() => {
|
<Button style={{marginLeft: "20px"}} size="large" onClick={_ => this.copySignupLink()}>
|
||||||
let defaultApplication;
|
|
||||||
if (this.state.invitation.owner === "built-in") {
|
|
||||||
defaultApplication = "app-built-in";
|
|
||||||
} else {
|
|
||||||
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
|
||||||
}
|
|
||||||
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
|
||||||
}}>
|
|
||||||
{i18next.t("application:Copy signup page URL")}
|
{i18next.t("application:Copy signup page URL")}
|
||||||
</Button>
|
</Button>
|
||||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||||
|
@ -228,6 +228,21 @@ class LdapEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Password type"), i18next.t("general:Password type - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={21}>
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.ldap.passwordType ?? []} onChange={(value => {
|
||||||
|
this.updateLdapField("passwordType", value);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Option key={"Plain"} value={"Plain"}>{i18next.t("general:Plain")}</Option>
|
||||||
|
<Option key={"SSHA"} value={"SSHA"} >SSHA</Option>
|
||||||
|
<Option key={"MD5"} value={"MD5"} >MD5</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||||
|
@ -241,7 +241,7 @@ function ManagementPage(props) {
|
|||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
<img className="logo" src={logo ?? props.logo} alt="logo" />
|
||||||
</Link>,
|
</Link>,
|
||||||
disabled: true,
|
disabled: true, key: "logo",
|
||||||
style: {
|
style: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
height: "auto",
|
height: "auto",
|
||||||
@ -323,9 +323,37 @@ function ManagementPage(props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navItems = props.account.organization.navItems;
|
||||||
|
|
||||||
|
if (!Array.isArray(navItems)) {
|
||||||
return res;
|
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) {
|
function renderLoginIfNotLoggedIn(component) {
|
||||||
if (props.account === null) {
|
if (props.account === null) {
|
||||||
sessionStorage.setItem("from", window.location.pathname);
|
sessionStorage.setItem("from", window.location.pathname);
|
||||||
|
@ -26,6 +26,7 @@ import LdapTable from "./table/LdapTable";
|
|||||||
import AccountTable from "./table/AccountTable";
|
import AccountTable from "./table/AccountTable";
|
||||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||||
import MfaTable from "./table/MfaTable";
|
import MfaTable from "./table/MfaTable";
|
||||||
|
import {NavItemTree} from "./common/NavItemTree";
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -522,6 +523,21 @@ class OrganizationEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general: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"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
||||||
|
@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
|
|||||||
payment: payment,
|
payment: payment,
|
||||||
});
|
});
|
||||||
if (payment.state === "Created") {
|
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({
|
this.setState({
|
||||||
timeout: setTimeout(async() => {
|
timeout: setTimeout(async() => {
|
||||||
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
||||||
|
@ -238,6 +238,8 @@ class ProductBuyPage extends React.Component {
|
|||||||
text = i18next.t("product:PayPal");
|
text = i18next.t("product:PayPal");
|
||||||
} else if (provider.type === "Stripe") {
|
} else if (provider.type === "Stripe") {
|
||||||
text = i18next.t("product:Stripe");
|
text = i18next.t("product:Stripe");
|
||||||
|
} else if (provider.type === "AirWallex") {
|
||||||
|
text = i18next.t("product:AirWallex");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -297,6 +297,8 @@ class ProviderEditPage extends React.Component {
|
|||||||
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
||||||
} else if (provider.type === "WeChat Pay") {
|
} else if (provider.type === "WeChat Pay") {
|
||||||
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
||||||
|
} else if (provider.type === "CUCloud") {
|
||||||
|
return Setting.getLabel(i18next.t("provider:Account ID"), i18next.t("provider:Account ID - Tooltip"));
|
||||||
} else {
|
} else {
|
||||||
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
||||||
}
|
}
|
||||||
@ -393,6 +395,9 @@ class ProviderEditPage extends React.Component {
|
|||||||
} else if (provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") {
|
} else if (provider.type === "Line" || provider.type === "Matrix" || provider.type === "Rocket Chat") {
|
||||||
text = i18next.t("provider:App Key");
|
text = i18next.t("provider:App Key");
|
||||||
tooltip = i18next.t("provider:App Key - Tooltip");
|
tooltip = i18next.t("provider:App Key - Tooltip");
|
||||||
|
} else if (provider.type === "CUCloud") {
|
||||||
|
text = i18next.t("provider:Topic name");
|
||||||
|
tooltip = i18next.t("provider:Topic name - Tooltip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -462,6 +467,39 @@ class ProviderEditPage extends React.Component {
|
|||||||
this.updateProviderField("issuerUrl", issuerUrl);
|
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() {
|
renderProvider() {
|
||||||
return (
|
return (
|
||||||
<Card size="small" title={
|
<Card size="small" title={
|
||||||
@ -771,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>
|
<React.Fragment>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
@ -784,7 +822,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{
|
{
|
||||||
(this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
|
(this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{this.getClientSecret2Label(this.state.provider)} :
|
{this.getClientSecret2Label(this.state.provider)} :
|
||||||
@ -870,9 +908,9 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email"].includes(this.state.provider.type) ? (
|
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<div>
|
<div>
|
||||||
{["Local File System"].includes(this.state.provider.type) ? null : (
|
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
|
{Setting.getLabel(i18next.t("provider:Endpoint"), i18next.t("provider:Region endpoint for Internet"))} :
|
||||||
@ -884,7 +922,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
||||||
@ -896,7 +934,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{["Casdoor"].includes(this.state.provider.type) ?
|
{["Casdoor"].includes(this.state.provider.type) ?
|
||||||
@ -910,7 +948,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
||||||
@ -922,7 +960,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||||
@ -946,7 +984,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO"].includes(this.state.provider.type) ? (
|
{["AWS S3", "Tencent Cloud COS", "Qiniu Cloud Kodo", "Casdoor", "CUCloud OSS", "MinIO", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{["Casdoor"].includes(this.state.provider.type) ?
|
{["Casdoor"].includes(this.state.provider.type) ?
|
||||||
@ -985,7 +1023,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
{["Custom HTTP"].includes(this.state.provider.type) ? (
|
{["Custom HTTP", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
|
||||||
@ -997,7 +1035,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
) : null}
|
) : null}
|
||||||
{["Google Chat"].includes(this.state.provider.type) ? (
|
{["Google Chat", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
||||||
@ -1237,6 +1275,21 @@ class ProviderEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("provider: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"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Metadata"), i18next.t("provider:Metadata - Tooltip"))} :
|
||||||
@ -1250,14 +1303,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
<Row style={{marginTop: "20px"}}>
|
<Row style={{marginTop: "20px"}}>
|
||||||
<Col style={{marginTop: "5px"}} span={2} />
|
<Col style={{marginTop: "5px"}} span={2} />
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button type="primary" onClick={() => {
|
<Button type="primary" onClick={() => {this.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"));
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{i18next.t("provider:Parse")}
|
{i18next.t("provider:Parse")}
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
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 {QuestionCircleTwoTone} from "@ant-design/icons";
|
||||||
import {isMobile as isMobileDevice} from "react-device-detect";
|
import {isMobile as isMobileDevice} from "react-device-detect";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
@ -25,6 +25,8 @@ import {Helmet} from "react-helmet";
|
|||||||
import * as Conf from "./Conf";
|
import * as Conf from "./Conf";
|
||||||
import * as phoneNumber from "libphonenumber-js";
|
import * as phoneNumber from "libphonenumber-js";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./auth/mfa/MfaAuthVerifyForm";
|
||||||
|
import {EmailMfaType, SmsMfaType, TotpMfaType} from "./auth/MfaSetupPage";
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -277,6 +279,10 @@ export const OtherProviderInfo = {
|
|||||||
logo: `${StaticBaseUrl}/img/social_stripe.png`,
|
logo: `${StaticBaseUrl}/img/social_stripe.png`,
|
||||||
url: "https://stripe.com/",
|
url: "https://stripe.com/",
|
||||||
},
|
},
|
||||||
|
"AirWallex": {
|
||||||
|
logo: `${StaticBaseUrl}/img/payment_airwallex.svg`,
|
||||||
|
url: "https://airwallex.com/",
|
||||||
|
},
|
||||||
"GC": {
|
"GC": {
|
||||||
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
||||||
url: "https://gc.org",
|
url: "https://gc.org",
|
||||||
@ -405,6 +411,10 @@ export const OtherProviderInfo = {
|
|||||||
logo: `${StaticBaseUrl}/img/social_viber.png`,
|
logo: `${StaticBaseUrl}/img/social_viber.png`,
|
||||||
url: "https://www.viber.com/",
|
url: "https://www.viber.com/",
|
||||||
},
|
},
|
||||||
|
"CUCloud": {
|
||||||
|
logo: `${StaticBaseUrl}/img/cucloud.png`,
|
||||||
|
url: "https://www.cucloud.cn/",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -985,6 +995,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Bilibili", name: "Bilibili"},
|
{id: "Bilibili", name: "Bilibili"},
|
||||||
{id: "Okta", name: "Okta"},
|
{id: "Okta", name: "Okta"},
|
||||||
{id: "Douyin", name: "Douyin"},
|
{id: "Douyin", name: "Douyin"},
|
||||||
|
{id: "Kwai", name: "Kwai"},
|
||||||
{id: "Line", name: "Line"},
|
{id: "Line", name: "Line"},
|
||||||
{id: "Amazon", name: "Amazon"},
|
{id: "Amazon", name: "Amazon"},
|
||||||
{id: "Auth0", name: "Auth0"},
|
{id: "Auth0", name: "Auth0"},
|
||||||
@ -1099,6 +1110,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "WeChat Pay", name: "WeChat Pay"},
|
{id: "WeChat Pay", name: "WeChat Pay"},
|
||||||
{id: "PayPal", name: "PayPal"},
|
{id: "PayPal", name: "PayPal"},
|
||||||
{id: "Stripe", name: "Stripe"},
|
{id: "Stripe", name: "Stripe"},
|
||||||
|
{id: "AirWallex", name: "AirWallex"},
|
||||||
{id: "GC", name: "GC"},
|
{id: "GC", name: "GC"},
|
||||||
]);
|
]);
|
||||||
} else if (category === "Captcha") {
|
} else if (category === "Captcha") {
|
||||||
@ -1136,6 +1148,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Reddit", name: "Reddit"},
|
{id: "Reddit", name: "Reddit"},
|
||||||
{id: "Rocket Chat", name: "Rocket Chat"},
|
{id: "Rocket Chat", name: "Rocket Chat"},
|
||||||
{id: "Viber", name: "Viber"},
|
{id: "Viber", name: "Viber"},
|
||||||
|
{id: "CUCloud", name: "CUCloud"},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
@ -1390,7 +1403,13 @@ export function getTag(color, text, icon) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getApplicationName(application) {
|
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) {
|
export function getApplicationDisplayName(application) {
|
||||||
@ -1582,3 +1601,114 @@ export function getCurrencyText(product) {
|
|||||||
export function isDarkTheme(themeAlgorithm) {
|
export function isDarkTheme(themeAlgorithm) {
|
||||||
return themeAlgorithm && themeAlgorithm.includes("dark");
|
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 * as Setting from "../Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import RedirectForm from "../common/RedirectForm";
|
import RedirectForm from "../common/RedirectForm";
|
||||||
|
import {renderLoginPanel} from "../Setting";
|
||||||
|
|
||||||
class AuthCallback extends React.Component {
|
class AuthCallback extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -131,6 +132,7 @@ class AuthCallback extends React.Component {
|
|||||||
// user is using casdoor as cas sso server, and wants the ticket to be acquired
|
// user is using casdoor as cas sso server, and wants the ticket to be acquired
|
||||||
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
|
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
|
const handleCasLogin = (res) => {
|
||||||
let msg = "Logged in successfully.";
|
let msg = "Logged in successfully.";
|
||||||
if (casService === "") {
|
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.
|
// If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
|
||||||
@ -144,6 +146,9 @@ class AuthCallback extends React.Component {
|
|||||||
newUrl.searchParams.append("ticket", st);
|
newUrl.searchParams.append("ticket", st);
|
||||||
window.location.href = newUrl.toString();
|
window.location.href = newUrl.toString();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||||
}
|
}
|
||||||
@ -159,6 +164,7 @@ class AuthCallback extends React.Component {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const responseType = this.getResponseType();
|
const responseType = this.getResponseType();
|
||||||
|
const handleLogin = (res) => {
|
||||||
if (responseType === "login") {
|
if (responseType === "login") {
|
||||||
if (res.data2) {
|
if (res.data2) {
|
||||||
sessionStorage.setItem("signinUrl", signinUrl);
|
sessionStorage.setItem("signinUrl", signinUrl);
|
||||||
@ -204,9 +210,12 @@ class AuthCallback extends React.Component {
|
|||||||
}
|
}
|
||||||
const SAMLResponse = res.data;
|
const SAMLResponse = res.data;
|
||||||
const redirectUri = res.data2.redirectUrl;
|
const redirectUri = res.data2.redirectUrl;
|
||||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Setting.checkLoginMfa(res, body, oAuthParams, handleLogin, this, window.location.origin);
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
msg: res.msg,
|
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} />;
|
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 (
|
return (
|
||||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||||
{
|
{
|
||||||
|
@ -264,6 +264,9 @@ class ForgetPage extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onValuesChange={(changedValues, allValues) => {
|
onValuesChange={(changedValues, allValues) => {
|
||||||
|
if (!changedValues.dest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email";
|
const verifyType = changedValues.dest?.indexOf("@") === -1 ? "phone" : "email";
|
||||||
this.setState({
|
this.setState({
|
||||||
dest: changedValues.dest,
|
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 LanguageSelect from "../common/select/LanguageSelect";
|
||||||
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
||||||
import RedirectForm from "../common/RedirectForm";
|
import RedirectForm from "../common/RedirectForm";
|
||||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||||
import * as ProviderButton from "./ProviderButton";
|
import * as ProviderButton from "./ProviderButton";
|
||||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||||
@ -438,25 +438,7 @@ class LoginPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
if (res.data === NextMfa) {
|
Setting.checkLoginMfa(res, values, casParams, loginHandler, this);
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||||
}
|
}
|
||||||
@ -505,39 +487,13 @@ class LoginPage extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
const SAMLResponse = res.data;
|
const SAMLResponse = res.data;
|
||||||
const redirectUri = res.data2.redirectUrl;
|
const redirectUri = res.data2.redirectUrl;
|
||||||
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
if (res.data === NextMfa) {
|
Setting.checkLoginMfa(res, values, oAuthParams, loginHandler, this);
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,10 @@ const authInfo = {
|
|||||||
scope: "user_info",
|
scope: "user_info",
|
||||||
endpoint: "https://open.douyin.com/platform/oauth/connect",
|
endpoint: "https://open.douyin.com/platform/oauth/connect",
|
||||||
},
|
},
|
||||||
|
Kwai: {
|
||||||
|
scope: "user_info",
|
||||||
|
endpoint: "https://open.kuaishou.com/oauth2/connect",
|
||||||
|
},
|
||||||
Custom: {
|
Custom: {
|
||||||
endpoint: "https://example.com/",
|
endpoint: "https://example.com/",
|
||||||
},
|
},
|
||||||
@ -470,6 +474,8 @@ export function getAuthUrl(application, provider, method, code) {
|
|||||||
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||||
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
|
} else if (provider.type === "Douyin" || provider.type === "TikTok") {
|
||||||
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||||
|
} else if (provider.type === "Kwai") {
|
||||||
|
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||||
} else if (provider.type === "Custom") {
|
} else if (provider.type === "Custom") {
|
||||||
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
|
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
|
||||||
} else if (provider.type === "Bilibili") {
|
} else if (provider.type === "Bilibili") {
|
||||||
|
@ -40,9 +40,11 @@ import SteamLoginButton from "./SteamLoginButton";
|
|||||||
import BilibiliLoginButton from "./BilibiliLoginButton";
|
import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||||
import OktaLoginButton from "./OktaLoginButton";
|
import OktaLoginButton from "./OktaLoginButton";
|
||||||
import DouyinLoginButton from "./DouyinLoginButton";
|
import DouyinLoginButton from "./DouyinLoginButton";
|
||||||
|
import KwaiLoginButton from "./KwaiLoginButton";
|
||||||
import LoginButton from "./LoginButton";
|
import LoginButton from "./LoginButton";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
import {WechatOfficialAccountModal} from "./Util";
|
import {WechatOfficialAccountModal} from "./Util";
|
||||||
|
import * as Setting from "../Setting";
|
||||||
|
|
||||||
function getSigninButton(provider) {
|
function getSigninButton(provider) {
|
||||||
const text = i18next.t("login:Sign in with {type}").replace("{type}", provider.displayName !== "" ? provider.displayName : provider.type);
|
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"} />;
|
return <OktaLoginButton text={text} align={"center"} />;
|
||||||
} else if (provider.type === "Douyin") {
|
} else if (provider.type === "Douyin") {
|
||||||
return <DouyinLoginButton text={text} align={"center"} />;
|
return <DouyinLoginButton text={text} align={"center"} />;
|
||||||
|
} else if (provider.type === "Kwai") {
|
||||||
|
return <KwaiLoginButton text={text} align={"center"} />;
|
||||||
} else {
|
} else {
|
||||||
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
|
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
|
||||||
}
|
}
|
||||||
@ -111,11 +115,15 @@ function goToSamlUrl(provider, location) {
|
|||||||
|
|
||||||
const relayState = `${clientId}&${state}&${providerName}&${realRedirectUri}&${redirectUri}`;
|
const relayState = `${clientId}&${state}&${providerName}&${realRedirectUri}&${redirectUri}`;
|
||||||
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`, btoa(relayState)).then((res) => {
|
AuthBackend.getSamlLogin(`${provider.owner}/${providerName}`, btoa(relayState)).then((res) => {
|
||||||
|
if (res.status === "ok") {
|
||||||
if (res.data2 === "POST") {
|
if (res.data2 === "POST") {
|
||||||
document.write(res.data);
|
document.write(res.data);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = res.data;
|
window.location.href = res.data;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Setting.showMessage("error", res.msg);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import * as Util from "./Util";
|
|||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import {authConfig} from "./Auth";
|
import {authConfig} from "./Auth";
|
||||||
|
import {renderLoginPanel} from "../Setting";
|
||||||
|
|
||||||
class SamlCallback extends React.Component {
|
class SamlCallback extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -81,13 +82,26 @@ class SamlCallback extends React.Component {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const responseType = this.getResponseType(redirectUri);
|
const responseType = this.getResponseType(redirectUri);
|
||||||
|
const handleLogin = (res2) => {
|
||||||
if (responseType === "login") {
|
if (responseType === "login") {
|
||||||
Setting.showMessage("success", "Logged in successfully");
|
Setting.showMessage("success", "Logged in successfully");
|
||||||
Setting.goToLink("/");
|
Setting.goToLink("/");
|
||||||
} else if (responseType === "code") {
|
} else if (responseType === "code") {
|
||||||
const code = res.data;
|
const code = res2.data;
|
||||||
Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
|
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 {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
msg: res.msg,
|
msg: res.msg,
|
||||||
@ -97,6 +111,11 @@ class SamlCallback extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.state.getVerifyTotp !== undefined) {
|
||||||
|
const application = Setting.getApplicationObj(this);
|
||||||
|
return renderLoginPanel(application, this.state.getVerifyTotp, this, window.location.origin);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||||
{
|
{
|
||||||
|
@ -33,7 +33,8 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
|||||||
|
|
||||||
const verify = ({passcode}) => {
|
const verify = ({passcode}) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const values = {...formValues, passcode, mfaType};
|
const values = {...formValues, passcode};
|
||||||
|
values["mfaType"] = mfaProps.mfaType;
|
||||||
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
|
const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login;
|
||||||
loginFunction(values, authParams).then((res) => {
|
loginFunction(values, authParams).then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
@ -71,7 +72,7 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application
|
|||||||
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
|
||||||
{i18next.t("mfa:Multi-factor authentication")}
|
{i18next.t("mfa:Multi-factor authentication")}
|
||||||
</div>
|
</div>
|
||||||
{mfaType === SmsMfaType || mfaType === EmailMfaType ? (
|
{mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{marginBottom: 24}}>
|
<div style={{marginBottom: 24}}>
|
||||||
{i18next.t("mfa:You have enabled Multi-Factor Authentication, Please click 'Send Code' to continue")}
|
{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 = {
|
const data = {
|
||||||
mfaType,
|
mfaType,
|
||||||
secret,
|
secret,
|
||||||
recoveryCodes,
|
|
||||||
dest,
|
dest,
|
||||||
countryCode,
|
countryCode,
|
||||||
...user,
|
...user,
|
||||||
};
|
};
|
||||||
|
data["recoveryCodes"] = recoveryCodes[0];
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
MfaBackend.MfaSetupEnable(data).then(res => {
|
MfaBackend.MfaSetupEnable(data).then(res => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
|
@ -27,7 +27,7 @@ export const generateCasdoorAppUrl = (accessToken, forQrCode = true) => {
|
|||||||
return {qrUrl, error};
|
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) {
|
if (forQrCode && qrUrl.length >= 2000) {
|
||||||
qrUrl = "";
|
qrUrl = "";
|
||||||
|
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 (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Prodáno",
|
"Sold": "Prodáno",
|
||||||
"Sold - Tooltip": "Prodávané množství",
|
"Sold - Tooltip": "Prodávané množství",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Štítek produktu",
|
"Tag - Tooltip": "Štítek produktu",
|
||||||
"Test buy page..": "Testovací stránka nákupu..",
|
"Test buy page..": "Testovací stránka nákupu..",
|
||||||
"There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.",
|
"There is no payment channel for this product.": "Pro tento produkt neexistuje žádný platební kanál.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Verkauft",
|
"Sold": "Verkauft",
|
||||||
"Sold - Tooltip": "Menge verkauft",
|
"Sold - Tooltip": "Menge verkauft",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag des Produkts",
|
"Tag - Tooltip": "Tag des Produkts",
|
||||||
"Test buy page..": "Testkaufseite.",
|
"Test buy page..": "Testkaufseite.",
|
||||||
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
|
"There is no payment channel for this product.": "Es gibt keinen Zahlungskanal für dieses Produkt.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Vendido",
|
"Sold": "Vendido",
|
||||||
"Sold - Tooltip": "Cantidad vendida",
|
"Sold - Tooltip": "Cantidad vendida",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Etiqueta de producto",
|
"Tag - Tooltip": "Etiqueta de producto",
|
||||||
"Test buy page..": "Página de compra de prueba.",
|
"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.",
|
"There is no payment channel for this product.": "No hay canal de pago para este producto.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "فروخته شده",
|
"Sold": "فروخته شده",
|
||||||
"Sold - Tooltip": "تعداد فروخته شده",
|
"Sold - Tooltip": "تعداد فروخته شده",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "برچسب محصول",
|
"Tag - Tooltip": "برچسب محصول",
|
||||||
"Test buy page..": "صفحه تست خرید..",
|
"Test buy page..": "صفحه تست خرید..",
|
||||||
"There is no payment channel for this product.": "برای این محصول کانال پرداختی وجود ندارد.",
|
"There is no payment channel for this product.": "برای این محصول کانال پرداختی وجود ندارد.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Vendu",
|
"Sold": "Vendu",
|
||||||
"Sold - Tooltip": "Quantité vendue",
|
"Sold - Tooltip": "Quantité vendue",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Étiquette de produit",
|
"Tag - Tooltip": "Étiquette de produit",
|
||||||
"Test buy page..": "Page d'achat de test.",
|
"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.",
|
"There is no payment channel for this product.": "Il n'y a aucun canal de paiement pour ce produit.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Terjual",
|
"Sold": "Terjual",
|
||||||
"Sold - Tooltip": "Jumlah terjual",
|
"Sold - Tooltip": "Jumlah terjual",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag produk",
|
"Tag - Tooltip": "Tag produk",
|
||||||
"Test buy page..": "Halaman pembelian uji coba.",
|
"Test buy page..": "Halaman pembelian uji coba.",
|
||||||
"There is no payment channel for this product.": "Tidak ada saluran pembayaran untuk produk ini.",
|
"There is no payment channel for this product.": "Tidak ada saluran pembayaran untuk produk ini.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "売れました",
|
"Sold": "売れました",
|
||||||
"Sold - Tooltip": "販売数量",
|
"Sold - Tooltip": "販売数量",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "製品のタグ",
|
"Tag - Tooltip": "製品のタグ",
|
||||||
"Test buy page..": "テスト購入ページ。",
|
"Test buy page..": "テスト購入ページ。",
|
||||||
"There is no payment channel for this product.": "この製品には支払いチャネルがありません。",
|
"There is no payment channel for this product.": "この製品には支払いチャネルがありません。",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "팔렸습니다",
|
"Sold": "팔렸습니다",
|
||||||
"Sold - Tooltip": "판매량",
|
"Sold - Tooltip": "판매량",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "제품 태그",
|
"Tag - Tooltip": "제품 태그",
|
||||||
"Test buy page..": "시험 구매 페이지.",
|
"Test buy page..": "시험 구매 페이지.",
|
||||||
"There is no payment channel for this product.": "이 제품에 대한 결제 채널이 없습니다.",
|
"There is no payment channel for this product.": "이 제품에 대한 결제 채널이 없습니다.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Vendido",
|
"Sold": "Vendido",
|
||||||
"Sold - Tooltip": "Quantidade vendida",
|
"Sold - Tooltip": "Quantidade vendida",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag do produto",
|
"Tag - Tooltip": "Tag do produto",
|
||||||
"Test buy page..": "Página de teste de compra...",
|
"Test buy page..": "Página de teste de compra...",
|
||||||
"There is no payment channel for this product.": "Não há canal de pagamento disponível para este produto.",
|
"There is no payment channel for this product.": "Não há canal de pagamento disponível para este produto.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Продано",
|
"Sold": "Продано",
|
||||||
"Sold - Tooltip": "Количество проданных",
|
"Sold - Tooltip": "Количество проданных",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Метка продукта",
|
"Tag - Tooltip": "Метка продукта",
|
||||||
"Test buy page..": "Страница для тестовой покупки.",
|
"Test buy page..": "Страница для тестовой покупки.",
|
||||||
"There is no payment channel for this product.": "Для этого продукта нет канала оплаты.",
|
"There is no payment channel for this product.": "Для этого продукта нет канала оплаты.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Predané",
|
"Sold": "Predané",
|
||||||
"Sold - Tooltip": "Množstvo predaných kusov",
|
"Sold - Tooltip": "Množstvo predaných kusov",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Štítok produktu",
|
"Tag - Tooltip": "Štítok produktu",
|
||||||
"Test buy page..": "Testovať stránku nákupu..",
|
"Test buy page..": "Testovať stránku nákupu..",
|
||||||
"There is no payment channel for this product.": "Pre tento produkt neexistuje platobný kanál.",
|
"There is no payment channel for this product.": "Pre tento produkt neexistuje platobný kanál.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Sold",
|
"Sold": "Sold",
|
||||||
"Sold - Tooltip": "Quantity sold",
|
"Sold - Tooltip": "Quantity sold",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Tag of product",
|
"Tag - Tooltip": "Tag of product",
|
||||||
"Test buy page..": "Test buy page..",
|
"Test buy page..": "Test buy page..",
|
||||||
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
"There is no payment channel for this product.": "There is no payment channel for this product.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Продано",
|
"Sold": "Продано",
|
||||||
"Sold - Tooltip": "Продана кількість",
|
"Sold - Tooltip": "Продана кількість",
|
||||||
"Stripe": "смужка",
|
"Stripe": "смужка",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Тег товару",
|
"Tag - Tooltip": "Тег товару",
|
||||||
"Test buy page..": "Сторінка тестової покупки..",
|
"Test buy page..": "Сторінка тестової покупки..",
|
||||||
"There is no payment channel for this product.": "Для цього продукту немає платіжного каналу.",
|
"There is no payment channel for this product.": "Для цього продукту немає платіжного каналу.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "Đã bán",
|
"Sold": "Đã bán",
|
||||||
"Sold - Tooltip": "Số lượng bán ra",
|
"Sold - Tooltip": "Số lượng bán ra",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "Nhãn sản phẩm",
|
"Tag - Tooltip": "Nhãn sản phẩm",
|
||||||
"Test buy page..": "Trang mua thử.",
|
"Test buy page..": "Trang mua thử.",
|
||||||
"There is no payment channel for this product.": "Không có kênh thanh toán cho sản phẩm này.",
|
"There is no payment channel for this product.": "Không có kênh thanh toán cho sản phẩm này.",
|
||||||
|
@ -757,6 +757,7 @@
|
|||||||
"Sold": "售出",
|
"Sold": "售出",
|
||||||
"Sold - Tooltip": "已售出的数量",
|
"Sold - Tooltip": "已售出的数量",
|
||||||
"Stripe": "Stripe",
|
"Stripe": "Stripe",
|
||||||
|
"AirWallex": "AirWallex",
|
||||||
"Tag - Tooltip": "商品类别",
|
"Tag - Tooltip": "商品类别",
|
||||||
"Test buy page..": "测试购买页面..",
|
"Test buy page..": "测试购买页面..",
|
||||||
"There is no payment channel for this product.": "该商品没有付款方式。",
|
"There is no payment channel for this product.": "该商品没有付款方式。",
|
||||||
|
Reference in New Issue
Block a user