Compare commits

...

40 Commits

Author SHA1 Message Date
811999b6cc feat: fix error handling in CheckPassword() related functions 2023-11-20 21:49:19 +08:00
7786018051 feat: use short state for OAuth provider (#2504)
* fix: use fixed length of state

* fix: use short state
2023-11-19 07:30:29 +08:00
6c72f86d03 fix: support LDAP in linux (#2500)
Co-authored-by: Xiang Zhen Gan <m1353825@163.com>
2023-11-16 23:58:09 +08:00
5b151f4ec4 feat: improve cert edit page UI 2023-11-13 15:57:46 +08:00
e9b7d1266f Fix API typo: /get-global-certs 2023-11-13 14:22:40 +08:00
2d4998228c Add organization.MasterVerificationCode 2023-11-13 13:53:41 +08:00
d3ed6c348b Improve GetOAuthToken() API's parameter handling 2023-11-13 02:30:32 +08:00
a22e05dcc1 feat: fix the UI and navigation errors on the prompt page (#2486) 2023-11-12 15:54:38 +08:00
0ac2b69f5a feat: support WeChat Pay via JSAPI (#2488)
* feat: support wechat jsapi payment

* feat: add log

* feat: update sign

* feat: process wechat pay result

* feat: process wechat pay result

* feat: save wechat openid for different app

* feat: save wechat openid for different app

* feat: add SetUserOAuthProperties for signup

* feat: fix openid for wechat

* feat: get user extra property in buyproduct

* feat: remove log

* feat: remove log

* feat: gofumpt code

* feat: change lr->crlf

* feat: change crlf->lf

* feat: improve code
2023-11-11 17:16:57 +08:00
d090e9c860 Improve downloadImage() 2023-11-10 08:35:21 +08:00
8ebb158765 feat: improve README 2023-11-09 21:52:52 +08:00
ea2f053630 feat: add fields like Email to user profile in JWT-Empty mode 2023-11-09 20:20:42 +08:00
988b14c6b5 Fix user's UpdatedTime in other APIs 2023-11-08 20:22:28 +08:00
a9e72ac3cb feat: fix bug in GetAllowedApplications() 2023-11-08 10:31:24 +08:00
498cd02d49 feat: add GetAllowedApplications() in user's app homepage 2023-11-08 09:48:31 +08:00
a389842f59 Improve Product fields 2023-11-06 19:44:21 +08:00
6c69daa666 feat: fix search for ldap users' name within an organization (#2476)
* fix: #2304

* fix: when logging in with OAuth2 and authenticating via WebAuthn, retrieve the application from the clientId.

* fix: search for ldap users' name within an organization

---------

Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-11-06 11:48:23 +08:00
53c89bbe89 feat: upgrade xorm-adapter to add id to CasbinRule 2023-11-03 02:48:01 +08:00
9442aa9f7a Remove useless PermissionRule 2023-11-03 00:39:16 +08:00
8a195715d0 Remove migrator code 2023-11-03 00:25:09 +08:00
b985bab3f3 fix: fix dropped errors in GetUser() (#2470)
* controllers: fix dropped errors

* Update user.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-11-01 23:07:24 +08:00
477a090aa0 feat: when logging in with OAuth2 and authenticating via WebAuthn, retrieve the application from the clientId (#2469)
* fix: #2304

* fix: when logging in with OAuth2 and authenticating via WebAuthn, retrieve the application from the clientId.

---------

Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-11-01 18:40:05 +08:00
e082cf10e0 fix: fix Okta provider no host issue (#2467) 2023-11-01 18:14:39 +08:00
3215b88eae fix: ADFS GetToken() and GetUserInfo() bug (#2468)
* fix adfs bug

* Update adfs.go

---------

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2023-11-01 17:58:17 +08:00
9703f3f712 Support Apple OAuth login now 2023-10-31 23:10:36 +08:00
140737b2f6 Fix some bugs in Apple OAuth login path 2023-10-31 23:10:36 +08:00
b285144a64 ci: support MySQL data sync (#2443)
* feat: support tool for mysql master-slave sync

* feat: support mysql master-master sync

* feat: improve log

* feat: improve code

* fix: fix bug when len(res) ==0

* fix: fix bug when len(res) ==0

* feat: support master-slave sync

* feat: add deleteSlaveUser for TestStopMasterSlaveSync

* feat: add deleteSlaveUser for TestStopMasterSlaveSync
2023-10-31 21:00:09 +08:00
49c6ce2221 refactor: New Crowdin translations (#1667)
* refactor: New Crowdin translations by Github Action

* refactor: New Crowdin Backend translations by Github Action

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-10-31 18:11:05 +08:00
2398e69012 Improve fastAutoSignin() 2023-10-31 16:54:30 +08:00
ade9de8256 Add DumpToFile() to export init_data.json 2023-10-31 14:39:50 +08:00
1bf5497d08 Improve error handling for GetUser() 2023-10-31 14:01:37 +08:00
cf10738f45 Fix typo in AddUserKeys() 2023-10-31 13:31:12 +08:00
ac00713c20 Improve error handling for object/user.go 2023-10-31 13:20:44 +08:00
febb27f765 Remove useless fields in GenerateCasToken() 2023-10-30 18:45:34 +08:00
49a981f787 fix: fix that GROUPS is a reserved keyword introduced in MySQL 8.0 (#2458)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-30 10:59:48 +08:00
34b1945180 feat: fix bugs in custom app sso login with WebAuthn authentication (#2457)
Co-authored-by: aidenlu <aiden_lu@wochacha.com>
2023-10-30 10:54:34 +08:00
b320cca789 Can disable ldapServerPort by setting to empty string 2023-10-29 23:55:08 +08:00
b38654a45a Add renderAiAssistant() 2023-10-28 23:58:51 +08:00
f77fafae24 Fix hidden top navbar item 2023-10-28 17:07:29 +08:00
8b6b5ffe81 feat: fix go-reddit module checksum mismatch (#2451) 2023-10-28 15:32:36 +08:00
99 changed files with 2333 additions and 1536 deletions

View File

@ -127,7 +127,7 @@ jobs:
release-and-push:
name: Release And Push
runs-on: ubuntu-latest
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push'
needs: [ frontend, backend, linter, e2e ]
steps:
- name: Checkout
@ -184,14 +184,14 @@ jobs:
- name: Log in to Docker Hub
uses: docker/login-action@v1
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Push to Docker Hub
uses: docker/build-push-action@v3
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
with:
context: .
target: STANDARD
@ -201,7 +201,7 @@ jobs:
- name: Push All In One Version to Docker Hub
uses: docker/build-push-action@v3
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
with:
context: .
target: ALLINONE

View File

@ -7,7 +7,7 @@ on:
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
if: github.repository == 'casbin/casdoor' && github.event_name == 'push'
steps:
- name: Checkout

View File

@ -1,5 +1,5 @@
<h1 align="center" style="border-bottom: none;">📦⚡️ Casdoor</h1>
<h3 align="center">A UI-first centralized authentication / Single-Sign-On (SSO) platform based on OAuth 2.0 / OIDC.</h3>
<h3 align="center">An open-source UI-first Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA and RADIUS</h3>
<p align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">

View File

@ -96,7 +96,7 @@ p, *, *, GET, /api/get-organization-names, *, *
sa := stringadapter.NewAdapter(ruleText)
// load all rules from string adapter to enforcer's memory
err := sa.LoadPolicy(Enforcer.GetModel())
err = sa.LoadPolicy(Enforcer.GetModel())
if err != nil {
panic(err)
}
@ -147,7 +147,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information

View File

@ -173,6 +173,12 @@ func (c *ApiController) GetOrganizationApplications() {
return
}
applications, err = object.GetAllowedApplications(applications, userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedApplications(applications, userId))
} else {
limit := util.ParseInt(limit)

View File

@ -34,6 +34,7 @@ import (
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
"github.com/google/uuid"
"golang.org/x/oauth2"
)
var (
@ -331,8 +332,6 @@ func (c *ApiController) Login() {
}
var user *object.User
var msg string
if authForm.Password == "" {
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
c.ResponseError(err.Error(), nil)
@ -354,20 +353,21 @@ func (c *ApiController) Login() {
}
// check result through Email or Phone
checkResult := object.CheckSigninCode(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
err = object.CheckSigninCode(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, err.Error()))
return
}
// disable the verification code
err := object.DisableVerificationCode(checkDest)
err = object.DisableVerificationCode(checkDest)
if err != nil {
c.ResponseError(err.Error(), nil)
return
}
} else {
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error(), nil)
return
@ -386,7 +386,8 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
} else if enableCaptcha {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
var isHuman bool
isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
@ -399,13 +400,15 @@ func (c *ApiController) Login() {
}
password := authForm.Password
user, msg = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
user, err = object.CheckUserPassword(authForm.Organization, authForm.Username, password, c.GetAcceptLanguage(), enableCaptcha)
}
if msg != "" {
resp = &Response{Status: "error", Msg: msg}
if err != nil {
c.ResponseError(err.Error())
return
} else {
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error())
return
@ -416,7 +419,8 @@ func (c *ApiController) Login() {
return
}
organization, err := object.GetOrganizationByUser(user)
var organization *object.Organization
organization, err = object.GetOrganizationByUser(user)
if err != nil {
c.ResponseError(err.Error())
}
@ -461,12 +465,15 @@ func (c *ApiController) Login() {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
return
}
organization, err := object.GetOrganization(util.GetId("admin", application.Organization))
var organization *object.Organization
organization, err = object.GetOrganization(util.GetId("admin", application.Organization))
if err != nil {
c.ResponseError(c.T(err.Error()))
}
provider, err := object.GetProvider(util.GetId("admin", authForm.Provider))
var provider *object.Provider
provider, err = object.GetProvider(util.GetId("admin", authForm.Provider))
if err != nil {
c.ResponseError(err.Error())
return
@ -488,7 +495,12 @@ func (c *ApiController) Login() {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idProvider := idp.GetIdProvider(idpInfo, authForm.RedirectUri)
var idProvider idp.IdProvider
idProvider, err = idp.GetIdProvider(idpInfo, authForm.RedirectUri)
if err != nil {
c.ResponseError(err.Error())
return
}
if idProvider == nil {
c.ResponseError(fmt.Sprintf(c.T("storage:The provider type: %s is not supported"), provider.Type))
return
@ -502,7 +514,8 @@ func (c *ApiController) Login() {
}
// https://github.com/golang/oauth2/issues/123#issuecomment-103715338
token, err := idProvider.GetToken(authForm.Code)
var token *oauth2.Token
token, err = idProvider.GetToken(authForm.Code)
if err != nil {
c.ResponseError(err.Error())
return
@ -543,7 +556,12 @@ func (c *ApiController) Login() {
if user.IsForbidden {
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
}
// sync info from 3rd-party if possible
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
if err != nil {
c.ResponseError(err.Error())
return
}
resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx)
@ -584,14 +602,16 @@ func (c *ApiController) Login() {
}
// Handle username conflicts
tmpUser, err := object.GetUser(util.GetId(application.Organization, userInfo.Username))
var tmpUser *object.User
tmpUser, err = object.GetUser(util.GetId(application.Organization, userInfo.Username))
if err != nil {
c.ResponseError(err.Error())
return
}
if tmpUser != nil {
uid, err := uuid.NewRandom()
var uid uuid.UUID
uid, err = uuid.NewRandom()
if err != nil {
c.ResponseError(err.Error())
return
@ -602,14 +622,16 @@ func (c *ApiController) Login() {
}
properties := map[string]string{}
count, err := object.GetUserCount(application.Organization, "", "", "")
var count int64
count, err = object.GetUserCount(application.Organization, "", "", "")
if err != nil {
c.ResponseError(err.Error())
return
}
properties["no"] = strconv.Itoa(int(count + 2))
initScore, err := organization.GetInitScore()
var initScore int
initScore, err = organization.GetInitScore()
if err != nil {
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
return
@ -641,7 +663,8 @@ func (c *ApiController) Login() {
Properties: properties,
}
affected, err := object.AddUser(user)
var affected bool
affected, err = object.AddUser(user)
if err != nil {
c.ResponseError(err.Error())
return
@ -663,7 +686,7 @@ func (c *ApiController) Login() {
}
// sync info from 3rd-party if possible
_, err := object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
if err != nil {
c.ResponseError(err.Error())
return
@ -699,7 +722,8 @@ func (c *ApiController) Login() {
return
}
oldUser, err := object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
var oldUser *object.User
oldUser, err = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
return
@ -710,7 +734,8 @@ func (c *ApiController) Login() {
return
}
user, err := object.GetUser(userId)
var user *object.User
user, err = object.GetUser(userId)
if err != nil {
c.ResponseError(err.Error())
return
@ -723,7 +748,8 @@ func (c *ApiController) Login() {
return
}
isLinked, err := object.LinkUserAccount(user, provider.Type, userInfo.Id)
var isLinked bool
isLinked, err = object.LinkUserAccount(user, provider.Type, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
return
@ -736,7 +762,8 @@ func (c *ApiController) Login() {
}
}
} else if c.getMfaUserSession() != "" {
user, err := object.GetUser(c.getMfaUserSession())
var user *object.User
user, err = object.GetUser(c.getMfaUserSession())
if err != nil {
c.ResponseError(err.Error())
return
@ -769,7 +796,8 @@ func (c *ApiController) Login() {
return
}
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error())
return
@ -790,7 +818,8 @@ func (c *ApiController) Login() {
} else {
if c.GetSessionUsername() != "" {
// user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in
application, err := object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
var application *object.Application
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -65,13 +65,13 @@ func (c *ApiController) GetCerts() {
}
}
// GetGlobleCerts
// @Title GetGlobleCerts
// GetGlobalCerts
// @Title GetGlobalCerts
// @Tag Cert API
// @Description get globle certs
// @Success 200 {array} object.Cert The Response object
// @router /get-globle-certs [get]
func (c *ApiController) GetGlobleCerts() {
// @router /get-global-certs [get]
func (c *ApiController) GetGlobalCerts() {
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
@ -80,7 +80,7 @@ func (c *ApiController) GetGlobleCerts() {
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
maskedCerts, err := object.GetMaskedCerts(object.GetGlobleCerts())
maskedCerts, err := object.GetMaskedCerts(object.GetGlobalCerts())
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -163,6 +163,8 @@ func (c *ApiController) BuyProduct() {
id := c.Input().Get("id")
host := c.Ctx.Request.Host
providerName := c.Input().Get("providerName")
paymentEnv := c.Input().Get("paymentEnv")
// buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
@ -187,11 +189,11 @@ func (c *ApiController) BuyProduct() {
return
}
payment, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
payment, attachInfo, err := object.BuyProduct(id, user, providerName, pricingName, planName, host, paymentEnv)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment)
c.ResponseOk(payment, attachInfo)
}

View File

@ -158,10 +158,9 @@ func (c *ApiController) DeleteToken() {
// @Success 401 {object} object.TokenError The Response object
// @router api/login/oauth/access_token [post]
func (c *ApiController) GetOAuthToken() {
grantType := c.Input().Get("grant_type")
refreshToken := c.Input().Get("refresh_token")
clientId := c.Input().Get("client_id")
clientSecret := c.Input().Get("client_secret")
grantType := c.Input().Get("grant_type")
code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier")
scope := c.Input().Get("scope")
@ -169,35 +168,61 @@ func (c *ApiController) GetOAuthToken() {
password := c.Input().Get("password")
tag := c.Input().Get("tag")
avatar := c.Input().Get("avatar")
refreshToken := c.Input().Get("refresh_token")
if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
if clientId == "" {
// If clientID is empty, try to read data from RequestBody
if len(c.Ctx.Input.RequestBody) != 0 {
// If clientId is empty, try to read data from RequestBody
var tokenRequest TokenRequest
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
clientId = tokenRequest.ClientId
clientSecret = tokenRequest.ClientSecret
grantType = tokenRequest.GrantType
refreshToken = tokenRequest.RefreshToken
code = tokenRequest.Code
verifier = tokenRequest.Verifier
scope = tokenRequest.Scope
username = tokenRequest.Username
password = tokenRequest.Password
tag = tokenRequest.Tag
avatar = tokenRequest.Avatar
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
if err == nil {
if clientId == "" {
clientId = tokenRequest.ClientId
}
if clientSecret == "" {
clientSecret = tokenRequest.ClientSecret
}
if grantType == "" {
grantType = tokenRequest.GrantType
}
if code == "" {
code = tokenRequest.Code
}
if verifier == "" {
verifier = tokenRequest.Verifier
}
if scope == "" {
scope = tokenRequest.Scope
}
if username == "" {
username = tokenRequest.Username
}
if password == "" {
password = tokenRequest.Password
}
if tag == "" {
tag = tokenRequest.Tag
}
if avatar == "" {
avatar = tokenRequest.Avatar
}
if refreshToken == "" {
refreshToken = tokenRequest.RefreshToken
}
}
}
host := c.Ctx.Request.Host
oAuthtoken, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = oAuthtoken
c.Data["json"] = token
c.SetTokenErrorHttpStatus()
c.ServeJSON()
}

View File

@ -15,10 +15,10 @@
package controllers
type TokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
GrantType string `json:"grant_type"`
Code string `json:"code"`
Verifier string `json:"code_verifier"`
Scope string `json:"scope"`
Username string `json:"username"`

View File

@ -161,7 +161,6 @@ func (c *ApiController) GetUser() {
}
var user *object.User
if id == "" && owner == "" {
switch {
case email != "":
@ -176,11 +175,16 @@ func (c *ApiController) GetUser() {
owner = util.GetOwnerFromId(id)
}
organization, err := object.GetOrganization(util.GetId("admin", owner))
var organization *object.Organization
organization, err = object.GetOrganization(util.GetId("admin", owner))
if err != nil {
c.ResponseError(err.Error())
return
}
if organization == nil {
c.ResponseError(fmt.Sprintf("the organization: %s is not found", owner))
return
}
if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername()
@ -472,16 +476,16 @@ func (c *ApiController) SetPassword() {
isAdmin := c.IsAdmin()
if isAdmin {
if oldPassword != "" {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
}
} else if code == "" {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
}
@ -514,11 +518,11 @@ func (c *ApiController) CheckUserPassword() {
return
}
_, msg := object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
if msg == "" {
c.ResponseOk()
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
} else {
c.ResponseError(msg)
c.ResponseOk()
}
}
@ -572,11 +576,11 @@ func (c *ApiController) GetUserCount() {
c.ResponseOk(count)
}
// AddUserkeys
// @Title AddUserkeys
// AddUserKeys
// @Title AddUserKeys
// @router /add-user-keys [post]
// @Tag User API
func (c *ApiController) AddUserkeys() {
func (c *ApiController) AddUserKeys() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
if err != nil {
@ -585,7 +589,7 @@ func (c *ApiController) AddUserkeys() {
}
isAdmin := c.IsAdmin()
affected, err := object.AddUserkeys(&user, isAdmin)
affected, err := object.AddUserKeys(&user, isAdmin)
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -154,6 +154,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
// @router /webauthn/signin/finish [post]
func (c *ApiController) WebAuthnSigninFinish() {
responseType := c.Input().Get("responseType")
clientId := c.Input().Get("clientId")
webauthnObj, err := object.GetWebAuthnObject(c.Ctx.Request.Host)
if err != nil {
c.ResponseError(err.Error())
@ -182,7 +183,13 @@ func (c *ApiController) WebAuthnSigninFinish() {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
application, err := object.GetApplicationByUser(user)
var application *object.Application
if clientId != "" && (responseType == ResponseTypeCode) {
application, err = object.GetApplicationByClientId(clientId)
} else {
application, err = object.GetApplicationByUser(user)
}
if err != nil {
c.ResponseError(err.Error())
return

5
go.mod
View File

@ -13,9 +13,9 @@ require (
github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.15.0
github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.44.0
github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.3.0
github.com/casdoor/xorm-adapter/v3 v3.0.4
github.com/casdoor/xorm-adapter/v3 v3.1.0
github.com/casvisor/casvisor-go-sdk v1.0.3
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/denisenkom/go-mssqldb v0.9.0
@ -32,6 +32,7 @@ require (
github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.1
github.com/json-iterator/go v1.1.12 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lestrrat-go/jwx v1.2.21
github.com/lib/pq v1.10.9

13
go.sum
View File

@ -920,16 +920,18 @@ github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/casbin/v2 v2.77.2 h1:yQinn/w9x8AswiwqwtrXz93VU48R1aYTXdHEx4RI3jM=
github.com/casbin/casbin/v2 v2.77.2/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk=
github.com/casdoor/go-reddit/v2 v2.1.0 h1:kIbfdJ7AA7H0uTQ8s0q4GGZqSS5V9wVE74RrXyD9XPs=
github.com/casdoor/go-reddit/v2 v2.1.0/go.mod h1:eagkvwlZ4Hcsuc/uQsLHYEulz5jN65SVSwV/AIE7zsc=
github.com/casdoor/go-sms-sender v0.15.0 h1:9SWj/jd5c7jIteTRUrqbkpWbtIXMDv+t1CEfDhO06m0=
github.com/casdoor/go-sms-sender v0.15.0/go.mod h1:cQs7qqohMJBgIVZebOCB8ko09naG1vzFJEH59VNIscs=
github.com/casdoor/gomail/v2 v2.0.1 h1:J+FG6x80s9e5lBHUn8Sv0Y56mud34KiWih5YdmudR/w=
github.com/casdoor/gomail/v2 v2.0.1/go.mod h1:VnGPslEAtpix5FjHisR/WKB1qvZDBaujbikxDe9d+2Q=
github.com/casdoor/notify v0.44.0 h1:/j2TqO5lXEKYyu2WWtmGh3jh4aeN8m6p+9tWb5j1PWU=
github.com/casdoor/notify v0.44.0/go.mod h1:HgLPFmSmy9+uB72cp2z3Tk5KxpZfStqpLMr+5RddXmw=
github.com/casdoor/notify v0.45.0 h1:OlaFvcQFjGOgA4mRx07M8AH1gvb5xNo21mcqrVGlLgk=
github.com/casdoor/notify v0.45.0/go.mod h1:wNHQu0tiDROMBIvz0j3Om3Lhd5yZ+AIfnFb8MYb8OLQ=
github.com/casdoor/oss v1.3.0 h1:D5pcz65tJRqJrWY11Ks7D9LUsmlhqqMHugjDhSxWTvk=
github.com/casdoor/oss v1.3.0/go.mod h1:YOi6KpG1pZHTkiy9AYaqI0UaPfE7YkaA07d89f1idqY=
github.com/casdoor/xorm-adapter/v3 v3.0.4 h1:vB04Ao8n2jA7aFBI9F+gGXo9+Aa1IQP6mTdo50913DM=
github.com/casdoor/xorm-adapter/v3 v3.0.4/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM=
github.com/casdoor/xorm-adapter/v3 v3.1.0 h1:NodWayRtSLVSeCvL9H3Hc61k0G17KhV9IymTCNfh3kk=
github.com/casdoor/xorm-adapter/v3 v3.1.0/go.mod h1:4WTcUw+bTgBylGHeGHzTtBvuTXRS23dtwzFLl9tsgFM=
github.com/casvisor/casvisor-go-sdk v1.0.3 h1:TKJQWKnhtznEBhzLPEdNsp7nJK2GgdD8JsB0lFPMW7U=
github.com/casvisor/casvisor-go-sdk v1.0.3/go.mod h1:frnNtH5GA0wxzAQLyZxxfL0RSsSub9GQPi2Ybe86ocE=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@ -1244,6 +1246,7 @@ github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo=
github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4=
github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -1829,8 +1832,6 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
github.com/vartanbeno/go-reddit/v2 v2.0.0 h1:fxYMqx5lhbmJ3yYRN1nnQC/gecRB3xpUS2BbG7GLpsk=
github.com/vartanbeno/go-reddit/v2 v2.0.0/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
github.com/volcengine/volc-sdk-golang v1.0.117 h1:ykFVSwsVq9qvIoWP9jeP+VKNAUjrblAdsZl46yVWiH8=
github.com/volcengine/volc-sdk-golang v1.0.117/go.mod h1:ojXSFvj404o2UKnZR9k9LUUWIUU+9XtlRlzk2+UFc/M=
github.com/wendal/errors v0.0.0-20181209125328-7f31f4b264ec/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=

View File

@ -19,7 +19,7 @@
"The provider: %s is not enabled for the application": "Le fournisseur :%s n'est pas activé pour l'application",
"Unauthorized operation": "Opération non autorisée",
"Unknown authentication type (not password or provider), form = %s": "Type d'authentification inconnu (pas de mot de passe ou de fournisseur), formulaire = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "Le tag de lutilisateur %s nest pas répertorié dans les tags de lapplication"
},
"cas": {
"Service %s and %s do not match": "Les services %s et %s ne correspondent pas"
@ -43,7 +43,7 @@
"Phone number is invalid": "Le numéro de téléphone est invalide",
"Session outdated, please login again": "Session expirée, veuillez vous connecter à nouveau",
"The user is forbidden to sign in, please contact the administrator": "L'utilisateur est interdit de se connecter, veuillez contacter l'administrateur",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The user: %s doesn't exist in LDAP server": "L'utilisateur %s n'existe pas sur le serveur LDAP",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Le nom d'utilisateur ne peut contenir que des caractères alphanumériques, des traits soulignés ou des tirets, ne peut pas avoir de tirets ou de traits soulignés consécutifs et ne peut pas commencer ou se terminer par un tiret ou un trait souligné.",
"Username already exists": "Nom d'utilisateur existe déjà",
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
@ -53,7 +53,7 @@
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect": "mot de passe ou code invalide",
"password or code is incorrect, you have %d remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %d chances",
"unsupported password type: %s": "Type de mot de passe non pris en charge : %s"
},
@ -61,8 +61,8 @@
"Missing parameter": "Paramètre manquant",
"Please login first": "Veuillez d'abord vous connecter",
"The user: %s doesn't exist": "L'utilisateur : %s n'existe pas",
"don't support captchaProvider: ": "Ne pas prendre en charge la captchaProvider",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
"don't support captchaProvider: ": "ne prend pas en charge captchaProvider: ",
"this operation is not allowed in demo mode": "cette opération nest pas autorisée en mode démo"
},
"ldap": {
"Ldap server exist": "Le serveur LDAP existe"

View File

@ -24,14 +24,6 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",

View File

@ -24,14 +24,6 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",

View File

@ -24,14 +24,6 @@
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"chat": {
"The chat type must be \\\"AI\\\"": "The chat type must be \\\"AI\\\"",
"The chat: %s is not found": "The chat: %s is not found",
"The message is invalid": "The message is invalid",
"The message: %s is not found": "The message: %s is not found",
"The provider: %s is invalid": "The provider: %s is invalid",
"The provider: %s is not found": "The provider: %s is not found"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",

View File

@ -43,7 +43,7 @@
"Phone number is invalid": "无效手机号",
"Session outdated, please login again": "会话已过期,请重新登录",
"The user is forbidden to sign in, please contact the administrator": "该用户被禁止登录,请联系管理员",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The user: %s doesn't exist in LDAP server": "用户: %s 在LDAP服务器中未找到",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "用户名只能包含字母数字字符、下划线或连字符,不能有连续的连字符或下划线,也不能以连字符或下划线开头或结尾",
"Username already exists": "用户名已存在",
"Username cannot be an email address": "用户名不可以是邮箱地址",
@ -62,7 +62,7 @@
"Please login first": "请先登录",
"The user: %s doesn't exist": "用户: %s不存在",
"don't support captchaProvider: ": "不支持验证码提供商: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
"this operation is not allowed in demo mode": "demo模式下不允许该操作"
},
"ldap": {
"Ldap server exist": "LDAP服务器已存在"

View File

@ -19,7 +19,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
@ -84,6 +83,7 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_id", idp.Config.ClientID)
payload.Set("client_secret", idp.Config.ClientSecret)
payload.Set("redirect_uri", idp.Config.RedirectURL)
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
if err != nil {
@ -118,11 +118,25 @@ func (idp *AdfsIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
keyset, err := jwk.ParseKey(body)
body, err := io.ReadAll(resp.Body)
var respKeys struct {
Keys []interface{} `json:"keys"`
}
if err := json.Unmarshal(body, &respKeys); err != nil {
return nil, err
}
respKey, err := json.Marshal(&(respKeys.Keys[0]))
if err != nil {
return nil, err
}
keyset, err := jwk.ParseKey(respKey)
if err != nil {
return nil, err
}
tokenSrc := []byte(token.AccessToken)
publicKey, _ := keyset.PublicKey()
idToken, _ := jwt.Parse(tokenSrc, jwt.WithVerify(jwa.RS256, publicKey))

View File

@ -89,7 +89,7 @@ type GothIdProvider struct {
Session goth.Session
}
func NewGothIdProvider(providerType string, clientId string, clientSecret string, redirectUrl string, hostUrl string) *GothIdProvider {
func NewGothIdProvider(providerType string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, redirectUrl string, hostUrl string) (*GothIdProvider, error) {
var idp GothIdProvider
switch providerType {
case "Amazon":
@ -101,8 +101,24 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
if !strings.Contains(redirectUrl, "/api/callback") {
redirectUrl = strings.Replace(redirectUrl, "/callback", "/api/callback", 1)
}
iat := time.Now().Unix()
exp := iat + 60*60
sp := apple.SecretParams{
ClientId: clientId,
TeamId: clientSecret,
KeyId: clientId2,
PKCS8PrivateKey: clientSecret2,
Iat: int(iat),
Exp: int(exp),
}
secret, err := apple.MakeSecret(sp)
if err != nil {
return nil, err
}
idp = GothIdProvider{
Provider: apple.New(clientId, clientSecret, redirectUrl, nil),
Provider: apple.New(clientId, *secret, redirectUrl, nil),
Session: &apple.Session{},
}
case "AzureAD":
@ -386,10 +402,10 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
Session: &zoom.Session{},
}
default:
return nil
return nil, fmt.Errorf("OAuth Goth provider type: %s is not supported", providerType)
}
return &idp
return &idp, nil
}
// SetHttpClient

View File

@ -15,6 +15,7 @@
package idp
import (
"fmt"
"net/http"
"strings"
@ -30,16 +31,19 @@ type UserInfo struct {
Phone string
CountryCode string
AvatarUrl string
Extra map[string]string
}
type ProviderInfo struct {
Type string
SubType string
ClientId string
ClientSecret string
AppId string
HostUrl string
RedirectUrl string
Type string
SubType string
ClientId string
ClientSecret string
ClientId2 string
ClientSecret2 string
AppId string
HostUrl string
RedirectUrl string
TokenURL string
AuthURL string
@ -53,71 +57,71 @@ type IdProvider interface {
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}
func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) IdProvider {
func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error) {
switch idpInfo.Type {
case "GitHub":
return NewGithubIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGithubIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Google":
return NewGoogleIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGoogleIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "QQ":
return NewQqIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewQqIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "WeChat":
return NewWeChatIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeChatIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Facebook":
return NewFacebookIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewFacebookIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "DingTalk":
return NewDingTalkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewDingTalkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Weibo":
return NewWeiBoIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeiBoIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Gitee":
return NewGiteeIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGiteeIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "LinkedIn":
return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "WeCom":
if idpInfo.SubType == "Internal" {
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
} else if idpInfo.SubType == "Third-party" {
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
} else {
return nil
return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType)
}
case "Lark":
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewLarkIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "GitLab":
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "ADFS":
return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Baidu":
return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Alipay":
return NewAlipayIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewAlipayIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Custom":
return NewCustomIdProvider(idpInfo, redirectUrl)
return NewCustomIdProvider(idpInfo, redirectUrl), nil
case "Infoflow":
if idpInfo.SubType == "Internal" {
return NewInfoflowInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl)
return NewInfoflowInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl), nil
} else if idpInfo.SubType == "Third-party" {
return NewInfoflowIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl)
return NewInfoflowIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.AppId, redirectUrl), nil
} else {
return nil
return nil, fmt.Errorf("Infoflow provider subType: %s is not supported", idpInfo.SubType)
}
case "Casdoor":
return NewCasdoorIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewCasdoorIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Okta":
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Douyin":
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Bilibili":
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "MetaMask":
return NewMetaMaskIdProvider()
return NewMetaMaskIdProvider(), nil
case "Web3Onboard":
return NewWeb3OnboardIdProvider()
return NewWeb3OnboardIdProvider(), nil
default:
if isGothSupport(idpInfo.Type) {
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.ClientId2, idpInfo.ClientSecret2, redirectUrl, idpInfo.HostUrl)
}
return nil
return nil, fmt.Errorf("OAuth provider type: %s is not supported", idpInfo.Type)
}
}

View File

@ -186,15 +186,24 @@ func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
id = wechatUserInfo.Openid
}
extra := make(map[string]string)
extra["wechat_unionid"] = wechatUserInfo.Openid
// For WeChat, different appId corresponds to different openId
extra[BuildWechatOpenIdKey(idp.Config.ClientID)] = wechatUserInfo.Openid
userInfo := UserInfo{
Id: id,
Username: wechatUserInfo.Nickname,
DisplayName: wechatUserInfo.Nickname,
AvatarUrl: wechatUserInfo.Headimgurl,
Extra: extra,
}
return &userInfo, nil
}
func BuildWechatOpenIdKey(appId string) string {
return fmt.Sprintf("wechat_openid_%s", appId)
}
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, error) {
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
request, err := http.NewRequest("GET", accessTokenUrl, nil)

View File

@ -25,6 +25,11 @@ import (
)
func StartLdapServer() {
ldapServerPort := conf.GetConfigString("ldapServerPort")
if ldapServerPort == "" || ldapServerPort == "0" {
return
}
server := ldap.NewServer()
routes := ldap.NewRouteMux()
@ -32,7 +37,7 @@ func StartLdapServer() {
routes.Search(handleSearch).Label(" SEARCH****")
server.Handle(routes)
err := server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
err := server.ListenAndServe("0.0.0.0:" + ldapServerPort)
if err != nil {
log.Printf("StartLdapServer() failed, err = %s", err.Error())
}
@ -44,20 +49,20 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
if r.AuthenticationChoice() == "simple" {
bindUsername, bindOrg, err := getNameAndOrgFromDN(string(r.Name()))
if err != "" {
log.Printf("Bind failed ,ErrMsg=%s", err)
if err != nil {
log.Printf("getNameAndOrgFromDN() error: %s", err.Error())
res.SetResultCode(ldap.LDAPResultInvalidDNSyntax)
res.SetDiagnosticMessage("bind failed ErrMsg: " + err)
res.SetDiagnosticMessage(fmt.Sprintf("getNameAndOrgFromDN() error: %s", err.Error()))
w.Write(res)
return
}
bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
if err != "" {
if err != nil {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
res.SetResultCode(ldap.LDAPResultInvalidCredentials)
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err)
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err.Error())
w.Write(res)
return
}
@ -73,7 +78,7 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
m.Client.OrgName = bindOrg
} else {
res.SetResultCode(ldap.LDAPResultAuthMethodNotSupported)
res.SetDiagnosticMessage("Authentication method not supported,Please use Simple Authentication")
res.SetDiagnosticMessage("Authentication method not supported, please use Simple Authentication")
}
w.Write(res)
}
@ -110,7 +115,8 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
for _, user := range users {
dn := fmt.Sprintf("cn=%s,%s", user.Name, string(r.BaseObject()))
e := ldap.NewSearchResultEntry(dn)
e.AddAttribute(message.AttributeDescription("uid"), message.AttributeValue(user.Id))
e.AddAttribute(message.AttributeDescription("cn"), message.AttributeValue(user.Name))
for _, attr := range r.Attributes() {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "cn" {

View File

@ -26,7 +26,7 @@ import (
ldap "github.com/forestmgy/ldapserver"
)
func getNameAndOrgFromDN(DN string) (string, string, string) {
func getNameAndOrgFromDN(DN string) (string, string, error) {
DNFields := strings.Split(DN, ",")
params := make(map[string]string, len(DNFields))
for _, field := range DNFields {
@ -37,12 +37,12 @@ func getNameAndOrgFromDN(DN string) (string, string, string) {
}
if params["cn"] == "" {
return "", "", "please use Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com"
return "", "", fmt.Errorf("please use Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com")
}
if params["ou"] == "" {
return params["cn"], object.CasdoorOrganization, ""
return params["cn"], object.CasdoorOrganization, nil
}
return params["cn"], params["ou"], ""
return params["cn"], params["ou"], nil
}
func getNameAndOrgFromFilter(baseDN, filter string) (string, string, int) {
@ -50,7 +50,11 @@ func getNameAndOrgFromFilter(baseDN, filter string) (string, string, int) {
return "", "", ldap.LDAPResultInvalidDNSyntax
}
name, org, _ := getNameAndOrgFromDN(fmt.Sprintf("cn=%s,", getUsername(filter)) + baseDN)
name, org, err := getNameAndOrgFromDN(fmt.Sprintf("cn=%s,", getUsername(filter)) + baseDN)
if err != nil {
panic(err)
}
return name, org, ldap.LDAPResultSuccess
}

View File

@ -34,7 +34,6 @@ func main() {
object.InitFlag()
object.InitAdapter()
object.CreateTables()
object.DoMigration()
object.InitDb()
object.InitFromFile()

View File

@ -319,6 +319,9 @@ func GetMaskedApplication(application *Application, userId string) *Application
if application.OrganizationObj.DefaultPassword != "" {
application.OrganizationObj.DefaultPassword = "***"
}
if application.OrganizationObj.MasterVerificationCode != "" {
application.OrganizationObj.MasterVerificationCode = "***"
}
if application.OrganizationObj.PasswordType != "" {
application.OrganizationObj.PasswordType = "***"
}
@ -345,6 +348,34 @@ func GetMaskedApplications(applications []*Application, userId string) []*Applic
return applications
}
func GetAllowedApplications(applications []*Application, userId string) ([]*Application, error) {
if userId == "" || isUserIdGlobalAdmin(userId) {
return applications, nil
}
user, err := GetUser(userId)
if err != nil {
return nil, err
}
if user != nil && user.IsAdmin {
return applications, nil
}
res := []*Application{}
for _, application := range applications {
var allowed bool
allowed, err = CheckLoginPermission(userId, application)
if err != nil {
return nil, err
}
if allowed {
res = append(res, application)
}
}
return res, nil
}
func UpdateApplication(id string, application *Application) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
oldApplication, err := getApplication(owner, name)

View File

@ -87,7 +87,7 @@ func GetGlobalCertsCount(field, value string) (int64, error) {
return session.Count(&Cert{})
}
func GetGlobleCerts() ([]*Cert, error) {
func GetGlobalCerts() ([]*Cert, error) {
certs := []*Cert{}
err := ormer.Engine.Desc("created_time").Find(&certs)
if err != nil {
@ -163,6 +163,12 @@ func UpdateCert(id string, cert *Cert) (bool, error) {
return false, err
}
}
err := cert.populateContent()
if err != nil {
return false, err
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(cert)
if err != nil {
return false, err
@ -172,10 +178,9 @@ func UpdateCert(id string, cert *Cert) (bool, error) {
}
func AddCert(cert *Cert) (bool, error) {
if cert.Certificate == "" || cert.PrivateKey == "" {
certificate, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
cert.Certificate = certificate
cert.PrivateKey = privateKey
err := cert.populateContent()
if err != nil {
return false, err
}
affected, err := ormer.Engine.Insert(cert)
@ -199,6 +204,20 @@ func (p *Cert) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}
func (p *Cert) populateContent() error {
if p.Certificate == "" || p.PrivateKey == "" {
certificate, privateKey, err := generateRsaKeys(p.BitSize, p.ExpireInYears, p.Name, p.Owner)
if err != nil {
return err
}
p.Certificate = certificate
p.PrivateKey = privateKey
}
return nil
}
func getCertByApplication(application *Application) (*Cert, error) {
if application.Cert != "" {
return getCertByName(application.Cert)

View File

@ -142,7 +142,7 @@ func CheckUserSignup(application *Application, organization *Organization, form
return ""
}
func checkSigninErrorTimes(user *User, lang string) string {
func checkSigninErrorTimes(user *User, lang string) error {
if user.SigninWrongTimes >= SigninWrongTimesLimit {
lastSignWrongTime, _ := time.Parse(time.RFC3339, user.LastSigninWrongTime)
passedTime := time.Now().UTC().Sub(lastSignWrongTime)
@ -150,37 +150,39 @@ func checkSigninErrorTimes(user *User, lang string) string {
// deny the login if the error times is greater than the limit and the last login time is less than the duration
if minutes > 0 {
return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutes)
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutes)
}
// reset the error times
user.SigninWrongTimes = 0
UpdateUser(user.GetId(), user, []string{"signin_wrong_times"}, false)
_, err := UpdateUser(user.GetId(), user, []string{"signin_wrong_times"}, false)
return err
}
return ""
return nil
}
func CheckPassword(user *User, password string, lang string, options ...bool) string {
func CheckPassword(user *User, password string, lang string, options ...bool) error {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// check the login error times
if !enableCaptcha {
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
err := checkSigninErrorTimes(user, lang)
if err != nil {
return err
}
}
organization, err := GetOrganizationByUser(user)
if err != nil {
panic(err)
return err
}
if organization == nil {
return i18n.Translate(lang, "check:Organization does not exist")
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
}
passwordType := user.PasswordType
@ -191,19 +193,17 @@ func CheckPassword(user *User, password string, lang string, options ...bool) st
if credManager != nil {
if organization.MasterPassword != "" {
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
resetUserSigninErrorTimes(user)
return ""
return resetUserSigninErrorTimes(user)
}
}
if credManager.IsPasswordCorrect(password, user.Password, user.PasswordSalt, organization.PasswordSalt) {
resetUserSigninErrorTimes(user)
return ""
return resetUserSigninErrorTimes(user)
}
return recordSigninErrorInfo(user, lang, enableCaptcha)
} else {
return fmt.Sprintf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
return fmt.Errorf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
}
}
@ -217,10 +217,10 @@ func CheckPasswordComplexity(user *User, password string) string {
return CheckPasswordComplexityByOrg(organization, password)
}
func checkLdapUserPassword(user *User, password string, lang string) string {
func checkLdapUserPassword(user *User, password string, lang string) error {
ldaps, err := GetLdaps(user.Owner)
if err != nil {
return err.Error()
return err
}
ldapLoginSuccess := false
@ -237,14 +237,14 @@ func checkLdapUserPassword(user *User, password string, lang string) string {
searchResult, err := conn.Conn.Search(searchReq)
if err != nil {
return err.Error()
return err
}
if len(searchResult.Entries) == 0 {
continue
}
if len(searchResult.Entries) > 1 {
return i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server")
return fmt.Errorf(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
}
hit = true
@ -257,45 +257,47 @@ func checkLdapUserPassword(user *User, password string, lang string) string {
if !ldapLoginSuccess {
if !hit {
return "user not exist"
return fmt.Errorf("user not exist")
}
return i18n.Translate(lang, "check:LDAP user name or password incorrect")
return fmt.Errorf(i18n.Translate(lang, "check:LDAP user name or password incorrect"))
}
return ""
return nil
}
func CheckUserPassword(organization string, username string, password string, lang string, options ...bool) (*User, string) {
func CheckUserPassword(organization string, username string, password string, lang string, options ...bool) (*User, error) {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
user, err := GetUserByFields(organization, username)
if err != nil {
panic(err)
return nil, err
}
if user == nil || user.IsDeleted {
return nil, fmt.Sprintf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username))
return nil, fmt.Errorf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username))
}
if user.IsForbidden {
return nil, i18n.Translate(lang, "check:The user is forbidden to sign in, please contact the administrator")
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user is forbidden to sign in, please contact the administrator"))
}
if user.Ldap != "" {
// ONLY for ldap users
if msg := checkLdapUserPassword(user, password, lang); msg != "" {
if msg == "user not exist" {
return nil, fmt.Sprintf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
// only for LDAP users
err = checkLdapUserPassword(user, password, lang)
if err != nil {
if err.Error() == "user not exist" {
return nil, fmt.Errorf(i18n.Translate(lang, "check:The user: %s doesn't exist in LDAP server"), username)
}
return nil, msg
return nil, err
}
} else {
if msg := CheckPassword(user, password, lang, enableCaptcha); msg != "" {
return nil, msg
err = CheckPassword(user, password, lang, enableCaptcha)
if err != nil {
return nil, err
}
}
return user, ""
return user, nil
}
func CheckUserPermission(requestUserId, userId string, strict bool, lang string) (bool, error) {
@ -308,7 +310,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
if userId != "" {
targetUser, err := GetUser(userId)
if err != nil {
panic(err)
return false, err
}
if targetUser == nil {
@ -351,8 +353,8 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
}
func CheckLoginPermission(userId string, application *Application) (bool, error) {
var err error
if userId == "built-in/admin" {
owner, _ := util.GetOwnerAndNameFromId(userId)
if owner == "built-in" {
return true, nil
}

View File

@ -36,20 +36,23 @@ func isValidRealName(s string) bool {
return reRealName.MatchString(s)
}
func resetUserSigninErrorTimes(user *User) {
func resetUserSigninErrorTimes(user *User) error {
// if the password is correct and wrong times is not zero, reset the error times
if user.SigninWrongTimes == 0 {
return
return nil
}
user.SigninWrongTimes = 0
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
_, err := UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
return err
}
func recordSigninErrorInfo(user *User, lang string, options ...bool) string {
func recordSigninErrorInfo(user *User, lang string, options ...bool) error {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// increase failed login count
if user.SigninWrongTimes < SigninWrongTimesLimit {
user.SigninWrongTimes++
@ -61,13 +64,18 @@ func recordSigninErrorInfo(user *User, lang string, options ...bool) string {
}
// update user
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
_, err := UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, false)
if err != nil {
return err
}
leftChances := SigninWrongTimesLimit - user.SigninWrongTimes
if leftChances == 0 && enableCaptcha {
return fmt.Sprint(i18n.Translate(lang, "check:password or code is incorrect"))
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect"))
} else if leftChances >= 0 {
return fmt.Sprintf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances)
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances)
}
// don't show the chance error message if the user has no chance left
return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes()))
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes()))
}

121
object/init_data_dump.go Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "github.com/casdoor/casdoor/util"
func DumpToFile(filePath string) error {
return writeInitDataToFile(filePath)
}
func writeInitDataToFile(filePath string) error {
organizations, err := GetOrganizations("admin")
if err != nil {
return err
}
applications, err := GetApplications("admin")
if err != nil {
return err
}
users, err := GetGlobalUsers()
if err != nil {
return err
}
certs, err := GetCerts("")
if err != nil {
return err
}
providers, err := GetGlobalProviders()
if err != nil {
return err
}
ldaps, err := GetLdaps("")
if err != nil {
return err
}
models, err := GetModels("")
if err != nil {
return err
}
permissions, err := GetPermissions("")
if err != nil {
return err
}
payments, err := GetPayments("")
if err != nil {
return err
}
products, err := GetProducts("")
if err != nil {
return err
}
resources, err := GetResources("", "")
if err != nil {
return err
}
roles, err := GetRoles("")
if err != nil {
return err
}
syncers, err := GetSyncers("")
if err != nil {
return err
}
tokens, err := GetTokens("", "")
if err != nil {
return err
}
webhooks, err := GetWebhooks("", "")
if err != nil {
return err
}
data := &InitData{
Organizations: organizations,
Applications: applications,
Users: users,
Certs: certs,
Providers: providers,
Ldaps: ldaps,
Models: models,
Permissions: permissions,
Payments: payments,
Products: products,
Resources: resources,
Roles: roles,
Syncers: syncers,
Tokens: tokens,
Webhooks: webhooks,
}
text := util.StructToJsonFormatted(data)
util.WriteStringToPath(text, filePath)
return nil
}

View File

@ -0,0 +1,29 @@
// Copyright 2023 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.
//go:build !skipCi
// +build !skipCi
package object
import "testing"
func TestDumpToFile(t *testing.T) {
InitConfig()
err := DumpToFile("./init_data_dump.json")
if err != nil {
panic(err)
}
}

View File

@ -305,7 +305,7 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
return nil, nil, err
}
name, err := syncUser.buildLdapUserName()
name, err := syncUser.buildLdapUserName(owner)
if err != nil {
return nil, nil, err
}
@ -354,10 +354,10 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
return existUuids, nil
}
func (ldapUser *LdapUser) buildLdapUserName() (string, error) {
func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) {
user := User{}
uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
has, err := ormer.Engine.Where("name = ? or name = ?", ldapUser.Uid, uidWithNumber).Get(&user)
has, err := ormer.Engine.Where("owner = ? and (name = ? or name = ?)", owner, ldapUser.Uid, uidWithNumber).Get(&user)
if err != nil {
return "", err
}

View File

@ -1,51 +0,0 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "github.com/xorm-io/xorm/migrate"
type Migrator interface {
IsMigrationNeeded() bool
DoMigration() *migrate.Migration
}
func DoMigration() {
migrators := []Migrator{
&Migrator_1_101_0_PR_1083{},
&Migrator_1_235_0_PR_1530{},
&Migrator_1_240_0_PR_1539{},
&Migrator_1_314_0_PR_1841{},
// more migrators add here in chronological order...
}
migrations := []*migrate.Migration{}
for _, migrator := range migrators {
if migrator.IsMigrationNeeded() {
migrations = append(migrations, migrator.DoMigration())
}
}
options := &migrate.Options{
TableName: "migration",
IDColumnName: "id",
}
m := migrate.New(ormer.Engine, options, migrations)
err := m.Migrate()
if err != nil {
panic(err)
}
}

View File

@ -1,70 +0,0 @@
// Copyright 2023 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 (
"strings"
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_101_0_PR_1083 struct{}
func (*Migrator_1_101_0_PR_1083) IsMigrationNeeded() bool {
exist1, _ := ormer.Engine.IsTableExist("model")
exist2, _ := ormer.Engine.IsTableExist("permission")
exist3, _ := ormer.Engine.IsTableExist("permission_rule")
if exist1 && exist2 && exist3 {
return true
}
return false
}
func (*Migrator_1_101_0_PR_1083) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20230209MigratePermissionRule--Use V5 instead of V1 to store permissionID",
Migrate: func(engine *xorm.Engine) error {
models := []*Model{}
err := engine.Table("model").Find(&models, &Model{})
if err != nil {
panic(err)
}
isHit := false
for _, model := range models {
if strings.Contains(model.ModelText, "permission") {
// update model table
model.ModelText = strings.Replace(model.ModelText, "permission,", "", -1)
UpdateModel(model.GetId(), model)
isHit = true
}
}
if isHit {
// update permission_rule table
sql := "UPDATE `permission_rule`SET V0 = V1, V1 = V2, V2 = V3, V3 = V4, V4 = V5 WHERE V0 IN (SELECT CONCAT(owner, '/', name) AS permission_id FROM `permission`)"
_, err = engine.Exec(sql)
if err != nil {
return err
}
}
return err
},
}
return &migration
}

View File

@ -1,46 +0,0 @@
// Copyright 2023 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 (
xormadapter "github.com/casdoor/xorm-adapter/v3"
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_235_0_PR_1530 struct{}
func (*Migrator_1_235_0_PR_1530) IsMigrationNeeded() bool {
exist, _ := ormer.Engine.IsTableExist("casbin_rule")
return exist
}
func (*Migrator_1_235_0_PR_1530) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20221015CasbinRule--fill ptype field with p",
Migrate: func(engine *xorm.Engine) error {
_, err := engine.Cols("ptype").Update(&xormadapter.CasbinRule{
Ptype: "p",
})
return err
},
Rollback: func(engine *xorm.Engine) error {
return engine.DropTables(&xormadapter.CasbinRule{})
},
}
return &migration
}

View File

@ -1,141 +0,0 @@
// Copyright 2023 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 (
"errors"
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_240_0_PR_1539 struct{}
func (*Migrator_1_240_0_PR_1539) IsMigrationNeeded() bool {
exist, _ := ormer.Engine.IsTableExist("session")
err := ormer.Engine.Table("session").Find(&[]*Session{})
if exist && err != nil {
return true
}
return false
}
func (*Migrator_1_240_0_PR_1539) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20230211MigrateSession--Create a new field 'application' for table `session`",
Migrate: func(engine *xorm.Engine) error {
if alreadyCreated, _ := engine.IsTableExist("session_tmp"); alreadyCreated {
return errors.New("there is already a table called 'session_tmp', please rename or delete it for casdoor version migration and restart")
}
type oldSession struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
SessionId []string `json:"sessionId"`
}
tx := engine.NewSession()
defer tx.Close()
err := tx.Begin()
if err != nil {
return err
}
err = tx.Table("session_tmp").CreateTable(&Session{})
if err != nil {
return err
}
oldSessions := []*oldSession{}
newSessions := []*Session{}
err = tx.Table("session").Find(&oldSessions)
if err != nil {
return err
}
for _, oldSession := range oldSessions {
newApplication := "null"
if oldSession.Owner == "built-in" {
newApplication = "app-built-in"
}
newSessions = append(newSessions, &Session{
Owner: oldSession.Owner,
Name: oldSession.Name,
Application: newApplication,
CreatedTime: oldSession.CreatedTime,
SessionId: oldSession.SessionId,
})
}
rollbackFlag := false
_, err = tx.Table("session_tmp").Insert(newSessions)
count1, _ := tx.Table("session_tmp").Count()
count2, _ := tx.Table("session").Count()
if err != nil || count1 != count2 {
rollbackFlag = true
}
delete := &Session{
Application: "null",
}
_, err = tx.Table("session_tmp").Delete(*delete)
if err != nil {
rollbackFlag = true
}
if rollbackFlag {
tx.DropTable("session_tmp")
return errors.New("there is something wrong with data migration for table `session`, if there is a table called `session_tmp` not created by you in casdoor, please drop it, then restart anyhow")
}
err = tx.DropTable("session")
if err != nil {
return errors.New("fail to drop table `session` for casdoor, please drop it and rename the table `session_tmp` to `session` manually and restart")
}
// Already drop table `session`
// Can't find an api from xorm for altering table name
err = tx.Table("session").CreateTable(&Session{})
if err != nil {
return errors.New("there is something wrong with data migration for table `session`, please restart")
}
sessions := []*Session{}
tx.Table("session_tmp").Find(&sessions)
_, err = tx.Table("session").Insert(sessions)
if err != nil {
return errors.New("there is something wrong with data migration for table `session`, please drop table `session` and rename table `session_tmp` to `session` and restart")
}
err = tx.DropTable("session_tmp")
if err != nil {
return errors.New("fail to drop table `session_tmp` for casdoor, please drop it manually and restart")
}
tx.Commit()
return nil
},
}
return &migration
}

View File

@ -1,68 +0,0 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/xorm-io/xorm"
"github.com/xorm-io/xorm/migrate"
)
type Migrator_1_314_0_PR_1841 struct{}
func (*Migrator_1_314_0_PR_1841) IsMigrationNeeded() bool {
count, err := ormer.Engine.Where("password_type=?", "").Count(&User{})
if err != nil {
// table doesn't exist
return false
}
return count > 100
}
func (*Migrator_1_314_0_PR_1841) DoMigration() *migrate.Migration {
migration := migrate.Migration{
ID: "20230515MigrateUser--Create a new field 'passwordType' for table `user`",
Migrate: func(engine *xorm.Engine) error {
tx := engine.NewSession()
defer tx.Close()
err := tx.Begin()
if err != nil {
return err
}
organizations := []*Organization{}
err = tx.Table("organization").Find(&organizations)
if err != nil {
return err
}
for _, organization := range organizations {
user := &User{PasswordType: organization.PasswordType}
_, err = tx.Where("owner = ?", organization.Name).Cols("password_type").Update(user)
if err != nil {
return err
}
}
tx.Commit()
return nil
},
}
return &migration
}

View File

@ -51,23 +51,24 @@ type Organization struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
@ -159,6 +160,9 @@ func GetMaskedOrganization(organization *Organization, errs ...error) (*Organiza
if organization.DefaultPassword != "" {
organization.DefaultPassword = "***"
}
if organization.MasterVerificationCode != "" {
organization.MasterVerificationCode = "***"
}
return organization, nil
}
@ -213,6 +217,9 @@ func UpdateOrganization(id string, organization *Organization) (bool, error) {
if organization.DefaultPassword == "***" {
session.Omit("default_password")
}
if organization.MasterVerificationCode == "***" {
session.Omit("master_verification_code")
}
affected, err := session.Update(organization)
if err != nil {

View File

@ -64,7 +64,6 @@ func InitConfig() {
InitAdapter()
CreateTables()
DoMigration()
}
func InitAdapter() {
@ -330,11 +329,6 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(PermissionRule))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(xormadapter.CasbinRule))
if err != nil {
panic(err)

View File

@ -54,7 +54,7 @@ type Payment struct {
// Order Info
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl""` // `successUrl` is redirected from `payUrl` after pay success
SuccessUrl string `xorm:"varchar(2000)" json:"successUrl"` // `successUrl` is redirected from `payUrl` after pay success
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
Message string `xorm:"varchar(2000)" json:"message"`
}

View File

@ -49,17 +49,6 @@ type Permission struct {
State string `xorm:"varchar(100)" json:"state"`
}
type PermissionRule struct {
Ptype string `xorm:"varchar(100) index not null default ''" json:"ptype"`
V0 string `xorm:"varchar(100) index not null default ''" json:"v0"`
V1 string `xorm:"varchar(100) index not null default ''" json:"v1"`
V2 string `xorm:"varchar(100) index not null default ''" json:"v2"`
V3 string `xorm:"varchar(100) index not null default ''" json:"v3"`
V4 string `xorm:"varchar(100) index not null default ''" json:"v4"`
V5 string `xorm:"varchar(100) index not null default ''" json:"v5"`
Id string `xorm:"varchar(100) index not null default ''" json:"id"`
}
const builtInAvailableField = 5 // Casdoor built-in adapter, use V5 to filter permission, so has 5 available field
func GetPermissionCount(owner, field, value string) (int64, error) {

View File

@ -17,6 +17,8 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
@ -30,8 +32,8 @@ type Product struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Image string `xorm:"varchar(100)" json:"image"`
Detail string `xorm:"varchar(255)" json:"detail"`
Description string `xorm:"varchar(100)" json:"description"`
Detail string `xorm:"varchar(1000)" json:"detail"`
Description string `xorm:"varchar(200)" json:"description"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
@ -158,30 +160,28 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
return provider, nil
}
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (*Payment, error) {
func BuyProduct(id string, user *User, providerName, pricingName, planName, host, paymentEnv string) (payment *Payment, attachInfo map[string]interface{}, err error) {
product, err := GetProduct(id)
if err != nil {
return nil, err
return nil, nil, err
}
if product == nil {
return nil, fmt.Errorf("the product: %s does not exist", id)
return nil, nil, fmt.Errorf("the product: %s does not exist", id)
}
provider, err := product.getProvider(providerName)
if err != nil {
return nil, err
return nil, nil, err
}
pProvider, err := GetPaymentProvider(provider)
if err != nil {
return nil, err
return nil, nil, err
}
owner := product.Owner
productName := product.Name
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
productDisplayName := product.DisplayName
originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
@ -191,26 +191,46 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if pricingName != "" && planName != "" {
plan, err := GetPlan(util.GetId(owner, planName))
if err != nil {
return nil, err
return nil, nil, err
}
if plan == nil {
return nil, fmt.Errorf("the plan: %s does not exist", planName)
return nil, nil, fmt.Errorf("the plan: %s does not exist", planName)
}
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
_, err = AddSubscription(sub)
if err != nil {
return nil, err
return nil, nil, err
}
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
}
}
// Create an OrderId and get the payUrl
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
// Create an order
payReq := &pp.PayReq{
ProviderName: providerName,
ProductName: product.Name,
PayerName: payerName,
PayerId: user.Id,
PaymentName: paymentName,
ProductDisplayName: product.DisplayName,
Price: product.Price,
Currency: product.Currency,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
PaymentEnv: paymentEnv,
}
// custom process for WeChat & WeChat Pay
if provider.Type == "WeChat Pay" {
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
if err != nil {
return nil, nil, err
}
}
payResp, err := pProvider.Pay(payReq)
if err != nil {
return nil, err
return nil, nil, err
}
// Create a Payment linked with Product and Order
payment := &Payment{
payment = &Payment{
Owner: product.Owner,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
@ -219,8 +239,8 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
Provider: provider.Name,
Type: provider.Type,
ProductName: productName,
ProductDisplayName: productDisplayName,
ProductName: product.Name,
ProductDisplayName: product.DisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,
@ -228,10 +248,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
ReturnUrl: product.ReturnUrl,
User: user.Name,
PayUrl: payUrl,
PayUrl: payResp.PayUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated,
OutOrderId: orderId,
OutOrderId: payResp.OrderId,
}
if provider.Type == "Dummy" {
@ -240,13 +260,13 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
affected, err := AddPayment(payment)
if err != nil {
return nil, err
return nil, nil, err
}
if !affected {
return nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
}
return payment, err
return payment, payResp.AttachInfo, nil
}
func ExtendProductWithProviders(product *Product) error {

View File

@ -39,7 +39,7 @@ type Provider struct {
ClientId string `xorm:"varchar(200)" json:"clientId"`
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
ClientSecret2 string `xorm:"varchar(500)" json:"clientSecret2"`
Cert string `xorm:"varchar(100)" json:"cert"`
CustomAuthUrl string `xorm:"varchar(200)" json:"customAuthUrl"`
CustomTokenUrl string `xorm:"varchar(200)" json:"customTokenUrl"`
@ -398,16 +398,18 @@ func providerChangeTrigger(oldName string, newName string) error {
func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.ProviderInfo {
providerInfo := &idp.ProviderInfo{
Type: provider.Type,
SubType: provider.SubType,
ClientId: provider.ClientId,
ClientSecret: provider.ClientSecret,
AppId: provider.AppId,
HostUrl: provider.Host,
TokenURL: provider.CustomTokenUrl,
AuthURL: provider.CustomAuthUrl,
UserInfoURL: provider.CustomUserInfoUrl,
UserMapping: provider.UserMapping,
Type: provider.Type,
SubType: provider.SubType,
ClientId: provider.ClientId,
ClientSecret: provider.ClientSecret,
ClientId2: provider.ClientId2,
ClientSecret2: provider.ClientSecret2,
AppId: provider.AppId,
HostUrl: provider.Host,
TokenURL: provider.CustomTokenUrl,
AuthURL: provider.CustomAuthUrl,
UserInfoURL: provider.CustomUserInfoUrl,
UserMapping: provider.UserMapping,
}
if provider.Type == "WeChat" {
@ -415,7 +417,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2
}
} else if provider.Type == "AzureAD" || provider.Type == "ADFS" {
} else if provider.Type == "AzureAD" || provider.Type == "ADFS" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain
}

View File

@ -272,9 +272,9 @@ func getRolesByUserInternal(userId string) ([]*Role, error) {
return roles, err
}
query := ormer.Engine.Where("users like ?", fmt.Sprintf("%%%s%%", userId))
query := ormer.Engine.Alias("r").Where("r.users like ?", fmt.Sprintf("%%%s%%", userId))
for _, group := range user.Groups {
query = query.Or("groups like ?", fmt.Sprintf("%%%s%%", group))
query = query.Or("r.groups like ?", fmt.Sprintf("%%%s%%", group))
}
err = query.Find(&roles)

View File

@ -621,25 +621,25 @@ func GetPasswordToken(application *Application, username string, password string
if err != nil {
return nil, nil, err
}
if user == nil {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "the user does not exist",
}, nil
}
var msg string
if user.Ldap != "" {
msg = checkLdapUserPassword(user, password, "en")
err = checkLdapUserPassword(user, password, "en")
} else {
msg = CheckPassword(user, password, "en")
err = CheckPassword(user, password, "en")
}
if msg != "" {
if err != nil {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "invalid username or password",
ErrorDescription: fmt.Sprintf("invalid username or password: %s", err.Error()),
}, nil
}
if user.IsForbidden {
return nil, &TokenError{
Error: InvalidGrant,

View File

@ -195,6 +195,9 @@ func GenerateCasToken(userId string, service string) (string, error) {
user, _ = GetMaskedUser(user, false)
user.WebauthnCredentials = nil
user.Properties = nil
authenticationSuccess := CasAuthenticationSuccess{
User: user.Name,
Attributes: &CasAttributes{

View File

@ -34,6 +34,12 @@ type Claims struct {
type UserShort struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
Id string `xorm:"varchar(100) index" json:"id"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
Email string `xorm:"varchar(100) index" json:"email"`
Phone string `xorm:"varchar(20) index" json:"phone"`
}
type UserWithoutThirdIdp struct {
@ -144,6 +150,12 @@ func getShortUser(user *User) *UserShort {
res := &UserShort{
Owner: user.Owner,
Name: user.Name,
Id: user.Id,
DisplayName: user.DisplayName,
Avatar: user.Avatar,
Email: user.Email,
Phone: user.Phone,
}
return res
}

View File

@ -24,14 +24,14 @@ import (
"time"
)
func generateRsaKeys(bitSize int, expireInYears int, commonName string, organization string) (string, string) {
func generateRsaKeys(bitSize int, expireInYears int, commonName string, organization string) (string, string, error) {
// https://stackoverflow.com/questions/64104586/use-golang-to-get-rsa-key-the-same-way-openssl-genrsa
// https://stackoverflow.com/questions/43822945/golang-can-i-create-x509keypair-using-rsa-key
// Generate RSA key.
key, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
panic(err)
return "", "", err
}
// Encode private key to PKCS#1 ASN.1 PEM.
@ -54,9 +54,10 @@ func generateRsaKeys(bitSize int, expireInYears int, commonName string, organiza
},
BasicConstraintsValid: true,
}
cert, err := x509.CreateCertificate(rand.Reader, &tml, &tml, &key.PublicKey, key)
if err != nil {
panic(err)
return "", "", err
}
// Generate a pem block with the certificate
@ -65,5 +66,5 @@ func generateRsaKeys(bitSize int, expireInYears int, commonName string, organiza
Bytes: cert,
})
return string(certPem), string(privateKeyPem)
return string(certPem), string(privateKeyPem), nil
}

View File

@ -23,7 +23,10 @@ import (
func TestGenerateRsaKeys(t *testing.T) {
fileId := "token_jwt_key"
certificate, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
certificate, privateKey, err := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
if err != nil {
panic(err)
}
// Write certificate (aka certificate) to file.
util.WriteStringToPath(certificate, fmt.Sprintf("%s.pem", fileId))

View File

@ -561,7 +561,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
return false, err
}
if oldUser == nil {
return false, nil
return false, fmt.Errorf("the user: %s is not found", id)
}
if name != user.Name {
@ -642,7 +642,7 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
}
if oldUser == nil {
return false, nil
return false, fmt.Errorf("the user: %s is not found", id)
}
if name != user.Name {
@ -664,6 +664,8 @@ func UpdateUserForAllFields(id string, user *User) (bool, error) {
}
}
user.UpdatedTime = util.GetCurrentTime()
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(user)
if err != nil {
return false, err
@ -688,12 +690,15 @@ func AddUser(user *User) (bool, error) {
}
if user.Owner == "" || user.Name == "" {
return false, nil
return false, fmt.Errorf("the user's owner and name should not be empty")
}
organization, _ := GetOrganizationByUser(user)
organization, err := GetOrganizationByUser(user)
if err != nil {
return false, err
}
if organization == nil {
return false, nil
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
}
if organization.DefaultPassword != "" && user.Password == "123" {
@ -704,7 +709,7 @@ func AddUser(user *User) (bool, error) {
user.UpdateUserPassword(organization)
}
err := user.UpdateUserHash()
err = user.UpdateUserHash()
if err != nil {
return false, err
}
@ -738,9 +743,8 @@ func AddUser(user *User) (bool, error) {
}
func AddUsers(users []*User) (bool, error) {
var err error
if len(users) == 0 {
return false, nil
return false, fmt.Errorf("no users are provided")
}
// organization := GetOrganizationByUser(users[0])
@ -748,7 +752,7 @@ func AddUsers(users []*User) (bool, error) {
// this function is only used for syncer or batch upload, so no need to encrypt the password
// user.UpdateUserPassword(organization)
err = user.UpdateUserHash()
err := user.UpdateUserHash()
if err != nil {
return false, err
}
@ -772,12 +776,12 @@ func AddUsers(users []*User) (bool, error) {
}
func AddUsersInBatch(users []*User) (bool, error) {
batchSize := conf.GetConfigBatchSize()
if len(users) == 0 {
return false, nil
return false, fmt.Errorf("no users are provided")
}
batchSize := conf.GetConfigBatchSize()
affected := false
for i := 0; i < len(users); i += batchSize {
start := i
@ -849,7 +853,7 @@ func (user *User) GetId() string {
}
func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/")
return strings.HasPrefix(userId, "built-in/") || strings.HasPrefix(userId, "app/")
}
func ExtendUserWithRolesAndPermissions(user *User) (err error) {
@ -945,9 +949,9 @@ func (user *User) GetPreferredMfaProps(masked bool) *MfaProps {
return user.GetMfaProps(user.PreferredMfaType, masked)
}
func AddUserkeys(user *User, isAdmin bool) (bool, error) {
func AddUserKeys(user *User, isAdmin bool) (bool, error) {
if user == nil {
return false, nil
return false, fmt.Errorf("the user is not found")
}
user.AccessKey = util.GenerateId()

View File

@ -35,11 +35,7 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
resp, err := client.Do(req)
if err != nil {
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, err.Error())
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "did not properly respond after a period of time") || strings.Contains(err.Error(), "unrecognized name") {
return nil, "", nil
} else {
return nil, "", err
}
return nil, "", nil
}
defer resp.Body.Close()
@ -58,6 +54,8 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
if strings.Contains(contentType, "text/html") {
fileExtension = ".html"
} else if contentType == "image/vnd.microsoft.icon" {
fileExtension = ".ico"
} else {
fileExtensions, err := mime.ExtensionsByType(contentType)
if err != nil {

View File

@ -186,10 +186,47 @@ func parseSize(sizes string) []int {
return nil
}
var publicEmailDomains = map[string]int{
"gmail.com": 1,
"163.com": 1,
"qq.com": 1,
"yahoo.com": 1,
"hotmail.com": 1,
"outlook.com": 1,
"icloud.com": 1,
"mail.com": 1,
"aol.com": 1,
"live.com": 1,
"yandex.com": 1,
"yahoo.co.jp": 1,
"yahoo.co.in": 1,
"yahoo.co.uk": 1,
"me.com": 1,
"msn.com": 1,
"comcast.net": 1,
"sbcglobal.net": 1,
"verizon.net": 1,
"earthlink.net": 1,
"cox.net": 1,
"rediffmail.com": 1,
"in.com": 1,
"hotmail.co.uk": 1,
"hotmail.fr": 1,
"zoho.com": 1,
"gmx.com": 1,
"gmx.de": 1,
"gmx.net": 1,
}
func isPublicEmailDomain(domain string) bool {
_, exists := publicEmailDomains[domain]
return exists
}
func getFaviconFileBuffer(client *http.Client, email string) (*bytes.Buffer, string, error) {
tokens := strings.Split(email, "@")
domain := tokens[1]
if domain == "gmail.com" || domain == "163.com" || domain == "qq.com" {
if isPublicEmailDomain(domain) {
return nil, "", nil
}

View File

@ -20,7 +20,10 @@ import (
"reflect"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
@ -110,6 +113,10 @@ func SetUserField(user *User, field string, value string) (bool, error) {
return false, err
}
if user != nil {
user.UpdatedTime = util.GetCurrentTime()
}
_, err = ormer.Engine.ID(core.PK{user.Owner, user.Name}).Cols("hash").Update(user)
if err != nil {
return false, err
@ -137,6 +144,25 @@ func setUserProperty(user *User, field string, value string) {
}
}
func getUserProperty(user *User, field string) string {
if user.Properties == nil {
return ""
}
return user.Properties[field]
}
func getUserExtraProperty(user *User, providerType, key string) (string, error) {
extraJson := getUserProperty(user, fmt.Sprintf("oauth_%s_extra", providerType))
if extraJson == "" {
return "", nil
}
extra := make(map[string]string)
if err := jsoniter.Unmarshal([]byte(extraJson), &extra); err != nil {
return "", err
}
return extra[key], nil
}
func SetUserOAuthProperties(organization *Organization, user *User, providerType string, userInfo *idp.UserInfo) (bool, error) {
if userInfo.Id != "" {
propertyName := fmt.Sprintf("oauth_%s_id", providerType)
@ -180,6 +206,27 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
}
}
if userInfo.Extra != nil {
// Save extra info as json string
propertyName := fmt.Sprintf("oauth_%s_extra", providerType)
oldExtraJson := getUserProperty(user, propertyName)
extra := make(map[string]string)
if oldExtraJson != "" {
if err := jsoniter.Unmarshal([]byte(oldExtraJson), &extra); err != nil {
return false, err
}
}
for k, v := range userInfo.Extra {
extra[k] = v
}
newExtraJson, err := jsoniter.Marshal(extra)
if err != nil {
return false, err
}
setUserProperty(user, propertyName, string(newExtraJson))
}
return UpdateUserForAllFields(user.GetId(), user)
}

View File

@ -82,7 +82,12 @@ func IsAllowSend(user *User, remoteAddr, recordType string) error {
func SendVerificationCodeToEmail(organization *Organization, user *User, provider *Provider, remoteAddr string, dest string) error {
sender := organization.DisplayName
title := provider.Title
code := getRandomCode(6)
if organization.MasterVerificationCode != "" {
code = organization.MasterVerificationCode
}
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := fmt.Sprintf(provider.Content, code)
@ -107,6 +112,10 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
}
code := getRandomCode(6)
if organization.MasterVerificationCode != "" {
code = organization.MasterVerificationCode
}
if err := SendSms(provider, code, dest); err != nil {
return err
}
@ -156,7 +165,7 @@ func getVerificationRecord(dest string) (*VerificationRecord, error) {
return &record, nil
}
func CheckVerificationCode(dest, code, lang string) *VerifyResult {
func CheckVerificationCode(dest string, code string, lang string) *VerifyResult {
record, err := getVerificationRecord(dest)
if err != nil {
panic(err)
@ -183,32 +192,32 @@ func CheckVerificationCode(dest, code, lang string) *VerifyResult {
return &VerifyResult{VerificationSuccess, ""}
}
func DisableVerificationCode(dest string) (err error) {
func DisableVerificationCode(dest string) error {
record, err := getVerificationRecord(dest)
if record == nil || err != nil {
return
return nil
}
record.IsUsed = true
_, err = ormer.Engine.ID(core.PK{record.Owner, record.Name}).AllCols().Update(record)
return
return err
}
func CheckSigninCode(user *User, dest, code, lang string) string {
func CheckSigninCode(user *User, dest, code, lang string) error {
// check the login error times
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
err := checkSigninErrorTimes(user, lang)
if err != nil {
return err
}
result := CheckVerificationCode(dest, code, lang)
switch result.Code {
case VerificationSuccess:
resetUserSigninErrorTimes(user)
return ""
return resetUserSigninErrorTimes(user)
case wrongCodeError:
return recordSigninErrorInfo(user, lang)
default:
return result.Msg
return fmt.Errorf(result.Msg)
}
}

View File

@ -49,20 +49,24 @@ func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey
return pp, nil
}
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *AlipayPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
pp.Client.SetReturnUrl(returnUrl)
pp.Client.SetNotifyUrl(notifyUrl)
bm.Set("subject", joinAttachString([]string{productName, productDisplayName, providerName}))
bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", priceFloat64ToString(price))
pp.Client.SetReturnUrl(r.ReturnUrl)
pp.Client.SetNotifyUrl(r.NotifyUrl)
bm.Set("subject", joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName}))
bm.Set("out_trade_no", r.PaymentName)
bm.Set("total_amount", priceFloat64ToString(r.Price))
payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil {
return "", "", err
return nil, err
}
return payUrl, paymentName, nil
payResp := &PayResp{
PayUrl: payUrl,
OrderId: r.PaymentName,
}
return payResp, nil
}
func (pp *AlipayPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -21,8 +21,10 @@ func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
return pp, nil
}
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
return returnUrl, "", nil
func (pp *DummyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
return &PayResp{
PayUrl: r.ReturnUrl,
}, nil
}
func (pp *DummyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -153,22 +153,22 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
return respBytes, nil
}
func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *GcPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(),
OrderNo: paymentName,
Amount: getPriceString(price),
OrderNo: r.PaymentName,
Amount: getPriceString(r.Price),
Xmpch: pp.Xmpch,
Body: productDisplayName,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
Remark1: payerName,
Remark2: productName,
Body: r.ProductDisplayName,
ReturnUrl: r.ReturnUrl,
NotifyUrl: r.NotifyUrl,
Remark1: r.PayerName,
Remark2: r.ProductName,
}
b, err := json.Marshal(payReqInfo)
if err != nil {
return "", "", err
return nil, err
}
body := GcRequestBody{
@ -184,36 +184,38 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", "", err
return nil, err
}
respBytes, err := pp.doPost(bodyBytes)
if err != nil {
return "", "", err
return nil, err
}
var respBody GcResponseBody
err = json.Unmarshal(respBytes, &respBody)
if err != nil {
return "", "", err
return nil, err
}
if respBody.ReturnCode != "SUCCESS" {
return "", "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
return nil, fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
}
payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
if err != nil {
return "", "", err
return nil, err
}
var payRespInfo GcPayRespInfo
err = json.Unmarshal(payRespInfoBytes, &payRespInfo)
if err != nil {
return "", "", err
return nil, err
}
return payRespInfo.PayUrl, "", nil
payResp := &PayResp{
PayUrl: payRespInfo.PayUrl,
}
return payResp, nil
}
func (pp *GcPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -49,16 +49,16 @@ func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentPro
return pp, nil
}
func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *PaypalPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// https://github.com/go-pay/gopay/blob/main/doc/paypal.md
units := make([]*paypal.PurchaseUnit, 0, 1)
unit := &paypal.PurchaseUnit{
ReferenceId: util.GetRandomString(16),
Amount: &paypal.Amount{
CurrencyCode: currency, // e.g."USD"
Value: priceFloat64ToString(price), // e.g."100.00"
CurrencyCode: r.Currency, // e.g."USD"
Value: priceFloat64ToString(r.Price), // e.g."100.00"
},
Description: joinAttachString([]string{productDisplayName, productName, providerName}),
Description: joinAttachString([]string{r.ProductDisplayName, r.ProductName, r.ProviderName}),
}
units = append(units, unit)
@ -68,23 +68,27 @@ func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, pa
bm.SetBodyMap("application_context", func(b gopay.BodyMap) {
b.Set("brand_name", "Casdoor")
b.Set("locale", "en-PT")
b.Set("return_url", returnUrl)
b.Set("cancel_url", returnUrl)
b.Set("return_url", r.ReturnUrl)
b.Set("cancel_url", r.ReturnUrl)
})
ppRsp, err := pp.Client.CreateOrder(context.Background(), bm)
if err != nil {
return "", "", err
return nil, err
}
if ppRsp.Code != paypal.Success {
return "", "", errors.New(ppRsp.Error)
return nil, errors.New(ppRsp.Error)
}
// {"id":"9BR68863NE220374S","status":"CREATED",
// "links":[{"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"self","method":"GET"},
// {"href":"https://www.sandbox.paypal.com/checkoutnow?token=9BR68863NE220374S","rel":"approve","method":"GET"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"update","method":"PATCH"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S/capture","rel":"capture","method":"POST"}]}
return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil
payResp := &PayResp{
PayUrl: ppRsp.Response.Links[1].Href,
OrderId: ppRsp.Response.Id,
}
return payResp, nil
}
func (pp *PaypalPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -24,6 +24,32 @@ const (
PaymentStateError PaymentState = "Error"
)
const (
PaymentEnvWechatBrowser = "WechatBrowser"
)
type PayReq struct {
ProviderName string
ProductName string
PayerName string
PayerId string
PaymentName string
ProductDisplayName string
Price float64
Currency string
ReturnUrl string
NotifyUrl string
PaymentEnv string
}
type PayResp struct {
PayUrl string
OrderId string
AttachInfo map[string]interface{}
}
type NotifyResult struct {
PaymentName string
PaymentStatus PaymentState
@ -39,7 +65,7 @@ type NotifyResult struct {
}
type PaymentProvider interface {
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error)
Pay(req *PayReq) (*PayResp, error)
Notify(body []byte, orderId string) (*NotifyResult, error)
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
GetResponseError(err error) string

View File

@ -46,30 +46,30 @@ func NewStripePaymentProvider(PublishableKey, SecretKey string) (*StripePaymentP
return pp, nil
}
func (pp *StripePaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (payUrl string, orderId string, err error) {
func (pp *StripePaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// Create a temp product
description := joinAttachString([]string{productName, productDisplayName, providerName})
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
productParams := &stripe.ProductParams{
Name: stripe.String(productDisplayName),
Name: stripe.String(r.ProductDisplayName),
Description: stripe.String(description),
DefaultPriceData: &stripe.ProductDefaultPriceDataParams{
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
Currency: stripe.String(currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(r.Price)),
Currency: stripe.String(r.Currency),
},
}
sProduct, err := stripeProduct.New(productParams)
if err != nil {
return "", "", err
return nil, err
}
// Create a price for an existing product
priceParams := &stripe.PriceParams{
Currency: stripe.String(currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
Currency: stripe.String(r.Currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(r.Price)),
Product: stripe.String(sProduct.ID),
}
sPrice, err := stripePrice.New(priceParams)
if err != nil {
return "", "", err
return nil, err
}
// Create a Checkout Session
checkoutParams := &stripe.CheckoutSessionParams{
@ -80,17 +80,21 @@ func (pp *StripePaymentProvider) Pay(providerName string, productName string, pa
},
},
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
SuccessURL: stripe.String(returnUrl),
CancelURL: stripe.String(returnUrl),
ClientReferenceID: stripe.String(paymentName),
SuccessURL: stripe.String(r.ReturnUrl),
CancelURL: stripe.String(r.ReturnUrl),
ClientReferenceID: stripe.String(r.PaymentName),
ExpiresAt: stripe.Int64(time.Now().Add(30 * time.Minute).Unix()),
}
checkoutParams.AddMetadata("product_description", description)
sCheckout, err := stripeCheckout.New(checkoutParams)
if err != nil {
return "", "", err
return nil, err
}
return sCheckout.URL, sCheckout.ID, nil
payResp := &PayResp{
PayUrl: sCheckout.URL,
OrderId: sCheckout.ID,
}
return payResp, nil
}
func (pp *StripePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -63,27 +63,66 @@ func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, seria
return pp, nil
}
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *WechatPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
bm := gopay.BodyMap{}
bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName}))
desc := joinAttachString([]string{r.ProductDisplayName, r.ProductName, r.ProviderName})
bm.Set("attach", desc)
bm.Set("appid", pp.AppId)
bm.Set("description", productDisplayName)
bm.Set("notify_url", notifyUrl)
bm.Set("out_trade_no", paymentName)
bm.Set("description", r.ProductDisplayName)
bm.Set("notify_url", r.NotifyUrl)
bm.Set("out_trade_no", r.PaymentName)
bm.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", priceFloat64ToInt64(price))
bm.Set("currency", currency)
bm.Set("total", priceFloat64ToInt64(r.Price))
bm.Set("currency", r.Currency)
})
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return "", "", err
// In Wechat browser, we use JSAPI
if r.PaymentEnv == PaymentEnvWechatBrowser {
if r.PayerId == "" {
return nil, errors.New("failed to get the payer's openid, please retry login")
}
bm.SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", r.PayerId) // If the account is signup via Wechat, the PayerId is the Wechat OpenId e.g.oxW9O1ZDvgreSHuBSQDiQ2F055PI
})
jsapiRsp, err := pp.Client.V3TransactionJsapi(context.Background(), bm)
if err != nil {
return nil, err
}
if jsapiRsp.Code != wechat.Success {
return nil, errors.New(jsapiRsp.Error)
}
// use RSA256 to sign the pay request
params, err := pp.Client.PaySignOfJSAPI(pp.AppId, jsapiRsp.Response.PrepayId)
if err != nil {
return nil, err
}
payResp := &PayResp{
PayUrl: "",
OrderId: r.PaymentName, // Wechat can use paymentName as the OutTradeNo to query order status
AttachInfo: map[string]interface{}{
"appId": params.AppId,
"timeStamp": params.TimeStamp,
"nonceStr": params.NonceStr,
"package": params.Package,
"signType": "RSA",
"paySign": params.PaySign,
},
}
return payResp, nil
} else {
// In other case, we use NativeAPI
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return nil, err
}
if nativeRsp.Code != wechat.Success {
return nil, errors.New(nativeRsp.Error)
}
payResp := &PayResp{
PayUrl: nativeRsp.Response.CodeUrl,
OrderId: r.PaymentName, // Wechat can use paymentName as the OutTradeNo to query order status
}
return payResp, nil
}
if nativeRsp.Code != wechat.Success {
return "", "", errors.New(nativeRsp.Error)
}
return nativeRsp.Response.CodeUrl, paymentName, nil // Wechat can use paymentName as the OutTradeNo to query order status
}
func (pp *WechatPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -55,15 +55,18 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
password := rfc2865.UserPassword_GetString(r.Packet)
organization := rfc2865.Class_GetString(r.Packet)
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
if organization == "" {
w.Write(r.Response(radius.CodeAccessReject))
return
}
_, msg := object.CheckUserPassword(organization, username, password, "en")
if msg != "" {
_, err := object.CheckUserPassword(organization, username, password, "en")
if err != nil {
w.Write(r.Response(radius.CodeAccessReject))
return
}
w.Write(r.Response(radius.CodeAccessAccept))
}

View File

@ -83,13 +83,12 @@ func AutoSigninFilter(ctx *context.Context) {
password := ctx.Input.Query("password")
if userId != "" && password != "" && ctx.Input.Query("grant_type") == "" {
owner, name := util.GetOwnerAndNameFromId(userId)
_, msg := object.CheckUserPassword(owner, name, password, "en")
if msg != "" {
responseError(ctx, msg)
_, err = object.CheckUserPassword(owner, name, password, "en")
if err != nil {
responseError(ctx, err.Error())
return
}
setSessionUser(ctx, userId)
return
}
}

View File

@ -51,6 +51,11 @@ func CorsFilter(ctx *context.Context) {
return
}
if originHostname == "appleid.apple.com" {
setCorsHeaders(ctx, origin)
return
}
if ctx.Request.Method == "POST" && ctx.Request.RequestURI == "/api/login/oauth/access_token" {
setCorsHeaders(ctx, origin)
return

View File

@ -79,7 +79,7 @@ func initAPI() {
beego.Router("/api/get-user-count", &controllers.ApiController{}, "GET:GetUserCount")
beego.Router("/api/get-user", &controllers.ApiController{}, "GET:GetUser")
beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser")
beego.Router("/api/add-user-keys", &controllers.ApiController{}, "POST:AddUserkeys")
beego.Router("/api/add-user-keys", &controllers.ApiController{}, "POST:AddUserKeys")
beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser")
beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
@ -204,7 +204,7 @@ func initAPI() {
beego.Router("/api/run-syncer", &controllers.ApiController{}, "GET:RunSyncer")
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
beego.Router("/api/get-globle-certs", &controllers.ApiController{}, "GET:GetGlobleCerts")
beego.Router("/api/get-global-certs", &controllers.ApiController{}, "GET:GetGlobalCerts")
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
beego.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")

View File

@ -83,7 +83,11 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
return "", fmt.Errorf(code.Message)
}
res := fmt.Sprintf("%s?code=%s&state=%s", redirectUri, code.Code, state)
sep := "?"
if strings.Contains(redirectUri, "?") {
sep = "&"
}
res := fmt.Sprintf("%s%scode=%s&state=%s", redirectUri, sep, code.Code, state)
return res, nil
}

View File

@ -2023,13 +2023,13 @@
}
}
},
"/api/get-globle-certs": {
"/api/get-global-certs": {
"get": {
"tags": [
"Cert API"
],
"description": "get globle certs",
"operationId": "ApiController.GetGlobleCerts",
"operationId": "ApiController.GetGlobalCerts",
"responses": {
"200": {
"description": "The Response object",

View File

@ -1311,12 +1311,12 @@ paths:
type: array
items:
$ref: '#/definitions/object.User'
/api/get-globle-certs:
/api/get-global-certs:
get:
tags:
- Cert API
description: get globle certs
operationId: ApiController.GetGlobleCerts
operationId: ApiController.GetGlobalCerts
responses:
"200":
description: The Response object

116
sync_v2/cmd_test.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2023 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.
//go:build !skipCi
// +build !skipCi
package sync_v2
import (
"testing"
_ "github.com/go-sql-driver/mysql"
)
/*
The following config should be added to my.cnf:
gtid_mode=on
enforce_gtid_consistency=on
binlog-format=ROW
server-id = 1 # this should be different for each mysql instance (1,2)
auto_increment_offset = 1 # this is same as server-id
auto_increment_increment = 2 # this is same as the number of mysql instances (2)
log-bin = mysql-bin
replicate-do-db = casdoor # this is the database name
binlog-do-db = casdoor # this is the database name
*/
var Configs = []Database{
{
host: "test-db.v2tl.com",
port: 3306,
username: "root",
password: "password",
database: "casdoor",
// the following two fields are used to create replication user, you don't need to change them
slaveUser: "repl_user",
slavePassword: "repl_user",
},
{
host: "localhost",
port: 3306,
username: "root",
password: "password",
database: "casdoor",
// the following two fields are used to create replication user, you don't need to change them
slaveUser: "repl_user",
slavePassword: "repl_user",
},
}
func TestStartMasterSlaveSync(t *testing.T) {
// for example, this is aliyun rds
db0 := newDatabase(&Configs[0])
// for example, this is local mysql instance
db1 := newDatabase(&Configs[1])
createSlaveUser(db0)
// db0 is master, db1 is slave
startSlave(db0, db1)
}
func TestStopMasterSlaveSync(t *testing.T) {
// for example, this is aliyun rds
db0 := newDatabase(&Configs[0])
// for example, this is local mysql instance
db1 := newDatabase(&Configs[1])
stopSlave(db1)
deleteSlaveUser(db0)
}
func TestStartMasterMasterSync(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
createSlaveUser(db0)
createSlaveUser(db1)
// db0 is master, db1 is slave
startSlave(db0, db1)
// db1 is master, db0 is slave
startSlave(db1, db0)
}
func TestStopMasterMasterSync(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
stopSlave(db0)
stopSlave(db1)
deleteSlaveUser(db0)
deleteSlaveUser(db1)
}
func TestShowSlaveStatus(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
slaveStatus(db0)
slaveStatus(db1)
}
func TestShowMasterStatus(t *testing.T) {
db0 := newDatabase(&Configs[0])
db1 := newDatabase(&Configs[1])
masterStatus(db0)
masterStatus(db1)
}

70
sync_v2/db.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright 2023 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 sync_v2
import (
"fmt"
"log"
"github.com/xorm-io/xorm"
)
type Database struct {
host string
port int
database string
username string
password string
slaveUser string
slavePassword string
engine *xorm.Engine
}
func (db *Database) exec(format string, args ...interface{}) []map[string]string {
sql := fmt.Sprintf(format, args...)
res, err := db.engine.QueryString(sql)
if err != nil {
panic(err)
}
return res
}
func createEngine(dataSourceName string) (*xorm.Engine, error) {
engine, err := xorm.NewEngine("mysql", dataSourceName)
if err != nil {
return nil, err
}
// ping mysql
err = engine.Ping()
if err != nil {
return nil, err
}
engine.ShowSQL(true)
log.Println("mysql connection success")
return engine, nil
}
func newDatabase(db *Database) *Database {
dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", db.username, db.password, db.host, db.port, db.database)
engine, err := createEngine(dataSourceName)
if err != nil {
panic(err)
}
db.engine = engine
return db
}

89
sync_v2/master.go Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2023 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 sync_v2
import (
"fmt"
"log"
)
func deleteSlaveUser(masterdb *Database) {
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
masterdb.exec("delete from mysql.user where user = '%v'", masterdb.slaveUser)
masterdb.exec("flush privileges")
}
func createSlaveUser(masterdb *Database) {
res := make([]map[string]string, 0)
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
res = masterdb.exec("show databases")
dbNames := make([]string, 0, len(res))
for _, dbInfo := range res {
dbName := dbInfo["Database"]
dbNames = append(dbNames, dbName)
}
log.Println("dbs in mysql: ", dbNames)
res = masterdb.exec("show tables")
tableNames := make([]string, 0, len(res))
for _, table := range res {
tableName := table[fmt.Sprintf("Tables_in_%v", masterdb.database)]
tableNames = append(tableNames, tableName)
}
log.Printf("tables in %v: %v", masterdb.database, tableNames)
// delete user to prevent user already exists
res = masterdb.exec("delete from mysql.user where user = '%v'", masterdb.slaveUser)
res = masterdb.exec("flush privileges")
// create replication user
res = masterdb.exec("create user '%s'@'%s' identified by '%s'", masterdb.slaveUser, "%", masterdb.slavePassword)
res = masterdb.exec("select host, user from mysql.user where user = '%v'", masterdb.slaveUser)
log.Println("user: ", res[0])
res = masterdb.exec("grant replication slave on *.* to '%s'@'%s'", masterdb.slaveUser, "%")
res = masterdb.exec("flush privileges")
res = masterdb.exec("show grants for '%s'@'%s'", masterdb.slaveUser, "%")
log.Println("grants: ", res[0])
// check env
res = masterdb.exec("show variables like 'server_id'")
log.Println("server_id: ", res[0]["Value"])
res = masterdb.exec("show variables like 'log_bin'")
log.Println("log_bin: ", res[0]["Value"])
res = masterdb.exec("show variables like 'binlog_format'")
log.Println("binlog_format: ", res[0]["Value"])
res = masterdb.exec("show variables like 'binlog_row_image'")
}
func masterStatus(masterdb *Database) {
res := masterdb.exec("show master status")
if len(res) == 0 {
log.Printf("no master status for master [%v:%v]\n", masterdb.host, masterdb.port)
return
}
pos := res[0]["Position"]
file := res[0]["File"]
log.Println("*****check master status*****")
log.Println("master:", masterdb.host, ":", masterdb.port)
log.Println("file:", file, ", position:", pos, ", master status:", res)
log.Println("*****************************")
}

84
sync_v2/slave.go Normal file
View File

@ -0,0 +1,84 @@
// Copyright 2023 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 sync_v2
import "log"
// slaveStatus shows slave status
func slaveStatus(slavedb *Database) {
res := slavedb.exec("show slave status")
if len(res) == 0 {
log.Printf("no slave status for slave [%v:%v]\n", slavedb.host, slavedb.port)
return
}
log.Println("*****check slave status*****")
log.Println("slave:", slavedb.host, ":", slavedb.port)
masterServerId := res[0]["Master_Server_Id"]
log.Println("master server id:", masterServerId)
lastError := res[0]["Last_Error"]
log.Println("last error:", lastError) // this should be empty
lastIoError := res[0]["Last_IO_Error"]
log.Println("last io error:", lastIoError) // this should be empty
slaveIoState := res[0]["Slave_IO_State"]
log.Println("slave io state:", slaveIoState)
slaveIoRunning := res[0]["Slave_IO_Running"]
log.Println("slave io running:", slaveIoRunning) // this should be Yes
slaveSqlRunning := res[0]["Slave_SQL_Running"]
log.Println("slave sql running:", slaveSqlRunning) // this should be Yes
slaveSqlRunningState := res[0]["Slave_SQL_Running_State"]
log.Println("slave sql running state:", slaveSqlRunningState)
slaveSecondsBehindMaster := res[0]["Seconds_Behind_Master"]
log.Println("seconds behind master:", slaveSecondsBehindMaster) // this should be 0, if not, it means the slave is behind the master
log.Println("slave status:", res)
log.Println("****************************")
}
// stopSlave stops slave
func stopSlave(slavedb *Database) {
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
slavedb.exec("stop slave")
slaveStatus(slavedb)
}
// startSlave starts slave
func startSlave(masterdb *Database, slavedb *Database) {
res := make([]map[string]string, 0)
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
stopSlave(slavedb)
// get the info about master
res = masterdb.exec("show master status")
if len(res) == 0 {
log.Println("no master status")
return
}
pos := res[0]["Position"]
file := res[0]["File"]
log.Println("file:", file, ", position:", pos, ", master status:", res)
res = slavedb.exec("stop slave")
res = slavedb.exec(
"change master to master_host='%v', master_port=%v, master_user='%v', master_password='%v', master_log_file='%v', master_log_pos=%v;",
masterdb.host, masterdb.port, masterdb.slaveUser, masterdb.slavePassword, file, pos,
)
res = slavedb.exec("start slave")
slaveStatus(slavedb)
}

66
sync_v2/table_test.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2023 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.
//go:build !skipCi
// +build !skipCi
package sync_v2
import (
"log"
"math/rand"
"testing"
"github.com/casdoor/casdoor/util"
)
type TestUser struct {
Id int64 `xorm:"pk autoincr"`
Username string `xorm:"varchar(50)"`
Address string `xorm:"varchar(50)"`
Card string `xorm:"varchar(50)"`
Age int
}
func TestCreateUserTable(t *testing.T) {
db := newDatabase(&Configs[0])
err := db.engine.Sync2(new(TestUser))
if err != nil {
log.Fatalln(err)
}
}
func TestInsertUser(t *testing.T) {
db := newDatabase(&Configs[0])
// random generate user
user := &TestUser{
Username: util.GetRandomName(),
Age: rand.Intn(100) + 10,
}
_, err := db.engine.Insert(user)
if err != nil {
log.Fatalln(err)
}
}
func TestDeleteUser(t *testing.T) {
db := newDatabase(&Configs[0])
user := &TestUser{
Id: 10,
}
_, err := db.engine.Delete(user)
if err != nil {
log.Fatalln(err)
}
}

View File

@ -17,11 +17,10 @@ import "./App.less";
import {Helmet} from "react-helmet";
import Dashboard from "./basic/Dashboard";
import ShortcutsPage from "./basic/ShortcutsPage";
import {MfaRuleRequired} from "./Setting";
import * as Setting from "./Setting";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {AppstoreTwoTone, BarsOutlined, DollarTwoTone, DownOutlined, HomeTwoTone, InfoCircleFilled, LockTwoTone, LogoutOutlined, SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone, WalletTwoTone} from "@ant-design/icons";
import {Alert, Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd";
import {AppstoreTwoTone, BarsOutlined, DeploymentUnitOutlined, DollarTwoTone, DownOutlined, GithubOutlined, HomeTwoTone, InfoCircleFilled, LockTwoTone, LogoutOutlined, SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone, ShareAltOutlined, WalletTwoTone} from "@ant-design/icons";
import {Alert, Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result, Tooltip} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage";
@ -110,6 +109,7 @@ class App extends Component {
themeData: Conf.ThemeDefault,
logo: this.getLogo(Setting.getAlgorithmNames(Conf.ThemeDefault)),
requiredEnableMfa: false,
isAiAssistantOpen: false,
};
Setting.initServerUrl();
@ -137,8 +137,8 @@ class App extends Component {
});
if (requiredEnableMfa === true) {
const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [MfaRuleRequired])
.find((item) => item.rule === MfaRuleRequired)?.name;
const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [Setting.MfaRuleRequired])
.find((item) => item.rule === Setting.MfaRuleRequired)?.name;
if (mfaType !== undefined) {
this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, {from: "/login"});
}
@ -380,6 +380,15 @@ class App extends Component {
});
}} />
<LanguageSelect languages={this.state.account.organization.languages} />
<Tooltip title="Click to open AI assitant">
<div className="select-box" onClick={() => {
this.setState({
isAiAssistantOpen: true,
});
}}>
<DeploymentUnitOutlined style={{fontSize: "24px", color: "rgb(77,77,77)"}} />
</div>
</Tooltip>
<OpenTour />
{Setting.isAdminUser(this.state.account) && !Setting.isMobile() &&
<OrganizationSelect
@ -579,7 +588,7 @@ class App extends Component {
}
}
};
const menuStyleRight = Setting.isAdminUser(this.state.account) && !Setting.isMobile() ? "calc(180px + 260px)" : "260px";
const menuStyleRight = Setting.isAdminUser(this.state.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "280px";
return (
<Layout id="parent-area">
<EnableMfaNotification account={this.state.account} />
@ -625,7 +634,12 @@ class App extends Component {
</Card>
}
</Content>
{this.renderFooter()}
{
this.renderFooter()
}
{
this.renderAiAssistant()
}
</Layout>
);
}
@ -651,6 +665,40 @@ class App extends Component {
);
}
renderAiAssistant() {
return (
<Drawer
title={
<React.Fragment>
<Tooltip title="Want to deploy your own AI assistant? Click to learn more!">
<a target="_blank" rel="noreferrer" href={"https://casdoor.com"}>
<img style={{width: "20px", marginRight: "10px", marginBottom: "2px"}} alt="help" src="https://casbin.org/img/casbin.svg" />
AI Assistant
</a>
</Tooltip>
<a className="custom-link" style={{float: "right", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://ai.casbin.com"}>
<ShareAltOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
</a>
<a className="custom-link" style={{float: "right", marginRight: "30px", marginTop: "2px"}} target="_blank" rel="noreferrer" href={"https://github.com/casibase/casibase"}>
<GithubOutlined className="custom-link" style={{fontSize: "20px", color: "rgb(140,140,140)"}} />
</a>
</React.Fragment>
}
placement="right"
width={500}
mask={false}
onClose={() => {
this.setState({
isAiAssistantOpen: false,
});
}}
visible={this.state.isAiAssistantOpen}
>
<iframe id="iframeHelper" title={"iframeHelper"} src={"https://ai.casbin.com/?isRaw=1"} width="100%" height="100%" scrolling="no" frameBorder="no" />
</Drawer>
);
}
isDoorPages() {
return this.isEntryPages() || window.location.pathname.startsWith("/callback");
}
@ -696,6 +744,9 @@ class App extends Component {
{
this.renderFooter()
}
{
this.renderAiAssistant()
}
</Layout>
);
}

View File

@ -171,10 +171,27 @@ class CertEditPage extends React.Component {
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.cryptoAlgorithm} onChange={(value => {
this.updateCertField("cryptoAlgorithm", value);
if (value === "RS256") {
this.updateCertField("bitSize", 2048);
} else if (value === "HS256" || value === "ES256") {
this.updateCertField("bitSize", 256);
} else if (value === "ES384") {
this.updateCertField("bitSize", 384);
} else if (value === "ES521") {
this.updateCertField("bitSize", 521);
} else {
this.updateCertField("bitSize", 0);
}
this.updateCertField("certificate", "");
this.updateCertField("privateKey", "");
})}>
{
[
{id: "RS256", name: "RS256"},
{id: "RS256", name: "RS256 (RSA + SHA256)"},
{id: "HS256", name: "HS256 (HMAC + SHA256)"},
{id: "ES256", name: "ES256 (ECDSA using P-256 + SHA256)"},
{id: "ES384", name: "ES384 (ECDSA using P-384 + SHA256)"},
{id: "ES521", name: "ES521 (ECDSA using P-521 + SHA256)"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
@ -185,9 +202,15 @@ class CertEditPage extends React.Component {
{Setting.getLabel(i18next.t("cert:Bit size"), i18next.t("cert:Bit size - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.cert.bitSize} onChange={value => {
<Select virtual={false} style={{width: "100%"}} value={this.state.cert.bitSize} onChange={(value => {
this.updateCertField("bitSize", value);
}} />
this.updateCertField("certificate", "");
this.updateCertField("privateKey", "");
})}>
{
Setting.getCryptoAlgorithmOptions(this.state.cert.cryptoAlgorithm).map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
@ -205,14 +228,14 @@ class CertEditPage extends React.Component {
{Setting.getLabel(i18next.t("cert:Certificate"), i18next.t("cert:Certificate - Tooltip"))} :
</Col>
<Col span={editorWidth} >
<Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
<Button style={{marginRight: "10px", marginBottom: "10px"}} disabled={this.state.cert.certificate === ""} onClick={() => {
copy(this.state.cert.certificate);
Setting.showMessage("success", i18next.t("cert:Certificate copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy certificate")}
</Button>
<Button type="primary" onClick={() => {
<Button type="primary" disabled={this.state.cert.certificate === ""} onClick={() => {
const blob = new Blob([this.state.cert.certificate], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.pem");
}}
@ -228,14 +251,14 @@ class CertEditPage extends React.Component {
{Setting.getLabel(i18next.t("cert:Private key"), i18next.t("cert:Private key - Tooltip"))} :
</Col>
<Col span={editorWidth} >
<Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
<Button style={{marginRight: "10px", marginBottom: "10px"}} disabled={this.state.cert.privateKey === ""} onClick={() => {
copy(this.state.cert.privateKey);
Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy private key")}
</Button>
<Button type="primary" onClick={() => {
<Button type="primary" disabled={this.state.cert.privateKey === ""} onClick={() => {
const blob = new Blob([this.state.cert.privateKey], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.key");
}}
@ -265,6 +288,7 @@ class CertEditPage extends React.Component {
this.props.history.push("/certs");
} else {
this.props.history.push(`/certs/${this.state.cert.owner}/${this.state.cert.name}`);
this.getCert();
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);

View File

@ -239,7 +239,7 @@ class CertListPage extends BaseListPage {
value = params.type;
}
this.setState({loading: true});
(Setting.isDefaultOrganizationSelected(this.props.account) ? CertBackend.getGlobleCerts(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
(Setting.isDefaultOrganizationSelected(this.props.account) ? CertBackend.getGlobalCerts(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
: CertBackend.getCerts(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
.then((res) => {
this.setState({

View File

@ -323,6 +323,16 @@ class OrganizationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Master verification code"), i18next.t("general:Master verification code - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.organization.masterVerificationCode} onChange={e => {
this.updateOrganizationField("masterVerificationCode", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("organization:Init score"), i18next.t("organization:Init score - Tooltip"))} :

View File

@ -101,7 +101,7 @@ class PaymentResultPage extends React.Component {
payment: payment,
});
if (payment.state === "Created") {
if (["PayPal", "Stripe", "Alipay"].includes(payment.type)) {
if (["PayPal", "Stripe", "Alipay", "WeChat Pay"].includes(payment.type)) {
this.setState({
timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);

View File

@ -31,6 +31,7 @@ class ProductBuyPage extends React.Component {
pricingName: props?.pricingName ?? props?.match?.params?.pricingName ?? null,
planName: params.get("plan"),
userName: params.get("user"),
paymentEnv: "",
product: null,
pricing: props?.pricing ?? null,
plan: null,
@ -38,8 +39,21 @@ class ProductBuyPage extends React.Component {
};
}
getPaymentEnv() {
let env = "";
const ua = navigator.userAgent.toLocaleLowerCase();
// Only support Wechat Pay in Wechat Browser for mobile devices
if (ua.indexOf("micromessenger") !== -1 && ua.indexOf("mobile") !== -1) {
env = "WechatBrowser";
}
this.setState({
paymentEnv: env,
});
}
UNSAFE_componentWillMount() {
this.getProduct();
this.getPaymentEnv();
}
setStateAsync(state) {
@ -127,23 +141,74 @@ class ProductBuyPage extends React.Component {
return `${this.getCurrencySymbol(product)}${product?.price} (${this.getCurrencyText(product)})`;
}
// Call Weechat Pay via jsapi
onBridgeReady(attachInfo) {
const {WeixinJSBridge} = window;
// Setting.showMessage("success", "attachInfo is " + JSON.stringify(attachInfo));
this.setState({
isPlacingOrder: false,
});
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": attachInfo.appId,
"timeStamp": attachInfo.timeStamp,
"nonceStr": attachInfo.nonceStr,
"package": attachInfo.package,
"signType": attachInfo.signType,
"paySign": attachInfo.paySign,
},
function(res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
Setting.goToLink(attachInfo.payment.successUrl);
return ;
} else {
if (res.err_msg === "get_brand_wcpay_request:cancel") {
Setting.showMessage("error", i18next.t("product:Payment cancelled"));
} else {
Setting.showMessage("error", i18next.t("product:Payment failed"));
}
}
}
);
}
// In Wechat browser, call this function to pay via jsapi
callWechatPay(attachInfo) {
const {WeixinJSBridge} = window;
if (typeof WeixinJSBridge === "undefined") {
if (document.addEventListener) {
document.addEventListener("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo), false);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
document.attachEvent("onWeixinJSBridgeReady", () => this.onBridgeReady(attachInfo));
}
} else {
this.onBridgeReady(attachInfo);
}
}
buyProduct(product, provider) {
this.setState({
isPlacingOrder: true,
});
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "")
ProductBackend.buyProduct(product.owner, product.name, provider.name, this.state.pricingName ?? "", this.state.planName ?? "", this.state.userName ?? "", this.state.paymentEnv)
.then((res) => {
if (res.status === "ok") {
const payment = res.data;
const attachInfo = res.data2;
let payUrl = payment.payUrl;
if (provider.type === "WeChat Pay") {
if (this.state.paymentEnv === "WechatBrowser") {
attachInfo.payment = payment;
this.callWechatPay(attachInfo);
return ;
}
payUrl = `/qrcode/${payment.owner}/${payment.name}?providerName=${provider.name}&payUrl=${encodeURI(payment.payUrl)}&successUrl=${encodeURI(payment.successUrl)}`;
}
Setting.goToLink(payUrl);
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.setState({
isPlacingOrder: false,
});
@ -218,7 +283,7 @@ class ProductBuyPage extends React.Component {
return (
<div className="login-content">
<Spin spinning={this.state.isPlacingOrder} size="large" tip={i18next.t("product:Placing order...")} style={{paddingTop: "10%"}} >
<Descriptions title={<span style={{fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 20} : {fontSize: 28}}>{i18next.t("product:Buy Product")}</span>} bordered>
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
<span style={{fontSize: 25}}>
{Setting.getLanguageText(product?.displayName)}

View File

@ -152,6 +152,12 @@ class ProviderEditPage extends React.Component {
}
getClientIdLabel(provider) {
switch (provider.category) {
case "OAuth":
if (provider.type === "Apple") {
return Setting.getLabel(i18next.t("provider:Service ID identifier"), i18next.t("provider:Service ID identifier - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
case "Email":
return Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"));
case "SMS":
@ -185,6 +191,12 @@ class ProviderEditPage extends React.Component {
getClientSecretLabel(provider) {
switch (provider.category) {
case "OAuth":
if (provider.type === "Apple") {
return Setting.getLabel(i18next.t("provider:Team ID"), i18next.t("provider:Team ID - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
case "Email":
if (provider.type === "Azure ACS") {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
@ -226,6 +238,12 @@ class ProviderEditPage extends React.Component {
getClientId2Label(provider) {
switch (provider.category) {
case "OAuth":
if (provider.type === "Apple") {
return Setting.getLabel(i18next.t("provider:Key ID"), i18next.t("provider:Key ID - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
}
case "Email":
return Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"));
default:
@ -241,6 +259,12 @@ class ProviderEditPage extends React.Component {
getClientSecret2Label(provider) {
switch (provider.category) {
case "OAuth":
if (provider.type === "Apple") {
return Setting.getLabel(i18next.t("provider:Key text"), i18next.t("provider:Key text - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"));
}
case "Email":
return Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"));
default:
@ -675,7 +699,7 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.category !== "Email" && this.state.provider.type !== "WeChat" && 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" ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>

View File

@ -1069,6 +1069,38 @@ export function getProviderTypeOptions(category) {
}
}
export function getCryptoAlgorithmOptions(cryptoAlgorithm) {
if (cryptoAlgorithm === "RS256") {
return (
[
{id: 1024, name: "1024"},
{id: 2048, name: "2048"},
{id: 4096, name: "4096"},
]
);
} else if (cryptoAlgorithm === "HS256" || cryptoAlgorithm === "ES256") {
return (
[
{id: 256, name: "256"},
]
);
} else if (cryptoAlgorithm === "ES384") {
return (
[
{id: 384, name: "384"},
]
);
} else if (cryptoAlgorithm === "ES521") {
return (
[
{id: 521, name: "521"},
]
);
} else {
return [];
}
}
export function renderLogo(application) {
if (application === null) {
return null;

View File

@ -766,7 +766,11 @@ class LoginPage extends React.Component {
const rawId = assertion.rawId;
const sig = assertion.response.signature;
const userHandle = assertion.response.userHandle;
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}`, {
let finishUrl = `${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}`;
if (values["type"] === "code") {
finishUrl = `${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}&clientId=${oAuthParams.clientId}&scope=${oAuthParams.scope}&redirectUri=${oAuthParams.redirectUri}&nonce=${oAuthParams.nonce}&state=${oAuthParams.state}&codeChallenge=${oAuthParams.codeChallenge}&challengeMethod=${oAuthParams.challengeMethod}`;
}
return fetch(finishUrl, {
method: "POST",
credentials: "include",
body: JSON.stringify({

View File

@ -262,7 +262,7 @@ class PromptPage extends React.Component {
initSteps(user, application) {
const steps = [];
if (!Setting.isPromptAnswered(user, application) && this.state.promptType === "provider") {
if (Setting.hasPromptPage(application)) {
steps.push({
content: this.renderPromptProvider(application),
name: "provider",

View File

@ -382,7 +382,7 @@ export function getAuthUrl(application, provider, method) {
let redirectUri = `${window.location.origin}/callback`;
const scope = authInfo[provider.type].scope;
const isShortState = provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger");
const isShortState = (provider.type === "WeChat" && navigator.userAgent.includes("MicroMessenger")) || (provider.type === "Twitter");
const state = Util.getStateFromQueryParams(application.name, provider.name, method, isShortState);
const codeChallenge = "P3S-a7dr8bgM4bF6vOyiKkKETDl16rcAzao9F8UIL1Y"; // SHA256(Base64-URL-encode("casdoor-verifier"))

View File

@ -24,8 +24,8 @@ export function getCerts(owner, page = "", pageSize = "", field = "", value = ""
}).then(res => res.json());
}
export function getGlobleCerts(page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-globle-certs?&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
export function getGlobalCerts(page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-global-certs?&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {

View File

@ -70,8 +70,8 @@ export function deleteProduct(product) {
}).then(res => res.json());
}
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "") {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}`, {
export function buyProduct(owner, name, providerName, pricingName = "", planName = "", userName = "", paymentEnv = "") {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerName}&pricingName=${pricingName}&planName=${planName}&userName=${userName}&paymentEnv=${paymentEnv}`, {
method: "POST",
credentials: "include",
headers: {

View File

@ -34,7 +34,7 @@ class OpenTour extends React.Component {
render() {
return (
this.canTour() ?
<Tooltip title="Click to enable the help wizard.">
<Tooltip title="Click to open tour">
<div className="select-box" style={{display: Setting.isMobile() ? "none" : null, ...this.props.style}} onClick={() => TourConfig.setIsTourVisible(true)} >
<QuestionCircleOutlined style={{fontSize: "24px", color: "#4d4d4d"}} />
</div>

View File

@ -45,3 +45,12 @@ code {
.ant-list-sm .ant-list-item {
padding: 2px !important;
}
.ant-drawer-body {
padding: 0 !important;
overflow: hidden !important;
}
.custom-link:hover {
color: rgb(64 64 64) !important;
}

View File

@ -1,13 +1,13 @@
{
"account": {
"Logout": "Abmelden",
"Logout": "Abmeldung",
"My Account": "Mein Konto",
"Sign Up": "Anmelden"
},
"adapter": {
"Duplicated policy rules": "Doppelte Richtlinienregeln",
"Edit Adapter": "Adapter bearbeiten",
"Failed to sync policies": "Fehler beim Synchronisieren der Richtlinien",
"Edit Adapter": "Bearbeiten Sie den Adapter",
"Failed to sync policies": "Fehler beim Synchronisieren von Richtlinien",
"New Adapter": "Neuer Adapter",
"Policies": "Richtlinien",
"Policies - Tooltip": "Casbin Richtlinienregeln",
@ -24,13 +24,13 @@
"Center": "Zentrum",
"Copy SAML metadata URL": "SAML-Metadaten-URL kopieren",
"Copy prompt page URL": "URL der Prompt-Seite kopieren",
"Copy signin page URL": "URL der Anmeldeseite kopieren",
"Copy signin page URL": "Kopieren Sie die URL der Anmeldeseite",
"Copy signup page URL": "URL der Anmeldeseite kopieren",
"Dynamic": "Dynamic",
"Edit Application": "Anwendung bearbeiten",
"Edit Application": "Bearbeitungsanwendung",
"Enable Email linking": "E-Mail-Verknüpfung aktivieren",
"Enable Email linking - Tooltip": "Bei der Verwendung von Drittanbietern zur Anmeldung wird, wenn es in der Organisation einen Benutzer mit der gleichen E-Mail gibt, automatisch die Drittanbieter-Anmelde-Methode mit diesem Benutzer verbunden",
"Enable SAML compression": "SAML-Komprimierung aktivieren",
"Enable SAML compression": "Aktivieren Sie SAML-Komprimierung",
"Enable SAML compression - Tooltip": "Ob SAML-Antwortnachrichten komprimiert werden sollen, wenn Casdoor als SAML-IdP verwendet wird",
"Enable WebAuthn signin": "Anmeldung mit WebAuthn aktivieren",
"Enable WebAuthn signin - Tooltip": "Ob Benutzern erlaubt werden soll, sich mit WebAuthn anzumelden",
@ -46,7 +46,7 @@
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"First, last": "First, last",
"Follow organization theme": "Folge dem Theme der Organisation",
"Form CSS": "Form CSS",
"Form CSS": "Formular CSS",
"Form CSS - Edit": "Form CSS - Bearbeiten",
"Form CSS - Tooltip": "CSS-Styling der Anmelde-, Registrierungs- und Passwort-vergessen-Seite (z. B. Hinzufügen von Rahmen und Schatten)",
"Form CSS Mobile": "Form CSS Mobile",
@ -151,7 +151,7 @@
"Change Password": "Passwort ändern",
"Choose email or phone": "Wählen Sie E-Mail oder Telefon",
"Next Step": "Nächster Schritt",
"Please input your username!": "Bitte geben Sie Ihren Benutzernamen ein!",
"Please input your username!": "Bitte gib deinen Benutzernamen ein!",
"Reset": "Zurücksetzen",
"Retrieve password": "Passwort abrufen",
"Unknown forget type": "Unbekannter Vergesslichkeitstyp",
@ -170,7 +170,7 @@
"Adapters": "Adapter",
"Add": "Hinzufügen",
"Admin": "Admin",
"Affiliation URL": "Affiliation-URL",
"Affiliation URL": "Zugehörigkeits-URL",
"Affiliation URL - Tooltip": "Die Homepage-URL für die Zugehörigkeit",
"Application": "Applikation",
"Application - Tooltip": "Application - Tooltip",
@ -285,7 +285,7 @@
"Preview - Tooltip": "Vorschau der konfigurierten Effekte",
"Pricings": "Preise",
"Products": "Produkte",
"Provider": "Provider",
"Provider": "Anbieter",
"Provider - Tooltip": "Zahlungsprovider, die konfiguriert werden müssen, inkl. PayPal, Alipay, WeChat Pay usw.",
"Providers": "Provider",
"Providers - Tooltip": "Provider, die konfiguriert werden müssen, einschließlich Drittanbieter-Logins, Objektspeicherung, Verifizierungscode usw.",
@ -299,7 +299,7 @@
"Save": "Speichern",
"Save & Exit": "Speichern und verlassen",
"Session ID": "Session-ID",
"Sessions": "Sessions",
"Sessions": "Sitzungen",
"Shortcuts": "Shortcuts",
"Signin URL": "Anmeldungs-URL",
"Signin URL - Tooltip": "Benutzerdefinierte URL für die Anmeldeseite. Wenn sie nicht festgelegt ist, wird die standardmäßige Casdoor-Anmeldeseite verwendet. Wenn sie festgelegt ist, leiten die Anmeldelinks auf verschiedenen Casdoor-Seiten zu dieser URL um",
@ -375,7 +375,7 @@
"Auto Sync - Tooltip": "Auto-Sync-Konfiguration, deaktiviert um 0 Uhr",
"Base DN": "Basis-DN",
"Base DN - Tooltip": "Basis-DN während der LDAP-Suche",
"CN": "CN",
"CN": "KN",
"Edit LDAP": "LDAP bearbeiten",
"Enable SSL": "Aktivieren Sie SSL",
"Enable SSL - Tooltip": "Ob SSL aktiviert werden soll",
@ -385,7 +385,7 @@
"Last Sync": "Letzte Synchronisation",
"Search Filter": "Search Filter",
"Search Filter - Tooltip": "Search Filter - Tooltip",
"Server": "Server",
"Server": "Serverh)",
"Server host": "Server Host",
"Server host - Tooltip": "LDAP-Server-Adresse",
"Server name": "Servername",
@ -557,7 +557,7 @@
"Approver - Tooltip": "Die Person, die die Genehmigung genehmigt hat",
"Deny": "Ablehnen",
"Edit Permission": "Recht bearbeiten",
"Effect": "Effekt",
"Effect": "Wirkung",
"Effect - Tooltip": "Erlauben oder ablehnen",
"New Permission": "Neue Genehmigung",
"Pending": "Ausstehend",
@ -649,7 +649,7 @@
"Auth URL - Tooltip": "Auth-URL",
"Base URL": "Base URL",
"Base URL - Tooltip": "Base URL - Tooltip",
"Bucket": "Bucket",
"Bucket": "Eimer",
"Bucket - Tooltip": "Name des Buckets",
"Can not parse metadata": "Kann Metadaten nicht durchsuchen / auswerten",
"Can signin": "Kann sich einloggen",
@ -676,7 +676,7 @@
"DB Test - Tooltip": "DB Test - Tooltip",
"Disable SSL": "SSL deaktivieren",
"Disable SSL - Tooltip": "Ob die Deaktivierung des SSL-Protokolls bei der Kommunikation mit dem STMP-Server erfolgen soll",
"Domain": "Domain",
"Domain": "Domäne",
"Domain - Tooltip": "Benutzerdefinierte Domain für Objektspeicher",
"Edit Provider": "Provider bearbeiten",
"Email content": "Email-Inhalt",
@ -685,8 +685,8 @@
"Email title - Tooltip": "Betreff der E-Mail",
"Enable QR code": "QR-Code aktivieren",
"Enable QR code - Tooltip": "Ob das Scannen von QR-Codes zum Einloggen aktiviert werden soll",
"Endpoint": "Endpoint",
"Endpoint (Intranet)": "Endpoint (Intranet)",
"Endpoint": "Endpunkt",
"Endpoint (Intranet)": "Endpunkt (Intranet)",
"Endpoint - Tooltip": "Endpoint - Tooltip",
"From address": "From address",
"From address - Tooltip": "From address - Tooltip",
@ -713,14 +713,14 @@
"Path prefix": "Pfadpräfix",
"Path prefix - Tooltip": "Bucket-Pfad-Präfix für Objektspeicher",
"Please use WeChat and scan the QR code to sign in": "Bitte verwenden Sie WeChat und scanne den QR-Code ein, um dich anzumelden",
"Port": "Port",
"Port": "Hafen",
"Port - Tooltip": "Stellen Sie sicher, dass der Port offen ist",
"Private Key": "Private Key",
"Private Key - Tooltip": "Private Key - Tooltip",
"Project Id": "Project Id",
"Project Id - Tooltip": "Project Id - Tooltip",
"Prompted": "ausgelöst",
"Provider URL": "Provider-URL",
"Provider URL": "Anbieter-URL",
"Provider URL - Tooltip": "URL zur Konfiguration des Dienstanbieters, dieses Feld dient nur als Referenz und wird in Casdoor nicht verwendet",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
@ -736,13 +736,13 @@
"SMS Test - Tooltip": "Telefonnummer für den Versand von Test-SMS",
"SMS account": "SMS-Konto",
"SMS account - Tooltip": "SMS-Konto",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP ACS URL": "SP-ACS-URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP-Entitäts-ID",
"Scene": "Scene",
"Scene": "Szene",
"Scene - Tooltip": "Szene",
"Scope": "Scope",
"Scope - Tooltip": "Scope",
"Scope": "Umfang",
"Scope - Tooltip": "Umfang",
"Secret access key": "Secret-Access-Key",
"Secret access key - Tooltip": "Geheimer Zugriffsschlüssel",
"Secret key": "Secret-Key",
@ -756,7 +756,7 @@
"Sender number - Tooltip": "Sender number - Tooltip",
"Sign Name": "Signatur Namen",
"Sign Name - Tooltip": "Name der Signatur, die verwendet werden soll",
"Sign request": "Signaturanfrage",
"Sign request": "Unterschriftsanforderung",
"Sign request - Tooltip": "Ob die Anfrage eine Signatur erfordert",
"Signin HTML": "Anmeldungs-HTML",
"Signin HTML - Edit": "Anmeldungs-HTML - Bearbeiten",
@ -786,14 +786,14 @@
"UserInfo URL - Tooltip": "UserInfo-URL",
"Wallets": "Wallets",
"Wallets - Tooltip": "Wallets - Tooltip",
"admin (Shared)": "admin (Shared)"
"admin (Shared)": "admin (Gemeinsam)"
},
"resource": {
"Copy Link": "Link kopieren",
"Copy Link": "Kopiere den Link",
"File name": "Dateiname",
"File size": "Dateigröße",
"Format": "Format",
"Parent": "Parent",
"Parent": "Elternteil",
"Upload a file...": "Hochladen einer Datei..."
},
"role": {
@ -810,11 +810,11 @@
"Accept": "Akzeptieren",
"Agreement": "Vereinbarung",
"Confirm": "Bestätigen",
"Decline": "Ablehnen",
"Decline": "Abnahme",
"Have account?": "Haben Sie ein Konto?",
"Please accept the agreement!": "Bitte akzeptieren Sie die Vereinbarung!",
"Please click the below button to sign in": "Bitte klicken Sie auf den untenstehenden Button, um sich anzumelden",
"Please confirm your password!": "Bitte bestätigen Sie Ihr Passwort!",
"Please confirm your password!": "Bitte bestätige dein Passwort!",
"Please input the correct ID card number!": "Bitte geben Sie die korrekte Ausweisnummer ein!",
"Please input your Email!": "Bitte geben Sie Ihre E-Mail-Adresse ein!",
"Please input your ID card number!": "Bitte geben Sie Ihre Personalausweisnummer ein!",
@ -886,7 +886,7 @@
"About Casdoor": "Über Casdoor",
"An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS": "Eine Identitäts- und Zugriffsverwaltung (IAM) / Single-Sign-On (SSO) Plattform mit Web-UI, die OAuth 2.0, OIDC, SAML und CAS unterstützt",
"CPU Usage": "CPU-Auslastung",
"Community": "Community",
"Community": "Gemeinschaft",
"Count": "Zählen",
"Failed to get CPU usage": "Konnte CPU-Auslastung nicht abrufen",
"Failed to get memory usage": "Fehler beim Abrufen der Speichernutzung",
@ -899,22 +899,22 @@
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Blossom": "Blüte",
"Border radius": "Border Radius",
"Compact": "Compact",
"Compact": "Kompakt",
"Customize theme": "Anpassen des Themes",
"Dark": "Dunkel",
"Default": "Standardeinstellungen",
"Document": "Dokument",
"Is compact": "Ist kompakt",
"Primary color": "Primärfarbe",
"Theme": "Theme",
"Theme": "Thema",
"Theme - Tooltip": "Stiltheme der Anwendung"
},
"token": {
"Access token": "Access-Token",
"Authorization code": "Authorisierungscode",
"Edit Token": "Token bearbeiten",
"Edit Token": "Edit-Token bearbeiten",
"Expires in": "läuft ab in",
"New Token": "Neuer Token",
"Token type": "Token-Typ"
@ -965,7 +965,7 @@
"Is online": "Is online",
"Karma": "Karma",
"Karma - Tooltip": "Karma - Tooltip",
"Keys": "Keys",
"Keys": "Schlüssel",
"Language": "Language",
"Language - Tooltip": "Language - Tooltip",
"Link": "Link",
@ -995,7 +995,7 @@
"Set Password": "Passwort festlegen",
"Set new profile picture": "Neues Profilbild festlegen",
"Set password...": "Passwort festlegen...",
"Tag": "Tag",
"Tag": "Markierung",
"Tag - Tooltip": "Tags des Benutzers",
"The password must contain at least one special character": "The password must contain at least one special character",
"The password must contain at least one uppercase letter, one lowercase letter and one digit": "The password must contain at least one uppercase letter, one lowercase letter and one digit",
@ -1014,7 +1014,7 @@
"Values": "Werte",
"Verification code sent": "Bestätigungscode gesendet",
"WebAuthn credentials": "WebAuthn-Anmeldeinformationen",
"input password": "Passwort eingeben"
"input password": "Eingabe des Passworts"
},
"webhook": {
"Content type": "Content-Type",

View File

@ -184,7 +184,7 @@
"Back Home": "Regreso a casa",
"Business & Payments": "Business & Payments",
"Cancel": "Cancelar",
"Captcha": "Captcha",
"Captcha": "Captcha (no se traduce)",
"Cert": "ificado",
"Cert - Tooltip": "El certificado de clave pública que necesita ser verificado por el SDK del cliente correspondiente a esta aplicación",
"Certs": "Certificaciones",
@ -221,7 +221,7 @@
"Failed to remove": "Failed to remove",
"Failed to save": "No se pudo guardar",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon": "Favicon (ícono de favoritos)",
"Favicon - Tooltip": "URL del icono Favicon utilizado en todas las páginas de Casdoor de la organización",
"First name": "Nombre de pila",
"Forget URL": "Olvide la URL",
@ -238,7 +238,7 @@
"Identity": "Identity",
"Is enabled": "Está habilitado",
"Is enabled - Tooltip": "Establecer si se puede usar",
"LDAPs": "LDAPs",
"LDAPs": "LDAPs (Secure LDAP)",
"LDAPs - Tooltip": "Servidores LDAP",
"Languages": "Idiomas",
"Languages - Tooltip": "Idiomas disponibles",
@ -424,7 +424,7 @@
"The input is not valid Email or phone number!": "¡La entrada no es un correo electrónico o número de teléfono válido!",
"To access": "para acceder",
"Verification code": "Código de verificación",
"WebAuthn": "WebAuthn",
"WebAuthn": "WebAuthn (Autenticación Web)",
"sign up now": "Regístrate ahora",
"username, Email or phone": "Nombre de usuario, correo electrónico o teléfono"
},
@ -595,7 +595,7 @@
"Alipay": "Alipay",
"Buy": "Comprar",
"Buy Product": "Comprar producto",
"CNY": "CNY",
"CNY": "Año Nuevo Chino (ANC)",
"Detail": "Detalle",
"Detail - Tooltip": "Detalle del producto",
"Dummy": "Dummy",
@ -617,7 +617,7 @@
"Quantity - Tooltip": "Cantidad de producto",
"Return URL": "URL de retorno",
"Return URL - Tooltip": "URL para regresar después de una compra exitosa",
"SKU": "SKU",
"SKU": "SKU (referencia de unidad de almacenamiento)",
"Sold": "Vendido",
"Sold - Tooltip": "Cantidad vendida",
"Stripe": "Stripe",

File diff suppressed because it is too large Load Diff

View File

@ -418,7 +418,7 @@
"Redirecting, please wait.": "Mengalihkan, harap tunggu.",
"Sign In": "Masuk",
"Sign in with WebAuthn": "Masuk dengan WebAuthn",
"Sign in with {type}": "Masuk dengan {jenis}",
"Sign in with {type}": "Masuk dengan {type}",
"Signing in...": "Masuk...",
"Successfully logged in with WebAuthn credentials": "Berhasil masuk dengan kredensial WebAuthn",
"The input is not valid Email or phone number!": "Input yang Anda masukkan tidak valid, tidak sesuai dengan Email atau nomor telepon!",
@ -859,7 +859,7 @@
"Column name": "Nama kolom",
"Column type": "Tipe kolom",
"Connect successfully": "Connect successfully",
"Database": "Database",
"Database": "Database (bahasa Indonesia)",
"Database - Tooltip": "Nama basis data asli",
"Database type": "Tipe Basis Data",
"Database type - Tooltip": "Jenis database, mendukung semua database yang didukung oleh XORM, seperti MySQL, PostgreSQL, SQL Server, Oracle, SQLite, dan lain-lain.",

View File

@ -1,23 +1,23 @@
{
"account": {
"Logout": "Logout",
"My Account": "My Account",
"Sign Up": "Sign Up"
"Logout": "Esci",
"My Account": "Profilo",
"Sign Up": "Registrati"
},
"adapter": {
"Duplicated policy rules": "Duplicated policy rules",
"Edit Adapter": "Edit Adapter",
"Failed to sync policies": "Failed to sync policies",
"New Adapter": "New Adapter",
"Policies": "Policies",
"Policies - Tooltip": "Casbin policy rules",
"Duplicated policy rules": "Policy Duplicate",
"Edit Adapter": "Modifica Adattatore",
"Failed to sync policies": "Impossibile sincronizzare le policy",
"New Adapter": "Nuovo Adattatore",
"Policies": "Policy",
"Policies - Tooltip": "Regole Casbin",
"Rule type": "Rule type",
"Sync policies successfully": "Sync policies successfully"
"Sync policies successfully": "Sincronizzazione delle policy completata correttamente"
},
"application": {
"Always": "Always",
"Auto signin": "Auto signin",
"Auto signin - Tooltip": "When a logged-in session exists in Casdoor, it is automatically used for application-side login",
"Always": "Sempre",
"Auto signin": "Accesso automatico",
"Auto signin - Tooltip": "Quando una sessione esiste in Casdoor, viene utilizzata automaticamente per il login lato applicazione",
"Background URL": "Background URL",
"Background URL - Tooltip": "URL of the background image used in the login page",
"Binding providers": "Binding providers",

View File

@ -238,7 +238,7 @@
"Identity": "Identity",
"Is enabled": "可能になっています",
"Is enabled - Tooltip": "使用可能かどうかを設定してください",
"LDAPs": "LDAPs",
"LDAPs": "LDAP",
"LDAPs - Tooltip": "LDAPサーバー",
"Languages": "言語",
"Languages - Tooltip": "利用可能な言語",
@ -367,7 +367,7 @@
"Total users": "Total users"
},
"ldap": {
"Admin": "Admin",
"Admin": "管理者",
"Admin - Tooltip": "LDAPサーバー管理者のCNまたはID",
"Admin Password": "管理者パスワード",
"Admin Password - Tooltip": "LDAPサーバーの管理者パスワード",
@ -737,7 +737,7 @@
"SMS account": "SMSアカウント",
"SMS account - Tooltip": "SMSアカウント",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - ツールチップ",
"SP Entity ID": "SPエンティティID",
"Scene": "シーン",
"Scene - Tooltip": "シーン",

View File

@ -737,7 +737,7 @@
"SMS account": "SMS 계정",
"SMS account - Tooltip": "SMS 계정",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP 개체 ID",
"Scene": "장면",
"Scene - Tooltip": "장면",

View File

@ -238,7 +238,7 @@
"Identity": "Identity",
"Is enabled": "Включен",
"Is enabled - Tooltip": "Установить, может ли использоваться",
"LDAPs": "LDAPs",
"LDAPs": "LDAPы",
"LDAPs - Tooltip": "LDAP серверы",
"Languages": "Языки",
"Languages - Tooltip": "Доступные языки",
@ -344,7 +344,7 @@
"User type - Tooltip": "Теги, к которым принадлежит пользователь, по умолчанию \"обычный пользователь\"",
"Users": "Пользователи",
"Users under all organizations": "Пользователи всех организаций",
"Webhooks": "Webhooks",
"Webhooks": "Вебхуки",
"You can only select one physical group": "You can only select one physical group",
"empty": "пустые",
"remove": "remove",
@ -367,7 +367,7 @@
"Total users": "Total users"
},
"ldap": {
"Admin": "Admin",
"Admin": "Админ",
"Admin - Tooltip": "CN или ID администратора сервера LDAP",
"Admin Password": "Пароль администратора",
"Admin Password - Tooltip": "Пароль администратора сервера LDAP",
@ -375,7 +375,7 @@
"Auto Sync - Tooltip": "Автоматическая синхронизация настроек отключена при значении 0",
"Base DN": "Базовый DN",
"Base DN - Tooltip": "Базовый DN во время поиска LDAP",
"CN": "CN",
"CN": "КНР",
"Edit LDAP": "Изменить LDAP",
"Enable SSL": "Включить SSL",
"Enable SSL - Tooltip": "Перевод: Следует ли включать SSL",
@ -694,7 +694,7 @@
"From name - Tooltip": "From name - Tooltip",
"Host": "Хост",
"Host - Tooltip": "Имя хоста",
"IdP": "IdP",
"IdP": "ИдП",
"IdP certificate": "Сертификат IdP",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
@ -737,7 +737,7 @@
"SMS account": "СМС-аккаунт",
"SMS account - Tooltip": "СМС-аккаунт",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL - Подсказка",
"SP Entity ID": "Идентификатор сущности SP",
"Scene": "Сцена",
"Scene - Tooltip": "Сцена",

View File

@ -27,7 +27,7 @@
"Copy signin page URL": "Sao chép URL trang đăng nhập",
"Copy signup page URL": "Sao chép URL trang đăng ký",
"Dynamic": "Dynamic",
"Edit Application": "Chỉnh sửa ứng dụng",
"Edit Application": "Sửa ứng dụng",
"Enable Email linking": "Cho phép liên kết Email",
"Enable Email linking - Tooltip": "Khi sử dụng nhà cung cấp bên thứ ba để đăng nhập, nếu có người dùng trong tổ chức có cùng địa chỉ Email, phương pháp đăng nhập bên thứ ba sẽ tự động được liên kết với người dùng đó",
"Enable SAML compression": "Cho phép nén SAML",
@ -44,10 +44,10 @@
"Enable signup - Tooltip": "Có cho phép người dùng đăng ký tài khoản mới không?",
"Failed to sign in": "Không đăng nhập được",
"File uploaded successfully": "Tệp được tải lên thành công",
"First, last": "First, last",
"Follow organization theme": "Theo chủ đề tổ chức",
"First, last": "Tên, Họ",
"Follow organization theme": "Theo giao diện tổ chức",
"Form CSS": "Mẫu CSS",
"Form CSS - Edit": "Biểu mẫu CSS - Chỉnh sửa",
"Form CSS - Edit": "Biểu mẫu CSS - Sửa",
"Form CSS - Tooltip": "Phong cách CSS của các biểu mẫu đăng ký, đăng nhập và quên mật khẩu (ví dụ: thêm đường viền và bóng)",
"Form CSS Mobile": "Form CSS Mobile",
"Form CSS Mobile - Edit": "Form CSS Mobile - Edit",
@ -56,7 +56,7 @@
"Form position - Tooltip": "Vị trí của các biểu mẫu đăng ký, đăng nhập và quên mật khẩu",
"Grant types": "Loại hỗ trợ",
"Grant types - Tooltip": "Chọn loại hỗ trợ được cho phép trong giao thức OAuth",
"Incremental": "Incremental",
"Incremental": "Tăng",
"Input": "Input",
"Invitation code": "Invitation code",
"Invitation code - Tooltip": "Invitation code - Tooltip",
@ -65,22 +65,22 @@
"Logged in successfully": "Đăng nhập thành công",
"Logged out successfully": "Đã đăng xuất thành công",
"New Application": "Ứng dụng mới",
"No verification": "No verification",
"Normal": "Normal",
"Only signup": "Only signup",
"No verification": "Không xác minh",
"Normal": "Bình thường",
"Only signup": "Chỉ đăng ký",
"Org choice mode": "Org choice mode",
"Org choice mode - Tooltip": "Org choice mode - Tooltip",
"Please input your application!": "Vui lòng nhập đơn của bạn!",
"Please input your organization!": "Vui lòng nhập tên tổ chức của bạn!",
"Please input your application!": "Vui lòng nhập ứng dụng của bạn!",
"Please input your organization!": "Vui lòng nhập tổ chức của bạn!",
"Please select a HTML file": "Vui lòng chọn tệp HTML",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Đã sao chép đường dẫn trang một cách thành công, hãy dán nó vào cửa sổ ẩn danh hoặc trình duyệt khác",
"Random": "Random",
"Real name": "Real name",
"Redirect URL": "Chuyển hướng đường dẫn URL",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Đã sao chép đường dẫn thành công, hãy dán nó vào cửa sổ ẩn danh hoặc trình duyệt khác",
"Random": "Ngẫu nhiên",
"Real name": "Tên thật",
"Redirect URL": "Chuyển hướng URL",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Điều hướng URL (URL khung POST Dịch vụ Tiêu thụ Khẳng định)",
"Redirect URLs": "Chuyển hướng URL",
"Redirect URLs - Tooltip": "Danh sách URL chuyển hướng được phép, hỗ trợ khớp biểu thức chính quy; các URL không có trong danh sách sẽ không được chuyển hướng",
"Refresh token expire": "Refresh token hết hạn",
"Refresh token expire": "Làm mới mã thông báo hết hạn",
"Refresh token expire - Tooltip": "Thời gian hết hạn của mã thông báo làm mới",
"Right": "Đúng",
"Rule": "Quy tắc",
@ -93,8 +93,8 @@
"Side panel HTML - Edit": "Bảng Panel Bên - Chỉnh sửa HTML",
"Side panel HTML - Tooltip": "Tùy chỉnh mã HTML cho bảng điều khiển bên của trang đăng nhập",
"Sign Up Error": "Lỗi đăng ký",
"Signin": "Signin",
"Signin (Default True)": "Signin (Default True)",
"Signin": "Đăng nhập",
"Signin (Default True)": "Đăng nhập (Mặc định đúng)",
"Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Đã sao chép thành công địa chỉ URL trang Đăng nhập vào clipboard, vui lòng dán nó vào cửa sổ ẩn danh hoặc trình duyệt khác",
"Signin session": "Phiên đăng nhập",
"Signup items": "Các mục đăng ký",
@ -116,7 +116,7 @@
"Certificate copied to clipboard successfully": "Chứng chỉ đã được sao chép vào bộ nhớ tạm thành công",
"Copy certificate": "Bản sao chứng chỉ",
"Copy private key": "Sao chép khóa riêng tư",
"Crypto algorithm": "Thuật toán mật mã",
"Crypto algorithm": "Thuật toán mã hóa",
"Crypto algorithm - Tooltip": "Thuật toán mã hóa được sử dụng bởi chứng chỉ",
"Download certificate": "Tải xuống chứng chỉ",
"Download private key": "Tải xuống khóa riêng tư",
@ -168,7 +168,7 @@
"Adapter": "Bộ chuyển đổi",
"Adapter - Tooltip": "Tên bảng của kho lưu trữ chính sách",
"Adapters": "Bộ chuyển đổi",
"Add": "Thêm vào",
"Add": "Tạo mới",
"Admin": "Admin",
"Affiliation URL": "Đường dẫn liên kết liên kết",
"Affiliation URL - Tooltip": "Đường dẫn URL trang chủ của liên kết",
@ -184,7 +184,7 @@
"Back Home": "Trở về nhà",
"Business & Payments": "Business & Payments",
"Cancel": "Hủy bỏ",
"Captcha": "Captcha",
"Captcha": "Mã xác minh",
"Cert": "Chứng chỉ",
"Cert - Tooltip": "Chứng chỉ khóa công khai cần được xác minh bởi SDK khách hàng tương ứng với ứng dụng này",
"Certs": "Chứng chỉ",
@ -207,7 +207,7 @@
"Display name": "Tên hiển thị",
"Display name - Tooltip": "Một tên dễ sử dụng, dễ đọc được hiển thị công khai trên giao diện người dùng",
"Down": "Xuống",
"Edit": "Chỉnh sửa",
"Edit": "Sửa",
"Email": "Email: Thư điện tử",
"Email - Tooltip": "Địa chỉ email hợp lệ",
"Enable": "Enable",
@ -219,12 +219,12 @@
"Failed to delete": "Không thể xoá",
"Failed to enable": "Failed to enable",
"Failed to remove": "Failed to remove",
"Failed to save": "Không thể lưu được",
"Failed to save": "Không thể lưu lại",
"Failed to verify": "Failed to verify",
"Favicon": "Favicon",
"Favicon - Tooltip": "URL biểu tượng Favicon được sử dụng trong tất cả các trang của tổ chức Casdoor",
"First name": "Tên đầu tiên",
"Forget URL": "Quên đường dẫn URL",
"First name": "Tên",
"Forget URL": "Quên URL",
"Forget URL - Tooltip": "Đường dẫn tùy chỉnh cho trang \"Quên mật khẩu\". Nếu không được thiết lập, trang \"Quên mật khẩu\" mặc định của Casdoor sẽ được sử dụng. Khi cài đặt, liên kết \"Quên mật khẩu\" trên trang đăng nhập sẽ chuyển hướng đến URL này",
"Found some texts still not translated? Please help us translate at": "Tìm thấy một số văn bản vẫn chưa được dịch? Vui lòng giúp chúng tôi dịch tại",
"Go to enable": "Go to enable",
@ -239,19 +239,19 @@
"Is enabled": "Đã được kích hoạt",
"Is enabled - Tooltip": "Đặt liệu nó có thể sử dụng hay không",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "Các máy chủ LDAP",
"LDAPs - Tooltip": "Máy chủ LDAP",
"Languages": "Ngôn ngữ",
"Languages - Tooltip": "Các ngôn ngữ hiện có",
"Languages - Tooltip": "Ngôn ngữ hiện có",
"Last name": "Họ",
"Later": "Later",
"Logging & Auditing": "Logging & Auditing",
"Logo": "Logo",
"Logo": "Biểu tượng",
"Logo - Tooltip": "Biểu tượng mà ứng dụng hiển thị ra ngoài thế giới",
"MFA items": "MFA items",
"MFA items - Tooltip": "MFA items - Tooltip",
"Master password": "Mật khẩu chính",
"Master password - Tooltip": "Có thể được sử dụng để đăng nhập vào tất cả các người dùng trong tổ chức này, giúp cho quản trị viên dễ dàng đăng nhập với tư cách người dùng này để giải quyết các vấn đề kỹ thuật",
"Menu": "Thực đơn",
"Menu": "Trình đơn",
"Method": "Phương pháp",
"Model": "Mô hình",
"Model - Tooltip": "Mô hình kiểm soát truy cập Casbin",
@ -259,7 +259,7 @@
"Name": "Tên",
"Name - Tooltip": "ID duy nhất dựa trên chuỗi",
"None": "None",
"OAuth providers": "Cung cấp OAuth",
"OAuth providers": "Nhà cung cấp OAuth",
"OK": "Được rồi",
"Organization": "Tổ chức",
"Organization - Tooltip": "Tương tự như các khái niệm như người thuê hoặc nhóm người dùng, mỗi người dùng và ứng dụng đều thuộc về một tổ chức",
@ -298,8 +298,8 @@
"Roles - Tooltip": "Các vai trò mà người dùng thuộc về",
"Save": "Lưu",
"Save & Exit": "Lưu và Thoát",
"Session ID": " phiên làm việc",
"Sessions": "Phiên họp",
"Session ID": "ID phiên làm việc",
"Sessions": "Phiên",
"Shortcuts": "Shortcuts",
"Signin URL": "Địa chỉ URL để đăng nhập",
"Signin URL - Tooltip": "URL tùy chỉnh cho trang đăng nhập. Nếu không được thiết lập, trang đăng nhập mặc định của Casdoor sẽ được sử dụng. Khi được thiết lập, các liên kết đăng nhập trên các trang Casdoor khác sẽ chuyển hướng đến URL này",
@ -324,7 +324,7 @@
"Sure to disable": "Sure to disable",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger",
"Sync": "Đồng bộ hoá",
"Sync": "Đồng bộ",
"Syncers": "Đồng bộ hóa",
"System Info": "Thông tin hệ thống",
"There was a problem signing you in..": "There was a problem signing you in..",
@ -367,7 +367,7 @@
"Total users": "Total users"
},
"ldap": {
"Admin": "Admin",
"Admin": "Quản trị",
"Admin - Tooltip": "CN hoặc ID của quản trị viên máy chủ LDAP",
"Admin Password": "Mật khẩu quản trị viên",
"Admin Password - Tooltip": "Mật khẩu quản trị viên của máy chủ LDAP",
@ -376,7 +376,7 @@
"Base DN": "DN cơ sở",
"Base DN - Tooltip": "Đơn vị căn bản (Base DN) trong quá trình tìm kiếm LDAP",
"CN": "CN",
"Edit LDAP": "Chỉnh sửa LDAP",
"Edit LDAP": "Sửa LDAP",
"Enable SSL": "Kích hoạt SSL",
"Enable SSL - Tooltip": "Có nên kích hoạt SSL hay không?",
"Filter fields": "Filter fields",
@ -467,7 +467,7 @@
"preferred": "preferred"
},
"model": {
"Edit Model": "Chỉnh sửa mô hình",
"Edit Model": "Sửa mô hình",
"Model text": "Văn bản mẫu",
"Model text - Tooltip": "Mô hình kiểm soát truy cập Casbin, bao gồm các mô hình tích hợp như ACL, RBAC, ABAC, RESTful, v.v. Bạn cũng có thể tạo các mô hình tùy chỉnh. Để biết thêm thông tin, vui lòng truy cập trang web Casbin",
"New Model": "Mô hình mới"
@ -476,8 +476,8 @@
"Account items": "Mục tài khoản",
"Account items - Tooltip": "Các mục trong trang Cài đặt cá nhân",
"All": "Tất cả",
"Edit Organization": "Chỉnh sửa tổ chức",
"Follow global theme": "Theo chủ đề toàn cầu",
"Edit Organization": "Sửa tổ chức",
"Follow global theme": "Theo giao diện chung",
"Init score": "Điểm khởi tạo",
"Init score - Tooltip": "Điểm số ban đầu được trao cho người dùng khi đăng ký",
"Is profile public": "Hồ sơ có công khai không?",
@ -548,7 +548,7 @@
"permission": {
"Actions": "Hành động",
"Actions - Tooltip": "Các hành động được phép",
"Admin": "Admin",
"Admin": "Quản trị",
"Allow": "Cho phép",
"Approve time": "Phê duyệt thời gian",
"Approve time - Tooltip": "Thời gian chấp thuận cho quyền này",
@ -599,7 +599,7 @@
"Detail": "Chi tiết",
"Detail - Tooltip": "Chi tiết sản phẩm",
"Dummy": "Dummy",
"Edit Product": "Chỉnh sửa sản phẩm",
"Edit Product": "Sửa sản phẩm",
"I have completed the payment": "Tôi đã thanh toán hoàn tất",
"Image": "Ảnh",
"Image - Tooltip": "Hình ảnh sản phẩm",
@ -696,8 +696,8 @@
"Host - Tooltip": "Tên của người chủ chỗ ở",
"IdP": "IdP",
"IdP certificate": "Chứng chỉ IdP",
"Intelligent Validation": "Intelligent Validation",
"Internal": "Internal",
"Intelligent Validation": "Xác nhận thông minh",
"Internal": "Nội bộ",
"Issuer URL": "Địa chỉ URL của người phát hành",
"Issuer URL - Tooltip": "Địa chỉ URL của nhà phát hành",
"Link copied to clipboard successfully": "Đã sao chép liên kết vào bộ nhớ tạm thành công",
@ -705,7 +705,7 @@
"Metadata - Tooltip": "SAML metadata: siêu dữ liệu SAML",
"Method - Tooltip": "Phương thức đăng nhập, mã QR hoặc đăng nhập im lặng",
"New Provider": "Nhà cung cấp mới",
"Normal": "Normal",
"Normal": "Thường",
"Parameter": "Parameter",
"Parameter - Tooltip": "Parameter - Tooltip",
"Parse": "Phân tích cú pháp",
@ -736,8 +736,8 @@
"SMS Test - Tooltip": "Số điện thoại để gửi tin nhắn kiểm tra",
"SMS account": "Tài khoản SMS",
"SMS account - Tooltip": "Tài khoản SMS",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP ACS URL": "SP ACC URL",
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
"SP Entity ID": "SP Entity ID: Định danh thực thể SP",
"Scene": "Cảnh",
"Scene - Tooltip": "Cảnh",
@ -764,10 +764,10 @@
"Signup HTML": "Đăng ký HTML",
"Signup HTML - Edit": "Đăng ký HTML - Chỉnh sửa",
"Signup HTML - Tooltip": "Trang HTML tùy chỉnh để thay thế phong cách trang đăng ký mặc định",
"Silent": "Silent",
"Silent": "Im lặng",
"Site key": "Khóa trang web",
"Site key - Tooltip": "Khóa trang web",
"Sliding Validation": "Sliding Validation",
"Sliding Validation": "Xác nhận trượt ngang",
"Sub type": "Loại phụ",
"Sub type - Tooltip": "Loại phụ",
"Template code": "Mã mẫu của template",
@ -775,9 +775,9 @@
"Test Email": "Thư Email kiểm tra",
"Test Email - Tooltip": "Địa chỉ email để nhận thư kiểm tra",
"Test SMTP Connection": "Kiểm tra kết nối SMTP",
"Third-party": "Third-party",
"Token URL": "Đường dẫn Token",
"Token URL - Tooltip": "Địa chỉ URL của Token",
"Third-party": "Bên thứ ba",
"Token URL": "Đường dẫn mã thông báo",
"Token URL - Tooltip": "Địa chỉ của mã thông báo",
"Type": "Kiểu",
"Type - Tooltip": "Chọn loại",
"User mapping": "User mapping",
@ -797,7 +797,7 @@
"Upload a file...": "Tải lên một tệp..."
},
"role": {
"Edit Role": "Chỉnh sửa vai trò",
"Edit Role": "Sửa vai trò",
"New Role": "Vai trò mới",
"Sub domains": "Các phân miền con",
"Sub domains - Tooltip": "Các lĩnh vực được bao gồm trong vai trò hiện tại",
@ -835,7 +835,7 @@
"The input is not valid Email!": "Đầu vào không phải là địa chỉ Email hợp lệ!",
"The input is not valid Phone!": "Đầu vào không hợp lệ! Số điện thoại không hợp lệ!",
"Username": "Tên đăng nhập",
"Username - Tooltip": "Username - Tooltip",
"Username - Tooltip": "Tên người dùng - Tooltip",
"Your account has been created!": "Tài khoản của bạn đã được tạo!",
"Your confirmed password is inconsistent with the password!": "Mật khẩu xác nhận của bạn không khớp với mật khẩu đã nhập!",
"sign in now": "Đăng nhập ngay bây giờ"
@ -902,21 +902,21 @@
"Blossom": "hoa nở",
"Border radius": "Bán kính đường viền",
"Compact": "Nhỏ gọn, tiện dụng",
"Customize theme": "Tùy chỉnh chủ đề",
"Customize theme": "Tùy chỉnh giao diện",
"Dark": "Tối tăm",
"Default": "Mặc định",
"Document": "Tài liệu",
"Is compact": "Có kích thước nhỏ gọn",
"Primary color": "Màu sắc cơ bản",
"Theme": "Chủ đề",
"Theme - Tooltip": "Chủ đề phong cách của ứng dụng"
"Theme": "Giao diện",
"Theme - Tooltip": "Trang trí giao diện của ứng dụng"
},
"token": {
"Access token": "Mã thông báo truy cập",
"Authorization code": "Mã xác thực",
"Edit Token": "Chỉnh sửa mã thông báo",
"Expires in": "Hết hạn vào",
"New Token": "Token mới",
"Expires in": "Hết hạn sau",
"New Token": "Tạo mã thông báo",
"Token type": "Loại mã thông báo"
},
"user": {
@ -1019,7 +1019,7 @@
"webhook": {
"Content type": "Loại nội dung",
"Content type - Tooltip": "Loại nội dung",
"Edit Webhook": "Chỉnh sửa Webhook",
"Edit Webhook": "Sửa Webhook",
"Events": "Sự kiện",
"Events - Tooltip": "Sự kiện",
"Headers": "Tiêu đề",

View File

@ -158,12 +158,12 @@
"Verify": "验证"
},
"general": {
"API key": "API key",
"API key - Tooltip": "API key - Tooltip",
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
"Access secret": "Access secret",
"Access secret - Tooltip": "Access secret - Tooltip",
"API key": "API 密钥",
"API key - Tooltip": "API 密钥",
"Access key": "访问密钥",
"Access key - Tooltip": "访问密钥",
"Access secret": "访问密码",
"Access secret - Tooltip": "访问密码",
"Action": "操作",
"Adapter": "适配器",
"Adapter - Tooltip": "策略存储的表名",
@ -173,7 +173,7 @@
"Affiliation URL": "工作单位URL",
"Affiliation URL - Tooltip": "工作单位的官网URL",
"Application": "应用",
"Application - Tooltip": "Application - Tooltip",
"Application - Tooltip": "应用",
"Applications": "应用",
"Applications that require authentication": "需要认证和鉴权的应用",
"Apps": "应用列表",
@ -221,7 +221,7 @@
"Failed to remove": "移除失败",
"Failed to save": "保存失败",
"Failed to verify": "验证失败",
"Favicon": "Favicon",
"Favicon": "组织Favicon",
"Favicon - Tooltip": "该组织所有Casdoor页面中所使用的Favicon图标URL",
"First name": "名字",
"Forget URL": "忘记密码URL",
@ -230,7 +230,7 @@
"Go to enable": "前往启用",
"Go to writable demo site?": "跳转至可写演示站点?",
"Groups": "群组",
"Groups - Tooltip": "Groups - Tooltip",
"Groups - Tooltip": "",
"Home": "首页",
"Home - Tooltip": "应用的首页",
"ID": "ID",
@ -317,7 +317,7 @@
"Successfully deleted": "删除成功",
"Successfully removed": "移除成功",
"Successfully saved": "保存成功",
"Successfully sent": "Successfully sent",
"Successfully sent": "发送成功",
"Supported country codes": "支持的国家代码",
"Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀",
"Sure to delete": "确定删除",
@ -360,11 +360,11 @@
"Virtual": "虚拟组"
},
"home": {
"New users past 30 days": "New users past 30 days",
"New users past 7 days": "New users past 7 days",
"New users today": "New users today",
"Past 30 Days": "Past 30 Days",
"Total users": "Total users"
"New users past 30 days": "过去 30 天新增的用户",
"New users past 7 days": "过去 7 天新增的用户",
"New users today": "今天新增的用户",
"Past 30 Days": "过去 30 天",
"Total users": "用户总数"
},
"ldap": {
"Admin": "管理员",
@ -383,8 +383,8 @@
"Filter fields - Tooltip": "使用ldap用户登录Casdoor时, 用于搜索ldap服务器中该用户的字段 - Tooltip",
"Group ID": "组ID",
"Last Sync": "最近同步",
"Search Filter": "Search Filter",
"Search Filter - Tooltip": "Search Filter - Tooltip",
"Search Filter": "搜索过滤",
"Search Filter - Tooltip": "搜索过滤",
"Server": "服务器",
"Server host": "域名",
"Server host - Tooltip": "LDAP服务器地址",
@ -401,7 +401,7 @@
"Continue with": "使用以下账号继续",
"Email or phone": "Email或手机号",
"Failed to obtain MetaMask authorization": "获取MetaMask授权失败",
"Failed to obtain Web3-Onboard authorization": "Failed to obtain Web3-Onboard authorization",
"Failed to obtain Web3-Onboard authorization": "获取 Web3-Onboard 授权失败",
"Forgot password?": "忘记密码?",
"Loading": "加载中",
"Logging out...": "正在退出登录...",
@ -424,7 +424,7 @@
"The input is not valid Email or phone number!": "您输入的电子邮箱格式或手机号有误!",
"To access": "访问",
"Verification code": "验证码",
"WebAuthn": "WebAuthn",
"WebAuthn": "Web身份验证",
"sign up now": "立即注册",
"username, Email or phone": "用户名、Email或手机号"
},
@ -538,7 +538,7 @@
"Return to Website": "返回原网站",
"The payment has been canceled": "付款已取消",
"The payment has failed": "支付失败",
"The payment has time out": "The payment has time out",
"The payment has time out": "支付超时",
"The payment is still under processing": "支付正在处理",
"Type - Tooltip": "商品购买时的支付方式",
"You have successfully completed the payment": "支付成功",
@ -629,26 +629,26 @@
"WeChat Pay": "微信支付"
},
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key",
"Access key": "访问密钥",
"Access key - Tooltip": "Access Key",
"Agent ID": "Agent ID",
"Agent ID - Tooltip": "Agent ID",
"Api Key": "Api Key",
"Api Key - Tooltip": "Api Key - Tooltip",
"Agent ID - Tooltip": "Agent ID - Tooltip",
"Api Key": "Api 密钥",
"Api Key - Tooltip": "Api 密钥 - 工具提示",
"App ID": "App ID",
"App ID - Tooltip": "App ID",
"App Key": "App Key",
"App Key - Tooltip": "App Key - Tooltip",
"App key": "App key",
"App key - Tooltip": "App key",
"App secret": "App secret",
"AppSecret - Tooltip": "App secret",
"Auth Key": "Auth Key",
"Auth Key - Tooltip": "Auth Key - Tooltip",
"App ID - Tooltip": "App ID - Tooltip",
"App Key": "App 密钥",
"App Key - Tooltip": "App 密钥 - 工具提示",
"App key": "App Key",
"App key - Tooltip": "App key - Tooltip",
"App secret": "App Secret",
"AppSecret - Tooltip": "App Secret",
"Auth Key": "授权密钥",
"Auth Key - Tooltip": "授权密钥 - 工具提示",
"Auth URL": "Auth URL",
"Auth URL - Tooltip": "Auth URL",
"Base URL": "Base URL",
"Base URL - Tooltip": "Base URL - Tooltip",
"Auth URL - Tooltip": "Auth URL - 工具提示",
"Base URL": "基本 URL",
"Base URL - Tooltip": "基本 URL - 工具提示",
"Bucket": "存储桶",
"Bucket - Tooltip": "Bucket名称",
"Can not parse metadata": "无法解析元数据",
@ -659,21 +659,21 @@
"Category - Tooltip": "分类",
"Channel No.": "Channel号码",
"Channel No. - Tooltip": "Channel号码",
"Chat ID": "Chat ID",
"Chat ID - Tooltip": "Chat ID - Tooltip",
"Client ID": "Client ID",
"Chat ID": "聊天 ID",
"Chat ID - Tooltip": "聊天 ID - 工具提示",
"Client ID": "客户端ID",
"Client ID - Tooltip": "Client ID",
"Client ID 2": "Client ID 2",
"Client ID 2": "客户端 ID 2",
"Client ID 2 - Tooltip": "第二个Client ID",
"Client secret": "Client secret",
"Client secret - Tooltip": "Client secret",
"Client secret 2": "Client secret 2",
"Client secret": "客户端密钥",
"Client secret - Tooltip": "客户端密钥",
"Client secret 2": "客户端密钥 2",
"Client secret 2 - Tooltip": "第二个Client secret",
"Content": "Content",
"Content - Tooltip": "Content - Tooltip",
"Content": "内容",
"Content - Tooltip": "内容 - 工具提示",
"Copy": "复制",
"DB Test": "DB Test",
"DB Test - Tooltip": "DB Test - Tooltip",
"DB Test": "DB 测试",
"DB Test - Tooltip": "DB 测试 - 工具提示",
"Disable SSL": "禁用SSL",
"Disable SSL - Tooltip": "与STMP服务器通信时是否禁用SSL协议",
"Domain": "域名",
@ -687,14 +687,14 @@
"Enable QR code - Tooltip": "是否允许扫描二维码登录",
"Endpoint": "地域节点 (外网)",
"Endpoint (Intranet)": "地域节点 (内网)",
"Endpoint - Tooltip": "Endpoint - Tooltip",
"Endpoint - Tooltip": "端点 - 工具提示",
"From address": "发件人地址",
"From address - Tooltip": "邮件里发件人的邮箱地址",
"From name": "发件人名称",
"From name - Tooltip": "邮件里发件人的显示名称",
"Host": "主机",
"Host - Tooltip": "主机名",
"IdP": "IdP",
"IdP": "身份提供商",
"IdP certificate": "IdP公钥证书",
"Intelligent Validation": "智能验证",
"Internal": "内部",
@ -706,8 +706,8 @@
"Method - Tooltip": "登录方法,二维码或者静默授权登录",
"New Provider": "添加提供商",
"Normal": "标准",
"Parameter": "Parameter",
"Parameter - Tooltip": "Parameter - Tooltip",
"Parameter": "参数",
"Parameter - Tooltip": "参数 - 工具提示",
"Parse": "解析",
"Parse metadata successfully": "解析元数据成功",
"Path prefix": "路径前缀",
@ -715,45 +715,45 @@
"Please use WeChat and scan the QR code to sign in": "请使用微信扫描二维码登录",
"Port": "端口",
"Port - Tooltip": "请确保端口号打开",
"Private Key": "Private Key",
"Private Key - Tooltip": "Private Key - Tooltip",
"Project Id": "Project Id",
"Project Id - Tooltip": "Project Id - Tooltip",
"Private Key": "私钥",
"Private Key - Tooltip": "私钥 - 工具提示",
"Project Id": "项目 Id",
"Project Id - Tooltip": "项目 Id - 工具提示",
"Prompted": "注册后提醒绑定",
"Provider URL": "提供商URL",
"Provider URL - Tooltip": "提供商网址配置对应的URL该字段仅用来方便跳转在Casdoor平台中未使用",
"Public key": "Public key",
"Public key - Tooltip": "Public key - Tooltip",
"Region": "Region",
"Region - Tooltip": "Region - Tooltip",
"Public key": "公钥",
"Public key - Tooltip": "公钥 - 工具提示",
"Region": "区域",
"Region - Tooltip": "区域 - 工具提示",
"Region ID": "地域ID",
"Region ID - Tooltip": "提供商服务所属的地域ID",
"Region endpoint for Internet": "地域节点 (外网)",
"Region endpoint for Intranet": "地域节点 (内网)",
"Required": "是否必填项",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 Endpoint (HTTP)",
"SAML 2.0 Endpoint (HTTP)": "SAML 2.0 端点 (HTTP)",
"SMS Test": "测试短信配置",
"SMS Test - Tooltip": "请输入测试手机号",
"SMS account": "SMS account",
"SMS account - Tooltip": "SMS account",
"SP ACS URL": "SP ACS URL",
"SP ACS URL - Tooltip": "SP ACS URL",
"SP Entity ID": "SP Entity ID",
"Scene": "Scene",
"Scene - Tooltip": "Scene",
"SMS account": "短信账户",
"SMS account - Tooltip": "SMS account - Tooltip",
"SP ACS URL": "SP ACS 网址",
"SP ACS URL - Tooltip": "SP ACS URL - 工具提示",
"SP Entity ID": "SP 实体 ID",
"Scene": "场景",
"Scene - Tooltip": "Scene - Tooltip",
"Scope": "Scope",
"Scope - Tooltip": "Scope",
"Secret access key": "Secret access key",
"Secret access key - Tooltip": "Secret access key",
"Scope - Tooltip": "Scope - 工具提示",
"Secret access key": "秘密访问密钥",
"Secret access key - Tooltip": "秘密访问密钥",
"Secret key": "Secret key",
"Secret key - Tooltip": "用于服务端调用验证码提供商API进行验证",
"Send Testing Email": "发送测试邮件",
"Send Testing Notification": "Send Testing Notification",
"Send Testing Notification": "发送测试通知",
"Send Testing SMS": "发送测试短信",
"Sender Id": "Sender Id",
"Sender Id - Tooltip": "Sender Id - Tooltip",
"Sender number": "Sender number",
"Sender number - Tooltip": "Sender number - Tooltip",
"Sender Id": "发件人 Id",
"Sender Id - Tooltip": "发件人 Id - 工具提示",
"Sender number": "发件人号码",
"Sender number - Tooltip": "发件人号码 - 工具提示",
"Sign Name": "签名名称",
"Sign Name - Tooltip": "签名名称",
"Sign request": "签名请求",
@ -766,7 +766,7 @@
"Signup HTML - Tooltip": "自定义HTML用于替换默认的注册页面样式",
"Silent": "静默",
"Site key": "Site key",
"Site key - Tooltip": "Site key",
"Site key - Tooltip": "站点密钥",
"Sliding Validation": "滑块验证",
"Sub type": "子类型",
"Sub type - Tooltip": "子类型",
@ -780,12 +780,12 @@
"Token URL - Tooltip": "自定义OAuth的Token URL",
"Type": "类型",
"Type - Tooltip": "类型",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"User mapping": "用户映射",
"User mapping - Tooltip": "用户映射 - 工具提示",
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "自定义OAuth的UserInfo URL",
"Wallets": "Wallets",
"Wallets - Tooltip": "Wallets - Tooltip",
"Wallets": "钱包",
"Wallets - Tooltip": "钱包 - 工具提示",
"admin (Shared)": "admin共享"
},
"resource": {
@ -804,9 +804,7 @@
"Sub roles": "包含角色",
"Sub roles - Tooltip": "当前角色所包含的子角色",
"Sub users": "包含用户",
"Sub users - Tooltip": "当前角色所包含的用户",
"Sub groups": "包含群组",
"Sub groups - Tooltip": "当前角色所包含的群组"
"Sub users - Tooltip": "当前角色所包含的用户"
},
"signup": {
"Accept": "阅读并接受",
@ -860,7 +858,7 @@
"Casdoor column": "Casdoor列名",
"Column name": "列名",
"Column type": "列类型",
"Connect successfully": "Connect successfully",
"Connect successfully": "连接成功",
"Database": "数据库",
"Database - Tooltip": "数据库名称",
"Database type": "数据库类型",
@ -868,11 +866,11 @@
"Edit Syncer": "编辑同步器",
"Error text": "错误信息",
"Error text - Tooltip": "错误信息",
"Failed to connect": "Failed to connect",
"Failed to connect": "连接失败",
"Is hashed": "是否参与哈希计算",
"Is key": "是否为主键",
"Is read-only": "是否只读",
"Is read-only - Tooltip": "Is read-only - Tooltip",
"Is read-only - Tooltip": "只读",
"New Syncer": "添加同步器",
"Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为秒",
@ -880,7 +878,7 @@
"Table - Tooltip": "数据库表名",
"Table columns": "表格列",
"Table columns - Tooltip": "参与数据同步的表格列,不参与同步的列不需要添加",
"Test DB Connection": "Test DB Connection"
"Test DB Connection": "测试 DB 连接"
},
"system": {
"API Latency": "API 延迟",
@ -930,8 +928,8 @@
"Affiliation - Tooltip": "工作单位,如公司、组织名称",
"Bio": "自我介绍",
"Bio - Tooltip": "用户的自我介绍",
"Birthday": "Birthday",
"Birthday - Tooltip": "Birthday - Tooltip",
"Birthday": "生日",
"Birthday - Tooltip": "生日",
"Captcha Verify Failed": "验证码校验失败",
"Captcha Verify Success": "验证码校验成功",
"Country code": "国家代码",
@ -964,9 +962,9 @@
"Is deleted - Tooltip": "被软删除的用户只保留数据库记录,无法进行任何操作",
"Is forbidden": "被禁用",
"Is forbidden - Tooltip": "被禁用的用户无法再登录",
"Is online": "Is online",
"Is online": "在线",
"Karma": "Karma",
"Karma - Tooltip": "Karma - Tooltip",
"Karma - Tooltip": "Karma - 工具提示",
"Keys": "键",
"Language": "语言",
"Language - Tooltip": "语言 - Tooltip",
@ -1032,4 +1030,4 @@
"New Webhook": "添加Webhook",
"Value": "值"
}
}
}