Compare commits

..

36 Commits

Author SHA1 Message Date
be74cb621f feat: Support sub-directory (#943)
By adding PUBLIC_URL to relative `.`

Signed-off-by: zzjin <tczzjin@gmail.com>
2022-08-02 00:21:15 +08:00
13404d6035 feat: fix binding after registration causes the page to crash (#945) 2022-08-01 21:08:10 +08:00
afa9c530ad fix: panic triggered when user is nil (#940) 2022-07-31 23:23:36 +08:00
1600615aca Support sqlite3 DB 2022-07-31 18:11:18 +08:00
2bb8491499 fix: unable to get user if profile is private (#936) 2022-07-31 10:54:41 +08:00
293283ed25 feat: add get user by phone (#934)
* fix: check reset phone & email modify rules

* Update verification.go

* Update organization.go

* feat: add get user by phone

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-31 01:02:28 +08:00
9cb519d1e9 fix: Admins should not be allowed to add third-party login for their members (#932)
* feat: admin can unlink the other user

* feat: global admin can unlink other user

* fix
2022-07-30 23:11:02 +08:00
fb9b8f1662 fix: skip the duplicated users when sync users (#928)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-30 22:24:23 +08:00
2fec3f72ae fix: check reset phone & email modify rules (#927)
* fix: check reset phone & email modify rules

* Update verification.go

* Update organization.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-30 18:17:13 +08:00
11695220a8 Use user.GetId() 2022-07-30 17:40:30 +08:00
155660b0d7 feat: get user api return roles and permissions (#929) 2022-07-30 17:31:56 +08:00
1c72f5300c feat: fix 'Enable code sign' is not displayed in the login page (#925) 2022-07-28 23:11:33 +08:00
3dd56195d9 fix: fix the problem of link error (#923) 2022-07-28 21:52:10 +08:00
8865244262 fix: add oauth login auto close page (#915) 2022-07-26 23:03:55 +08:00
3400fa1e9c feat: support local login for non-built-in users (#911)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-26 19:27:24 +08:00
bdc5c92ef0 fix: send code missing parameter & show more detail responseError (#910) 2022-07-25 23:46:38 +08:00
4e3eedf246 feat: fix bug that the default permission prevents admin to login in (#907)
* fix:The certs page is displayed incorrectly

* Translations for each language are added

* Replace the variables certificat with Certificat with certificate and Certificate

* Replace the variables certificat with Certificat with certificate and Certificate

* Variable names are more accurate

* Variable names are more accurate

* Modify the variable name

* fix: Default action prevents admin to login in
2022-07-24 23:36:55 +08:00
8e98fc5a9f feat: rename all publicKey occurrences to certificate (#894)
* fix:The certs page is displayed incorrectly

* Translations for each language are added

* Replace the variables certificat with Certificat with certificate and Certificate

* Replace the variables certificat with Certificat with certificate and Certificate

* Variable names are more accurate

* Variable names are more accurate

* Modify the variable name
2022-07-23 09:40:51 +08:00
6f6159be07 feat: add GET method of logout API (#903) 2022-07-22 21:13:49 +08:00
3e4dbc2dcb fix: URL bug in getUploadFileUrl function 2022-07-20 17:49:11 +08:00
48b5b27982 fix: invalid redirect url after sign up (#896)
* fix: invalid redirect url after sign up

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* Update App.js

* Update Setting.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-19 23:31:17 +08:00
1839252c30 chore(web): sort import members (#895) 2022-07-18 20:57:38 +08:00
1fff1db6a7 fix(web): fix the bug of infinity loop animate when unauthorized (#891)
* fix(web): fix the bug of infinity loop when unauthorized

* fix

* fix

* fix

* Update BaseListPage.js

* Update OrganizationListPage.js

* Update OrganizationListPage.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-17 18:20:52 +08:00
a0b0e186b7 Improve i18n code and data. 2022-07-17 17:56:43 +08:00
8c7f235ee1 Fix bug in uploadFile()'s URL. 2022-07-17 14:29:06 +08:00
a0a762aa6f fix: typo in field tag in BilibiliUserInfo (#890) 2022-07-17 11:31:43 +08:00
2eec53a6d0 fix: actions initialized to null and model/resources not updated with the owner (#887)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-16 15:00:42 +08:00
117dec4542 feat: failed to sync keycloak users in the PostgreSQL database (#886)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-07-16 12:14:35 +08:00
895cdd024d fix: Typo in user model xorm tag (#883) 2022-07-15 12:01:27 +08:00
f0b0891ac9 feat: query user by userId (#879)
* feat: add `getUserByUserId` func

* Update user.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-07-14 21:46:13 +08:00
10449e89ab Fix owner bug in GetUser(). 2022-07-13 22:56:35 +08:00
6e70f0fc58 Refactor CheckAccessPermission(). 2022-07-13 00:50:32 +08:00
2bca424370 feat: implement access control using casbin (#806)
* feat: implement access control using casbin

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* chore: sort imports

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: remove

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* Update auth.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-07-13 00:34:35 +08:00
de49a45e19 Add escapePath for getUploadFileUrl(). 2022-07-12 23:24:24 +08:00
f7243f879b Fix some JS warnings. 2022-07-12 20:47:11 +08:00
7f3b2500b3 feat: support webauthn (#407)
* feat: support webauthn

* Update init.go

* Update user_webauthn.go

* Update UserEditPage.js

* Update WebauthnCredentialTable.js

* Update LoginPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-07-12 20:06:01 +08:00
85 changed files with 1654 additions and 255 deletions

View File

@ -78,6 +78,7 @@ p, *, *, POST, /api/get-email-and-phone, *, *
p, *, *, POST, /api/login, *, * p, *, *, POST, /api/login, *, *
p, *, *, GET, /api/get-app-login, *, * p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, * p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/logout, *, *
p, *, *, GET, /api/get-account, *, * p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, * p, *, *, GET, /api/userinfo, *, *
p, *, *, *, /api/login/oauth, *, * p, *, *, *, /api/login/oauth, *, *
@ -92,6 +93,7 @@ p, *, *, GET, /api/get-payment, *, *
p, *, *, POST, /api/update-payment, *, * p, *, *, POST, /api/update-payment, *, *
p, *, *, POST, /api/invoice-payment, *, * p, *, *, POST, /api/invoice-payment, *, *
p, *, *, GET, /api/get-providers, *, * p, *, *, GET, /api/get-providers, *, *
p, *, *, POST, /api/notify-payment, *, *
p, *, *, POST, /api/unlink, *, * p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, * p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, * p, *, *, POST, /api/send-verification-code, *, *
@ -105,6 +107,7 @@ p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, * p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, * p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, * p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, *
` `
sa := stringadapter.NewAdapter(ruleText) sa := stringadapter.NewAdapter(ruleText)

View File

@ -217,7 +217,7 @@ func (c *ApiController) Signup() {
record.User = user.Name record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) }) util.SafeGoroutine(func() { object.AddRecord(record) })
userId := fmt.Sprintf("%s/%s", user.Owner, user.Name) userId := user.GetId()
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId) util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
c.ResponseOk(userId) c.ResponseOk(userId)
@ -228,7 +228,7 @@ func (c *ApiController) Signup() {
// @Tag Login API // @Tag Login API
// @Description logout the current user // @Description logout the current user
// @Success 200 {object} controllers.Response The Response object // @Success 200 {object} controllers.Response The Response object
// @router /logout [post] // @router /logout [get,post]
func (c *ApiController) Logout() { func (c *ApiController) Logout() {
user := c.GetSessionUsername() user := c.GetSessionUsername()
util.LogInfo(c.Ctx, "API: [%s] logged out", user) util.LogInfo(c.Ctx, "API: [%s] logged out", user)

View File

@ -50,6 +50,17 @@ func tokenToResponse(token *object.Token) *Response {
// HandleLoggedIn ... // HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) { func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
userId := user.GetId() userId := user.GetId()
allowed, err := object.CheckAccessPermission(userId, application)
if err != nil {
c.ResponseError(err.Error(), nil)
return
}
if !allowed {
c.ResponseError("Unauthorized operation")
return
}
if form.Type == ResponseTypeLogin { if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId) c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId) util.LogInfo(c.Ctx, "API: [%s] signed in", userId)

View File

@ -21,7 +21,8 @@ import (
) )
type LinkForm struct { type LinkForm struct {
ProviderType string `json:"providerType"` ProviderType string `json:"providerType"`
User object.User `json:"user"`
} }
// Unlink ... // Unlink ...
@ -40,16 +41,55 @@ func (c *ApiController) Unlink() {
} }
providerType := form.ProviderType providerType := form.ProviderType
// the user will be unlinked from the provider
unlinkedUser := form.User
user := object.GetUser(userId) user := object.GetUser(userId)
value := object.GetUserField(user, providerType)
if user.Id != unlinkedUser.Id && !user.IsGlobalAdmin {
// if the user is not the same as the one we are unlinking, we need to make sure the user is the global admin.
c.ResponseError("You are not the global admin, you can't unlink other users")
return
}
if user.Id == unlinkedUser.Id && !user.IsGlobalAdmin {
// if the user is unlinking themselves, should check the provider can be unlinked, if not, we should return an error.
application := object.GetApplicationByUser(user)
if application == nil {
c.ResponseError("You can't unlink yourself, you are not a member of any application")
return
}
if len(application.Providers) == 0 {
c.ResponseError("This application has no providers")
return
}
provider := application.GetProviderItemByType(providerType)
if provider == nil {
c.ResponseError("This application has no providers of type " + providerType)
return
}
if !provider.CanUnlink {
c.ResponseError("This provider can't be unlinked")
return
}
}
// only two situations can happen here
// 1. the user is the global admin
// 2. the user is unlinking themselves and provider can be unlinked
value := object.GetUserField(&unlinkedUser, providerType)
if value == "" { if value == "" {
c.ResponseError("Please link first", value) c.ResponseError("Please link first", value)
return return
} }
object.ClearUserOAuthProperties(user, providerType) object.ClearUserOAuthProperties(&unlinkedUser, providerType)
object.LinkUserAccount(user, providerType, "") object.LinkUserAccount(&unlinkedUser, providerType, "")
c.ResponseOk() c.ResponseOk()
} }

View File

@ -80,19 +80,27 @@ func (c *ApiController) GetUsers() {
// @Title GetUser // @Title GetUser
// @Tag User API // @Tag User API
// @Description get user // @Description get user
// @Param id query string true "The id of the user" // @Param id query string true "The id of the user"
// @Param owner query string false "The owner of the user"
// @Param email query string false "The email of the user"
// @Param phone query string false "The phone of the user"
// @Success 200 {object} object.User The Response object // @Success 200 {object} object.User The Response object
// @router /get-user [get] // @router /get-user [get]
func (c *ApiController) GetUser() { func (c *ApiController) GetUser() {
id := c.Input().Get("id") id := c.Input().Get("id")
owner := c.Input().Get("owner")
email := c.Input().Get("email") email := c.Input().Get("email")
userOwner, _ := util.GetOwnerAndNameFromId(id) phone := c.Input().Get("phone")
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", userOwner)) userId := c.Input().Get("userId")
owner := c.Input().Get("owner")
if owner == "" {
owner, _ = util.GetOwnerAndNameFromId(id)
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner))
if !organization.IsProfilePublic { if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, false) hasPermission, err := object.CheckUserPermission(requestUserId, id, owner, false)
if !hasPermission { if !hasPermission {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -100,10 +108,22 @@ func (c *ApiController) GetUser() {
} }
var user *object.User var user *object.User
if email == "" { switch {
user = object.GetUser(id) case email != "":
} else {
user = object.GetUserByEmail(owner, email) user = object.GetUserByEmail(owner, email)
case phone != "":
user = object.GetUserByPhone(owner, phone)
case userId != "":
user = object.GetUserByUserId(owner, userId)
default:
user = object.GetUser(id)
}
if user != nil {
roles := object.GetRolesByUser(user.GetId())
user.Roles = roles
permissions := object.GetPermissionsByUser(user.GetId())
user.Permissions = permissions
} }
c.Data["json"] = object.GetMaskedUser(user) c.Data["json"] = object.GetMaskedUser(user)
@ -246,7 +266,7 @@ func (c *ApiController) SetPassword() {
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
userId := fmt.Sprintf("%s/%s", userOwner, userName) userId := fmt.Sprintf("%s/%s", userOwner, userName)
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true) hasPermission, err := object.CheckUserPermission(requestUserId, userId, userOwner, true)
if !hasPermission { if !hasPermission {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -49,8 +49,24 @@ func (c *ApiController) SendVerificationCode() {
applicationId := c.Ctx.Request.Form.Get("applicationId") applicationId := c.Ctx.Request.Form.Get("applicationId")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request) remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if destType == "" || dest == "" || applicationId == "" || !strings.Contains(applicationId, "/") || checkType == "" { if destType == "" {
c.ResponseError("Missing parameter.") c.ResponseError("Missing parameter: type.")
return
}
if dest == "" {
c.ResponseError("Missing parameter: dest.")
return
}
if applicationId == "" {
c.ResponseError("Missing parameter: applicationId.")
return
}
if !strings.Contains(applicationId, "/") {
c.ResponseError("Wrong parameter: applicationId.")
return
}
if checkType == "" {
c.ResponseError("Missing parameter: checkType.")
return return
} }
@ -152,13 +168,35 @@ func (c *ApiController) ResetEmailOrPhone() {
} }
checkDest := dest checkDest := dest
org := object.GetOrganizationByUser(user)
if destType == "phone" { if destType == "phone" {
org := object.GetOrganizationByUser(user) phoneItem := object.GetAccountItemByName("Phone", org)
if phoneItem == nil {
c.ResponseError("Unable to get the phone modify rule.")
return
}
if pass, errMsg := object.CheckAccountItemModifyRule(phoneItem, user); !pass {
c.ResponseError(errMsg)
return
}
phonePrefix := "86" phonePrefix := "86"
if org != nil && org.PhonePrefix != "" { if org != nil && org.PhonePrefix != "" {
phonePrefix = org.PhonePrefix phonePrefix = org.PhonePrefix
} }
checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest) checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest)
} else if destType == "email" {
emailItem := object.GetAccountItemByName("Email", org)
if emailItem == nil {
c.ResponseError("Unable to get the email modify rule.")
return
}
if pass, errMsg := object.CheckAccountItemModifyRule(emailItem, user); !pass {
c.ResponseError(errMsg)
return
}
} }
if ret := object.CheckVerificationCode(checkDest, code); len(ret) != 0 { if ret := object.CheckVerificationCode(checkDest, code); len(ret) != 0 {
c.ResponseError(ret) c.ResponseError(ret)

138
controllers/webauthn.go Normal file
View File

@ -0,0 +1,138 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"bytes"
"io/ioutil"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
// @Title WebAuthnSignupBegin
// @Tag User API
// @Description WebAuthn Registration Flow 1st stage
// @Success 200 {object} protocol.CredentialCreation The CredentialCreationOptions object
// @router /webauthn/signup/begin [get]
func (c *ApiController) WebAuthnSignupBegin() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
user := c.getCurrentUser()
if user == nil {
c.ResponseError("Please login first.")
return
}
registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
}
options, sessionData, err := webauthnObj.BeginRegistration(
user,
registerOptions,
)
if err != nil {
c.ResponseError(err.Error())
return
}
c.SetSession("registration", *sessionData)
c.Data["json"] = options
c.ServeJSON()
}
// @Title WebAuthnSignupFinish
// @Tag User API
// @Description WebAuthn Registration Flow 2nd stage
// @Param body body protocol.CredentialCreationResponse true "authenticator attestation Response"
// @Success 200 {object} Response "The Response object"
// @router /webauthn/signup/finish [post]
func (c *ApiController) WebAuthnSignupFinish() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
user := c.getCurrentUser()
if user == nil {
c.ResponseError("Please login first.")
return
}
sessionObj := c.GetSession("registration")
sessionData, ok := sessionObj.(webauthn.SessionData)
if !ok {
c.ResponseError("Please call WebAuthnSignupBegin first")
return
}
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
credential, err := webauthnObj.FinishRegistration(user, sessionData, c.Ctx.Request)
if err != nil {
c.ResponseError(err.Error())
return
}
isGlobalAdmin := c.IsGlobalAdmin()
user.AddCredentials(*credential, isGlobalAdmin)
c.ResponseOk()
}
// @Title WebAuthnSigninBegin
// @Tag Login API
// @Description WebAuthn Login Flow 1st stage
// @Param owner query string true "owner"
// @Param name query string true "name"
// @Success 200 {object} protocol.CredentialAssertion The CredentialAssertion object
// @router /webauthn/signin/begin [get]
func (c *ApiController) WebAuthnSigninBegin() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
userOwner := c.Input().Get("owner")
userName := c.Input().Get("name")
user := object.GetUserByFields(userOwner, userName)
if user == nil {
c.ResponseError("Please Giveout Owner and Username.")
return
}
options, sessionData, err := webauthnObj.BeginLogin(user)
if err != nil {
c.ResponseError(err.Error())
return
}
c.SetSession("authentication", *sessionData)
c.Data["json"] = options
c.ServeJSON()
}
// @Title WebAuthnSigninBegin
// @Tag Login API
// @Description WebAuthn Login Flow 2nd stage
// @Param body body protocol.CredentialAssertionResponse true "authenticator assertion Response"
// @Success 200 {object} Response "The Response object"
// @router /webauthn/signin/finish [post]
func (c *ApiController) WebAuthnSigninFinish() {
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
sessionObj := c.GetSession("authentication")
sessionData, ok := sessionObj.(webauthn.SessionData)
if !ok {
c.ResponseError("Please call WebAuthnSigninBegin first")
return
}
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
userId := string(sessionData.UserID)
user := object.GetUser(userId)
_, err := webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
if err != nil {
c.ResponseError(err.Error())
return
}
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
c.ResponseOk(userId)
}

1
go.mod
View File

@ -14,6 +14,7 @@ require (
github.com/casdoor/goth v1.69.0-FIX2 github.com/casdoor/goth v1.69.0-FIX2
github.com/casdoor/oss v1.2.0 github.com/casdoor/oss v1.2.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-pay/gopay v1.5.72 github.com/go-pay/gopay v1.5.72

14
go.sum
View File

@ -111,6 +111,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1CUzYdAZXijuvLuRMirgiXdf3zsM2Ig=
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
@ -124,6 +126,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M= github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY= github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b h1:L63RATZFZuFMXy6ixnKmv3eNAXwYQF6HW1vd4IYsQqQ=
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b/go.mod h1:EYSpSkwoEcryMmQGfhol2IiB3IMN9IIIaNd/wcAQMGQ=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
@ -135,6 +139,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ=
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
@ -164,6 +170,7 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@ -201,6 +208,8 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -298,6 +307,8 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -397,6 +408,8 @@ github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnD
github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA= github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA=
github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4= github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -413,6 +426,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

View File

@ -39,6 +39,7 @@ func readI18nFile(language string) *I18nData {
func writeI18nFile(language string, data *I18nData) { func writeI18nFile(language string, data *I18nData) {
s := util.StructToJsonFormatted(data) s := util.StructToJsonFormatted(data)
s = strings.ReplaceAll(s, "\\u0026", "&") s = strings.ReplaceAll(s, "\\u0026", "&")
s += "\n"
println(s) println(s)
util.WriteStringToPath(s, getI18nFilePath(language)) util.WriteStringToPath(s, getI18nFilePath(language))

View File

@ -144,7 +144,7 @@ func (idp *BilibiliIdProvider) GetToken(code string) (*oauth2.Token, error) {
type BilibiliUserInfo struct { type BilibiliUserInfo struct {
Name string `json:"name"` Name string `json:"name"`
Face string `json:"face"` Face string `json:"face"`
OpenId string `json:"openid` OpenId string `json:"openid"`
} }
type BilibiliUserInfoResponse struct { type BilibiliUserInfoResponse struct {

View File

@ -139,7 +139,7 @@
"cryptoAlgorithm": "RS256", "cryptoAlgorithm": "RS256",
"bitSize": 4096, "bitSize": 4096,
"expireInYears": 20, "expireInYears": 20,
"publicKey": "", "certificate": "",
"privateKey": "" "privateKey": ""
} }
], ],

View File

@ -22,8 +22,9 @@ import (
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
//_ "github.com/denisenkom/go-mssqldb" // db = mssql //_ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql _ "github.com/go-sql-driver/mysql" // db = mysql
//_ "github.com/lib/pq" // db = postgres //_ "github.com/lib/pq" // db = postgres
//_ "github.com/mattn/go-sqlite3" // db = sqlite3
"xorm.io/core" "xorm.io/core"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -203,6 +204,11 @@ func (a *Adapter) createTable() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(PermissionRule))
if err != nil {
panic(err)
}
} }
func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session { func GetSession(owner string, offset, limit int, field, value, sortField, sortOrder string) *xorm.Session {

View File

@ -47,6 +47,7 @@ type Application struct {
EnableSigninSession bool `json:"enableSigninSession"` EnableSigninSession bool `json:"enableSigninSession"`
EnableCodeSignin bool `json:"enableCodeSignin"` EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"` EnableSamlCompress bool `json:"enableSamlCompress"`
EnableWebAuthn bool `json:"enableWebAuthn"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`

View File

@ -33,7 +33,7 @@ type Cert struct {
BitSize int `json:"bitSize"` BitSize int `json:"bitSize"`
ExpireInYears int `json:"expireInYears"` ExpireInYears int `json:"expireInYears"`
PublicKey string `xorm:"mediumtext" json:"publicKey"` Certificate string `xorm:"mediumtext" json:"certificate"`
PrivateKey string `xorm:"mediumtext" json:"privateKey"` PrivateKey string `xorm:"mediumtext" json:"privateKey"`
AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"` AuthorityPublicKey string `xorm:"mediumtext" json:"authorityPublicKey"`
AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"` AuthorityRootPublicKey string `xorm:"mediumtext" json:"authorityRootPublicKey"`
@ -123,9 +123,9 @@ func UpdateCert(id string, cert *Cert) bool {
} }
func AddCert(cert *Cert) bool { func AddCert(cert *Cert) bool {
if cert.PublicKey == "" || cert.PrivateKey == "" { if cert.Certificate == "" || cert.PrivateKey == "" {
publicKey, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner) certificate, privateKey := generateRsaKeys(cert.BitSize, cert.ExpireInYears, cert.Name, cert.Owner)
cert.PublicKey = publicKey cert.Certificate = certificate
cert.PrivateKey = privateKey cert.PrivateKey = privateKey
} }

View File

@ -197,14 +197,18 @@ func filterField(field string) bool {
return reFieldWhiteList.MatchString(field) return reFieldWhiteList.MatchString(field)
} }
func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error) { func CheckUserPermission(requestUserId, userId, userOwner string, strict bool) (bool, error) {
if requestUserId == "" { if requestUserId == "" {
return false, fmt.Errorf("please login first") return false, fmt.Errorf("please login first")
} }
targetUser := GetUser(userId) if userId != "" {
if targetUser == nil { targetUser := GetUser(userId)
return false, fmt.Errorf("the user: %s doesn't exist", userId) if targetUser == nil {
return false, fmt.Errorf("the user: %s doesn't exist", userId)
}
userOwner = targetUser.Owner
} }
hasPermission := false hasPermission := false
@ -219,7 +223,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error
hasPermission = true hasPermission = true
} else if requestUserId == userId { } else if requestUserId == userId {
hasPermission = true hasPermission = true
} else if targetUser.Owner == requestUser.Owner { } else if userOwner == requestUser.Owner {
if strict { if strict {
hasPermission = requestUser.IsAdmin hasPermission = requestUser.IsAdmin
} else { } else {
@ -230,3 +234,29 @@ func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error
return hasPermission, fmt.Errorf("you don't have the permission to do this") return hasPermission, fmt.Errorf("you don't have the permission to do this")
} }
func CheckAccessPermission(userId string, application *Application) (bool, error) {
permissions := GetPermissions(application.Organization)
allowed := true
var err error
for _, permission := range permissions {
if !permission.IsEnabled || len(permission.Users) == 0 {
continue
}
isHit := false
for _, resource := range permission.Resources {
if application.Name == resource {
isHit = true
break
}
}
if isHit {
enforcer := getEnforcer(permission)
allowed, err = enforcer.Enforce(userId, application.Name, "read")
break
}
}
return allowed, err
}

View File

@ -15,20 +15,25 @@
package object package object
import ( import (
"encoding/gob"
"io/ioutil" "io/ioutil"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn"
) )
func InitDb() { func InitDb() {
existed := initBuiltInOrganization() existed := initBuiltInOrganization()
if !existed { if !existed {
initBuiltInPermission()
initBuiltInProvider() initBuiltInProvider()
initBuiltInUser() initBuiltInUser()
initBuiltInApplication() initBuiltInApplication()
initBuiltInCert() initBuiltInCert()
initBuiltInLdap() initBuiltInLdap()
} }
initWebAuthn()
} }
func initBuiltInOrganization() bool { func initBuiltInOrganization() bool {
@ -66,12 +71,15 @@ func initBuiltInOrganization() bool {
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"}, {Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
}, },
} }
AddOrganization(organization) AddOrganization(organization)
@ -162,7 +170,7 @@ func readTokenFromFile() (string, string) {
} }
func initBuiltInCert() { func initBuiltInCert() {
tokenJwtPublicKey, tokenJwtPrivateKey := readTokenFromFile() tokenJwtCertificate, tokenJwtPrivateKey := readTokenFromFile()
cert := getCert("admin", "cert-built-in") cert := getCert("admin", "cert-built-in")
if cert != nil { if cert != nil {
return return
@ -178,7 +186,7 @@ func initBuiltInCert() {
CryptoAlgorithm: "RS256", CryptoAlgorithm: "RS256",
BitSize: 4096, BitSize: 4096,
ExpireInYears: 20, ExpireInYears: 20,
PublicKey: tokenJwtPublicKey, Certificate: tokenJwtCertificate,
PrivateKey: tokenJwtPrivateKey, PrivateKey: tokenJwtPrivateKey,
} }
AddCert(cert) AddCert(cert)
@ -221,3 +229,29 @@ func initBuiltInProvider() {
} }
AddProvider(provider) AddProvider(provider)
} }
func initWebAuthn() {
gob.Register(webauthn.SessionData{})
}
func initBuiltInPermission() {
permission := GetPermission("built-in/permission-built-in")
if permission != nil {
return
}
permission = &Permission{
Owner: "built-in",
Name: "permission-built-in",
CreatedTime: util.GetCurrentTime(),
DisplayName: "Built-in Permission",
Users: []string{"built-in/admin"},
Roles: []string{},
ResourceType: "Application",
Resources: []string{"app-built-in"},
Actions: []string{"Read", "Write", "Admin"},
Effect: "Allow",
IsEnabled: true,
}
AddPermission(permission)
}

View File

@ -89,6 +89,8 @@ func initDefinedOrganization(organization *Organization) {
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"}, {Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"}, {Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"}, {Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"}, {Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},

View File

@ -97,7 +97,7 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
//link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html //link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
//or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key //or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
for _, cert := range certs { for _, cert := range certs {
certPemBlock := []byte(cert.PublicKey) certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock) certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes) x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)

View File

@ -15,6 +15,8 @@
package object package object
import ( import (
"fmt"
"github.com/casdoor/casdoor/cred" "github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"xorm.io/core" "xorm.io/core"
@ -186,3 +188,31 @@ func DeleteOrganization(organization *Organization) bool {
func GetOrganizationByUser(user *User) *Organization { func GetOrganizationByUser(user *User) *Organization {
return getOrganization("admin", user.Owner) return getOrganization("admin", user.Owner)
} }
func GetAccountItemByName(name string, organization *Organization) *AccountItem {
if organization == nil {
return nil
}
for _, accountItem := range organization.AccountItems {
if accountItem.Name == name {
return accountItem
}
}
return nil
}
func CheckAccountItemModifyRule(accountItem *AccountItem, user *User) (bool, string) {
switch accountItem.ModifyRule {
case "Admin":
if !(user.IsAdmin || user.IsGlobalAdmin) {
return false, fmt.Sprintf("Only admin can modify the %s.", accountItem.Name)
}
case "Immutable":
return false, fmt.Sprintf("The %s is immutable.", accountItem.Name)
case "Self":
break
default:
return false, fmt.Sprintf("Unknown modify rule %s.", accountItem.ModifyRule)
}
return true, ""
}

View File

@ -16,7 +16,12 @@ package object
import ( import (
"fmt" "fmt"
"strings"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
xormadapter "github.com/casbin/xorm-adapter/v2"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"xorm.io/core" "xorm.io/core"
) )
@ -39,6 +44,16 @@ type Permission struct {
IsEnabled bool `json:"isEnabled"` IsEnabled bool `json:"isEnabled"`
} }
type PermissionRule struct {
PType string `xorm:"varchar(100) index not null default ''"`
V0 string `xorm:"varchar(100) index not null default ''"`
V1 string `xorm:"varchar(100) index not null default ''"`
V2 string `xorm:"varchar(100) index not null default ''"`
V3 string `xorm:"varchar(100) index not null default ''"`
V4 string `xorm:"varchar(100) index not null default ''"`
V5 string `xorm:"varchar(100) index not null default ''"`
}
func GetPermissionCount(owner, field, value string) int { func GetPermissionCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Permission{}) count, err := session.Count(&Permission{})
@ -95,7 +110,8 @@ func GetPermission(id string) *Permission {
func UpdatePermission(id string, permission *Permission) bool { func UpdatePermission(id string, permission *Permission) bool {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if getPermission(owner, name) == nil { oldPermission := getPermission(owner, name)
if oldPermission == nil {
return false return false
} }
@ -104,6 +120,11 @@ func UpdatePermission(id string, permission *Permission) bool {
panic(err) panic(err)
} }
if affected != 0 {
removePolicies(oldPermission)
addPolicies(permission)
}
return affected != 0 return affected != 0
} }
@ -113,6 +134,10 @@ func AddPermission(permission *Permission) bool {
panic(err) panic(err)
} }
if affected != 0 {
addPolicies(permission)
}
return affected != 0 return affected != 0
} }
@ -122,9 +147,95 @@ func DeletePermission(permission *Permission) bool {
panic(err) panic(err)
} }
if affected != 0 {
removePolicies(permission)
}
return affected != 0 return affected != 0
} }
func (permission *Permission) GetId() string { func (permission *Permission) GetId() string {
return fmt.Sprintf("%s/%s", permission.Owner, permission.Name) return fmt.Sprintf("%s/%s", permission.Owner, permission.Name)
} }
func getEnforcer(permission *Permission) *casbin.Enforcer {
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), "permission_rule", tableNamePrefix, true)
if err != nil {
panic(err)
}
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = permission, sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act`
permissionModel := getModel(permission.Owner, permission.Model)
if permissionModel != nil {
modelText = permissionModel.ModelText
}
m, err := model.NewModelFromString(modelText)
if err != nil {
panic(err)
}
enforcer, err := casbin.NewEnforcer(m, adapter)
if err != nil {
panic(err)
}
err = enforcer.LoadFilteredPolicy(xormadapter.Filter{V0: []string{permission.GetId()}})
if err != nil {
panic(err)
}
return enforcer
}
func getPolicies(permission *Permission) [][]string {
var policies [][]string
for _, user := range permission.Users {
for _, resource := range permission.Resources {
for _, action := range permission.Actions {
policies = append(policies, []string{permission.GetId(), user, resource, strings.ToLower(action)})
}
}
}
return policies
}
func addPolicies(permission *Permission) {
enforcer := getEnforcer(permission)
policies := getPolicies(permission)
_, err := enforcer.AddPolicies(policies)
if err != nil {
panic(err)
}
}
func removePolicies(permission *Permission) {
enforcer := getEnforcer(permission)
_, err := enforcer.RemoveFilteredPolicy(0, permission.GetId())
if err != nil {
panic(err)
}
}
func GetPermissionsByUser(userId string) []*Permission {
permissions := []*Permission{}
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&permissions)
if err != nil {
panic(err)
}
return permissions
}

View File

@ -30,7 +30,7 @@ func TestProduct(t *testing.T) {
product := GetProduct("admin/product_123") product := GetProduct("admin/product_123")
provider := getProvider(product.Owner, "provider_pay_alipay") provider := getProvider(product.Owner, "provider_pay_alipay")
cert := getCert(product.Owner, "cert-pay-alipay") cert := getCert(product.Owner, "cert-pay-alipay")
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey) pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
paymentName := util.GenerateTimeId() paymentName := util.GenerateTimeId()
returnUrl := "" returnUrl := ""

View File

@ -214,7 +214,7 @@ func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
} }
} }
pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey) pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.Certificate, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
if pProvider == nil { if pProvider == nil {
return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type) return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
} }

View File

@ -33,6 +33,15 @@ func (application *Application) GetProviderItem(providerName string) *ProviderIt
return nil return nil
} }
func (application *Application) GetProviderItemByType(providerType string) *ProviderItem {
for _, item := range application.Providers {
if item.Provider.Type == providerType {
return item
}
}
return nil
}
func (pi *ProviderItem) IsProviderVisible() bool { func (pi *ProviderItem) IsProviderVisible() bool {
if pi.Provider == nil { if pi.Provider == nil {
return false return false

View File

@ -121,3 +121,13 @@ func DeleteRole(role *Role) bool {
func (role *Role) GetId() string { func (role *Role) GetId() string {
return fmt.Sprintf("%s/%s", role.Owner, role.Name) return fmt.Sprintf("%s/%s", role.Owner, role.Name)
} }
func GetRolesByUser(userId string) []*Role {
roles := []*Role{}
err := adapter.Engine.Where("users like ?", "%"+userId+"%").Find(&roles)
if err != nil {
panic(err)
}
return roles
}

View File

@ -36,7 +36,7 @@ import (
) )
//returns a saml2 response //returns a saml2 response
func NewSamlResponse(user *User, host string, publicKey string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) { func NewSamlResponse(user *User, host string, certificate string, destination string, iss string, requestId string, redirectUri []string) (*etree.Element, error) {
samlResponse := &etree.Element{ samlResponse := &etree.Element{
Space: "samlp", Space: "samlp",
Tag: "Response", Tag: "Response",
@ -177,8 +177,8 @@ type Attribute struct {
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) { func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
//_, originBackend := getOriginFromHost(host) //_, originBackend := getOriginFromHost(host)
cert := getCertByApplication(application) cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.PublicKey)) block, _ := pem.Decode([]byte(cert.Certificate))
publicKey := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
origin := beego.AppConfig.String("origin") origin := beego.AppConfig.String("origin")
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
@ -199,7 +199,7 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
KeyInfo: KeyInfo{ KeyInfo: KeyInfo{
X509Data: X509Data{ X509Data: X509Data{
X509Certificate: X509Certificate{ X509Certificate: X509Certificate{
Cert: publicKey, Cert: certificate,
}, },
}, },
}, },
@ -248,18 +248,18 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
return "", "", fmt.Errorf("err: invalid issuer url") return "", "", fmt.Errorf("err: invalid issuer url")
} }
// get public key string // get certificate string
cert := getCertByApplication(application) cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.PublicKey)) block, _ := pem.Decode([]byte(cert.Certificate))
publicKey := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
_, originBackend := getOriginFromHost(host) _, originBackend := getOriginFromHost(host)
// build signedResponse // build signedResponse
samlResponse, _ := NewSamlResponse(user, originBackend, publicKey, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris) samlResponse, _ := NewSamlResponse(user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
randomKeyStore := &X509Key{ randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey, PrivateKey: cert.PrivateKey,
X509Certificate: publicKey, X509Certificate: certificate,
} }
ctx := dsig.NewDefaultSigningContext(randomKeyStore) ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1 ctx.Hash = crypto.SHA1

View File

@ -17,6 +17,7 @@ package object
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"net/url"
"strings" "strings"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -42,8 +43,19 @@ func getProviderEndpoint(provider *Provider) string {
return endpoint return endpoint
} }
func escapePath(path string) string {
tokens := strings.Split(path, "/")
if len(tokens) > 0 {
tokens[len(tokens)-1] = url.QueryEscape(tokens[len(tokens)-1])
}
res := strings.Join(tokens, "/")
return res
}
func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) { func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), fullFilePath) escapedPath := escapePath(fullFilePath)
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
host := "" host := ""
if provider.Type != "Local File System" { if provider.Type != "Local File System" {
@ -60,9 +72,9 @@ func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
host = fmt.Sprintf("%s/%s", host, provider.Bucket) host = fmt.Sprintf("%s/%s", host, provider.Bucket)
} }
fileUrl := util.UrlJoin(host, objectKey) fileUrl := util.UrlJoin(host, escapePath(objectKey))
if hasTimestamp { if hasTimestamp {
fileUrl = fmt.Sprintf("%s?t=%s", util.UrlJoin(host, objectKey), util.GetCurrentUnixTime()) fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime())
} }
return fileUrl, objectKey return fileUrl, objectKey

View File

@ -22,7 +22,7 @@ import (
func (syncer *Syncer) syncUsers() { func (syncer *Syncer) syncUsers() {
fmt.Printf("Running syncUsers()..\n") fmt.Printf("Running syncUsers()..\n")
users, userMap := syncer.getUserMap() users, userMap, userNameMap := syncer.getUserMap()
oUsers, oUserMap, err := syncer.getOriginalUserMap() oUsers, oUserMap, err := syncer.getOriginalUserMap()
if err != nil { if err != nil {
fmt.Printf(err.Error()) fmt.Printf(err.Error())
@ -44,9 +44,11 @@ func (syncer *Syncer) syncUsers() {
for _, oUser := range oUsers { for _, oUser := range oUsers {
id := oUser.Id id := oUser.Id
if _, ok := userMap[id]; !ok { if _, ok := userMap[id]; !ok {
newUser := syncer.createUserFromOriginalUser(oUser, affiliationMap) if _, ok := userNameMap[oUser.Name]; !ok {
fmt.Printf("New user: %v\n", newUser) newUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
newUsers = append(newUsers, newUser) fmt.Printf("New user: %v\n", newUser)
newUsers = append(newUsers, newUser)
}
} else { } else {
user := userMap[id] user := userMap[id]
oHash := syncer.calculateHash(oUser) oHash := syncer.calculateHash(oUser)

View File

@ -151,6 +151,8 @@ func (syncer *Syncer) initAdapter() {
var dataSourceName string var dataSourceName string
if syncer.DatabaseType == "mssql" { if syncer.DatabaseType == "mssql" {
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database) dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else if syncer.DatabaseType == "postgres" {
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
} else { } else {
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port) dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
} }

View File

@ -173,16 +173,21 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
} }
for _, tableColumn := range syncer.TableColumns { for _, tableColumn := range syncer.TableColumns {
tableColumnName := tableColumn.Name
if syncer.Type == "Keycloak" && syncer.DatabaseType == "postgres" {
tableColumnName = strings.ToLower(tableColumnName)
}
value := "" value := ""
if strings.Contains(tableColumn.Name, "+") { if strings.Contains(tableColumnName, "+") {
names := strings.Split(tableColumn.Name, "+") names := strings.Split(tableColumnName, "+")
var values []string var values []string
for _, name := range names { for _, name := range names {
values = append(values, result[strings.Trim(name, " ")]) values = append(values, result[strings.Trim(name, " ")])
} }
value = strings.Join(values, " ") value = strings.Join(values, " ")
} else { } else {
value = result[tableColumn.Name] value = result[tableColumnName]
} }
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value) syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value)
} }
@ -198,7 +203,7 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
originalUser.PasswordSalt = credential.Salt originalUser.PasswordSalt = credential.Salt
} }
// query and set signup application from user group table // query and set signup application from user group table
sql = fmt.Sprintf("select name from keycloak_group where id = " + sql = fmt.Sprintf("select name from keycloak_group where id = "+
"(select group_id as gid from user_group_membership where user_id = '%s')", originalUser.Id) "(select group_id as gid from user_group_membership where user_id = '%s')", originalUser.Id)
groupResult, _ := syncer.Adapter.Engine.QueryString(sql) groupResult, _ := syncer.Adapter.Engine.QueryString(sql)
if len(groupResult) > 0 { if len(groupResult) > 0 {
@ -209,7 +214,12 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
tm := time.Unix(i/int64(1000), 0) tm := time.Unix(i/int64(1000), 0)
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00") originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
// enable // enable
originalUser.IsForbidden = !(result["ENABLED"] == "\x01") value, ok := result["ENABLED"]
if ok {
originalUser.IsForbidden = !util.ParseBool(value)
} else {
originalUser.IsForbidden = !util.ParseBool(result["enabled"])
}
} }
users = append(users, originalUser) users = append(users, originalUser)

View File

@ -19,12 +19,15 @@ func (syncer *Syncer) getUsers() []*User {
return users return users
} }
func (syncer *Syncer) getUserMap() ([]*User, map[string]*User) { func (syncer *Syncer) getUserMap() ([]*User, map[string]*User, map[string]*User) {
users := syncer.getUsers() users := syncer.getUsers()
m := map[string]*User{} m1 := map[string]*User{}
m2 := map[string]*User{}
for _, user := range users { for _, user := range users {
m[user.Id] = user m1[user.Id] = user
m2[user.Name] = user
} }
return users, m
return users, m1, m2
} }

View File

@ -241,11 +241,11 @@ func GetValidationBySaml(samlRequest string, host string) (string, string, error
samlResponse := NewSamlResponse11(user, request.RequestID, host) samlResponse := NewSamlResponse11(user, request.RequestID, host)
cert := getCertByApplication(application) cert := getCertByApplication(application)
block, _ := pem.Decode([]byte(cert.PublicKey)) block, _ := pem.Decode([]byte(cert.Certificate))
publicKey := base64.StdEncoding.EncodeToString(block.Bytes) certificate := base64.StdEncoding.EncodeToString(block.Bytes)
randomKeyStore := &X509Key{ randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey, PrivateKey: cert.PrivateKey,
X509Certificate: publicKey, X509Certificate: certificate,
} }
ctx := dsig.NewDefaultSigningContext(randomKeyStore) ctx := dsig.NewDefaultSigningContext(randomKeyStore)

View File

@ -129,13 +129,13 @@ func ParseJwtToken(token string, cert *Cert) (*Claims, error) {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
// RSA public key // RSA certificate
publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.PublicKey)) certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return publicKey, nil return certificate, nil
}) })
if t != nil { if t != nil {

View File

@ -23,10 +23,10 @@ import (
func TestGenerateRsaKeys(t *testing.T) { func TestGenerateRsaKeys(t *testing.T) {
fileId := "token_jwt_key" fileId := "token_jwt_key"
publicKey, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization") certificate, privateKey := generateRsaKeys(4096, 20, "Casdoor Cert", "Casdoor Organization")
// Write certificate (aka public key) to file. // Write certificate (aka certificate) to file.
util.WriteStringToPath(publicKey, fmt.Sprintf("%s.pem", fileId)) util.WriteStringToPath(certificate, fmt.Sprintf("%s.pem", fileId))
// Write private key to file. // Write private key to file.
util.WriteStringToPath(privateKey, fmt.Sprintf("%s.key", fileId)) util.WriteStringToPath(privateKey, fmt.Sprintf("%s.key", fileId))

View File

@ -20,6 +20,7 @@ import (
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/duo-labs/webauthn/webauthn"
"xorm.io/core" "xorm.io/core"
) )
@ -72,7 +73,7 @@ type User struct {
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"` LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"` LastSigninIp string `xorm:"varchar(100)" json:"lastSigninIp"`
Github string `xorm:"varchar(100)" json:"github"` GitHub string `xorm:"github varchar(100)" json:"github"`
Google string `xorm:"varchar(100)" json:"google"` Google string `xorm:"varchar(100)" json:"google"`
QQ string `xorm:"qq varchar(100)" json:"qq"` QQ string `xorm:"qq varchar(100)" json:"qq"`
WeChat string `xorm:"wechat varchar(100)" json:"wechat"` WeChat string `xorm:"wechat varchar(100)" json:"wechat"`
@ -96,11 +97,16 @@ type User struct {
Steam string `xorm:"steam varchar(100)" json:"steam"` Steam string `xorm:"steam varchar(100)" json:"steam"`
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"` Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Okta string `xorm:"okta varchar(100)" json:"okta"` Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin vachar(100)" json:"douyin"` Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
Custom string `xorm:"custom varchar(100)" json:"custom"` Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"` Properties map[string]string `json:"properties"`
Roles []*Role `json:"roles"`
Permissions []*Permission `json:"permissions"`
} }
type Userinfo struct { type Userinfo struct {
@ -267,6 +273,42 @@ func GetUserByEmail(owner string, email string) *User {
} }
} }
func GetUserByPhone(owner string, phone string) *User {
if owner == "" || phone == "" {
return nil
}
user := User{Owner: owner, Phone: phone}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}
func GetUserByUserId(owner string, userId string) *User {
if owner == "" || userId == "" {
return nil
}
user := User{Owner: owner, Id: userId}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}
func GetUser(id string) *User { func GetUser(id string) *User {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
return getUser(owner, name) return getUser(owner, name)
@ -326,9 +368,11 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
} }
if len(columns) == 0 { if len(columns) == 0 {
columns = []string{"owner", "display_name", "avatar", columns = []string{
"owner", "display_name", "avatar",
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application", "location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties"} "is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials",
}
} }
if isGlobalAdmin { if isGlobalAdmin {
columns = append(columns, "name", "email", "phone") columns = append(columns, "name", "email", "phone")
@ -395,10 +439,10 @@ func AddUsers(users []*User) bool {
return false return false
} }
//organization := GetOrganizationByUser(users[0]) // organization := GetOrganizationByUser(users[0])
for _, user := range users { for _, user := range users {
// this function is only used for syncer or batch upload, so no need to encrypt the password // this function is only used for syncer or batch upload, so no need to encrypt the password
//user.UpdateUserPassword(organization) // user.UpdateUserPassword(organization)
user.UpdateUserHash() user.UpdateUserHash()
user.PreHash = user.Hash user.PreHash = user.Hash

View File

@ -37,11 +37,11 @@ func TestSyncAvatarsFromGitHub(t *testing.T) {
users := GetGlobalUsers() users := GetGlobalUsers()
for _, user := range users { for _, user := range users {
if user.Github == "" { if user.GitHub == "" {
continue continue
} }
user.Avatar = fmt.Sprintf("https://avatars.githubusercontent.com/%s", user.Github) user.Avatar = fmt.Sprintf("https://avatars.githubusercontent.com/%s", user.GitHub)
updateUserColumn("avatar", user) updateUserColumn("avatar", user)
} }
} }

View File

@ -106,6 +106,10 @@ func setUserProperty(user *User, field string, value string) {
if value == "" { if value == "" {
delete(user.Properties, field) delete(user.Properties, field)
} else { } else {
if user.Properties == nil {
user.Properties = make(map[string]string)
}
user.Properties[field] = value user.Properties[field] = value
} }
} }

102
object/user_webauthn.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2022 The casbin 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 (
"encoding/base64"
"net/url"
"strings"
"github.com/astaxie/beego"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
func GetWebAuthnObject(host string) *webauthn.WebAuthn {
var err error
origin := beego.AppConfig.String("origin")
if origin == "" {
_, origin = getOriginFromHost(host)
}
localUrl, err := url.Parse(origin)
if err != nil {
panic("error when parsing origin:" + err.Error())
}
webAuthn, err := webauthn.New(&webauthn.Config{
RPDisplayName: beego.AppConfig.String("appname"), // Display Name for your site
RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array
RPOrigin: origin, // The origin URL for WebAuthn requests
// RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
})
if err != nil {
panic(err)
}
return webAuthn
}
// implementation of webauthn.User interface
func (u *User) WebAuthnID() []byte {
return []byte(u.GetId())
}
func (u *User) WebAuthnName() string {
return u.Name
}
func (u *User) WebAuthnDisplayName() string {
return u.DisplayName
}
func (u *User) WebAuthnCredentials() []webauthn.Credential {
return u.WebauthnCredentials
}
func (u *User) WebAuthnIcon() string {
return u.Avatar
}
// CredentialExcludeList returns a CredentialDescriptor array filled with all the user's credentials
func (u *User) CredentialExcludeList() []protocol.CredentialDescriptor {
credentials := u.WebAuthnCredentials()
credentialExcludeList := []protocol.CredentialDescriptor{}
for _, cred := range credentials {
descriptor := protocol.CredentialDescriptor{
Type: protocol.PublicKeyCredentialType,
CredentialID: cred.ID,
}
credentialExcludeList = append(credentialExcludeList, descriptor)
}
return credentialExcludeList
}
func (u *User) AddCredentials(credential webauthn.Credential, isGlobalAdmin bool) bool {
u.WebauthnCredentials = append(u.WebauthnCredentials, credential)
return UpdateUser(u.GetId(), u, []string{"webauthnCredentials"}, isGlobalAdmin)
}
func (u *User) DeleteCredentials(credentialIdBase64 string) bool {
for i, credential := range u.WebauthnCredentials {
if base64.StdEncoding.EncodeToString(credential.ID) == credentialIdBase64 {
u.WebauthnCredentials = append(u.WebauthnCredentials[0:i], u.WebauthnCredentials[i+1:]...)
return UpdateUserForAllFields(u.GetId(), u)
}
}
return false
}

View File

@ -28,7 +28,7 @@ type AlipayPaymentProvider struct {
Client *alipay.Client Client *alipay.Client
} }
func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider { func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) *AlipayPaymentProvider {
pp := &AlipayPaymentProvider{} pp := &AlipayPaymentProvider{}
client, err := alipay.NewClient(appId, appPrivateKey, true) client, err := alipay.NewClient(appId, appPrivateKey, true)
@ -36,7 +36,7 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
panic(err) panic(err)
} }
err = client.SetCertSnByContent([]byte(appPublicKey), []byte(authorityRootPublicKey), []byte(authorityPublicKey)) err = client.SetCertSnByContent([]byte(appCertificate), []byte(authorityRootPublicKey), []byte(authorityPublicKey))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -22,9 +22,9 @@ type PaymentProvider interface {
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
} }
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider { func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appCertificate string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {
if typ == "Alipay" { if typ == "Alipay" {
return NewAlipayPaymentProvider(appId, appPublicKey, appPrivateKey, authorityPublicKey, authorityRootPublicKey) return NewAlipayPaymentProvider(appId, appCertificate, appPrivateKey, authorityPublicKey, authorityRootPublicKey)
} else if typ == "GC" { } else if typ == "GC" {
return NewGcPaymentProvider(appId, clientSecret, host) return NewGcPaymentProvider(appId, clientSecret, host)
} }

View File

@ -109,6 +109,10 @@ func getUrlPath(urlPath string) string {
return "/api/login/oauth" return "/api/login/oauth"
} }
if strings.HasPrefix(urlPath, "/api/webauthn") {
return "/api/webauthn"
}
return urlPath return urlPath
} }
@ -118,6 +122,10 @@ func AuthzFilter(ctx *context.Context) {
urlPath := getUrlPath(ctx.Request.URL.Path) urlPath := getUrlPath(ctx.Request.URL.Path)
objOwner, objName := getObject(ctx) objOwner, objName := getObject(ctx)
if strings.HasPrefix(urlPath, "/api/notify-payment") {
urlPath = "/api/notify-payment"
}
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName) isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName)
result := "deny" result := "deny"

View File

@ -48,7 +48,7 @@ func initAPI() {
beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup") beego.Router("/api/signup", &controllers.ApiController{}, "POST:Signup")
beego.Router("/api/login", &controllers.ApiController{}, "POST:Login") beego.Router("/api/login", &controllers.ApiController{}, "POST:Login")
beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin") beego.Router("/api/get-app-login", &controllers.ApiController{}, "GET:GetApplicationLogin")
beego.Router("/api/logout", &controllers.ApiController{}, "POST:Logout") beego.Router("/api/logout", &controllers.ApiController{}, "GET,POST:Logout")
beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount") beego.Router("/api/get-account", &controllers.ApiController{}, "GET:GetAccount")
beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo") beego.Router("/api/userinfo", &controllers.ApiController{}, "GET:GetUserinfo")
beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink") beego.Router("/api/unlink", &controllers.ApiController{}, "POST:Unlink")
@ -191,4 +191,9 @@ func initAPI() {
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate") beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate") beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "Get:WebAuthnSignupBegin")
beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "Post:WebAuthnSignupFinish")
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin")
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish")
} }

View File

@ -2418,6 +2418,21 @@
} }
}, },
"/api/logout": { "/api/logout": {
"get": {
"tags": [
"Login API"
],
"description": "logout the current user",
"operationId": "ApiController.Logout",
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
},
"post": { "post": {
"tags": [ "tags": [
"Login API" "Login API"
@ -3096,14 +3111,120 @@
], ],
"operationId": "ApiController.VerifyCaptcha" "operationId": "ApiController.VerifyCaptcha"
} }
},
"/api/webauthn/signin/begin": {
"get": {
"tags": [
"Login API"
],
"description": "WebAuthn Login Flow 1st stage",
"operationId": "ApiController.WebAuthnSigninBegin",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "owner",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "name",
"description": "name",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The CredentialAssertion object",
"schema": {
"$ref": "#/definitions/protocol.CredentialAssertion"
}
}
}
}
},
"/api/webauthn/signin/finish": {
"post": {
"tags": [
"Login API"
],
"description": "WebAuthn Login Flow 2nd stage",
"operationId": "ApiController.WebAuthnSigninBegin",
"parameters": [
{
"in": "body",
"name": "body",
"description": "authenticator assertion Response",
"required": true,
"schema": {
"$ref": "#/definitions/protocol.CredentialAssertionResponse"
}
}
],
"responses": {
"200": {
"description": "\"The Response object\"",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
}
},
"/api/webauthn/signup/begin": {
"get": {
"tags": [
"User API"
],
"description": "WebAuthn Registration Flow 1st stage",
"operationId": "ApiController.WebAuthnSignupBegin",
"responses": {
"200": {
"description": "The CredentialCreationOptions object",
"schema": {
"$ref": "#/definitions/protocol.CredentialCreation"
}
}
}
}
},
"/api/webauthn/signup/finish": {
"post": {
"tags": [
"User API"
],
"description": "WebAuthn Registration Flow 2nd stage",
"operationId": "ApiController.WebAuthnSignupFinish",
"parameters": [
{
"in": "body",
"name": "body",
"description": "authenticator attestation Response",
"required": true,
"schema": {
"$ref": "#/definitions/protocol.CredentialCreationResponse"
}
}
],
"responses": {
"200": {
"description": "\"The Response object\"",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
"2127.0xc000398090.false": { "2127.0xc000427560.false": {
"title": "false", "title": "false",
"type": "object" "type": "object"
}, },
"2161.0xc0003980c0.false": { "2161.0xc000427590.false": {
"title": "false", "title": "false",
"type": "object" "type": "object"
}, },
@ -3221,10 +3342,10 @@
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {
"$ref": "#/definitions/2127.0xc000398090.false" "$ref": "#/definitions/2127.0xc000427560.false"
}, },
"data2": { "data2": {
"$ref": "#/definitions/2161.0xc0003980c0.false" "$ref": "#/definitions/2161.0xc000427590.false"
}, },
"msg": { "msg": {
"type": "string" "type": "string"
@ -3329,12 +3450,18 @@
"enablePassword": { "enablePassword": {
"type": "boolean" "type": "boolean"
}, },
"enableSamlCompress": {
"type": "boolean"
},
"enableSignUp": { "enableSignUp": {
"type": "boolean" "type": "boolean"
}, },
"enableSigninSession": { "enableSigninSession": {
"type": "boolean" "type": "boolean"
}, },
"enableWebAuthn": {
"type": "boolean"
},
"expireInHours": { "expireInHours": {
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
@ -3444,7 +3571,7 @@
"privateKey": { "privateKey": {
"type": "string" "type": "string"
}, },
"publicKey": { "certificate": {
"type": "string" "type": "string"
}, },
"scope": { "scope": {
@ -4507,6 +4634,12 @@
"updatedTime": { "updatedTime": {
"type": "string" "type": "string"
}, },
"webauthnCredentials": {
"type": "array",
"items": {
"$ref": "#/definitions/webauthn.Credential"
}
},
"wechat": { "wechat": {
"type": "string" "type": "string"
}, },
@ -4596,6 +4729,26 @@
} }
} }
}, },
"protocol.CredentialAssertion": {
"title": "CredentialAssertion",
"type": "object"
},
"protocol.CredentialAssertionResponse": {
"title": "CredentialAssertionResponse",
"type": "object"
},
"protocol.CredentialCreation": {
"title": "CredentialCreation",
"type": "object"
},
"protocol.CredentialCreationResponse": {
"title": "CredentialCreationResponse",
"type": "object"
},
"webauthn.Credential": {
"title": "Credential",
"type": "object"
},
"xorm.Engine": { "xorm.Engine": {
"title": "Engine", "title": "Engine",
"type": "object" "type": "object"

View File

@ -1584,6 +1584,16 @@ paths:
schema: schema:
$ref: '#/definitions/object.TokenError' $ref: '#/definitions/object.TokenError'
/api/logout: /api/logout:
get:
tags:
- Login API
description: logout the current user
operationId: ApiController.Logout
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
post: post:
tags: tags:
- Login API - Login API
@ -2028,11 +2038,80 @@ paths:
tags: tags:
- Verification API - Verification API
operationId: ApiController.VerifyCaptcha operationId: ApiController.VerifyCaptcha
/api/webauthn/signin/begin:
get:
tags:
- Login API
description: WebAuthn Login Flow 1st stage
operationId: ApiController.WebAuthnSigninBegin
parameters:
- in: query
name: owner
description: owner
required: true
type: string
- in: query
name: name
description: name
required: true
type: string
responses:
"200":
description: The CredentialAssertion object
schema:
$ref: '#/definitions/protocol.CredentialAssertion'
/api/webauthn/signin/finish:
post:
tags:
- Login API
description: WebAuthn Login Flow 2nd stage
operationId: ApiController.WebAuthnSigninBegin
parameters:
- in: body
name: body
description: authenticator assertion Response
required: true
schema:
$ref: '#/definitions/protocol.CredentialAssertionResponse'
responses:
"200":
description: '"The Response object"'
schema:
$ref: '#/definitions/Response'
/api/webauthn/signup/begin:
get:
tags:
- User API
description: WebAuthn Registration Flow 1st stage
operationId: ApiController.WebAuthnSignupBegin
responses:
"200":
description: The CredentialCreationOptions object
schema:
$ref: '#/definitions/protocol.CredentialCreation'
/api/webauthn/signup/finish:
post:
tags:
- User API
description: WebAuthn Registration Flow 2nd stage
operationId: ApiController.WebAuthnSignupFinish
parameters:
- in: body
name: body
description: authenticator attestation Response
required: true
schema:
$ref: '#/definitions/protocol.CredentialCreationResponse'
responses:
"200":
description: '"The Response object"'
schema:
$ref: '#/definitions/Response'
definitions: definitions:
2127.0xc000398090.false: 2127.0xc000427560.false:
title: "false" title: "false"
type: object type: object
2161.0xc0003980c0.false: 2161.0xc000427590.false:
title: "false" title: "false"
type: object type: object
Response: Response:
@ -2113,9 +2192,9 @@ definitions:
type: object type: object
properties: properties:
data: data:
$ref: '#/definitions/2127.0xc000398090.false' $ref: '#/definitions/2127.0xc000427560.false'
data2: data2:
$ref: '#/definitions/2161.0xc0003980c0.false' $ref: '#/definitions/2161.0xc000427590.false'
msg: msg:
type: string type: string
name: name:
@ -2185,10 +2264,14 @@ definitions:
type: boolean type: boolean
enablePassword: enablePassword:
type: boolean type: boolean
enableSamlCompress:
type: boolean
enableSignUp: enableSignUp:
type: boolean type: boolean
enableSigninSession: enableSigninSession:
type: boolean type: boolean
enableWebAuthn:
type: boolean
expireInHours: expireInHours:
type: integer type: integer
format: int64 format: int64
@ -2263,7 +2346,7 @@ definitions:
type: string type: string
privateKey: privateKey:
type: string type: string
publicKey: certificate:
type: string type: string
scope: scope:
type: string type: string
@ -2977,6 +3060,10 @@ definitions:
type: string type: string
updatedTime: updatedTime:
type: string type: string
webauthnCredentials:
type: array
items:
$ref: '#/definitions/webauthn.Credential'
wechat: wechat:
type: string type: string
wecom: wecom:
@ -3035,6 +3122,21 @@ definitions:
type: string type: string
url: url:
type: string type: string
protocol.CredentialAssertion:
title: CredentialAssertion
type: object
protocol.CredentialAssertionResponse:
title: CredentialAssertionResponse
type: object
protocol.CredentialCreation:
title: CredentialCreation
type: object
protocol.CredentialCreationResponse:
title: CredentialCreationResponse
type: object
webauthn.Credential:
title: Credential
type: object
xorm.Engine: xorm.Engine:
title: Engine title: Engine
type: object type: object

View File

@ -52,8 +52,10 @@ func ParseFloat(s string) float64 {
} }
func ParseBool(s string) bool { func ParseBool(s string) bool {
if s == "\x01" { if s == "\x01" || s == "true" {
return true return true
} else if s == "false" {
return false
} }
i := ParseInt(s) i := ParseInt(s)

1
web/.env Normal file
View File

@ -0,0 +1 @@
PUBLIC_URL=.

View File

@ -45,6 +45,10 @@
"curly": ["error", "all"], "curly": ["error", "all"],
"brace-style": ["error", "1tbs", { "allowSingleLine": true }], "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"no-mixed-spaces-and-tabs": "error", "no-mixed-spaces-and-tabs": "error",
"sort-imports": ["error", {
"ignoreDeclarationSort": true
}],
"react/prop-types": "off", "react/prop-types": "off",
"react/display-name": "off", "react/display-name": "off",

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd"; import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
@ -86,12 +86,15 @@ class AccountTable extends React.Component {
{name: "Bio", displayName: i18next.t("user:Bio")}, {name: "Bio", displayName: i18next.t("user:Bio")},
{name: "Tag", displayName: i18next.t("user:Tag")}, {name: "Tag", displayName: i18next.t("user:Tag")},
{name: "Signup application", displayName: i18next.t("general:Signup application")}, {name: "Signup application", displayName: i18next.t("general:Signup application")},
{name: "Roles", displayName: i18next.t("general:Roles")},
{name: "Permissions", displayName: i18next.t("general:Permissions")},
{name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")}, {name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")},
{name: "Properties", displayName: i18next.t("user:Properties")}, {name: "Properties", displayName: i18next.t("user:Properties")},
{name: "Is admin", displayName: i18next.t("user:Is admin")}, {name: "Is admin", displayName: i18next.t("user:Is admin")},
{name: "Is global admin", displayName: i18next.t("user:Is global admin")}, {name: "Is global admin", displayName: i18next.t("user:Is global admin")},
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")}, {name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")}, {name: "Is deleted", displayName: i18next.t("user:Is deleted")},
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
]; ];
const getItemDisplayName = (text) => { const getItemDisplayName = (text) => {

View File

@ -17,7 +17,7 @@ import "./App.less";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons"; import {DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
import {Avatar, BackTop, Dropdown, Layout, Menu, Card, Result, Button} from "antd"; import {Avatar, BackTop, Button, Card, Dropdown, Layout, Menu, Result} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom"; import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage"; import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage"; import OrganizationEditPage from "./OrganizationEditPage";
@ -235,6 +235,8 @@ class App extends Component {
AuthBackend.logout() AuthBackend.logout()
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const owner = this.state.account.owner;
this.setState({ this.setState({
account: null account: null
}); });
@ -243,7 +245,9 @@ class App extends Component {
let redirectUri = res.data2; let redirectUri = res.data2;
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") { if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
Setting.goToLink(redirectUri); Setting.goToLink(redirectUri);
}else{ } else if (owner !== "built-in") {
Setting.goToLink(`${window.location.origin}/login/${owner}`);
} else {
Setting.goToLinkSoft(this, "/"); Setting.goToLinkSoft(this, "/");
} }
} else { } else {
@ -287,10 +291,12 @@ class App extends Component {
<Menu onClick={this.handleRightDropdownClick.bind(this)}> <Menu onClick={this.handleRightDropdownClick.bind(this)}>
<Menu.Item key="/account"> <Menu.Item key="/account">
<SettingOutlined /> <SettingOutlined />
&nbsp;
{i18next.t("account:My Account")} {i18next.t("account:My Account")}
</Menu.Item> </Menu.Item>
<Menu.Item key="/logout"> <Menu.Item key="/logout">
<LogoutOutlined /> <LogoutOutlined />
&nbsp;
{i18next.t("account:Logout")} {i18next.t("account:Logout")}
</Menu.Item> </Menu.Item>
</Menu> </Menu>
@ -667,7 +673,9 @@ class App extends Component {
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} /> <Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />)} /> <Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />)} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} /> <Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
<Route exact path="/signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} /> <Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} />
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} /> <Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} /> <Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} /> <Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />

View File

@ -353,6 +353,16 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable WebAuthn signin"), i18next.t("application:Enable WebAuthn signin - Tooltip"))} :
</Col>
<Col span={1} >
<Switch checked={this.state.application.enableWebAuthn} onChange={checked => {
this.updateApplicationField("enableWebAuthn", checked);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} :

View File

@ -30,6 +30,7 @@ class BaseListPage extends React.Component {
loading: false, loading: false,
searchText: "", searchText: "",
searchedColumn: "", searchedColumn: "",
isAuthorized: true,
}; };
} }

View File

@ -164,25 +164,25 @@ class CertEditPage extends React.Component {
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("cert:Public key"), i18next.t("cert:Public key - Tooltip"))} : {Setting.getLabel(i18next.t("cert:Certificate"), i18next.t("cert:Certificate - Tooltip"))} :
</Col> </Col>
<Col span={9} > <Col span={9} >
<Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => { <Button style={{marginRight: "10px", marginBottom: "10px"}} onClick={() => {
copy(this.state.cert.publicKey); copy(this.state.cert.certificate);
Setting.showMessage("success", i18next.t("cert:Public key copied to clipboard successfully")); Setting.showMessage("success", i18next.t("cert:Certificate copied to clipboard successfully"));
}} }}
> >
{i18next.t("cert:Copy public key")} {i18next.t("cert:Copy certificate")}
</Button> </Button>
<Button type="primary" onClick={() => { <Button type="primary" onClick={() => {
const blob = new Blob([this.state.cert.publicKey], {type: "text/plain;charset=utf-8"}); const blob = new Blob([this.state.cert.certificate], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "token_jwt_key.pem"); FileSaver.saveAs(blob, "token_jwt_key.pem");
}} }}
> >
{i18next.t("cert:Download public key")} {i18next.t("cert:Download certificate")}
</Button> </Button>
<TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.publicKey} onChange={e => { <TextArea autoSize={{minRows: 30, maxRows: 30}} value={this.state.cert.certificate} onChange={e => {
this.updateCertField("publicKey", e.target.value); this.updateCertField("certificate", e.target.value);
}} /> }} />
</Col> </Col>
<Col span={1} /> <Col span={1} />

View File

@ -34,7 +34,7 @@ class CertListPage extends BaseListPage {
cryptoAlgorithm: "RS256", cryptoAlgorithm: "RS256",
bitSize: 4096, bitSize: 4096,
expireInYears: 20, expireInYears: 20,
publicKey: "", certificate: "",
privateKey: "", privateKey: "",
}; };
} }

View File

@ -16,7 +16,7 @@ import React, {useState} from "react";
import Cropper from "react-cropper"; import Cropper from "react-cropper";
import "cropperjs/dist/cropper.css"; import "cropperjs/dist/cropper.css";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {Button, Row, Col, Modal} from "antd"; import {Button, Col, Modal, Row} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import * as ResourceBackend from "./backend/ResourceBackend"; import * as ResourceBackend from "./backend/ResourceBackend";

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Col, Row, Table, Popconfirm} from "antd"; import {Button, Col, Popconfirm, Row, Table} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend"; import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next"; import i18next from "i18next";

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from "antd"; import {Button, Popconfirm, Result, Switch, Table} from "antd";
import moment from "moment"; import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
@ -57,6 +57,8 @@ class OrganizationListPage extends BaseListPage {
{name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"}, {name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"}, {name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"}, {name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
@ -235,6 +237,17 @@ class OrganizationListPage extends BaseListPage {
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total), showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
}; };
if (!this.state.isAuthorized) {
return (
<Result
status="403"
title="403 Unauthorized"
subTitle={i18next.t("general:Sorry, you do not have permission to access this page.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
/>
);
}
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={organizations} rowKey="name" size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={organizations} rowKey="name" size="middle" bordered pagination={paginationProps}
@ -272,6 +285,13 @@ class OrganizationListPage extends BaseListPage {
searchText: params.searchText, searchText: params.searchText,
searchedColumn: params.searchedColumn, searchedColumn: params.searchedColumn,
}); });
} else {
if (res.msg.includes("Unauthorized")) {
this.setState({
loading: false,
isAuthorized: false,
});
}
} }
}); });
}; };

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {Button, Col, Modal, Row, Input} from "antd"; import {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import React from "react"; import React from "react";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";

View File

@ -21,6 +21,7 @@ import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend"; import * as RoleBackend from "./backend/RoleBackend";
import * as ModelBackend from "./backend/ModelBackend"; import * as ModelBackend from "./backend/ModelBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
const {Option} = Select; const {Option} = Select;
@ -36,6 +37,7 @@ class PermissionEditPage extends React.Component {
users: [], users: [],
roles: [], roles: [],
models: [], models: [],
resources: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
} }
@ -55,6 +57,7 @@ class PermissionEditPage extends React.Component {
this.getUsers(permission.owner); this.getUsers(permission.owner);
this.getRoles(permission.owner); this.getRoles(permission.owner);
this.getModels(permission.owner); this.getModels(permission.owner);
this.getResources(permission.owner);
}); });
} }
@ -94,6 +97,15 @@ class PermissionEditPage extends React.Component {
}); });
} }
getResources(organizationName) {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
resources: (res.msg === undefined) ? res : [],
});
});
}
parsePermissionField(key, value) { parsePermissionField(key, value) {
if ([""].includes(key)) { if ([""].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
@ -131,6 +143,8 @@ class PermissionEditPage extends React.Component {
this.getUsers(owner); this.getUsers(owner);
this.getRoles(owner); this.getRoles(owner);
this.getModels(owner);
this.getResources(owner);
})}> })}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
@ -212,6 +226,18 @@ class PermissionEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Resources"), i18next.t("permission:Resources - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: "100%"}} value={this.state.permission.resources} onChange={(value => {this.updatePermissionField("resources", value);})}>
{
this.state.resources.map((resource, index) => <Option key={index} value={`${resource.name}`}>{`${resource.name}`}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("permission:Actions"), i18next.t("permission:Actions - Tooltip"))} : {Setting.getLabel(i18next.t("permission:Actions"), i18next.t("permission:Actions - Tooltip"))} :

View File

@ -33,7 +33,7 @@ class PermissionListPage extends BaseListPage {
roles: [], roles: [],
resourceType: "Application", resourceType: "Application",
resources: ["app-built-in"], resources: ["app-built-in"],
action: "Read", actions: ["Read"],
effect: "Allow", effect: "Allow",
isEnabled: true, isEnabled: true,
}; };

View File

@ -622,7 +622,7 @@ class ProviderEditPage extends React.Component {
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:IdP"), i18next.t("provider:IdP public key"))} : {Setting.getLabel(i18next.t("provider:IdP"), i18next.t("provider:IdP certificate"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.provider.idP} onChange={e => { <Input value={this.state.provider.idP} onChange={e => {

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd"; import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {Button, Col, Modal, Row, Input} from "antd"; import {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import React from "react"; import React from "react";
import * as Setting from "./Setting"; import * as Setting from "./Setting";

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {Menu, Dropdown} from "antd"; import {Dropdown, Menu} from "antd";
import {createFromIconfontCN} from "@ant-design/icons"; import {createFromIconfontCN} from "@ant-design/icons";
import "./App.less"; import "./App.less";

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {message, Tag, Tooltip} from "antd"; import {Tag, Tooltip, message} from "antd";
import {QuestionCircleTwoTone} from "@ant-design/icons"; import {QuestionCircleTwoTone} from "@ant-design/icons";
import React from "react"; import React from "react";
import {isMobile as isMobileDevice} from "react-device-detect"; import {isMobile as isMobileDevice} from "react-device-detect";
@ -662,8 +662,8 @@ export function goToLogin(ths, application) {
return; return;
} }
if (!application.enablePassword && window.location.pathname.includes("/signup/oauth/authorize")) { if (!application.enablePassword && window.location.pathname.includes("/auto-signup/oauth/authorize")) {
const link = window.location.href.replace("/signup/oauth/authorize", "/login/oauth/authorize"); const link = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
goToLink(link); goToLink(link);
return; return;
} }
@ -685,7 +685,7 @@ export function goToSignup(ths, application) {
} }
if (!application.enablePassword && window.location.pathname.includes("/login/oauth/authorize")) { if (!application.enablePassword && window.location.pathname.includes("/login/oauth/authorize")) {
const link = window.location.href.replace("/login/oauth/authorize", "/signup/oauth/authorize"); const link = window.location.href.replace("/login/oauth/authorize", "/auto-signup/oauth/authorize");
goToLink(link); goToLink(link);
return; return;
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd"; import {Button, Col, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Select, Switch, Table, Tooltip} from "antd"; import {Button, Col, Input, Row, Select, Switch, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined, LinkOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, LinkOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Table, Tooltip} from "antd"; import {Button, Col, Input, Row, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";

View File

@ -15,6 +15,7 @@
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from "antd"; import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from "antd";
import * as UserBackend from "./backend/UserBackend"; import * as UserBackend from "./backend/UserBackend";
import * as UserWebauthnBackend from "./backend/UserWebauthnBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import {LinkOutlined} from "@ant-design/icons"; import {LinkOutlined} from "@ant-design/icons";
@ -27,6 +28,7 @@ import AffiliationSelect from "./common/AffiliationSelect";
import OAuthWidget from "./common/OAuthWidget"; import OAuthWidget from "./common/OAuthWidget";
import SamlWidget from "./common/SamlWidget"; import SamlWidget from "./common/SamlWidget";
import SelectRegionBox from "./SelectRegionBox"; import SelectRegionBox from "./SelectRegionBox";
import WebAuthnCredentialTable from "./WebauthnCredentialTable";
import {Controlled as CodeMirror} from "react-codemirror2"; import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
@ -289,7 +291,7 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
<Col span={11} > <Col span={11} >
{this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null} {this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null}
</Col> </Col>
</Row> </Row>
); );
@ -307,7 +309,7 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
<Col span={11} > <Col span={11} >
{this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null} {this.state.user.id === this.props.account?.id ? (<ResetModal application={this.state.application} disabled={disabled} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
</Col> </Col>
</Row> </Row>
); );
@ -425,6 +427,32 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if (accountItem.name === "Roles") {
return (
<Row style={{marginTop: "20px", alignItems: "center"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Roles"), i18next.t("general:Roles - Tooltip"))} :
</Col>
<Col span={22} >
{
Setting.getTags(this.state.user.roles.map(role => role.name))
}
</Col>
</Row>
);
} else if (accountItem.name === "Permissions") {
return (
<Row style={{marginTop: "20px", alignItems: "center"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Permissions"), i18next.t("general:Permissions - Tooltip"))} :
</Col>
<Col span={22} >
{
Setting.getTags(this.state.user.permissions.map(permission => permission.name))
}
</Col>
</Row>
);
} else if (accountItem.name === "3rd-party logins") { } else if (accountItem.name === "3rd-party logins") {
return ( return (
!this.isSelfOrAdmin() ? null : ( !this.isSelfOrAdmin() ? null : (
@ -438,7 +466,7 @@ class UserEditPage extends React.Component {
(this.state.application === null || this.state.user === null) ? null : ( (this.state.application === null || this.state.user === null) ? null : (
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) => this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) =>
(providerItem.provider.category === "OAuth") ? ( (providerItem.provider.category === "OAuth") ? (
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} /> <OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />
) : ( ) : (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} /> <SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />
) )
@ -516,6 +544,17 @@ class UserEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
); );
} else if(accountItem.name === "WebAuthn credentials") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:WebAuthn credentials"), i18next.t("user:WebAuthn credentials"))} :
</Col>
<Col span={22} >
<WebAuthnCredentialTable table={this.state.user.webauthnCredentials} updateTable={(table) => {this.updateUserField("webauthnCredentials", table);}} refresh={this.getUser.bind(this)} />
</Col>
</Row>
);
} }
} }

View File

@ -0,0 +1,76 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Table} from "antd";
import i18next from "i18next";
import * as UserWebauthnBackend from "./backend/UserWebauthnBackend";
import * as Setting from "./Setting";
class WebAuthnCredentialTable extends React.Component {
render() {
const columns = [
{
title: i18next.t("user:WebAuthn credentials"),
dataIndex: "ID",
key: "ID",
},
{
title: i18next.t("general:Action"),
key: "action",
render: (text, record, index) => {
return (
<Button style={{marginTop: "5px", marginBottom: "5px", marginRight: "5px"}} type="danger" onClick={() => {this.deleteRow(this.props.table, index);}}>
{i18next.t("general:Delete")}
</Button>
);
}
}
];
return (
<Table scroll={{x: "max-content"}} rowKey={"ID"} columns={columns} dataSource={this.props.table} size="middle" bordered pagination={false}
title={() => (
<div>
{i18next.t("user:WebAuthn credentials")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => {this.registerWebAuthn();}}>
{i18next.t("general:Add")}
</Button>
</div>
)}
/>
);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.props.updateTable(table);
}
registerWebAuthn() {
UserWebauthnBackend.registerWebauthnCredential().then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully added webauthn credentials");
} else {
Setting.showMessage("error", res.msg);
}
this.props.refresh();
}).catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
}
export default WebAuthnCredentialTable;

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined} from "@ant-design/icons"; import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Input, Row, Table, Tooltip} from "antd"; import {Button, Col, Input, Row, Table, Tooltip} from "antd";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";

View File

@ -62,7 +62,7 @@ class ForgetPage extends React.Component {
} else { } else {
Util.showMessage( Util.showMessage(
"error", "error",
i18next.t("forget:Unknown forgot type: ") + this.state.type i18next.t("forget:Unknown forget type: ") + this.state.type
); );
} }
} }

View File

@ -14,8 +14,9 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin} from "antd"; import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd";
import {LockOutlined, UserOutlined} from "@ant-design/icons"; import {LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Provider from "./Provider"; import * as Provider from "./Provider";
@ -49,6 +50,8 @@ import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput"; import {CountDownInput} from "../common/CountDownInput";
import BilibiliLoginButton from "./BilibiliLoginButton"; import BilibiliLoginButton from "./BilibiliLoginButton";
const {TabPane} = Tabs;
class LoginPage extends React.Component { class LoginPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -65,7 +68,9 @@ class LoginPage extends React.Component {
validEmailOrPhone: false, validEmailOrPhone: false,
validEmail: false, validEmail: false,
validPhone: false, validPhone: false,
loginMethod: "password"
}; };
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) { if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
this.state.owner = props.match?.params.owner; this.state.owner = props.match?.params.owner;
this.state.applicationName = props.match?.params.casApplicationName; this.state.applicationName = props.match?.params.casApplicationName;
@ -186,6 +191,10 @@ class LoginPage extends React.Component {
values["type"] = "saml"; values["type"] = "saml";
} }
if (this.state.owner != null) {
values["organization"] = this.state.owner;
}
AuthBackend.login(values, oAuthParams) AuthBackend.login(values, oAuthParams)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
@ -198,6 +207,7 @@ class LoginPage extends React.Component {
} else if (responseType === "code") { } else if (responseType === "code") {
const code = res.data; const code = res.data;
const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?"; const concatChar = oAuthParams?.redirectUri?.includes("?") ? "&" : "?";
const noRedirect = oAuthParams.noRedirect;
if (Setting.hasPromptPage(application)) { if (Setting.hasPromptPage(application)) {
AuthBackend.getAccount("") AuthBackend.getAccount("")
@ -219,7 +229,19 @@ class LoginPage extends React.Component {
} }
}); });
} else { } else {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`); if (noRedirect === "true") {
window.close();
const newWindow = window.open(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
if (newWindow) {
setInterval(() => {
if (!newWindow.closed) {
newWindow.close();
}
}, 1000);
}
} else {
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
}
} }
// Util.showMessage("success", `Authorization code: ${res.data}`); // Util.showMessage("success", `Authorization code: ${res.data}`);
@ -407,6 +429,7 @@ class LoginPage extends React.Component {
]} ]}
> >
</Form.Item> </Form.Item>
{this.renderMethodChoiceBox()}
<Form.Item <Form.Item
name="username" name="username"
rules={[ rules={[
@ -421,13 +444,15 @@ class LoginPage extends React.Component {
this.setState({validEmailOrPhone: false}); this.setState({validEmailOrPhone: false});
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!")); return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
} }
if (Setting.isValidPhone(this.state.username)) {
this.setState({validPhone: true});
}
if (Setting.isValidEmail(this.state.username)) {
this.setState({validEmail: true});
}
} }
if (Setting.isValidPhone(this.state.username)) {
this.setState({validPhone: true});
}
if (Setting.isValidEmail(this.state.username)) {
this.setState({validEmail: true});
}
this.setState({validEmailOrPhone: true}); this.setState({validEmailOrPhone: true});
return Promise.resolve(); return Promise.resolve();
} }
@ -446,29 +471,7 @@ class LoginPage extends React.Component {
/> />
</Form.Item> </Form.Item>
{ {
this.state.isCodeSignin ? ( this.renderPasswordOrCodeInput()
<Form.Item
name="code"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
/>
</Form.Item>
) : (
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("login:Password")}
disabled={!application.enablePassword}
/>
</Form.Item>
)
} }
<Form.Item> <Form.Item>
<Form.Item name="autoSignin" valuePropName="checked" noStyle> <Form.Item name="autoSignin" valuePropName="checked" noStyle>
@ -483,16 +486,26 @@ class LoginPage extends React.Component {
</a> </a>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Button
type="primary"
htmlType="submit"
style={{width: "100%", marginBottom: "5px"}}
disabled={!application.enablePassword}
>
{i18next.t("login:Sign In")}
</Button>
{ {
!application.enableSignUp ? null : this.renderFooter(application) this.state.loginMethod === "password" ?
(
<Button
type="primary"
htmlType="submit"
style={{width: "100%", marginBottom: "5px"}}
disabled={!application.enablePassword}
>
{i18next.t("login:Sign In")}
</Button>
) :
(
<Button type="primary" style={{width: "100%", marginBottom: "5px"}} onClick={() => this.signInWithWebAuthn()}>
{i18next.t("login:Sign in with WebAuthn")}
</Button>
)
}
{
this.renderFooter(application)
} }
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
@ -522,16 +535,12 @@ class LoginPage extends React.Component {
return this.renderProviderLogo(providerItem.provider, application, 40, 10, "big"); return this.renderProviderLogo(providerItem.provider, application, 40, 10, "big");
}) })
} }
{ <div>
!application.enableSignUp ? null : ( <br />
<div> {
<br /> this.renderFooter(application)
{ }
this.renderFooter(application) </div>
}
</div>
)
}
</div> </div>
); );
} }
@ -566,13 +575,19 @@ class LoginPage extends React.Component {
} }
</span> </span>
<span style={{float: "right"}}> <span style={{float: "right"}}>
{i18next.t("login:No account?")}&nbsp; {
<a onClick={() => { !application.enableSignUp ? null : (
sessionStorage.setItem("signinUrl", window.location.href); <>
Setting.goToSignup(this, application); {i18next.t("login:No account?")}&nbsp;
}}> <a onClick={() => {
{i18next.t("login:sign up now")} sessionStorage.setItem("signinUrl", window.location.href);
</a> Setting.goToSignup(this, application);
}}>
{i18next.t("login:sign up now")}
</a>
</>
)
}
</span> </span>
</React.Fragment> </React.Fragment>
); );
@ -624,6 +639,111 @@ class LoginPage extends React.Component {
); );
} }
signInWithWebAuthn() {
if (this.state.username === null || this.state.username === "") {
Setting.showMessage("error", "username is required for webauthn login");
return;
}
let application = this.getApplicationObj();
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}&name=${this.state.username}`, {
method: "GET",
credentials: "include"
})
.then(res => res.json())
.then((credentialRequestOptions) => {
if ("status" in credentialRequestOptions) {
Setting.showMessage("error", credentialRequestOptions.msg);
throw credentialRequestOptions.status.msg;
}
credentialRequestOptions.certificate.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.certificate.challenge);
credentialRequestOptions.certificate.allowCredentials.forEach(function(listItem) {
listItem.id = UserWebauthnBackend.webAuthnBufferDecode(listItem.id);
});
return navigator.credentials.get({
certificate: credentialRequestOptions.certificate
});
})
.then((assertion) => {
let authData = assertion.response.authenticatorData;
let clientDataJSON = assertion.response.clientDataJSON;
let rawId = assertion.rawId;
let sig = assertion.response.signature;
let userHandle = assertion.response.userHandle;
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/finish`, {
method: "POST",
credentials: "include",
body: JSON.stringify({
id: assertion.id,
rawId: UserWebauthnBackend.webAuthnBufferEncode(rawId),
type: assertion.type,
response: {
authenticatorData: UserWebauthnBackend.webAuthnBufferEncode(authData),
clientDataJSON: UserWebauthnBackend.webAuthnBufferEncode(clientDataJSON),
signature: UserWebauthnBackend.webAuthnBufferEncode(sig),
userHandle: UserWebauthnBackend.webAuthnBufferEncode(userHandle),
},
})
})
.then(res => res.json()).then((res) => {
if (res.msg === "") {
Setting.showMessage("success", "Successfully logged in with webauthn credentials");
Setting.goToLink("/");
} else {
Setting.showMessage("error", res.msg);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
});
}
renderPasswordOrCodeInput() {
let application = this.getApplicationObj();
if (this.state.loginMethod === "password") {
return this.state.isCodeSignin ? (
<Form.Item
name="code"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
>
<CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
/>
</Form.Item>
) : (
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder={i18next.t("login:Password")}
disabled={!application.enablePassword}
/>
</Form.Item>
);
}
}
renderMethodChoiceBox() {
let application = this.getApplicationObj();
if (application.enableWebAuthn) {
return (
<div>
<Tabs defaultActiveKey="password" onChange={(key) => {this.setState({loginMethod: key});}} centered>
<TabPane tab={i18next.t("login:Password")} key="password" />
<TabPane tab={"WebAuthn"} key="webAuthn" />
</Tabs>
</div>
);
}
}
render() { render() {
const application = this.getApplicationObj(); const application = this.getApplicationObj();
if (application === null) { if (application === null) {

View File

@ -117,7 +117,7 @@ class PromptPage extends React.Component {
<div> <div>
{ {
(application === null || this.state.user === null) ? null : ( (application === null || this.state.user === null) ? null : (
application?.providers.filter(providerItem => Setting.isProviderPrompted(providerItem)).map((providerItem, index) => <OAuthWidget key={providerItem.name} labelSpan={6} user={this.state.user} application={application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />) application?.providers.filter(providerItem => Setting.isProviderPrompted(providerItem)).map((providerItem, index) => <OAuthWidget key={providerItem.name} labelSpan={6} user={this.state.user} application={application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />)
) )
} }
</div> </div>

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Result, Button} from "antd"; import {Button, Result} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import {authConfig} from "./Auth"; import {authConfig} from "./Auth";
import * as Util from "./Util"; import * as Util from "./Util";

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Form, Input, Checkbox, Button, Row, Col, Result, Modal} from "antd"; import {Button, Checkbox, Col, Form, Input, Modal, Result, Row} from "antd";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import i18next from "i18next"; import i18next from "i18next";
@ -79,19 +79,28 @@ class SignupPage extends React.Component {
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
if (this.state.applicationName !== undefined) { let applicationName = this.state.applicationName;
this.getApplication(); const oAuthParams = Util.getOAuthGetParameters();
if (oAuthParams !== null) {
applicationName = oAuthParams.state;
this.setState({applicationName: oAuthParams.state});
const signinUrl = window.location.href.replace("/signup/oauth/authorize", "/login/oauth/authorize");
sessionStorage.setItem("signinUrl", signinUrl);
}
if (applicationName !== undefined) {
this.getApplication(applicationName);
} else { } else {
Util.showMessage("error", `Unknown application name: ${this.state.applicationName}`); Util.showMessage("error", `Unknown application name: ${applicationName}`);
} }
} }
getApplication() { getApplication(applicationName) {
if (this.state.applicationName === undefined) { if (applicationName === undefined) {
return; return;
} }
ApplicationBackend.getApplication("admin", this.state.applicationName) ApplicationBackend.getApplication("admin", applicationName)
.then((application) => { .then((application) => {
this.setState({ this.setState({
application: application, application: application,

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Alert, Button, message, Result} from "antd"; import {Alert, Button, Result, message} from "antd";
export function showMessage(type, text) { export function showMessage(type, text) {
if (type === "success") { if (type === "success") {
@ -103,6 +103,7 @@ export function getOAuthGetParameters(params) {
const codeChallenge = getRefinedValue(queries.get("code_challenge")); const codeChallenge = getRefinedValue(queries.get("code_challenge"));
const samlRequest = getRefinedValue(queries.get("SAMLRequest")); const samlRequest = getRefinedValue(queries.get("SAMLRequest"));
const relayState = getRefinedValue(queries.get("RelayState")); const relayState = getRefinedValue(queries.get("RelayState"));
const noRedirect = getRefinedValue(queries.get("noRedirect"));
if ((clientId === undefined || clientId === null || clientId === "") && (samlRequest === "" || samlRequest === undefined)) { if ((clientId === undefined || clientId === null || clientId === "") && (samlRequest === "" || samlRequest === undefined)) {
// login // login
@ -120,6 +121,7 @@ export function getOAuthGetParameters(params) {
codeChallenge: codeChallenge, codeChallenge: codeChallenge,
samlRequest: samlRequest, samlRequest: samlRequest,
relayState: relayState, relayState: relayState,
noRedirect: noRedirect,
}; };
} }
} }

View File

@ -0,0 +1,79 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function registerWebauthnCredential() {
return fetch(`${Setting.ServerUrl}/api/webauthn/signup/begin`, {
method: "GET",
credentials: "include"
})
.then(res => res.json())
.then((credentialCreationOptions) => {
credentialCreationOptions.certificate.challenge = webAuthnBufferDecode(credentialCreationOptions.certificate.challenge);
credentialCreationOptions.certificate.user.id = webAuthnBufferDecode(credentialCreationOptions.certificate.user.id);
if (credentialCreationOptions.certificate.excludeCredentials) {
for (var i = 0; i < credentialCreationOptions.certificate.excludeCredentials.length; i++) {
credentialCreationOptions.certificate.excludeCredentials[i].id = webAuthnBufferDecode(credentialCreationOptions.certificate.excludeCredentials[i].id);
}
}
return navigator.credentials.create({
certificate: credentialCreationOptions.certificate
});
})
.then((credential) => {
let attestationObject = credential.response.attestationObject;
let clientDataJSON = credential.response.clientDataJSON;
let rawId = credential.rawId;
return fetch(`${Setting.ServerUrl}/api/webauthn/signup/finish`, {
method: "POST",
credentials: "include",
body: JSON.stringify({
id: credential.id,
rawId: webAuthnBufferEncode(rawId),
type: credential.type,
response: {
attestationObject: webAuthnBufferEncode(attestationObject),
clientDataJSON: webAuthnBufferEncode(clientDataJSON),
},
})
})
.then(res => res.json());
});
}
export function deleteUserWebAuthnCredential(credentialID) {
let form = new FormData();
form.append("credentialID", credentialID);
return fetch(`${Setting.ServerUrl}/api/webauthn/delete-credential`, {
method: "POST",
credentials: "include",
body: form,
dataType: "text"
}).then(res => res.json());
}
// Base64 to ArrayBuffer
export function webAuthnBufferDecode(value) {
return Uint8Array.from(atob(value), c => c.charCodeAt(0));
}
// ArrayBuffer to URLBase64
export function webAuthnBufferEncode(value) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}

View File

@ -91,6 +91,8 @@ class OAuthWidget extends React.Component {
unlinkUser(providerType) { unlinkUser(providerType) {
const body = { const body = {
providerType: providerType, providerType: providerType,
// should add the unlink user's info, cause the user may not be logged in, but a admin want to unlink the user.
user: this.props.user,
}; };
AuthBackend.unlink(body) AuthBackend.unlink(body)
.then((res) => { .then((res) => {
@ -113,6 +115,8 @@ class OAuthWidget extends React.Component {
const displayName = this.getUserProperty(user, provider.type, "displayName"); const displayName = this.getUserProperty(user, provider.type, "displayName");
const email = this.getUserProperty(user, provider.type, "email"); const email = this.getUserProperty(user, provider.type, "email");
let avatarUrl = this.getUserProperty(user, provider.type, "avatarUrl"); let avatarUrl = this.getUserProperty(user, provider.type, "avatarUrl");
// the account user
const account = this.props.account;
if (avatarUrl === "" || avatarUrl === undefined) { if (avatarUrl === "" || avatarUrl === undefined) {
avatarUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAQAAACROWYpAAAAHElEQVR42mNkoAAwjmoe1TyqeVTzqOZRzcNZMwB18wAfEFQkPQAAAABJRU5ErkJggg=="; avatarUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAQAAACROWYpAAAAHElEQVR42mNkoAAwjmoe1TyqeVTzqOZRzcNZMwB18wAfEFQkPQAAAABJRU5ErkJggg==";
@ -161,10 +165,10 @@ class OAuthWidget extends React.Component {
{ {
linkedValue === "" ? ( linkedValue === "" ? (
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "link")}> <a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "link")}>
<Button style={{marginLeft: "20px", width: "80px"}} type="primary">{i18next.t("user:Link")}</Button> <Button style={{marginLeft: "20px", width: "80px"}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
</a> </a>
) : ( ) : (
<Button disabled={!providerItem.canUnlink} style={{marginLeft: "20px", width: "80px"}} onClick={() => this.unlinkUser(provider.type)}>{i18next.t("user:Unlink")}</Button> <Button disabled={!providerItem.canUnlink && !account.isGlobalAdmin} style={{marginLeft: "20px", width: "80px"}} onClick={() => this.unlinkUser(provider.type)}>{i18next.t("user:Unlink")}</Button>
) )
} }
</Col> </Col>

View File

@ -13,6 +13,8 @@
"Edit Application": "Anwendung bearbeiten", "Edit Application": "Anwendung bearbeiten",
"Enable SAML compress": "Enable SAML compress", "Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip", "Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable WebAuthn signin": "Enable WebAuthn signin",
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
"Enable code signin": "Code-Anmeldung aktivieren", "Enable code signin": "Code-Anmeldung aktivieren",
"Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip", "Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip",
"Enable signin session - Tooltip": "Aktiviere Anmeldesession - Tooltip", "Enable signin session - Tooltip": "Aktiviere Anmeldesession - Tooltip",
@ -49,11 +51,11 @@
"Bit size": "Bitgröße", "Bit size": "Bitgröße",
"Bit size - Tooltip": "Bit Größe - Tooltip", "Bit size - Tooltip": "Bit Größe - Tooltip",
"Copy private key": "Privaten Schlüssel kopieren", "Copy private key": "Privaten Schlüssel kopieren",
"Copy public key": "Öffentlichen Schlüssel kopieren", "Copy certificate": "Kopieren des Zertifikats",
"Crypto algorithm": "Crypto-Algorithmus", "Crypto algorithm": "Crypto-Algorithmus",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip", "Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Privaten Schlüssel herunterladen", "Download private key": "Privaten Schlüssel herunterladen",
"Download public key": "Öffentlichen Schlüssel herunterladen", "Download certificate": "Zertifikat herunterladen",
"Edit Cert": "Zitat bearbeiten", "Edit Cert": "Zitat bearbeiten",
"Expire in years": "Gültig in Jahren", "Expire in years": "Gültig in Jahren",
"Expire in years - Tooltip": "Verfällt in Jahren - Tooltip", "Expire in years - Tooltip": "Verfällt in Jahren - Tooltip",
@ -61,9 +63,9 @@
"Private key": "Privater Schlüssel", "Private key": "Privater Schlüssel",
"Private key - Tooltip": "Privater Schlüssel - Tooltip", "Private key - Tooltip": "Privater Schlüssel - Tooltip",
"Private key copied to clipboard successfully": "Privater Schlüssel erfolgreich in die Zwischenablage kopiert", "Private key copied to clipboard successfully": "Privater Schlüssel erfolgreich in die Zwischenablage kopiert",
"Public key": "Öffentlicher Schlüssel", "Certificate": "Zertifikat",
"Public key - Tooltip": "Öffentlicher Schlüssel - Tooltip", "Certificate - Tooltip": "Zertifikat - Tooltip",
"Public key copied to clipboard successfully": "Öffentlicher Schlüssel erfolgreich in die Zwischenablage kopiert", "Certificate copied to clipboard successfully": "Das Zertifikat wurde erfolgreich in die Zwischenablage kopiert",
"Scope": "Bereich", "Scope": "Bereich",
"Scope - Tooltip": "Bereich - Tooltip", "Scope - Tooltip": "Bereich - Tooltip",
"Type": "Typ", "Type": "Typ",
@ -91,6 +93,7 @@
"Please input your username!": "Bitte geben Sie Ihren Benutzernamen ein!", "Please input your username!": "Bitte geben Sie Ihren Benutzernamen ein!",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "Passwort abrufen", "Retrieve password": "Passwort abrufen",
"Unknown forget type": "Unknown forget type",
"Verify": "Überprüfen" "Verify": "Überprüfen"
}, },
"general": { "general": {
@ -185,6 +188,7 @@
"Signup application - Tooltip": "Signup application - Tooltip", "Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.", "Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.", "Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
"State": "State", "State": "State",
"State - Tooltip": "State - Tooltip", "State - Tooltip": "State - Tooltip",
"Swagger": "Swagger", "Swagger": "Swagger",
@ -248,6 +252,7 @@
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!", "Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!",
"Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!", "Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!",
"Sign In": "Anmelden", "Sign In": "Anmelden",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with code": "Mit Code anmelden", "Sign in with code": "Mit Code anmelden",
"Sign in with password": "Mit Passwort anmelden", "Sign in with password": "Mit Passwort anmelden",
"Sign in with {type}": "Mit {type} anmelden", "Sign in with {type}": "Mit {type} anmelden",
@ -342,7 +347,8 @@
"New Permission": "New Permission", "New Permission": "New Permission",
"Resource type": "Ressourcentyp", "Resource type": "Ressourcentyp",
"Resource type - Tooltip": "Ressourcentyp - Tooltip", "Resource type - Tooltip": "Ressourcentyp - Tooltip",
"Resources": "Ressourcen" "Resources": "Ressourcen",
"Resources - Tooltip": "Resources - Tooltip"
}, },
"product": { "product": {
"Alipay": "Alipay", "Alipay": "Alipay",
@ -420,7 +426,7 @@
"Host": "Host", "Host": "Host",
"Host - Tooltip": "Unique string-style identifier", "Host - Tooltip": "Unique string-style identifier",
"IdP": "IdP", "IdP": "IdP",
"IdP public key": "IdP-öffentlicher Schlüssel", "IdP certificate": "IdP-öffentlicher Schlüssel",
"Issuer URL": "Ausgabe-URL", "Issuer URL": "Ausgabe-URL",
"Issuer URL - Tooltip": "Ausgabe-URL - Tooltip", "Issuer URL - Tooltip": "Ausgabe-URL - Tooltip",
"Link copied to clipboard successfully": "Link erfolgreich in die Zwischenablage kopiert", "Link copied to clipboard successfully": "Link erfolgreich in die Zwischenablage kopiert",
@ -639,6 +645,7 @@
"Unlink": "Link aufheben", "Unlink": "Link aufheben",
"Upload (.xlsx)": "Upload (.xlsx)", "Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Foto hochladen", "Upload a photo": "Foto hochladen",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "Passwort eingeben" "input password": "Passwort eingeben"
}, },
"webhook": { "webhook": {

View File

@ -13,6 +13,8 @@
"Edit Application": "Edit Application", "Edit Application": "Edit Application",
"Enable SAML compress": "Enable SAML compress", "Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip", "Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable WebAuthn signin": "Enable WebAuthn signin",
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
"Enable code signin": "Enable code signin", "Enable code signin": "Enable code signin",
"Enable code signin - Tooltip": "Enable code signin - Tooltip", "Enable code signin - Tooltip": "Enable code signin - Tooltip",
"Enable signin session - Tooltip": "Enable signin session - Tooltip", "Enable signin session - Tooltip": "Enable signin session - Tooltip",
@ -49,11 +51,11 @@
"Bit size": "Bit size", "Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip", "Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key", "Copy private key": "Copy private key",
"Copy public key": "Copy public key", "Copy certificate": "Copy certificate",
"Crypto algorithm": "Crypto algorithm", "Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip", "Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key", "Download private key": "Download private key",
"Download public key": "Download public key", "Download certificate": "Download certificate",
"Edit Cert": "Edit Cert", "Edit Cert": "Edit Cert",
"Expire in years": "Expire in years", "Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip", "Expire in years - Tooltip": "Expire in years - Tooltip",
@ -61,9 +63,9 @@
"Private key": "Private key", "Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip", "Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully", "Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key", "Certificate": "Certificate",
"Public key - Tooltip": "Public key - Tooltip", "Certificate - Tooltip": "Certificate - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully", "Certificate copied to clipboard successfully": "Certificate copied to clipboard successfully",
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "Scope - Tooltip": "Scope - Tooltip",
"Type": "Type", "Type": "Type",
@ -91,6 +93,7 @@
"Please input your username!": "Please input your username!", "Please input your username!": "Please input your username!",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "Retrieve password", "Retrieve password": "Retrieve password",
"Unknown forget type": "Unknown forget type",
"Verify": "Verify" "Verify": "Verify"
}, },
"general": { "general": {
@ -185,6 +188,7 @@
"Signup application - Tooltip": "Signup application - Tooltip", "Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.", "Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.", "Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
"State": "State", "State": "State",
"State - Tooltip": "State - Tooltip", "State - Tooltip": "State - Tooltip",
"Swagger": "Swagger", "Swagger": "Swagger",
@ -248,6 +252,7 @@
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!", "Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!", "Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Sign In": "Sign In", "Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with code": "Sign in with code", "Sign in with code": "Sign in with code",
"Sign in with password": "Sign in with password", "Sign in with password": "Sign in with password",
"Sign in with {type}": "Sign in with {type}", "Sign in with {type}": "Sign in with {type}",
@ -342,7 +347,8 @@
"New Permission": "New Permission", "New Permission": "New Permission",
"Resource type": "Resource type", "Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip", "Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources" "Resources": "Resources",
"Resources - Tooltip": "Resources - Tooltip"
}, },
"product": { "product": {
"Alipay": "Alipay", "Alipay": "Alipay",
@ -420,7 +426,7 @@
"Host": "Host", "Host": "Host",
"Host - Tooltip": "Host - Tooltip", "Host - Tooltip": "Host - Tooltip",
"IdP": "IdP", "IdP": "IdP",
"IdP public key": "IdP public key", "IdP certificate": "IdP certificate",
"Issuer URL": "Issuer URL", "Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL - Tooltip", "Issuer URL - Tooltip": "Issuer URL - Tooltip",
"Link copied to clipboard successfully": "Link copied to clipboard successfully", "Link copied to clipboard successfully": "Link copied to clipboard successfully",
@ -639,6 +645,7 @@
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)", "Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "input password" "input password": "input password"
}, },
"webhook": { "webhook": {

View File

@ -13,6 +13,8 @@
"Edit Application": "Modifier l'application", "Edit Application": "Modifier l'application",
"Enable SAML compress": "Enable SAML compress", "Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip", "Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable WebAuthn signin": "Enable WebAuthn signin",
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
"Enable code signin": "Activer la connexion au code", "Enable code signin": "Activer la connexion au code",
"Enable code signin - Tooltip": "Activer la connexion au code - infobulle", "Enable code signin - Tooltip": "Activer la connexion au code - infobulle",
"Enable signin session - Tooltip": "Activer la session de connexion - infobulle", "Enable signin session - Tooltip": "Activer la session de connexion - infobulle",
@ -49,21 +51,21 @@
"Bit size": "Taille du bit", "Bit size": "Taille du bit",
"Bit size - Tooltip": "Taille du bit - Infobulle", "Bit size - Tooltip": "Taille du bit - Infobulle",
"Copy private key": "Copier la clé privée", "Copy private key": "Copier la clé privée",
"Copy public key": "Copier la clé publique", "Copy certificate": "Copier le certificate",
"Crypto algorithm": "Algorithme de cryptomonnaie", "Crypto algorithm": "Algorithme de cryptomonnaie",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip", "Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Télécharger la clé privée", "Download private key": "Télécharger la clé privée",
"Download public key": "Télécharger la clé publique", "Download certificate": "Télécharger le certificate",
"Edit Cert": "Modifier le certificat", "Edit Cert": "Modifier le certificate",
"Expire in years": "Expire dans les années", "Expire in years": "Expire dans les années",
"Expire in years - Tooltip": "Expire dans les années - infobulle", "Expire in years - Tooltip": "Expire dans les années - infobulle",
"New Cert": "New Cert", "New Cert": "New Cert",
"Private key": "Clé privée", "Private key": "Clé privée",
"Private key - Tooltip": "Clé privée - Infobulle", "Private key - Tooltip": "Clé privée - Infobulle",
"Private key copied to clipboard successfully": "Clé privée copiée dans le presse-papiers avec succès", "Private key copied to clipboard successfully": "Clé privée copiée dans le presse-papiers avec succès",
"Public key": "Clé publique", "Certificate": "certificate",
"Public key - Tooltip": "Clé publique - Infobulle", "Certificate - Tooltip": "certificate - Infobulle",
"Public key copied to clipboard successfully": "Clé publique copiée dans le presse-papiers avec succès", "Certificate copied to clipboard successfully": "Le certificate a été copié avec succès dans le presse-papiers",
"Scope": "Périmètre d'application", "Scope": "Périmètre d'application",
"Scope - Tooltip": "Scope - Infobulle", "Scope - Tooltip": "Scope - Infobulle",
"Type": "Type de texte", "Type": "Type de texte",
@ -91,6 +93,7 @@
"Please input your username!": "Veuillez entrer votre nom d'utilisateur !", "Please input your username!": "Veuillez entrer votre nom d'utilisateur !",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "Récupérer le mot de passe", "Retrieve password": "Récupérer le mot de passe",
"Unknown forget type": "Unknown forget type",
"Verify": "Vérifier" "Verify": "Vérifier"
}, },
"general": { "general": {
@ -185,6 +188,7 @@
"Signup application - Tooltip": "Signup application - Tooltip", "Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.", "Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.", "Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
"Sorry, you do not have permission to access this page.": "Désolé, vous n'avez pas la permission d'accéder à cette page.",
"State": "State", "State": "State",
"State - Tooltip": "State - Tooltip", "State - Tooltip": "State - Tooltip",
"Swagger": "Swagger", "Swagger": "Swagger",
@ -248,6 +252,7 @@
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !", "Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !",
"Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!", "Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!",
"Sign In": "Se connecter", "Sign In": "Se connecter",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with code": "Se connecter avec le code", "Sign in with code": "Se connecter avec le code",
"Sign in with password": "Se connecter avec le mot de passe", "Sign in with password": "Se connecter avec le mot de passe",
"Sign in with {type}": "Se connecter avec {type}", "Sign in with {type}": "Se connecter avec {type}",
@ -342,7 +347,8 @@
"New Permission": "New Permission", "New Permission": "New Permission",
"Resource type": "Type de ressource", "Resource type": "Type de ressource",
"Resource type - Tooltip": "Type de ressource - infobulle", "Resource type - Tooltip": "Type de ressource - infobulle",
"Resources": "Ressource" "Resources": "Ressource",
"Resources - Tooltip": "Resources - Tooltip"
}, },
"product": { "product": {
"Alipay": "Alipay", "Alipay": "Alipay",
@ -420,7 +426,7 @@
"Host": "Hôte", "Host": "Hôte",
"Host - Tooltip": "Unique string-style identifier", "Host - Tooltip": "Unique string-style identifier",
"IdP": "IDP", "IdP": "IDP",
"IdP public key": "Clé publique IdP", "IdP certificate": "Clé publique IdP",
"Issuer URL": "URL de l'émetteur", "Issuer URL": "URL de l'émetteur",
"Issuer URL - Tooltip": "URL de l'émetteur - infobulle", "Issuer URL - Tooltip": "URL de l'émetteur - infobulle",
"Link copied to clipboard successfully": "Lien copié dans le presse-papiers avec succès", "Link copied to clipboard successfully": "Lien copié dans le presse-papiers avec succès",
@ -639,6 +645,7 @@
"Unlink": "Délier", "Unlink": "Délier",
"Upload (.xlsx)": "Télécharger (.xlsx)", "Upload (.xlsx)": "Télécharger (.xlsx)",
"Upload a photo": "Télécharger une photo", "Upload a photo": "Télécharger une photo",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "saisir le mot de passe" "input password": "saisir le mot de passe"
}, },
"webhook": { "webhook": {

View File

@ -13,6 +13,8 @@
"Edit Application": "アプリケーションを編集", "Edit Application": "アプリケーションを編集",
"Enable SAML compress": "Enable SAML compress", "Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip", "Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable WebAuthn signin": "Enable WebAuthn signin",
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
"Enable code signin": "コードサインインを有効にする", "Enable code signin": "コードサインインを有効にする",
"Enable code signin - Tooltip": "Enable code signin - Tooltip", "Enable code signin - Tooltip": "Enable code signin - Tooltip",
"Enable signin session - Tooltip": "Enable signin session - Tooltip", "Enable signin session - Tooltip": "Enable signin session - Tooltip",
@ -49,11 +51,11 @@
"Bit size": "ビットサイズ", "Bit size": "ビットサイズ",
"Bit size - Tooltip": "ビットサイズ - ツールチップ", "Bit size - Tooltip": "ビットサイズ - ツールチップ",
"Copy private key": "秘密鍵をコピー", "Copy private key": "秘密鍵をコピー",
"Copy public key": "公開鍵をコピー", "Copy certificate": "証明書をコピーします",
"Crypto algorithm": "暗号化アルゴリズム", "Crypto algorithm": "暗号化アルゴリズム",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip", "Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "秘密鍵をダウンロード", "Download private key": "秘密鍵をダウンロード",
"Download public key": "公開鍵をダウンロード", "Download certificate": "証明書をダウンロードします",
"Edit Cert": "Certを編集", "Edit Cert": "Certを編集",
"Expire in years": "有効期限", "Expire in years": "有効期限",
"Expire in years - Tooltip": "年間有効期限 - ツールチップ", "Expire in years - Tooltip": "年間有効期限 - ツールチップ",
@ -61,9 +63,9 @@
"Private key": "Private key", "Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip", "Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "秘密鍵を正常にクリップボードにコピーしました", "Private key copied to clipboard successfully": "秘密鍵を正常にクリップボードにコピーしました",
"Public key": "公開キー", "Certificate": "Certificate",
"Public key - Tooltip": "Public key - Tooltip", "Certificate - Tooltip": "Certificate - Tooltip",
"Public key copied to clipboard successfully": "公開鍵を正常にクリップボードにコピーました", "Certificate copied to clipboard successfully": "証明書はクリップボードに正常にコピーされました",
"Scope": "スコープ", "Scope": "スコープ",
"Scope - Tooltip": "スコープ → ツールチップ", "Scope - Tooltip": "スコープ → ツールチップ",
"Type": "タイプ", "Type": "タイプ",
@ -91,6 +93,7 @@
"Please input your username!": "ユーザー名を入力してください!", "Please input your username!": "ユーザー名を入力してください!",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "パスワードの取得", "Retrieve password": "パスワードの取得",
"Unknown forget type": "Unknown forget type",
"Verify": "確認する" "Verify": "確認する"
}, },
"general": { "general": {
@ -185,6 +188,7 @@
"Signup application - Tooltip": "Signup application - Tooltip", "Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。", "Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.", "Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
"Sorry, you do not have permission to access this page.": "申し訳ありませんが、このページにアクセスする権限がありません。",
"State": "State", "State": "State",
"State - Tooltip": "State - Tooltip", "State - Tooltip": "State - Tooltip",
"Swagger": "Swagger", "Swagger": "Swagger",
@ -248,6 +252,7 @@
"Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください", "Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください",
"Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。", "Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。",
"Sign In": "サインイン", "Sign In": "サインイン",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with code": "コードでサインイン", "Sign in with code": "コードでサインイン",
"Sign in with password": "パスワードでサインイン", "Sign in with password": "パスワードでサインイン",
"Sign in with {type}": "{type} でサインイン", "Sign in with {type}": "{type} でサインイン",
@ -342,7 +347,8 @@
"New Permission": "New Permission", "New Permission": "New Permission",
"Resource type": "リソースタイプ", "Resource type": "リソースタイプ",
"Resource type - Tooltip": "リソースタイプ - ツールチップ", "Resource type - Tooltip": "リソースタイプ - ツールチップ",
"Resources": "リソース" "Resources": "リソース",
"Resources - Tooltip": "Resources - Tooltip"
}, },
"product": { "product": {
"Alipay": "Alipay", "Alipay": "Alipay",
@ -420,7 +426,7 @@
"Host": "ホスト", "Host": "ホスト",
"Host - Tooltip": "Unique string-style identifier", "Host - Tooltip": "Unique string-style identifier",
"IdP": "IdP", "IdP": "IdP",
"IdP public key": "IdP public key", "IdP certificate": "IdP certificate",
"Issuer URL": "Issuer URL", "Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL - ツールチップ", "Issuer URL - Tooltip": "Issuer URL - ツールチップ",
"Link copied to clipboard successfully": "リンクをクリップボードにコピーしました", "Link copied to clipboard successfully": "リンクをクリップボードにコピーしました",
@ -639,6 +645,7 @@
"Unlink": "リンクを解除", "Unlink": "リンクを解除",
"Upload (.xlsx)": "アップロード (.xlsx)", "Upload (.xlsx)": "アップロード (.xlsx)",
"Upload a photo": "写真をアップロード", "Upload a photo": "写真をアップロード",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "パスワードを入力" "input password": "パスワードを入力"
}, },
"webhook": { "webhook": {

View File

@ -13,6 +13,8 @@
"Edit Application": "Edit Application", "Edit Application": "Edit Application",
"Enable SAML compress": "Enable SAML compress", "Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip", "Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable WebAuthn signin": "Enable WebAuthn signin",
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
"Enable code signin": "Enable code signin", "Enable code signin": "Enable code signin",
"Enable code signin - Tooltip": "Enable code signin - Tooltip", "Enable code signin - Tooltip": "Enable code signin - Tooltip",
"Enable signin session - Tooltip": "Enable signin session - Tooltip", "Enable signin session - Tooltip": "Enable signin session - Tooltip",
@ -49,11 +51,11 @@
"Bit size": "Bit size", "Bit size": "Bit size",
"Bit size - Tooltip": "Bit size - Tooltip", "Bit size - Tooltip": "Bit size - Tooltip",
"Copy private key": "Copy private key", "Copy private key": "Copy private key",
"Copy public key": "Copy public key", "Copy certificate": "Copy certificate",
"Crypto algorithm": "Crypto algorithm", "Crypto algorithm": "Crypto algorithm",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip", "Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Download private key", "Download private key": "Download private key",
"Download public key": "Download public key", "Download certificate": "Download certificate",
"Edit Cert": "Edit Cert", "Edit Cert": "Edit Cert",
"Expire in years": "Expire in years", "Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip", "Expire in years - Tooltip": "Expire in years - Tooltip",
@ -61,9 +63,9 @@
"Private key": "Private key", "Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip", "Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully", "Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
"Public key": "Public key", "Certificate": "Certificate",
"Public key - Tooltip": "Public key - Tooltip", "Certificate - Tooltip": "Certificate - Tooltip",
"Public key copied to clipboard successfully": "Public key copied to clipboard successfully", "Certificate copied to clipboard successfully": "Certificate copied to clipboard successfully",
"Scope": "Scope", "Scope": "Scope",
"Scope - Tooltip": "Scope - Tooltip", "Scope - Tooltip": "Scope - Tooltip",
"Type": "Type", "Type": "Type",
@ -91,6 +93,7 @@
"Please input your username!": "Please input your username!", "Please input your username!": "Please input your username!",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "Retrieve password", "Retrieve password": "Retrieve password",
"Unknown forget type": "Unknown forget type",
"Verify": "Verify" "Verify": "Verify"
}, },
"general": { "general": {
@ -185,6 +188,7 @@
"Signup application - Tooltip": "Signup application - Tooltip", "Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.", "Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.", "Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
"Sorry, you do not have permission to access this page.": "Sorry, you do not have permission to access this page.",
"State": "State", "State": "State",
"State - Tooltip": "State - Tooltip", "State - Tooltip": "State - Tooltip",
"Swagger": "Swagger", "Swagger": "Swagger",
@ -248,6 +252,7 @@
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!", "Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!", "Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Sign In": "Sign In", "Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with code": "Sign in with code", "Sign in with code": "Sign in with code",
"Sign in with password": "Sign in with password", "Sign in with password": "Sign in with password",
"Sign in with {type}": "Sign in with {type}", "Sign in with {type}": "Sign in with {type}",
@ -342,7 +347,8 @@
"New Permission": "New Permission", "New Permission": "New Permission",
"Resource type": "Resource type", "Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip", "Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources" "Resources": "Resources",
"Resources - Tooltip": "Resources - Tooltip"
}, },
"product": { "product": {
"Alipay": "Alipay", "Alipay": "Alipay",
@ -420,7 +426,7 @@
"Host": "Host", "Host": "Host",
"Host - Tooltip": "Unique string-style identifier", "Host - Tooltip": "Unique string-style identifier",
"IdP": "IdP", "IdP": "IdP",
"IdP public key": "IdP public key", "IdP certificate": "IdP certificate",
"Issuer URL": "Issuer URL", "Issuer URL": "Issuer URL",
"Issuer URL - Tooltip": "Issuer URL - Tooltip", "Issuer URL - Tooltip": "Issuer URL - Tooltip",
"Link copied to clipboard successfully": "Link copied to clipboard successfully", "Link copied to clipboard successfully": "Link copied to clipboard successfully",
@ -639,6 +645,7 @@
"Unlink": "Unlink", "Unlink": "Unlink",
"Upload (.xlsx)": "Upload (.xlsx)", "Upload (.xlsx)": "Upload (.xlsx)",
"Upload a photo": "Upload a photo", "Upload a photo": "Upload a photo",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "input password" "input password": "input password"
}, },
"webhook": { "webhook": {

View File

@ -13,6 +13,8 @@
"Edit Application": "Изменить приложение", "Edit Application": "Изменить приложение",
"Enable SAML compress": "Enable SAML compress", "Enable SAML compress": "Enable SAML compress",
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip", "Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
"Enable WebAuthn signin": "Enable WebAuthn signin",
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
"Enable code signin": "Включить кодовый вход", "Enable code signin": "Включить кодовый вход",
"Enable code signin - Tooltip": "Включить вход с кодом - Tooltip", "Enable code signin - Tooltip": "Включить вход с кодом - Tooltip",
"Enable signin session - Tooltip": "Включить сеанс входа - Подсказка", "Enable signin session - Tooltip": "Включить сеанс входа - Подсказка",
@ -49,11 +51,11 @@
"Bit size": "Размер бита", "Bit size": "Размер бита",
"Bit size - Tooltip": "Размер бита - Подсказка", "Bit size - Tooltip": "Размер бита - Подсказка",
"Copy private key": "Копировать закрытый ключ", "Copy private key": "Копировать закрытый ключ",
"Copy public key": "Копировать открытый ключ", "Copy certificate": "Копирование сертификата",
"Crypto algorithm": "Алгоритм крипто", "Crypto algorithm": "Алгоритм крипто",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip", "Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "Скачать закрытый ключ", "Download private key": "Скачать закрытый ключ",
"Download public key": "Скачать открытый ключ", "Download certificate": "Скачать сертификат",
"Edit Cert": "Изменить сертификат", "Edit Cert": "Изменить сертификат",
"Expire in years": "Истекает через годы", "Expire in years": "Истекает через годы",
"Expire in years - Tooltip": "Истекает через годы - Подсказка", "Expire in years - Tooltip": "Истекает через годы - Подсказка",
@ -61,9 +63,9 @@
"Private key": "Приватный ключ", "Private key": "Приватный ключ",
"Private key - Tooltip": "Приватный ключ - Подсказка", "Private key - Tooltip": "Приватный ключ - Подсказка",
"Private key copied to clipboard successfully": "Приватный ключ скопирован в буфер обмена", "Private key copied to clipboard successfully": "Приватный ключ скопирован в буфер обмена",
"Public key": "Публичный ключ", "Certificate": "сертификат",
"Public key - Tooltip": "Открытый ключ - Подсказка", "Certificate - Tooltip": "сертификат - Подсказка",
"Public key copied to clipboard successfully": "Открытый ключ успешно скопирован в буфер обмена", "Certificate copied to clipboard successfully": "Сертификат успешно скопирован в буфер обмена",
"Scope": "Сфера охвата", "Scope": "Сфера охвата",
"Scope - Tooltip": "Область применения - Подсказка", "Scope - Tooltip": "Область применения - Подсказка",
"Type": "Тип", "Type": "Тип",
@ -91,6 +93,7 @@
"Please input your username!": "Пожалуйста, введите ваше имя пользователя!", "Please input your username!": "Пожалуйста, введите ваше имя пользователя!",
"Reset": "Reset", "Reset": "Reset",
"Retrieve password": "Получить пароль", "Retrieve password": "Получить пароль",
"Unknown forget type": "Unknown forget type",
"Verify": "Подтвердить" "Verify": "Подтвердить"
}, },
"general": { "general": {
@ -185,6 +188,7 @@
"Signup application - Tooltip": "Signup application - Tooltip", "Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.", "Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.", "Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
"Sorry, you do not have permission to access this page.": "Извините, вы не имеете права доступа к этой странице.",
"State": "State", "State": "State",
"State - Tooltip": "State - Tooltip", "State - Tooltip": "State - Tooltip",
"Swagger": "Swagger", "Swagger": "Swagger",
@ -248,6 +252,7 @@
"Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!", "Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!",
"Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!", "Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!",
"Sign In": "Войти", "Sign In": "Войти",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with code": "Войти с помощью кода", "Sign in with code": "Войти с помощью кода",
"Sign in with password": "Войти с помощью пароля", "Sign in with password": "Войти с помощью пароля",
"Sign in with {type}": "Войти с помощью {type}", "Sign in with {type}": "Войти с помощью {type}",
@ -342,7 +347,8 @@
"New Permission": "New Permission", "New Permission": "New Permission",
"Resource type": "Тип ресурса", "Resource type": "Тип ресурса",
"Resource type - Tooltip": "Тип ресурса - Подсказка", "Resource type - Tooltip": "Тип ресурса - Подсказка",
"Resources": "Ресурсы" "Resources": "Ресурсы",
"Resources - Tooltip": "Resources - Tooltip"
}, },
"product": { "product": {
"Alipay": "Alipay", "Alipay": "Alipay",
@ -420,7 +426,7 @@
"Host": "Хост", "Host": "Хост",
"Host - Tooltip": "Unique string-style identifier", "Host - Tooltip": "Unique string-style identifier",
"IdP": "ИдП", "IdP": "ИдП",
"IdP public key": "Публичный ключ IdP", "IdP certificate": "Публичный ключ IdP",
"Issuer URL": "URL эмитента", "Issuer URL": "URL эмитента",
"Issuer URL - Tooltip": "URL эмитента - Tooltip", "Issuer URL - Tooltip": "URL эмитента - Tooltip",
"Link copied to clipboard successfully": "Ссылка скопирована в буфер обмена", "Link copied to clipboard successfully": "Ссылка скопирована в буфер обмена",
@ -639,6 +645,7 @@
"Unlink": "Отвязать", "Unlink": "Отвязать",
"Upload (.xlsx)": "Загрузить (.xlsx)", "Upload (.xlsx)": "Загрузить (.xlsx)",
"Upload a photo": "Загрузить фото", "Upload a photo": "Загрузить фото",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "пароль для ввода" "input password": "пароль для ввода"
}, },
"webhook": { "webhook": {

View File

@ -13,6 +13,8 @@
"Edit Application": "编辑应用", "Edit Application": "编辑应用",
"Enable SAML compress": "压缩SAML响应", "Enable SAML compress": "压缩SAML响应",
"Enable SAML compress - Tooltip": "Casdoor作为SAML idp时是否压缩SAML响应信息", "Enable SAML compress - Tooltip": "Casdoor作为SAML idp时是否压缩SAML响应信息",
"Enable WebAuthn signin": "Enable WebAuthn signin",
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
"Enable code signin": "启用验证码登录", "Enable code signin": "启用验证码登录",
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录", "Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
"Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话", "Enable signin session - Tooltip": "从应用登录Casdoor后Casdoor是否保持会话",
@ -49,11 +51,11 @@
"Bit size": "位大小", "Bit size": "位大小",
"Bit size - Tooltip": "位大小 - 工具提示", "Bit size - Tooltip": "位大小 - 工具提示",
"Copy private key": "复制私钥", "Copy private key": "复制私钥",
"Copy public key": "复制公钥", "Copy certificate": "复制证书",
"Crypto algorithm": "加密算法", "Crypto algorithm": "加密算法",
"Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip", "Crypto algorithm - Tooltip": "Crypto algorithm - Tooltip",
"Download private key": "下载私钥", "Download private key": "下载私钥",
"Download public key": "下载公钥", "Download certificate": "下载证书",
"Edit Cert": "编辑证书", "Edit Cert": "编辑证书",
"Expire in years": "有效期(年)", "Expire in years": "有效期(年)",
"Expire in years - Tooltip": "到期年份-工具提示", "Expire in years - Tooltip": "到期年份-工具提示",
@ -61,9 +63,9 @@
"Private key": "私钥", "Private key": "私钥",
"Private key - Tooltip": "私钥 - 工具提示", "Private key - Tooltip": "私钥 - 工具提示",
"Private key copied to clipboard successfully": "私钥已成功复制到剪贴板", "Private key copied to clipboard successfully": "私钥已成功复制到剪贴板",
"Public key": "公钥", "Certificate": "证书",
"Public key - Tooltip": "公钥 - 工具提示", "Certificate - Tooltip": "证书 - 工具提示",
"Public key copied to clipboard successfully": "公钥已成功复制到剪贴板", "Certificate copied to clipboard successfully": "证书已成功复制到剪贴板",
"Scope": "用途", "Scope": "用途",
"Scope - Tooltip": "范围 - 工具提示", "Scope - Tooltip": "范围 - 工具提示",
"Type": "类型", "Type": "类型",
@ -91,6 +93,7 @@
"Please input your username!": "请输入您的用户名!", "Please input your username!": "请输入您的用户名!",
"Reset": "重置", "Reset": "重置",
"Retrieve password": "找回密码", "Retrieve password": "找回密码",
"Unknown forget type": "未知的忘记密码类型",
"Verify": "验证" "Verify": "验证"
}, },
"general": { "general": {
@ -185,6 +188,7 @@
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的", "Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在", "Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "抱歉,您访问的用户不存在或您无权访问该用户", "Sorry, the user you visited does not exist or you are not authorized to access this user.": "抱歉,您访问的用户不存在或您无权访问该用户",
"Sorry, you do not have permission to access this page.": "抱歉,您无权访问该页面",
"State": "状态", "State": "状态",
"State - Tooltip": "状态", "State - Tooltip": "状态",
"Swagger": "API文档", "Swagger": "API文档",
@ -248,6 +252,7 @@
"Please input your password, at least 6 characters!": "请输入您的密码不少于6位", "Please input your password, at least 6 characters!": "请输入您的密码不少于6位",
"Please input your username, Email or phone!": "请输入您的用户名、Email或手机号", "Please input your username, Email or phone!": "请输入您的用户名、Email或手机号",
"Sign In": "登录", "Sign In": "登录",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with code": "验证码登录", "Sign in with code": "验证码登录",
"Sign in with password": "密码登录", "Sign in with password": "密码登录",
"Sign in with {type}": "{type}登录", "Sign in with {type}": "{type}登录",
@ -342,7 +347,8 @@
"New Permission": "添加权限", "New Permission": "添加权限",
"Resource type": "资源类型", "Resource type": "资源类型",
"Resource type - Tooltip": "授权资源的类型", "Resource type - Tooltip": "授权资源的类型",
"Resources": "资源" "Resources": "资源",
"Resources - Tooltip": "被授权的资源"
}, },
"product": { "product": {
"Alipay": "支付宝", "Alipay": "支付宝",
@ -420,7 +426,7 @@
"Host": "主机", "Host": "主机",
"Host - Tooltip": "主机", "Host - Tooltip": "主机",
"IdP": "IdP", "IdP": "IdP",
"IdP public key": "IdP 公钥", "IdP certificate": "IdP 公钥",
"Issuer URL": "发行者网址", "Issuer URL": "发行者网址",
"Issuer URL - Tooltip": "发行者URL - 工具提示", "Issuer URL - Tooltip": "发行者URL - 工具提示",
"Link copied to clipboard successfully": "链接已成功复制到剪贴板", "Link copied to clipboard successfully": "链接已成功复制到剪贴板",
@ -639,6 +645,7 @@
"Unlink": "解绑", "Unlink": "解绑",
"Upload (.xlsx)": "上传(.xlsx", "Upload (.xlsx)": "上传(.xlsx",
"Upload a photo": "上传头像", "Upload a photo": "上传头像",
"WebAuthn credentials": "WebAuthn credentials",
"input password": "输入密码" "input password": "输入密码"
}, },
"webhook": { "webhook": {