Compare commits

...

63 Commits

Author SHA1 Message Date
Copilot
55257d6190 feat: support user.Id parameter in /update-user API (#4249) 2025-10-13 23:10:17 +08:00
Attack825
b9046bec01 feat: support Form.Tag and remove unique formType checks (#4002) 2025-10-09 22:15:08 +08:00
Kevin D'souza
40d4e3a1a9 feat: accept if the SAML certificate is of type PEM as well (#4232) 2025-10-08 22:57:53 +08:00
Attack825
60bfc8891a fix: fix label's i18n in form edit page (#4226) 2025-10-05 23:18:52 +08:00
Attack825
126879533b feat: add Form.Label and change form.Width to string (#4225) 2025-10-05 21:03:36 +08:00
DacongDA
469b6036fd feat: fix JSON parse error with ObjectWithOrg in authz_filter.go's getObject() (#4224) 2025-10-04 23:36:15 +08:00
DacongDA
6c750867b0 feat: support different SAML hash algorithms: "SHA1", "SHA256", "SHA512" for SAML signature in application edit page (#4221) 2025-10-02 11:58:34 +08:00
Austin Riendeau
625b3e2c63 feat: fix missing subject in Sendgrid Email provider (#4220) 2025-10-02 11:45:31 +08:00
DacongDA
28dff8083a feat: fix bug that notify-payment webhook was not triggered (#4219) 2025-10-02 11:44:01 +08:00
Yang Luo
02c4bddb5f feat: improve user-upload button 2025-09-30 14:01:12 +08:00
Enze Wu
df65fb3525 feat: skip old password verification for OAuth users without a password (#4211) 2025-09-29 17:43:45 +08:00
DacongDA
d3bbf954f8 feat: fix issue that couldn't auto set invitatonCode when using oAuth signup link (#4212) 2025-09-29 17:19:58 +08:00
Attack825
f3755d925c feat: improve form edit page UI and add preview (#4210) 2025-09-27 16:26:12 +08:00
Attack825
ca819e7e83 feat: add form to customize columns (#4202) 2025-09-25 22:04:43 +08:00
M3ti
d619e91d9e feat: add support for custom attribute mapping to user properties in LDAP (#4201) 2025-09-23 00:57:12 +08:00
Robin Ye
5079c37818 feat: improve compatibility for MinIO storage provider (#4196) 2025-09-21 16:02:19 +08:00
DacongDA
d5f29d716a feat: change User.Avatar's DB type to text (#4199) 2025-09-20 21:32:51 +08:00
DacongDA
00b278a00f feat: check roles in CheckLoginPermission (#4198) 2025-09-20 19:50:36 +08:00
DacongDA
d883db907b feat: improve authz_filter (#4195) 2025-09-18 23:46:00 +08:00
Attack825
8e7efe5c23 feat: add code verification label in signin items (#4187) 2025-09-18 10:41:47 +08:00
DacongDA
bf75508d95 feat: add token attribute table to provide a more flexible Jwt-custom token in application edit page (#4191) 2025-09-17 21:57:17 +08:00
Mirko Rapisarda
986b94cc90 feat: improve domain field text in provider edit page (#4181) 2025-09-16 20:57:40 +08:00
Attack825
890f528556 feat: separate getLocalPrimaryKey() and getTargetTablePrimaryKey() in DB syncer (#4180) 2025-09-15 17:45:18 +08:00
Robin Ye
b46e779235 feat: persist custom signin item label in signin items table (#4179) 2025-09-15 17:43:20 +08:00
Attack825
5c80948a06 feat: add tag filtering in app list page (#4163) 2025-09-14 15:32:13 +08:00
DacongDA
1467199159 feat: add webhook for buy-product and add resp data (#4177) 2025-09-12 23:53:59 +08:00
DacongDA
64c2b8f0c2 feat: fix issue that init will add duplicate policy and not add permission policies to adapter (#4175) 2025-09-11 21:21:07 +08:00
DacongDA
8f7ea7f0a0 feat: fix Data Missing From casbin_rule Table After Importing init_data.json (#4167) 2025-09-09 21:20:25 +08:00
DacongDA
2ab85c0c44 feat: fix bug that send code type will be "phone" when logged-in via autofill (#4164) 2025-09-08 18:13:52 +08:00
Dev Hjz
bf67be2af6 feat: add username and loginHint to redirect URL in HandleSamlRedirect of SAML IdP (#4162) 2025-09-07 14:18:35 +08:00
DacongDA
bc94735a8d feat: add the username parameter in SAML or OAuth2 (#4161) 2025-09-06 22:03:18 +08:00
DacongDA
89c6ef5aae feat: support "permissionNames" field in JWT-Custom token (#4154) 2025-09-06 00:05:47 +08:00
anhuv
21da9f5ff2 feat: remove port from client IP in getIpInfo() (#4145) 2025-09-04 08:10:42 +08:00
karatekaneen
3b11e778e7 feat(i18n): Update faulty Swedish translations (#4149) 2025-09-03 20:45:57 +08:00
Attack825
ad240a373f feat: fix non-standard CAS bug (#4146) 2025-09-03 20:20:08 +08:00
amaankm
01000f7022 feat: update parameter descriptions in Session API (#4140) 2025-09-02 16:31:06 +08:00
Cleidson Oliveira
f93aeb5350 feat: improve pt i18n strings (#4143) 2025-09-02 15:53:51 +08:00
gongzhongqiang
8fa681f883 feat: add password change validation to ensure new password differs from current password (#4134) 2025-09-01 17:22:06 +08:00
DacongDA
3b16406442 feat: add signinMethod in JWT token (#4136) 2025-08-31 18:01:05 +08:00
Attack825
fbc16ef124 feat: change builtInMaxFields to 6 in Casbin policy length (#4130) 2025-08-29 22:39:39 +08:00
DacongDA
f26f56e88b feat: support auto signup with SAML (#4129) 2025-08-29 11:51:52 +08:00
DacongDA
9cb633c9e2 feat: use a more popular format for nameid to ensure compatibility in NewSamlResponse() (#4123) 2025-08-27 22:33:14 +08:00
DacongDA
d0d059d42f feat: fix Email and SMS check failures when enabling or verifying MFA (#4122) 2025-08-27 21:11:53 +08:00
biankasyo
c184dc7f3a feat(lark): support enterprise_email field as email fallback (#4128) 2025-08-27 20:55:32 +08:00
DacongDA
2fa0890c11 feat: fix bug that custom JWT token no longer includes properties (#4124) 2025-08-27 20:41:27 +08:00
DacongDA
a0e2be7ba8 feat: support inserting user's field to SAML attribute (#4105) 2025-08-22 11:27:21 +08:00
IsAurora6
09b389b1f7 feat: add a loading animation in DashboardPage (#4117) 2025-08-22 00:11:08 +08:00
DacongDA
a23033758f feat: Add "Send Invitation Email" action to User Invitation flow (#4113) 2025-08-21 18:53:43 +08:00
Attack825
f7bc822087 feat: refresh inline captcha on login failure (#4108) 2025-08-21 10:29:56 +08:00
kevin kwok
e533ff1ee1 feat: add support for casbin-dotnet-cli auto-download (#4110) 2025-08-20 18:38:00 +08:00
Attack825
9f187f690e feat: add copy button in ApplicationListPage (#4097) 2025-08-19 16:18:28 +08:00
Jerry
fe5aa1f214 feat: Add Phantom web3 onboard wallet support (#4100) 2025-08-19 13:31:35 +08:00
DacongDA
eda742a848 feat: support e164 phone number in GetUserByPhone() (#4099) 2025-08-19 02:19:15 +08:00
Attack825
83df077a02 feat: add Application.Order for sorting in Apps page (#4085) 2025-08-18 08:34:32 +08:00
DacongDA
ad6080e763 feat: fix issue that signing up via provider in shared application will sign up to built-in app (#4093) 2025-08-17 22:32:47 +08:00
DacongDA
c179324de4 feat: fix bug that SelfLoginButton will re-render when username field updates (#4091) 2025-08-17 19:50:34 +08:00
DacongDA
645716e485 feat: add country code to validate phone number when Code method's rule is Phone only (#4089) 2025-08-17 16:56:25 +08:00
IsAurora6
955e73ddd1 feat: fix the asynchronous issue in handleOrganizationChange in BaseListPage (#4090) 2025-08-17 14:59:49 +08:00
Robin Ye
2493ae9cfe feat: fix issue that a user can belong to two physical groups at the same time (#4084) 2025-08-15 23:42:09 +08:00
Attack825
b5c80513fb feat: change username too when "username as email" switch is enabled in ResetEmailOrPhone API (#4081) 2025-08-14 21:03:45 +08:00
Attack825
0653353be1 feat: update social_osonsms.svg URL (#4082) 2025-08-14 20:35:39 +08:00
Robin Ye
d6778fb4e6 feat: improve inline captcha UI by increasing spacing (#4079) 2025-08-14 16:15:34 +08:00
DacongDA
fee7773839 feat: add First name and Last name to account items (#4077) 2025-08-14 08:41:18 +08:00
155 changed files with 4384 additions and 867 deletions

View File

@@ -61,6 +61,7 @@ p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-organization-applications, *, *
p, *, *, GET, /api/get-user, *, *
p, *, *, GET, /api/get-user-application, *, *
p, *, *, POST, /api/upload-users, *, *
p, *, *, GET, /api/get-resources, *, *
p, *, *, GET, /api/get-records, *, *
p, *, *, GET, /api/get-product, *, *
@@ -143,6 +144,10 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
return false
}
if user.IsGlobalAdmin() {
return true
}
if user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
return true
}

View File

@@ -237,7 +237,7 @@ func (c *ApiController) UpdateApplication() {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application))
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application, c.IsGlobalAdmin()))
c.ServeJSON()
}

View File

@@ -157,7 +157,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
c.ResponseError(c.T("auth:Challenge method should be S256"))
return
}
code, err := object.GetOAuthCode(userId, clientId, form.Provider, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error(), nil)
return
@@ -719,7 +719,8 @@ func (c *ApiController) Login() {
setHttpClient(idProvider, provider.Type)
if authForm.State != conf.GetConfigString("authState") && authForm.State != application.Name {
stateApplicationName := strings.Split(authForm.State, "-org-")[0]
if authForm.State != conf.GetConfigString("authState") && stateApplicationName != application.Name {
c.ResponseError(fmt.Sprintf(c.T("auth:State expected: %s, but got: %s"), conf.GetConfigString("authState"), authForm.State))
return
}
@@ -788,7 +789,7 @@ func (c *ApiController) Login() {
resp = c.HandleLoggedIn(application, user, &authForm)
c.Ctx.Input.SetParam("recordUserId", user.GetId())
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
} else if provider.Category == "OAuth" || provider.Category == "Web3" || provider.Category == "SAML" {
// Sign up via OAuth
if application.EnableLinkWithEmail {
if userInfo.Email != "" {
@@ -817,7 +818,7 @@ func (c *ApiController) Login() {
}
if !providerItem.CanSignUp {
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up"), provider.Type, userInfo.Username, userInfo.DisplayName, provider.Type))
c.ResponseError(fmt.Sprintf(c.T("auth:The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up"), provider.Type, userInfo.Username, userInfo.DisplayName, provider.Type))
return
}

View File

@@ -25,6 +25,7 @@ const (
goCliRepo = "https://api.github.com/repos/casbin/casbin-go-cli/releases/latest"
rustCliRepo = "https://api.github.com/repos/casbin-rs/casbin-rust-cli/releases/latest"
pythonCliRepo = "https://api.github.com/repos/casbin/casbin-python-cli/releases/latest"
dotnetCliRepo = "https://api.github.com/repos/casbin-net/casbin-dotnet-cli/releases/latest"
downloadFolder = "bin"
)
@@ -45,6 +46,7 @@ func getBinaryNames() map[string]string {
java = "java"
rust = "rust"
python = "python"
dotnet = "dotnet"
)
arch := runtime.GOARCH
@@ -65,6 +67,7 @@ func getBinaryNames() map[string]string {
java: "casbin-java-cli.jar",
rust: fmt.Sprintf("casbin-rust-cli-%s-pc-windows-gnu", archNames.rustArch),
python: fmt.Sprintf("casbin-python-cli-windows-%s.exe", archNames.goArch),
dotnet: fmt.Sprintf("casbin-dotnet-cli-windows-%s.exe", archNames.goArch),
}
case "darwin":
return map[string]string{
@@ -72,6 +75,7 @@ func getBinaryNames() map[string]string {
java: "casbin-java-cli.jar",
rust: fmt.Sprintf("casbin-rust-cli-%s-apple-darwin", archNames.rustArch),
python: fmt.Sprintf("casbin-python-cli-darwin-%s", archNames.goArch),
dotnet: fmt.Sprintf("casbin-dotnet-cli-darwin-%s", archNames.goArch),
}
case "linux":
return map[string]string{
@@ -79,6 +83,7 @@ func getBinaryNames() map[string]string {
java: "casbin-java-cli.jar",
rust: fmt.Sprintf("casbin-rust-cli-%s-unknown-linux-gnu", archNames.rustArch),
python: fmt.Sprintf("casbin-python-cli-linux-%s", archNames.goArch),
dotnet: fmt.Sprintf("casbin-dotnet-cli-linux-%s", archNames.goArch),
}
default:
return nil
@@ -108,6 +113,11 @@ func getFinalBinaryName(lang string) string {
return "casbin-python-cli.exe"
}
return "casbin-python-cli"
case "dotnet":
if runtime.GOOS == "windows" {
return "casbin-dotnet-cli.exe"
}
return "casbin-dotnet-cli"
default:
return ""
}
@@ -347,6 +357,7 @@ func downloadCLI() error {
"go": goCliRepo,
"rust": rustCliRepo,
"python": pythonCliRepo,
"dotnet": dotnetCliRepo,
}
for lang, repo := range repos {

175
controllers/form.go Normal file
View File

@@ -0,0 +1,175 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetGlobalForms
// @Title GetGlobalForms
// @Tag Form API
// @Description get global forms
// @Success 200 {array} object.Form The Response object
// @router /get-global-forms [get]
func (c *ApiController) GetGlobalForms() {
forms, err := object.GetGlobalForms()
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedForms(forms, true))
}
// GetForms
// @Title GetForms
// @Tag Form API
// @Description get forms
// @Param owner query string true "The owner of form"
// @Success 200 {array} object.Form The Response object
// @router /get-forms [get]
func (c *ApiController) GetForms() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
forms, err := object.GetForms(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedForms(forms, true))
} else {
limit := util.ParseInt(limit)
count, err := object.GetFormCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
forms, err := object.GetPaginationForms(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(forms, paginator.Nums())
}
}
// GetForm
// @Title GetForm
// @Tag Form API
// @Description get form
// @Param id query string true "The id (owner/name) of form"
// @Success 200 {object} object.Form The Response object
// @router /get-form [get]
func (c *ApiController) GetForm() {
id := c.Input().Get("id")
form, err := object.GetForm(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(object.GetMaskedForm(form, true))
}
// UpdateForm
// @Title UpdateForm
// @Tag Form API
// @Description update form
// @Param id query string true "The id (owner/name) of the form"
// @Param body body object.Form true "The details of the form"
// @Success 200 {object} controllers.Response The Response object
// @router /update-form [post]
func (c *ApiController) UpdateForm() {
id := c.Input().Get("id")
var form object.Form
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
c.ResponseError(err.Error())
return
}
success, err := object.UpdateForm(id, &form)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(success)
}
// AddForm
// @Title AddForm
// @Tag Form API
// @Description add form
// @Param body body object.Form true "The details of the form"
// @Success 200 {object} controllers.Response The Response object
// @router /add-form [post]
func (c *ApiController) AddForm() {
var form object.Form
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
c.ResponseError(err.Error())
return
}
success, err := object.AddForm(&form)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(success)
}
// DeleteForm
// @Title DeleteForm
// @Tag Form API
// @Description delete form
// @Param body body object.Form true "The details of the form"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-form [post]
func (c *ApiController) DeleteForm() {
var form object.Form
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
c.ResponseError(err.Error())
return
}
success, err := object.DeleteForm(&form)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(success)
}

View File

@@ -16,6 +16,8 @@ package controllers
import (
"encoding/json"
"fmt"
"strings"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
@@ -188,3 +190,73 @@ func (c *ApiController) VerifyInvitation() {
c.ResponseOk(payment, attachInfo)
}
// SendInvitation
// @Title VerifyInvitation
// @Tag Invitation API
// @Description verify invitation
// @Param id query string true "The id ( owner/name ) of the invitation"
// @Param body body []string true "The details of the invitation"
// @Success 200 {object} controllers.Response The Response object
// @router /send-invitation [post]
func (c *ApiController) SendInvitation() {
id := c.Input().Get("id")
var destinations []string
err := json.Unmarshal(c.Ctx.Input.RequestBody, &destinations)
if !c.IsAdmin() {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
invitation, err := object.GetInvitation(id)
if err != nil {
c.ResponseError(err.Error())
return
}
if invitation == nil {
c.ResponseError(fmt.Sprintf(c.T("invitation:Invitation %s does not exist"), id))
return
}
organization, err := object.GetOrganization(fmt.Sprintf("admin/%s", invitation.Owner))
if err != nil {
c.ResponseError(err.Error())
return
}
application, err := object.GetApplicationByOrganizationName(invitation.Owner)
if err != nil {
c.ResponseError(err.Error())
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The organization: %s should have one application at least"), invitation.Owner))
return
}
provider, err := application.GetEmailProvider("Invitation")
if err != nil {
c.ResponseError(err.Error())
return
}
if provider == nil {
c.ResponseError(fmt.Sprintf(c.T("verification:please add an Email provider to the \"Providers\" list for the application: %s"), invitation.Owner))
return
}
content := provider.Metadata
content = strings.ReplaceAll(content, "%code", invitation.Code)
content = strings.ReplaceAll(content, "%link", invitation.GetInvitationLink(c.Ctx.Request.Host, application.Name))
err = object.SendEmail(provider, provider.Title, content, destinations, organization.DisplayName)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk()
}

View File

@@ -364,7 +364,7 @@ func (c *ApiController) UploadResource() {
}
applicationObj.TermsOfUse = fileUrl
_, err = object.UpdateApplication(applicationId, applicationObj)
_, err = object.UpdateApplication(applicationId, applicationObj, true)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -59,8 +59,10 @@ func (c *ApiController) HandleSamlRedirect() {
relayState := c.Input().Get("RelayState")
samlRequest := c.Input().Get("SAMLRequest")
username := c.Input().Get("username")
loginHint := c.Input().Get("login_hint")
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host)
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host, username, loginHint)
c.Redirect(targetURL, http.StatusSeeOther)
}

View File

@@ -144,7 +144,7 @@ func (c *ApiController) SendEmail() {
content = strings.Replace(content, string(matchContent), "", -1)
for _, receiver := range emailForm.Receivers {
err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender)
err = object.SendEmail(provider, emailForm.Title, content, []string{receiver}, emailForm.Sender)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -68,7 +68,7 @@ func (c *ApiController) GetSessions() {
// @Title GetSingleSession
// @Tag Session API
// @Description Get session for one user in one application.
// @Param id query string true "The id(organization/application/user) of session"
// @Param sessionPkId query string true "The id(organization/user/application) of session"
// @Success 200 {array} string The Response object
// @router /get-session [get]
func (c *ApiController) GetSingleSession() {
@@ -87,7 +87,7 @@ func (c *ApiController) GetSingleSession() {
// @Title UpdateSession
// @Tag Session API
// @Description Update session for one user in one application.
// @Param id query string true "The id(organization/application/user) of session"
// @Param id query string true "The id(organization/user/application) of session"
// @Success 200 {array} string The Response object
// @router /update-session [post]
func (c *ApiController) UpdateSession() {
@@ -106,7 +106,7 @@ func (c *ApiController) UpdateSession() {
// @Title AddSession
// @Tag Session API
// @Description Add session for one user in one application. If there are other existing sessions, join the session into the list.
// @Param id query string true "The id(organization/application/user) of session"
// @Param id query string true "The id(organization/user/application) of session"
// @Param sessionId query string true "sessionId to be added"
// @Success 200 {array} string The Response object
// @router /add-session [post]
@@ -126,7 +126,7 @@ func (c *ApiController) AddSession() {
// @Title DeleteSession
// @Tag Session API
// @Description Delete session for one user in one application.
// @Param id query string true "The id(organization/application/user) of session"
// @Param id query string true "The id(organization/user/application) of session"
// @Success 200 {array} string The Response object
// @router /delete-session [post]
func (c *ApiController) DeleteSession() {
@@ -145,7 +145,7 @@ func (c *ApiController) DeleteSession() {
// @Title IsSessionDuplicated
// @Tag Session API
// @Description Check if there are other different sessions for one user in one application.
// @Param id query string true "The id(organization/application/user) of session"
// @Param sessionPkId query string true "The id(organization/user/application) of session"
// @Param sessionId query string true "sessionId to be checked"
// @Success 200 {array} string The Response object
// @router /is-session-duplicated [get]

View File

@@ -103,7 +103,7 @@ func (c *ApiController) UpdateSyncer() {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateSyncer(id, &syncer))
c.Data["json"] = wrapActionResponse(object.UpdateSyncer(id, &syncer, c.IsGlobalAdmin()))
c.ServeJSON()
}

View File

@@ -105,7 +105,7 @@ func (c *ApiController) UpdateToken() {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token))
c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token, c.IsGlobalAdmin()))
c.ServeJSON()
}

View File

@@ -252,12 +252,16 @@ func (c *ApiController) GetUser() {
// @Title UpdateUser
// @Tag User API
// @Description update user
// @Param id query string true "The id ( owner/name ) of the user"
// @Param id query string false "The id ( owner/name ) of the user"
// @Param userId query string false "The userId (UUID) of the user"
// @Param owner query string false "The owner of the user (required when using userId)"
// @Param body body object.User true "The details of the user"
// @Success 200 {object} controllers.Response The Response object
// @router /update-user [post]
func (c *ApiController) UpdateUser() {
id := c.Input().Get("id")
userId := c.Input().Get("userId")
owner := c.Input().Get("owner")
columnsStr := c.Input().Get("columns")
var user object.User
@@ -267,17 +271,38 @@ func (c *ApiController) UpdateUser() {
return
}
if id == "" {
if id == "" && userId == "" {
id = c.GetSessionUsername()
if id == "" {
c.ResponseError(c.T("general:Missing parameter"))
return
}
}
oldUser, err := object.GetUser(id)
if err != nil {
c.ResponseError(err.Error())
return
var userFromUserId *object.User
if userId != "" && owner != "" {
userFromUserId, err = object.GetUserByUserId(owner, userId)
if err != nil {
c.ResponseError(err.Error())
return
}
if userFromUserId == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user with userId: %s doesn't exist"), userId))
return
}
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
}
var oldUser *object.User
if userId != "" {
oldUser = userFromUserId
} else {
oldUser, err = object.GetUser(id)
if err != nil {
c.ResponseError(err.Error())
return
}
}
if oldUser == nil {
@@ -524,14 +549,16 @@ func (c *ApiController) SetPassword() {
}
}
} else if code == "" {
if user.Ldap == "" {
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
} else {
err = object.CheckLdapUserPassword(targetUser, oldPassword, c.GetAcceptLanguage())
}
if err != nil {
c.ResponseError(err.Error())
return
if targetUser.Password != "" || user.Ldap != "" {
if user.Ldap == "" {
err = object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
} else {
err = object.CheckLdapUserPassword(targetUser, oldPassword, c.GetAcceptLanguage())
}
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
@@ -551,6 +578,12 @@ func (c *ApiController) SetPassword() {
return
}
// Check if the new password is the same as the current password
if !object.CheckPasswordNotSameAsCurrent(targetUser, newPassword, organization) {
c.ResponseError(c.T("user:The new password must be different from your current password"))
return
}
application, err := object.GetApplicationByUser(targetUser)
if err != nil {
c.ResponseError(err.Error())

View File

@@ -40,6 +40,17 @@ func saveFile(path string, file *multipart.File) (err error) {
}
func (c *ApiController) UploadUsers() {
if !c.IsAdmin() {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
userObj := c.getCurrentUser()
if userObj == nil {
c.ResponseError(c.T("auth:Unauthorized operation"))
return
}
userId := c.GetSessionUsername()
owner, user := util.GetOwnerAndNameFromId(userId)
@@ -58,7 +69,7 @@ func (c *ApiController) UploadUsers() {
return
}
affected, err := object.UploadUsers(owner, path)
affected, err := object.UploadUsers(owner, path, userObj, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -435,9 +435,15 @@ func (c *ApiController) ResetEmailOrPhone() {
switch destType {
case object.VerifyTypeEmail:
id := user.GetId()
user.Email = dest
user.EmailVerified = true
_, err = object.UpdateUser(user.GetId(), user, []string{"email", "email_verified"}, false)
columns := []string{"email", "email_verified"}
if organization.UseEmailAsUsername {
user.Name = user.Email
columns = append(columns, "name")
}
_, err = object.UpdateUser(id, user, columns, false)
case object.VerifyTypePhone:
user.Phone = dest
_, err = object.SetUserField(user, "phone", user.Phone)
@@ -449,6 +455,9 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseError(err.Error())
return
}
if organization.UseEmailAsUsername {
c.SetSessionUsername(user.GetId())
}
err = object.DisableVerificationCode(checkDest)
if err != nil {

View File

@@ -105,7 +105,7 @@ func (c *ApiController) UpdateWebhook() {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateWebhook(id, &webhook))
c.Data["json"] = wrapActionResponse(object.UpdateWebhook(id, &webhook, c.IsGlobalAdmin()))
c.ServeJSON()
}

View File

@@ -96,15 +96,17 @@ func NewAzureACSEmailProvider(accessKey string, endpoint string) *AzureACSEmailP
}
}
func newEmail(fromAddress string, toAddress string, subject string, content string) *Email {
func newEmail(fromAddress string, toAddress []string, subject string, content string) *Email {
var to []EmailAddress
for _, addr := range toAddress {
to = append(to, EmailAddress{
DisplayName: addr,
Address: addr,
})
}
return &Email{
Recipients: Recipients{
To: []EmailAddress{
{
DisplayName: toAddress,
Address: toAddress,
},
},
To: to,
},
SenderAddress: fromAddress,
Content: Content{
@@ -116,7 +118,7 @@ func newEmail(fromAddress string, toAddress string, subject string, content stri
}
}
func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error {
email := newEmail(fromAddress, toAddress, subject, content)
postBody, err := json.Marshal(email)

View File

@@ -48,21 +48,23 @@ func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string
return client
}
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error {
var req *http.Request
var err error
fromNameField := "fromName"
toAddressField := "toAddress"
toAddressesField := "toAddresses"
subjectField := "subject"
contentField := "content"
for k, v := range c.bodyMapping {
switch k {
case "fromName":
fromNameField = v
case "toAddress":
toAddressField = v
case "toAddresses":
toAddressesField = v
case "subject":
subjectField = v
case "content":
@@ -73,7 +75,6 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" {
bodyMap := make(map[string]string)
bodyMap[fromNameField] = fromName
bodyMap[toAddressField] = toAddress
bodyMap[subjectField] = subject
bodyMap[contentField] = content
@@ -89,6 +90,13 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
for k, v := range bodyMap {
formValues.Add(k, v)
}
if len(toAddress) == 1 {
formValues.Add(toAddressField, toAddress[0])
} else {
for _, addr := range toAddress {
formValues.Add(toAddressesField, addr)
}
}
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
}
@@ -105,7 +113,13 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
q := req.URL.Query()
q.Add(fromNameField, fromName)
q.Add(toAddressField, toAddress)
if len(toAddress) == 1 {
q.Add(toAddressField, toAddress[0])
} else {
for _, addr := range toAddress {
q.Add(toAddressesField, addr)
}
}
q.Add(subjectField, subject)
q.Add(contentField, content)
req.URL.RawQuery = q.Encode()

View File

@@ -15,7 +15,7 @@
package email
type EmailProvider interface {
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
Send(fromAddress string, fromName string, toAddress []string, subject string, content string) error
}
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) EmailProvider {

View File

@@ -41,13 +41,24 @@ func NewSendgridEmailProvider(apiKey string, host string, endpoint string) *Send
return &SendgridEmailProvider{ApiKey: apiKey, Host: host, Endpoint: endpoint}
}
func (s *SendgridEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
client := s.initSendgridClient()
func (s *SendgridEmailProvider) Send(fromAddress string, fromName string, toAddresses []string, subject string, content string) error {
from := mail.NewEmail(fromName, fromAddress)
to := mail.NewEmail("", toAddress)
message := mail.NewSingleEmail(from, subject, to, "", content)
message := mail.NewV3Mail()
message.SetFrom(from)
message.AddContent(mail.NewContent("text/html", content))
personalization := mail.NewPersonalization()
for _, toAddress := range toAddresses {
to := mail.NewEmail(toAddress, toAddress)
personalization.AddTos(to)
}
personalization.Subject = subject
message.AddPersonalizations(personalization)
client := s.initSendgridClient()
resp, err := client.Send(message)
if err != nil {
return err

View File

@@ -44,11 +44,15 @@ func NewSmtpEmailProvider(userName string, password string, host string, port in
return &SmtpEmailProvider{Dialer: dialer}
}
func (s *SmtpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
func (s *SmtpEmailProvider) Send(fromAddress string, fromName string, toAddresses []string, subject string, content string) error {
message := gomail.NewMessage()
message.SetAddressHeader("From", fromAddress, fromName)
message.SetHeader("To", toAddress)
var addresses []string
for _, address := range toAddresses {
addresses = append(addresses, message.FormatAddress(address, ""))
}
message.SetHeader("To", addresses...)
message.SetHeader("Subject", subject)
message.SetBody("text/html", content)

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "فشل تسجيل الدخول: %s",
"Invalid token": "الرمز غير صالح",
"State expected: %s, but got: %s": "كان من المتوقع الحالة: %s، لكن حصلنا على: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "الحساب الخاص بالمزود: %s واسم المستخدم: %s (%s) غير موجود ولا يُسمح بالتسجيل كحساب جديد عبر %%s، يرجى استخدام طريقة أخرى للتسجيل",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "الحساب الخاص بالمزود: %s واسم المستخدم: %s (%s) غير موجود ولا يُسمح بالتسجيل كحساب جديد عبر %s، يرجى استخدام طريقة أخرى للتسجيل",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "الحساب الخاص بالمزود: %s واسم المستخدم: %s (%s) غير موجود ولا يُسمح بالتسجيل كحساب جديد، يرجى الاتصال بدعم تكنولوجيا المعلومات",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "الحساب الخاص بالمزود: %s واسم المستخدم: %s (%s) مرتبط بالفعل بحساب آخر: %s (%s)",
"The application: %s does not exist": "التطبيق: %s غير موجود",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "عنوان IP الخاص بك: %s تم حظره وفقًا لتكوين: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Şifrenizin süresi doldu. Lütfen \\\"Şifremi unuttum\\\"a tıklayarak şifrenizi sıfırlayın",
"Your region is not allow to signup by phone": "منطقتك لا تسمح بالتسجيل عبر الهاتف",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "كلمة المرور أو الرمز غير صحيح",
"password or code is incorrect, you have %s remaining chances": "كلمة المرور أو الرمز غير صحيح، لديك %s فرصة متبقية",
"unsupported password type: %s": "نوع كلمة المرور غير مدعوم: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "هذه العملية غير مسموح بها في وضع العرض التوضيحي",
"this operation requires administrator to perform": "هذه العملية تتطلب مسؤولاً لتنفيذها"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "خادم LDAP موجود"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "تم تمكين MFA للبريد الإلكتروني لكن البريد الإلكتروني فارغ",
"MFA phone is enabled but phone number is empty": "تم تمكين MFA للهاتف لكن رقم الهاتف فارغ",
"New password cannot contain blank space.": "كلمة المرور الجديدة لا يمكن أن تحتوي على مسافات.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "مالك المستخدم واسمه لا يجب أن يكونا فارغين"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Giriş uğursuz oldu: %s",
"Invalid token": "Etibarsız token",
"State expected: %s, but got: %s": "Gözlənilən vəziyyət: %s, lakin alınan: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Provayder üçün hesab: %s və istifadəçi adı: %s (%s) mövcud deyil və %%s vasitəsilə yeni hesab olaraq qeydiyyatdan keçməyə icazə verilmir, xahiş edirik qeydiyyat üçün başqa üsul istifadə edin",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Provayder üçün hesab: %s və istifadəçi adı: %s (%s) mövcud deyil və %s vasitəsilə yeni hesab olaraq qeydiyyatdan keçməyə icazə verilmir, xahiş edirik qeydiyyat üçün başqa üsul istifadə edin",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Provayder üçün hesab: %s və istifadəçi adı: %s (%s) mövcud deyil və yeni hesab olaraq qeydiyyatdan keçməyə icazə verilmir, xahiş edirik IT dəstəyinizlə əlaqə saxlayın",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Provayder üçün hesab: %s və istifadəçi adı: %s (%s) artıq başqa hesabla əlaqələndirilmişdir: %s (%s)",
"The application: %s does not exist": "Tətbiq: %s mövcud deyil",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "IP ünvanınız: %s konfiqurasiyaya görə qadağan edilib: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Şifrənizin vaxtı keçib. Xahiş edirik \\\"Şifrəni unutdum\\\" düyməsinə basaraq şifrənizi sıfırlayın",
"Your region is not allow to signup by phone": "Regionunuzda telefonla qeydiyyata icazə verilmir",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "şifrə və ya kod yanlışdır",
"password or code is incorrect, you have %s remaining chances": "şifrə və ya kod yanlışdır, %s şansınız qalıb",
"unsupported password type: %s": "dəstəklənməyən şifrə növü: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "bu əməliyyat demo rejimində icazə verilmir",
"this operation requires administrator to perform": "bu əməliyyat administrator tərəfindən yerinə yetirilməsini tələb edir"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP serveri mövcuddur"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA email aktiv edilib, lakin email boşdur",
"MFA phone is enabled but phone number is empty": "MFA telefon aktiv edilib, lakin telefon nömrəsi boşdur",
"New password cannot contain blank space.": "Yeni şifrə boş yer ehtiva edə bilməz.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "istifadəçinin sahibi və adı boş olmamalıdır"
},
"util": {
@@ -190,4 +195,4 @@
"Found no credentials for this user": "Bu istifadəçi üçün heç bir etimadnamə tapılmadı",
"Please call WebAuthnSigninBegin first": "Xahiş edirik əvvəlcə WebAuthnSigninBegin çağırın"
}
}
}

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Nepodařilo se přihlásit: %s",
"Invalid token": "Neplatný token",
"State expected: %s, but got: %s": "Očekávaný stav: %s, ale získán: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) neexistuje a není povoleno se registrovat jako nový účet přes %%s, prosím použijte jiný způsob registrace",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) neexistuje a není povoleno se registrovat jako nový účet přes %s, prosím použijte jiný způsob registrace",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) neexistuje a není povoleno se registrovat jako nový účet, prosím kontaktujte svou IT podporu",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Účet pro poskytovatele: %s a uživatelské jméno: %s (%s) je již propojen s jiným účtem: %s (%s)",
"The application: %s does not exist": "Aplikace: %s neexistuje",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Vaše IP adresa: %s byla zablokována podle konfigurace: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Vaše heslo vypršelo. Prosím resetujte si heslo kliknutím na \\\"Zapomněl jsem heslo\\\"",
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "heslo nebo kód je nesprávný",
"password or code is incorrect, you have %s remaining chances": "heslo nebo kód je nesprávné, máte %s zbývajících pokusů",
"unsupported password type: %s": "nepodporovaný typ hesla: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "tato operace není povolena v demo režimu",
"this operation requires administrator to perform": "tato operace vyžaduje administrátora k provedení"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Ldap server existuje"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA e-mail je povolen, ale e-mail je prázdný",
"MFA phone is enabled but phone number is empty": "MFA telefon je povolen, ale telefonní číslo je prázdné",
"New password cannot contain blank space.": "Nové heslo nemůže obsahovat prázdné místo.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "vlastník a jméno uživatele by neměly být prázdné"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Konnte nicht anmelden: %s",
"Invalid token": "Ungültiges Token",
"State expected: %s, but got: %s": "Erwarteter Zustand: %s, aber erhalten: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Das Konto für den Anbieter: %s und Benutzernamen: %s (%s) existiert nicht und darf nicht über %%s als neues Konto erstellt werden. Bitte nutzen Sie einen anderen Weg, um sich anzumelden",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Das Konto für den Anbieter: %s und Benutzernamen: %s (%s) existiert nicht und darf nicht über %s als neues Konto erstellt werden. Bitte nutzen Sie einen anderen Weg, um sich anzumelden",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Das Konto für den Anbieter %s und Benutzernamen %s (%s) existiert nicht und es ist nicht erlaubt, ein neues Konto anzumelden. Bitte wenden Sie sich an Ihren IT-Support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Das Konto für den Anbieter %s und Benutzernamen %s (%s) ist bereits mit einem anderen Konto verknüpft: %s (%s)",
"The application: %s does not exist": "Die Anwendung: %s existiert nicht",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Ihre IP-Adresse: %s wurde laut Konfiguration gesperrt von: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Ihr Passwort ist abgelaufen. Bitte setzen Sie Ihr Passwort zurück, indem Sie auf \\\"Passwort vergessen\\\" klicken",
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "Passwort oder Code ist falsch",
"password or code is incorrect, you have %s remaining chances": "Das Passwort oder der Code ist falsch. Du hast noch %s Versuche übrig",
"unsupported password type: %s": "Nicht unterstützter Passworttyp: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "Dieser Vorgang ist im Demo-Modus nicht erlaubt",
"this operation requires administrator to perform": "Dieser Vorgang erfordert einen Administrator zur Ausführung"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Es gibt einen LDAP-Server"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA-E-Mail ist aktiviert, aber E-Mail ist leer",
"MFA phone is enabled but phone number is empty": "MFA-Telefon ist aktiviert, aber Telefonnummer ist leer",
"New password cannot contain blank space.": "Das neue Passwort darf keine Leerzeichen enthalten.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "Eigentümer und Name des Benutzers dürfen nicht leer sein"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Your IP address: %s has been banned according to the configuration of: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %s remaining chances": "password or code is incorrect, you have %s remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
"this operation requires administrator to perform": "this operation requires administrator to perform"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA email is enabled but email is empty",
"MFA phone is enabled but phone number is empty": "MFA phone is enabled but phone number is empty",
"New password cannot contain blank space.": "New password cannot contain blank space.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "the user's owner and name should not be empty"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "No se ha podido iniciar sesión en: %s",
"Invalid token": "Token inválido",
"State expected: %s, but got: %s": "Estado esperado: %s, pero se obtuvo: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "La cuenta para el proveedor: %s y nombre de usuario: %s (%s) no existe y no está permitido registrarse como una cuenta nueva a través de %%s, por favor use otro método para registrarse",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "La cuenta para el proveedor: %s y nombre de usuario: %s (%s) no existe y no está permitido registrarse como una cuenta nueva a través de %s, por favor use otro método para registrarse",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "La cuenta para el proveedor: %s y el nombre de usuario: %s (%s) no existe y no se permite registrarse como una nueva cuenta, por favor contacte a su soporte de TI",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "La cuenta para proveedor: %s y nombre de usuario: %s (%s) ya está vinculada a otra cuenta: %s (%s)",
"The application: %s does not exist": "La aplicación: %s no existe",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Su dirección IP: %s ha sido bloqueada según la configuración de: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Su contraseña ha expirado. Restablezca su contraseña haciendo clic en \\\"Olvidé mi contraseña\\\"",
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "contraseña o código incorrecto",
"password or code is incorrect, you have %s remaining chances": "Contraseña o código incorrecto, tienes %s intentos restantes",
"unsupported password type: %s": "Tipo de contraseña no compatible: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "esta operación no está permitida en modo de demostración",
"this operation requires administrator to perform": "esta operación requiere que el administrador la realice"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "El servidor LDAP existe"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "El correo electrónico MFA está habilitado pero el correo está vacío",
"MFA phone is enabled but phone number is empty": "El teléfono MFA está habilitado pero el número de teléfono está vacío",
"New password cannot contain blank space.": "La nueva contraseña no puede contener espacios en blanco.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "el propietario y el nombre del usuario no deben estar vacíos"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "عدم موفقیت در ورود: %s",
"Invalid token": "توکن نامعتبر",
"State expected: %s, but got: %s": "وضعیت مورد انتظار: %s، اما دریافت شد: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید از طریق %%s نیست، لطفاً از روش دیگری برای ثبت‌نام استفاده کنید",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید از طریق %s نیست، لطفاً از روش دیگری برای ثبت‌نام استفاده کنید",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) وجود ندارد و مجاز به ثبت‌نام به‌عنوان حساب جدید نیست، لطفاً با پشتیبانی IT خود تماس بگیرید",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "حساب برای ارائه‌دهنده: %s و نام کاربری: %s (%s) در حال حاضر به حساب دیگری مرتبط است: %s (%s)",
"The application: %s does not exist": "برنامه: %s وجود ندارد",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "آدرس IP شما: %s طبق تنظیمات بلاک شده است: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "رمز عبور شما منقضی شده است. لطفاً با کلیک بر روی \\\"فراموشی رمز عبور\\\" رمز عبور خود را بازنشانی کنید",
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبت‌نام با تلفن را ندارد",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "رمز عبور یا کد نادرست است",
"password or code is incorrect, you have %s remaining chances": "رمز عبور یا کد نادرست است، شما %s فرصت باقی‌مانده دارید",
"unsupported password type: %s": "نوع رمز عبور پشتیبانی نشده: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "این عملیات در حالت دمو مجاز نیست",
"this operation requires administrator to perform": "این عملیات نیاز به مدیر برای انجام دارد"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "سرور LDAP وجود دارد"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "ایمیل MFA فعال است اما ایمیل خالی است",
"MFA phone is enabled but phone number is empty": "تلفن MFA فعال است اما شماره تلفن خالی است",
"New password cannot contain blank space.": "رمز عبور جدید نمی‌تواند حاوی فاصله خالی باشد.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "مالک و نام کاربر نباید خالی باشند"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Sisäänkirjautuminen epäonnistui: %s",
"Invalid token": "Virheellinen token",
"State expected: %s, but got: %s": "Odotettiin tilaa: %s, mutta saatiin: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Tiliä providerille: %s ja käyttäjälle: %s (%s) ei ole olemassa, eikä sitä voi rekisteröidä uutena tilinä kautta %%s, käytä toista tapaa rekisteröityä",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Tiliä providerille: %s ja käyttäjälle: %s (%s) ei ole olemassa, eikä sitä voi rekisteröidä uutena tilinä kautta %s, käytä toista tapaa rekisteröityä",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Tiliä providerille: %s ja käyttäjälle: %s (%s) ei ole olemassa, eikä sitä voi rekisteröidä uutena tilinä, ota yhteyttä IT-tukeen",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Tili providerille: %s ja käyttäjälle: %s (%s) on jo linkitetty toiseen tiliin: %s (%s)",
"The application: %s does not exist": "Sovellus: %s ei ole olemassa",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "IP-osoitteesi: %s on estetty asetuksen mukaan: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Salasanasi on vanhentunut. Nollaa salasanasi klikkaamalla \\\"Unohdin salasanan\\\"",
"Your region is not allow to signup by phone": "Alueellasi ei ole sallittua rekisteröityä puhelimella",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "salasana tai koodi on virheellinen",
"password or code is incorrect, you have %s remaining chances": "salasana tai koodi on virheellinen, sinulla on %s yritystä jäljellä",
"unsupported password type: %s": "ei-tuettu salasanatyyppi: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "tämä toiminto ei ole sallittu demo-tilassa",
"this operation requires administrator to perform": "tämä toiminto vaatii ylläpitäjän suorittamista"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP-palvelin on olemassa"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA-sähköposti on käytössä, mutta sähköposti on tyhjä",
"MFA phone is enabled but phone number is empty": "MFA-puhelin on käytössä, mutta puhelinnumero on tyhjä",
"New password cannot contain blank space.": "Uusi salasana ei voi sisältää välilyöntejä.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "käyttäjän omistaja ja nimi eivät saa olla tyhjiä"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Échec de la connexion : %s",
"Invalid token": "Jeton invalide",
"State expected: %s, but got: %s": "État attendu : %s, mais obtenu : %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Le compte pour le fournisseur : %s et le nom d'utilisateur : %s (%s) n'existe pas et n'est pas autorisé à s'inscrire en tant que nouveau compte via %%s, veuillez utiliser une autre méthode pour vous inscrire",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Le compte pour le fournisseur : %s et le nom d'utilisateur : %s (%s) n'existe pas et n'est pas autorisé à s'inscrire en tant que nouveau compte via %s, veuillez utiliser une autre méthode pour vous inscrire",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Le compte pour le fournisseur : %s et le nom d'utilisateur : %s (%s) n'existe pas et n'est pas autorisé à s'inscrire comme nouveau compte, veuillez contacter votre support informatique",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Le compte du fournisseur : %s et le nom d'utilisateur : %s (%s) sont déjà liés à un autre compte : %s (%s)",
"The application: %s does not exist": "L'application : %s n'existe pas",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Votre adresse IP : %s a été bannie selon la configuration de : ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Votre mot de passe a expiré. Veuillez le réinitialiser en cliquant sur \\\"Mot de passe oublié\\\"",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "mot de passe ou code incorrect",
"password or code is incorrect, you have %s remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %s chances",
"unsupported password type: %s": "Type de mot de passe non pris en charge : %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "cette opération n'est pas autorisée en mode démo",
"this operation requires administrator to perform": "cette opération nécessite un administrateur pour être effectuée"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Le serveur LDAP existe"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "L'authentification MFA par e-mail est activée mais l'e-mail est vide",
"MFA phone is enabled but phone number is empty": "L'authentification MFA par téléphone est activée mais le numéro de téléphone est vide",
"New password cannot contain blank space.": "Le nouveau mot de passe ne peut pas contenir d'espace.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "le propriétaire et le nom de l'utilisateur ne doivent pas être vides"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "כניסה נכשלה: %s",
"Invalid token": "אסימון שגוי",
"State expected: %s, but got: %s": "מצב צפוי: %s, אך התקבל: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "החשבון עבור ספק: %s ושם משתמש: %s (%s) אינו קיים ואינו מאופשר להרשמה כחשבון חדש דרך %%s, אנא השתמש בדרך אחרת להרשמה",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "החשבון עבור ספק: %s ושם משתמש: %s (%s) אינו קיים ואינו מאופשר להרשמה כחשבון חדש דרך %s, אנא השתמש בדרך אחרת להרשמה",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "החשבון עבור ספק: %s ושם משתמש: %s (%s) אינו קיים ואינו מאופשר להרשמה כחשבון חדש, אנא צור קשר עם התמיכה הטכנית",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "החשבון עבור ספק: %s ושם משתמש: %s (%s) כבר מקושר לחשבון אחר: %s (%s)",
"The application: %s does not exist": "האפליקציה: %s אינה קיימת",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "כתובת ה-IP שלך: %s נחסמה לפי התצורה של: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "הסיסמה שלך פגה. אנא אפס את הסיסמה שלך על ידי לחיצה על \\\"שכחתי סיסמה\\\"",
"Your region is not allow to signup by phone": "האזור שלך אינו מאפשר הרשמה באמצעות טלפון",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "סיסמה או קוד שגויים",
"password or code is incorrect, you have %s remaining chances": "סיסמה או קוד שגויים, יש לך %s ניסיונות נותרים",
"unsupported password type: %s": "סוג סיסמה לא נתמך: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "פעולה זו אינה מותרת במצב הדגמה",
"this operation requires administrator to perform": "פעולה זו דורשת מנהל לביצוע"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "שרת LDAP קיים"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA דוא\"ל מופעל אך הדוא\"ל ריק",
"MFA phone is enabled but phone number is empty": "MFA טלפון מופעל אך מספר הטלפון ריק",
"New password cannot contain blank space.": "הסיסמה החדשה אינה יכולה להכיל רווחים.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "הבעלים והשם של המשתמש אינם יכולים להיות ריקים"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Gagal masuk: %s",
"Invalid token": "Token tidak valid",
"State expected: %s, but got: %s": "Diharapkan: %s, tapi diperoleh: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru melalui %%s, silakan gunakan cara lain untuk mendaftar",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru melalui %s, silakan gunakan cara lain untuk mendaftar",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru, silakan hubungi dukungan IT Anda",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Akun untuk penyedia: %s dan username: %s (%s) sudah terhubung dengan akun lain: %s (%s)",
"The application: %s does not exist": "Aplikasi: %s tidak ada",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Alamat IP Anda: %s telah diblokir sesuai konfigurasi: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Kata sandi Anda telah kedaluwarsa. Silakan atur ulang kata sandi dengan mengklik \\\"Lupa kata sandi\\\"",
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "kata sandi atau kode salah",
"password or code is incorrect, you have %s remaining chances": "Sandi atau kode salah, Anda memiliki %s kesempatan tersisa",
"unsupported password type: %s": "jenis sandi tidak didukung: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "operasi ini tidak diizinkan dalam mode demo",
"this operation requires administrator to perform": "operasi ini memerlukan administrator untuk melakukannya"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Server ldap ada"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "Email MFA diaktifkan tetapi email kosong",
"MFA phone is enabled but phone number is empty": "Telepon MFA diaktifkan tetapi nomor telepon kosong",
"New password cannot contain blank space.": "Sandi baru tidak boleh mengandung spasi kosong.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "pemilik dan nama pengguna tidak boleh kosong"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Login fallito: %s",
"Invalid token": "Token non valido",
"State expected: %s, but got: %s": "Stato atteso: %s, ricevuto: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Account per provider: %s e utente: %s (%s) non esiste, registrazione tramite %%s non consentita",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Account per provider: %s e utente: %s (%s) non esiste, registrazione tramite %s non consentita",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Account per provider: %s e utente: %s (%s) non esiste, registrazione non consentita: contatta l'assistenza IT",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Account per provider: %s e utente: %s (%s) già collegato a un altro account: %s (%s)",
"The application: %s does not exist": "L'app: %s non esiste",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Il tuo indirizzo IP: %s è stato bannato secondo la configurazione di: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "La tua password è scaduta. Reimposta la password cliccando \\\"Password dimenticata\\\"",
"Your region is not allow to signup by phone": "Registrazione via telefono non consentita nella tua regione",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "password o codice errati",
"password or code is incorrect, you have %s remaining chances": "password o codice errati, %s tentativi rimasti",
"unsupported password type: %s": "tipo password non supportato: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "questa operazione non è consentita in modalità demo",
"this operation requires administrator to perform": "questa operazione richiede un amministratore"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Server LDAP esistente"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "L'email MFA è abilitata ma l'email è vuota",
"MFA phone is enabled but phone number is empty": "Il telefono MFA è abilitato ma il numero di telefono è vuoto",
"New password cannot contain blank space.": "Nuova password non può contenere spazi",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "il proprietario e il nome dell'utente non devono essere vuoti"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "ログインできませんでした:%s",
"Invalid token": "無効なトークン",
"State expected: %s, but got: %s": "期待される状態: %s、実際には%s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "プロバイダーのアカウント:%s とユーザー名:%s%sが存在せず、新しいアカウントを %%s 経由でサインアップすることはできません。他の方法でサインアップしてください",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "プロバイダーのアカウント:%s とユーザー名:%s%sが存在せず、新しいアカウントを %s 経由でサインアップすることはできません。他の方法でサインアップしてください",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "プロバイダー名:%sとユーザー名%s%sのアカウントは存在しません。新しいアカウントとしてサインアップすることはできません。 ITサポートに連絡してください",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "プロバイダのアカウント:%s とユーザー名:%s (%s) は既に別のアカウント:%s (%s) にリンクされています",
"The application: %s does not exist": "アプリケーション: %sは存在しません",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "あなたの IP アドレス「%s」は設定によりアクセスが禁止されています: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "パスワードの有効期限が切れています。「パスワードを忘れた方はこちら」をクリックしてリセットしてください",
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "パスワードまたはコードが正しくありません",
"password or code is incorrect, you have %s remaining chances": "パスワードまたはコードが間違っています。あと %s 回の試行機会があります",
"unsupported password type: %s": "サポートされていないパスワードタイプ:%s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "この操作はデモモードでは許可されていません",
"this operation requires administrator to perform": "この操作は管理者権限が必要です"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAPサーバーは存在します"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA メールが有効になっていますが、メールアドレスが空です",
"MFA phone is enabled but phone number is empty": "MFA 電話番号が有効になっていますが、電話番号が空です",
"New password cannot contain blank space.": "新しいパスワードにはスペースを含めることはできません。",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "ユーザーのオーナーと名前は空にできません"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Inloggen mislukt: %s",
"Invalid token": "Ongeldige token",
"State expected: %s, but got: %s": "Verwachte state: %s, maar kreeg: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Account voor provider: %s en gebruikersnaam: %s (%s) bestaat niet en mag niet registreren via %%s, gebruik een andere methode",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Account voor provider: %s en gebruikersnaam: %s (%s) bestaat niet en mag niet registreren via %s, gebruik een andere methode",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Account voor provider: %s en gebruikersnaam: %s (%s) bestaat niet en mag niet registreren, contacteer IT-support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Account voor provider: %s en gebruikersnaam: %s (%s) is al gelinkt aan ander account: %s (%s)",
"The application: %s does not exist": "Applicatie: %s bestaat niet",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Je IP-adres: %s is verbannen volgens configuratie van: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Сіздің пароліңіз мерзімі аяқталды. Кликтап \\\"Парольді ұмытып қалдым\\\" нұсқасын қалпына келтіріңіз",
"Your region is not allow to signup by phone": "Registratie per telefoon niet toegestaan in jouw regio",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "wachtwoord of code onjuist",
"password or code is incorrect, you have %s remaining chances": "wachtwoord of code onjuist, nog %s pogingen over",
"unsupported password type: %s": "niet-ondersteund wachtwoordtype: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "deze handeling is niet toegestaan in demo-modus",
"this operation requires administrator to perform": "deze handeling vereist beheerder"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP-server bestaat al"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA-e-mail is ingeschakeld maar e-mailadres is leeg",
"MFA phone is enabled but phone number is empty": "MFA-telefoon is ingeschakeld maar telefoonnummer is leeg",
"New password cannot contain blank space.": "Nieuw wachtwoord mag geen spaties bevatten.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "eigenaar en naam van gebruiker mogen niet leeg zijn"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "로그인에 실패했습니다.: %s",
"Invalid token": "유효하지 않은 토큰",
"State expected: %s, but got: %s": "예상한 상태: %s, 실제 상태: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "제공자 계정: %s와 사용자 이름: %s (%s)은(는) 존재하지 않으며 %%s를 통해 새 계정으로 가입하는 것이 허용되지 않습니다. 다른 방법으로 가입하십시오",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "제공자 계정: %s와 사용자 이름: %s (%s)은(는) 존재하지 않으며 %s를 통해 새 계정으로 가입하는 것이 허용되지 않습니다. 다른 방법으로 가입하십시오",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "공급자 계정 %s과 사용자 이름 %s (%s)는 존재하지 않으며 새 계정으로 등록할 수 없습니다. IT 지원팀에 문의하십시오",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "공급자 계정 %s과 사용자 이름 %s(%s)는 이미 다른 계정 %s(%s)에 연결되어 있습니다",
"The application: %s does not exist": "해당 애플리케이션(%s)이 존재하지 않습니다",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "IP 주소 %s이(가) 설정에 따라 차단되었습니다: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "비밀번호가 만료되었습니다. \\\"비밀번호 찾기\\\"를 클릭하여 비밀번호를 재설정하세요",
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "비밀번호 또는 코드가 올바르지 않습니다",
"password or code is incorrect, you have %s remaining chances": "암호 또는 코드가 올바르지 않습니다. %s 번의 기회가 남아 있습니다",
"unsupported password type: %s": "지원되지 않는 암호 유형: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "이 작업은 데모 모드에서 허용되지 않습니다",
"this operation requires administrator to perform": "이 작업은 관리자 권한이 필요합니다"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP 서버가 존재합니다"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA 이메일이 활성화되었지만 이메일이 비어 있습니다",
"MFA phone is enabled but phone number is empty": "MFA 전화번호가 활성화되었지만 전화번호가 비어 있습니다",
"New password cannot contain blank space.": "새 비밀번호에는 공백이 포함될 수 없습니다.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "사용자의 소유자와 이름은 비워둘 수 없습니다"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Gagal log masuk: %s",
"Invalid token": "Token tidak sah",
"State expected: %s, but got: %s": "Jangkaan keadaan: %s, tetapi dapat: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Akaun untuk pembekal: %s dan nama pengguna: %s (%s) tidak wujud dan tidak dibenarkan daftar melalui %%s, sila guna cara lain",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Akaun untuk pembekal: %s dan nama pengguna: %s (%s) tidak wujud dan tidak dibenarkan daftar melalui %s, sila guna cara lain",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Akaun untuk pembekal: %s dan nama pengguna: %s (%s) tidak wujud dan tidak dibenarkan daftar, sila hubungi sokongan IT",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Akaun untuk pembekal: %s dan nama pengguna: %s (%s) sudah dipautkan kepada akaun lain: %s (%s)",
"The application: %s does not exist": "Aplikasi: %s tidak wujud",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Alamat IP anda: %s telah disekat mengikut konfigurasi: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Kata laluan anda tamat. Sila tetapkan semula dengan klik \\\"Lupa kata laluan\\\"",
"Your region is not allow to signup by phone": "Wilayah anda tidak dibenarkan daftar melalui telefon",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "kata laluan atau kod salah",
"password or code is incorrect, you have %s remaining chances": "kata laluan atau kod salah, anda ada %s peluang lagi",
"unsupported password type: %s": "jenis kata laluan tidak disokong: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "operasi ini tidak dibenarkan dalam mod demo",
"this operation requires administrator to perform": "operasi ini perlukan pentadbir untuk jalankan"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Pelayan LDAP sudah wujud"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA emel dibenarkan tetapi emel kosong",
"MFA phone is enabled but phone number is empty": "MFA telefon dibenarkan tetapi nombor telefon kosong",
"New password cannot contain blank space.": "Kata laluan baharu tidak boleh ada ruang kosong.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "pemilik dan nama pengguna tidak boleh kosong"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Inloggen mislukt: %s",
"Invalid token": "Ongeldige token",
"State expected: %s, but got: %s": "Verwachtte state: %s, gekregen: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Gebruiker bestaat niet; aanmelden via %%s niet toegestaan, kies andere methode",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Gebruiker bestaat niet; aanmelden via %s niet toegestaan, kies andere methode",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Gebruiker bestaat niet; aanmelden niet toegestaan, neem contact op met IT",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Account al gekoppeld aan andere gebruiker: %s (%s)",
"The application: %s does not exist": "Applicatie %s bestaat niet",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "IP-adres %s geblokkeerd volgens configuratie: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Wachtwoord verlopen; klik op \"Wachtwoord vergeten\"",
"Your region is not allow to signup by phone": "Registratie per telefoon niet toegestaan in jouw regio",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "Verkeerd wachtwoord of code",
"password or code is incorrect, you have %s remaining chances": "Verkeerd wachtwoord/code, nog %s pogingen",
"unsupported password type: %s": "Niet-ondersteund wachtwoordtype: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "Handeling niet toegestaan in demo-modus",
"this operation requires administrator to perform": "Alleen beheerder kan deze handeling uitvoeren"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP-server bestaat al"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA-e-mail ingeschakeld maar e-mailadres leeg",
"MFA phone is enabled but phone number is empty": "MFA-telefoon ingeschakeld maar nummer leeg",
"New password cannot contain blank space.": "Nieuw wachtwoord mag geen spaties bevatten",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "Eigenaar en naam van gebruiker mogen niet leeg zijn"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Logowanie nie powiodło się: %s",
"Invalid token": "Nieprawidłowy token",
"State expected: %s, but got: %s": "Oczekiwano stanu: %s, ale otrzymano: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Konto dla dostawcy: %s i nazwy użytkownika: %s (%s) nie istnieje i nie można się zarejestrować jako nowe konto przez %%s, użyj innej metody rejestracji",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Konto dla dostawcy: %s i nazwy użytkownika: %s (%s) nie istnieje i nie można się zarejestrować jako nowe konto przez %s, użyj innej metody rejestracji",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Konto dla dostawcy: %s i nazwy użytkownika: %s (%s) nie istnieje i nie można się zarejestrować jako nowe konto, skontaktuj się z pomocą IT",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Konto dla dostawcy: %s i nazwy użytkownika: %s (%s) jest już powiązane z innym kontem: %s (%s)",
"The application: %s does not exist": "Aplikacja: %s nie istnieje",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Twój adres IP: %s został zablokowany zgodnie z konfiguracją: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Twoje hasło wygasło. Zresetuj hasło klikając \\\"Zapomniałem hasła\\\"",
"Your region is not allow to signup by phone": "Twój region nie pozwala na rejestrację przez telefon",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "hasło lub kod jest nieprawidłowe",
"password or code is incorrect, you have %s remaining chances": "hasło lub kod jest nieprawidłowe, masz jeszcze %s prób",
"unsupported password type: %s": "nieobsługiwany typ hasła: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "ta operacja nie jest dozwolona w trybie demo",
"this operation requires administrator to perform": "ta operacja wymaga administratora do wykonania"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Serwer LDAP istnieje"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA email jest włączone, ale email jest pusty",
"MFA phone is enabled but phone number is empty": "MFA telefon jest włączony, ale numer telefonu jest pusty",
"New password cannot contain blank space.": "Nowe hasło nie może zawierać spacji.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "właściciel i nazwa użytkownika nie powinny być puste"
},
"util": {

View File

@@ -1,60 +1,60 @@
{
"account": {
"Failed to add user": "Falha ao adicionar usuário",
"Get init score failed, error: %w": "Obter pontuação inicial falhou, erro: %w",
"Please sign out first": "Por favor, saia da sessão primeiro",
"The application does not allow to sign up new account": "O aplicativo não permite a criação de uma nova conta"
"Get init score failed, error: %w": "Falha ao obter pontuação inicial, erro: %w",
"Please sign out first": "Por favor, saia primeiro",
"The application does not allow to sign up new account": "O aplicativo não permite a criação de novas contas"
},
"auth": {
"Challenge method should be S256": "Método de desafio deve ser S256",
"Challenge method should be S256": "O método de desafio deve ser S256",
"DeviceCode Invalid": "Código de dispositivo inválido",
"Failed to create user, user information is invalid: %s": "Falha ao criar usuário, informação do usuário inválida: %s",
"Failed to login in: %s": "Falha ao entrar em: %s",
"Failed to create user, user information is invalid: %s": "Falha ao criar usuário, as informações são inválidas: %s",
"Failed to login in: %s": "Falha ao fazer login em: %s",
"Invalid token": "Token inválido",
"State expected: %s, but got: %s": "Estado esperado: %s, mas recebeu: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "A conta para o provedor: %s e nome de usuário: %s (%s) não existe e não é permitido inscrever-se como uma nova conta via %%s, por favor, use outra forma de se inscrever",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "A conta para o provedor: %s e nome de usuário: %s (%s) não existe e não é permitido inscrever-se como uma nova conta entre em contato com seu suporte de TI",
"State expected: %s, but got: %s": "Estado esperado: %s, mas recebido: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "A conta do provedor: %s e nome de usuário: %s (%s) não existe e não é permitido criar nova conta via %s, por favor, use outra forma de cadastro",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "A conta do provedor: %s e nome de usuário: %s (%s) não existe e não é permitido criar nova conta, entre em contato com o suporte de TI",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "A conta do provedor: %s e nome de usuário: %s (%s) já está vinculada a outra conta: %s (%s)",
"The application: %s does not exist": "O aplicativo: %s não existe",
"The application: %s has disabled users to signin": "The application: %s has disabled users to signin",
"The login method: login with LDAP is not enabled for the application": "O método de login: login com LDAP não está ativado para a aplicação",
"The login method: login with SMS is not enabled for the application": "O método de login: login com SMS não está ativado para a aplicação",
"The login method: login with email is not enabled for the application": "O método de login: login com e-mail não está ativado para a aplicação",
"The login method: login with face is not enabled for the application": "O método de login: login com reconhecimento facial não está ativado para a aplicação",
"The login method: login with password is not enabled for the application": "O método de login: login com senha não está habilitado para o aplicativo",
"The application: %s has disabled users to signin": "O aplicativo: %s desativou o login de usuários",
"The login method: login with LDAP is not enabled for the application": "O método de login com LDAP não está habilitado para o aplicativo",
"The login method: login with SMS is not enabled for the application": "O método de login com SMS não está habilitado para o aplicativo",
"The login method: login with email is not enabled for the application": "O método de login com e-mail não está habilitado para o aplicativo",
"The login method: login with face is not enabled for the application": "O método de login com reconhecimento facial não está habilitado para o aplicativo",
"The login method: login with password is not enabled for the application": "O método de login com senha não está habilitado para o aplicativo",
"The organization: %s does not exist": "A organização: %s não existe",
"The organization: %s has disabled users to signin": "The organization: %s has disabled users to signin",
"The organization: %s has disabled users to signin": "A organização: %s desativou o login de usuários",
"The provider: %s does not exist": "O provedor: %s não existe",
"The provider: %s is not enabled for the application": "O provedor: %s não está habilitado para o aplicativo",
"Unauthorized operation": "Operação não autorizada",
"Unknown authentication type (not password or provider), form = %s": "Tipo de autenticação desconhecido (não é senha ou provedor), formulário = %s",
"User's tag: %s is not listed in the application's tags": "A tag do usuário: %s não está listada nas tags da aplicação",
"User's tag: %s is not listed in the application's tags": "A tag do usuário: %s não está listada nas tags do aplicativo",
"UserCode Expired": "Código de usuário expirado",
"UserCode Invalid": "Código de usuário inválido",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "usuário pago %s não possui assinatura ativa ou pendente e a aplicação: %s não possui preço padrão",
"the application for user %s is not found": "a aplicação para o usuário %s não foi encontrada",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "O usuário pago %s não possui assinatura ativa ou pendente e o aplicativo: %s não possui preço padrão",
"the application for user %s is not found": "o aplicativo para o usuário %s não foi encontrado",
"the organization: %s is not found": "a organização: %s não foi encontrada"
},
"cas": {
"Service %s and %s do not match": "O serviço %s e %s não correspondem"
},
"check": {
"%s does not meet the CIDR format requirements: %s": "%s não atende aos requisitos de formato CIDR: %s",
"Affiliation cannot be blank": "A filiação não pode estar em branco",
"CIDR for IP: %s should not be empty": "CIDR para IP: %s não deve estar vazio",
"Default code does not match the code's matching rules": "O código padrão não corresponde às regras de correspondência do código",
"%s does not meet the CIDR format requirements: %s": "%s não atende aos requisitos do formato CIDR: %s",
"Affiliation cannot be blank": "A afiliação não pode estar em branco",
"CIDR for IP: %s should not be empty": "O CIDR para o IP: %s não pode estar vazio",
"Default code does not match the code's matching rules": "O código padrão não corresponde às regras de validação do código",
"DisplayName cannot be blank": "O nome de exibição não pode estar em branco",
"DisplayName is not valid real name": "O nome de exibição não é um nome real válido",
"Email already exists": "O e-mail já existe",
"Email cannot be empty": "O e-mail não pode estar vazio",
"Email is invalid": "O e-mail é inválido",
"Email is invalid": "E-mail inválido",
"Empty username.": "Nome de usuário vazio.",
"Face data does not exist, cannot log in": "Dados faciais não existem, não é possível fazer login",
"Face data mismatch": "Incompatibilidade de dados faciais",
"Failed to parse client IP: %s": "Falha ao analisar IP do cliente: %s",
"Face data mismatch": "Dados faciais não correspondem",
"Failed to parse client IP: %s": "Falha ao analisar o IP do cliente: %s",
"FirstName cannot be blank": "O primeiro nome não pode estar em branco",
"Invitation code cannot be blank": "O código de convite não pode estar em branco",
"Invitation code exhausted": "Código de convite esgotado",
"Invitation code exhausted": "O código de convite foi esgotado",
"Invitation code is invalid": "Código de convite inválido",
"Invitation code suspended": "Código de convite suspenso",
"LDAP user name or password incorrect": "Nome de usuário ou senha LDAP incorretos",
@@ -64,7 +64,7 @@
"Password cannot be empty": "A senha não pode estar vazia",
"Phone already exists": "O telefone já existe",
"Phone cannot be empty": "O telefone não pode estar vazio",
"Phone number is invalid": "O número de telefone é inválido",
"Phone number is invalid": "Número de telefone inválido",
"Please register using the email corresponding to the invitation code": "Por favor, registre-se usando o e-mail correspondente ao código de convite",
"Please register using the phone corresponding to the invitation code": "Por favor, registre-se usando o telefone correspondente ao código de convite",
"Please register using the username corresponding to the invitation code": "Por favor, registre-se usando o nome de usuário correspondente ao código de convite",
@@ -73,66 +73,70 @@
"The user is forbidden to sign in, please contact the administrator": "O usuário está proibido de entrar, entre em contato com o administrador",
"The user: %s doesn't exist in LDAP server": "O usuário: %s não existe no servidor LDAP",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "O nome de usuário pode conter apenas caracteres alfanuméricos, sublinhados ou hífens, não pode ter hífens ou sublinhados consecutivos e não pode começar ou terminar com hífen ou sublinhado.",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "O valor \\\"%s\\\" para o campo de conta \\\"%s\\\" não corresponde à expressão regular do item de conta",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "O valor \\\"%s\\\" para o campo de registro \\\"%s\\\" não corresponde à expressão regular do item de registro da aplicação \\\"%s\\\"",
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "O valor \\\"%s\\\" para o campo de conta \\\"%s\\\" não corresponde à expressão regular definida",
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "O valor \\\"%s\\\" para o campo de registro \\\"%s\\\" não corresponde à expressão regular definida na aplicação \\\"%s\\\"",
"Username already exists": "O nome de usuário já existe",
"Username cannot be an email address": "O nome de usuário não pode ser um endereço de e-mail",
"Username cannot contain white spaces": "O nome de usuário não pode conter espaços em branco",
"Username cannot start with a digit": "O nome de usuário não pode começar com um dígito",
"Username is too long (maximum is 255 characters).": "Nome de usuário é muito longo (máximo é 255 caracteres).",
"Username must have at least 2 characters": "Nome de usuário deve ter pelo menos 2 caracteres",
"Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.": "O nome de usuário suporta formato de e-mail. Além disso, o nome de usuário pode conter apenas caracteres alfanuméricos, sublinhados ou hífens, não pode ter hífens ou sublinhados consecutivos e não pode começar ou terminar com hífen ou sublinhado. Preste atenção também ao formato de e-mail.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Você digitou a senha ou o código incorretos muitas vezes, aguarde %d minutos e tente novamente",
"Your IP address: %s has been banned according to the configuration of: ": "Seu endereço IP: %s foi banido de acordo com a configuração de: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Sua senha expirou. Por favor, redefina sua senha clicando em \\\"Esqueci a senha\\\"",
"Username cannot start with a digit": "O nome de usuário não pode começar com um número",
"Username is too long (maximum is 255 characters).": "O nome de usuário é muito longo (máximo de 255 caracteres).",
"Username must have at least 2 characters": "O nome de usuário deve ter pelo menos 2 caracteres",
"Username supports email format. Also The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline. Also pay attention to the email format.": "O nome de usuário suporta o formato de e-mail. Além disso, pode conter apenas caracteres alfanuméricos, sublinhados ou hífens, não pode ter hífens ou sublinhados consecutivos e não pode começar ou terminar com hífen ou sublinhado. Também preste atenção ao formato do e-mail.",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Você digitou a senha ou o código incorretos muitas vezes. Aguarde %d minutos e tente novamente",
"Your IP address: %s has been banned according to the configuration of: ": "Seu endereço IP: %s foi bloqueado de acordo com a configuração de: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Sua senha expirou. Por favor, redefina-a clicando em \\\"Esqueci a senha\\\"",
"Your region is not allow to signup by phone": "Sua região não permite cadastro por telefone",
"password or code is incorrect": "senha ou código incorreto",
"password or code is incorrect, you have %s remaining chances": "senha ou código está incorreto, você tem %s chances restantes",
"unsupported password type: %s": "tipo de senha não suportado: %s"
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "Senha ou código incorreto",
"password or code is incorrect, you have %s remaining chances": "Senha ou código incorreto, você tem %s tentativas restantes",
"unsupported password type: %s": "Tipo de senha não suportado: %s"
},
"enforcer": {
"the adapter: %s is not found": "o adaptador: %s não foi encontrado"
"the adapter: %s is not found": "O adaptador: %s não foi encontrado"
},
"general": {
"Failed to import groups": "Falha ao importar grupos",
"Failed to import users": "Falha ao importar usuários",
"Missing parameter": "Parâmetro faltante",
"Only admin user can specify user": "Apenas usuário administrador pode especificar usuário",
"Please login first": "Faça login primeiro",
"The organization: %s should have one application at least": "A organização: %s deve ter pelo menos uma aplicação",
"Missing parameter": "Parâmetro ausente",
"Only admin user can specify user": "Apenas um administrador pode especificar um usuário",
"Please login first": "Por favor, faça login primeiro",
"The organization: %s should have one application at least": "A organização: %s deve ter pelo menos um aplicativo",
"The user: %s doesn't exist": "O usuário: %s não existe",
"Wrong userId": "ID de usuário incorreto",
"don't support captchaProvider: ": "não suporta captchaProvider: ",
"don't support captchaProvider: ": "captchaProvider não suportado: ",
"this operation is not allowed in demo mode": "esta operação não é permitida no modo de demonstração",
"this operation requires administrator to perform": "esta operação requer um administrador para executar"
"this operation requires administrator to perform": "esta operação requer privilégios de administrador"
},
"invitation": {
"Invitation %s does not exist": "O convite %s não existe"
},
"ldap": {
"Ldap server exist": "Servidor LDAP existe"
"Ldap server exist": "O servidor LDAP existe"
},
"link": {
"Please link first": "Vincule primeiro",
"This application has no providers": "Este aplicativo não tem provedores",
"This application has no providers of type": "Este aplicativo não tem provedores do tipo",
"Please link first": "Por favor, vincule primeiro",
"This application has no providers": "Este aplicativo não possui provedores",
"This application has no providers of type": "Este aplicativo não possui provedores do tipo especificado",
"This provider can't be unlinked": "Este provedor não pode ser desvinculado",
"You are not the global admin, you can't unlink other users": "Você não é o administrador global, não pode desvincular outros usuários",
"You can't unlink yourself, you are not a member of any application": "Você não pode se desvincular, não é membro de nenhum aplicativo"
"You can't unlink yourself, you are not a member of any application": "Você não pode se desvincular, pois não é membro de nenhum aplicativo"
},
"organization": {
"Only admin can modify the %s.": "Apenas o administrador pode modificar o %s.",
"The %s is immutable.": "O %s é imutável.",
"Unknown modify rule %s.": "Regra de modificação %s desconhecida.",
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "nte desativada. Observe que todos os usuários na organização 'built-in' são administradores globais no Casdoor. Consulte a documentação: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Se ainda desejar criar um usuário para a organização 'built-in', acesse a página de configurações da organização e habilite a opção 'Possui consentimento de privilégios'."
"Unknown modify rule %s.": "Regra de modificação desconhecida: %s.",
"adding a new user to the 'built-in' organization is currently disabled. Please note: all users in the 'built-in' organization are global administrators in Casdoor. Refer to the docs: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. If you still wish to create a user for the 'built-in' organization, go to the organization's settings page and enable the 'Has privilege consent' option.": "A adição de novos usuários à organização 'built-in' está desativada. Observe que todos os usuários nessa organização são administradores globais no Casdoor. Consulte a documentação: https://casdoor.org/docs/basic/core-concepts#how-does-casdoor-manage-itself. Se ainda desejar criar um usuário, vá até a página de configurações da organização e habilite a opção 'Possui consentimento de privilégios'."
},
"permission": {
"The permission: \\\"%s\\\" doesn't exist": "A permissão: \\\"%s\\\" não existe"
},
"provider": {
"Invalid application id": "Id do aplicativo inválido",
"the provider: %s does not exist": "o provedor: %s não existe"
"Invalid application id": "ID de aplicativo inválido",
"the provider: %s does not exist": "O provedor: %s não existe"
},
"resource": {
"User is nil for tag: avatar": "Usuário é nulo para tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Nome de usuário ou fullFilePath está vazio: username = %s, fullFilePath = %s"
"User is nil for tag: avatar": "Usuário nulo para a tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Nome de usuário ou fullFilePath vazio: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Aplicativo %s não encontrado"
@@ -146,7 +150,7 @@
"Invalid phone receivers: %s": "Destinatários de telefone inválidos: %s"
},
"storage": {
"The objectKey: %s is not allowed": "O objectKey: %s não é permitido",
"The objectKey: %s is not allowed": "A chave de objeto: %s não é permitida",
"The provider type: %s is not supported": "O tipo de provedor: %s não é suportado"
},
"subscription": {
@@ -154,40 +158,41 @@
},
"token": {
"Grant_type: %s is not supported in this application": "Grant_type: %s não é suportado neste aplicativo",
"Invalid application or wrong clientSecret": "Aplicativo inválido ou clientSecret errado",
"Invalid application or wrong clientSecret": "Aplicativo inválido ou clientSecret incorreto",
"Invalid client_id": "client_id inválido",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "URI de redirecionamento: %s não existe na lista de URI de redirecionamento permitida",
"Token not found, invalid accessToken": "Token não encontrado, token de acesso inválido"
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "O URI de redirecionamento: %s não existe na lista de URIs permitidos",
"Token not found, invalid accessToken": "Token não encontrado, accessToken inválido"
},
"user": {
"Display name cannot be empty": "Nome de exibição não pode ser vazio",
"MFA email is enabled but email is empty": "MFA por e-mail está ativado, mas o e-mail está vazio",
"MFA phone is enabled but phone number is empty": "MFA por telefone está ativado, mas o número de telefone está vazio",
"New password cannot contain blank space.": "A nova senha não pode conter espaço em branco.",
"the user's owner and name should not be empty": "o proprietário e o nome do usuário não devem estar vazios"
"Display name cannot be empty": "O nome de exibição não pode estar vazio",
"MFA email is enabled but email is empty": "MFA por e-mail está habilitado, mas o e-mail está vazio",
"MFA phone is enabled but phone number is empty": "MFA por telefone está habilitado, mas o número de telefone está vazio",
"New password cannot contain blank space.": "A nova senha não pode conter espaços em branco.",
"The new password must be different from your current password": "A nova senha deve ser diferente da senha atual",
"the user's owner and name should not be empty": "O proprietário e o nome do usuário não devem estar vazios"
},
"util": {
"No application is found for userId: %s": "Nenhum aplicativo encontrado para userId: %s",
"No provider for category: %s is found for application: %s": "Nenhum provedor para categoria: %s encontrado para aplicativo: %s",
"No application is found for userId: %s": "Nenhum aplicativo encontrado para o userId: %s",
"No provider for category: %s is found for application: %s": "Nenhum provedor da categoria: %s encontrado para o aplicativo: %s",
"The provider: %s is not found": "O provedor: %s não foi encontrado"
},
"verification": {
"Invalid captcha provider.": "Provedor de captcha inválido.",
"Phone number is invalid in your region %s": "Número de telefone é inválido em sua região %s",
"Phone number is invalid in your region %s": "Número de telefone inválido na sua região %s",
"The verification code has already been used!": "O código de verificação já foi utilizado!",
"The verification code has not been sent yet!": "O código de verificação ainda não foi enviado!",
"Turing test failed.": "Teste de Turing falhou.",
"Turing test failed.": "O teste de Turing falhou.",
"Unable to get the email modify rule.": "Não foi possível obter a regra de modificação de e-mail.",
"Unable to get the phone modify rule.": "Não foi possível obter a regra de modificação de telefone.",
"Unknown type": "Tipo desconhecido",
"Wrong verification code!": "Código de verificação incorreto!",
"You should verify your code in %d min!": "Você deve verificar seu código em %d min!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "por favor, adicione um provedor de SMS à lista \\\"Providers\\\" da aplicação: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "por favor, adicione um provedor de e-mail à lista \\\"Providers\\\" da aplicação: %s",
"the user does not exist, please sign up first": "o usuário não existe, cadastre-se primeiro"
"You should verify your code in %d min!": "Você deve verificar seu código em %d minuto(s)!",
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "Adicione um provedor de SMS à lista \\\"Providers\\\" do aplicativo: %s",
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "Adicione um provedor de e-mail à lista \\\"Providers\\\" do aplicativo: %s",
"the user does not exist, please sign up first": "O usuário não existe, cadastre-se primeiro"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Por favor, chame WebAuthnSigninBegin primeiro"
"Found no credentials for this user": "Nenhuma credencial encontrada para este usuário",
"Please call WebAuthnSigninBegin first": "Por favor, inicie com WebAuthnSigninBegin primeiro"
}
}

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Не удалось войти в систему: %s",
"Invalid token": "Недействительный токен",
"State expected: %s, but got: %s": "Ожидался статус: %s, но получен: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Аккаунт провайдера: %s и имя пользователя: %s (%s) не существует и не может быть зарегистрирован через %%s, пожалуйста, используйте другой способ регистрации",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Аккаунт провайдера: %s и имя пользователя: %s (%s) не существует и не может быть зарегистрирован через %s, пожалуйста, используйте другой способ регистрации",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Аккаунт для провайдера: %s и имя пользователя: %s (%s) не существует и не может быть зарегистрирован как новый аккаунт. Пожалуйста, обратитесь в службу поддержки IT",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Аккаунт поставщика: %s и имя пользователя: %s (%s) уже связаны с другим аккаунтом: %s (%s)",
"The application: %s does not exist": "Приложение: %s не существует",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Ваш IP-адрес: %s заблокирован согласно конфигурации: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Срок действия вашего пароля истек. Пожалуйста, сбросьте пароль, нажав \\\"Забыли пароль\\\"",
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "пароль или код неверны",
"password or code is incorrect, you have %s remaining chances": "Неправильный пароль или код, у вас осталось %s попыток",
"unsupported password type: %s": "неподдерживаемый тип пароля: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "эта операция недоступна в демонстрационном режиме",
"this operation requires administrator to perform": "эта операция требует прав администратора"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP-сервер существует"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA по электронной почте включен, но электронная почта не указана",
"MFA phone is enabled but phone number is empty": "MFA по телефону включен, но номер телефона не указан",
"New password cannot contain blank space.": "Новый пароль не может содержать пробелы.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "владелец и имя пользователя не должны быть пустыми"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Prihlásenie zlyhalo: %s",
"Invalid token": "Neplatný token",
"State expected: %s, but got: %s": "Očakávaný stav: %s, ale dostali sme: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Účet pre poskytovateľa: %s a používateľské meno: %s (%s) neexistuje a nie je povolené zaregistrovať nový účet cez %%s, prosím použite iný spôsob registrácie",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Účet pre poskytovateľa: %s a používateľské meno: %s (%s) neexistuje a nie je povolené zaregistrovať nový účet cez %s, prosím použite iný spôsob registrácie",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Účet pre poskytovateľa: %s a používateľské meno: %s (%s) neexistuje a nie je povolené zaregistrovať nový účet, prosím kontaktujte vašu IT podporu",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Účet pre poskytovateľa: %s a používateľské meno: %s (%s) je už prepojený s iným účtom: %s (%s)",
"The application: %s does not exist": "Aplikácia: %s neexistuje",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Vaša IP adresa: %s bola zablokovaná podľa konfigurácie: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Vaše heslo je expirované. Prosím, resetujte svoje heslo kliknutím na \\\"Zabudnuté heslo\\\"",
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "heslo alebo kód je nesprávne",
"password or code is incorrect, you have %s remaining chances": "heslo alebo kód je nesprávne, máte %s zostávajúcich pokusov",
"unsupported password type: %s": "nepodporovaný typ hesla: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "táto operácia nie je povolená v demo režime",
"this operation requires administrator to perform": "táto operácia vyžaduje vykonanie administrátorom"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP server existuje"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA e-mail je zapnutý, ale e-mail je prázdny",
"MFA phone is enabled but phone number is empty": "MFA telefón je zapnutý, ale telefónne číslo je prázdne",
"New password cannot contain blank space.": "Nové heslo nemôže obsahovať medzery.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "vlastník a meno používateľa nesmú byť prázdne"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Misslyckades logga in: %s",
"Invalid token": "Ogiltig token",
"State expected: %s, but got: %s": "Förväntat tillstånd: %s, men fick: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Kontot för leverantör: %s och användarnamn: %s (%s) finns inte och det är inte tillåtet att registrera ett nytt konto via %%s, använd ett annat sätt att registrera dig",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Kontot för leverantör: %s och användarnamn: %s (%s) finns inte och det är inte tillåtet att registrera ett nytt konto via %s, använd ett annat sätt att registrera dig",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Kontot för leverantör: %s och användarnamn: %s (%s) finns inte och det är inte tillåtet att registrera ett nytt konto, kontakta din IT-support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Kontot för leverantör: %s och användarnamn: %s (%s) är redan länkat till ett annat konto: %s (%s)",
"The application: %s does not exist": "Applikationen: %s finns inte",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Din IP-adress: %s har blockerats enligt konfigurationen av: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Ditt lösenord har gått ut. Återställ det genom att klicka på \\\"Glömt lösenord\\\"",
"Your region is not allow to signup by phone": "Din region tillåter inte registrering via telefon",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "lösenord eller kod är felaktig",
"password or code is incorrect, you have %s remaining chances": "lösenord eller kod är felaktig, du har %s försök kvar",
"unsupported password type: %s": "lösenordstypen stöds inte: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "denna åtgärd är inte tillåten i demoläge",
"this operation requires administrator to perform": "denna åtgärd kräver administratör för att genomföras"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP-servern finns redan"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA-e-post är aktiverat men e-post är tom",
"MFA phone is enabled but phone number is empty": "MFA-telefon är aktiverat men telefonnummer är tomt",
"New password cannot contain blank space.": "Nytt lösenord får inte innehålla mellanslag.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "användarens ägare och namn får inte vara tomma"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Giriş yapılamadı: %s",
"Invalid token": "Geçersiz token",
"State expected: %s, but got: %s": "Beklenen durum: %s, fakat alınan: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Provider: %s ve kullanıcı adı: %s (%s) için hesap mevcut değil ve %%s ile yeni hesap açılmasına izin verilmiyor, lütfen başka yöntemle kaydolun",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Provider: %s ve kullanıcı adı: %s (%s) için hesap mevcut değil ve %s ile yeni hesap açılmasına izin verilmiyor, lütfen başka yöntemle kaydolun",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Provider: %s ve kullanıcı adı: %s (%s) için hesap mevcut değil ve yeni hesap açılmasına izin verilmiyor, lütfen BT destek ile iletişime geçin",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Provider: %s ve kullanıcı adı: %s (%s) zaten başka bir hesaba bağlı: %s (%s)",
"The application: %s does not exist": "Uygulama: %s bulunamadı",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "IP adresiniz: %s, yapılandırmaya göre yasaklandı: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Şifrenizin süresi doldu. Lütfen \\\"Şifremi unuttum\\\"a tıklayarak şifrenizi sıfırlayın",
"Your region is not allow to signup by phone": "Bölgeniz telefonla kayıt yapmaya izin verilmiyor",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "şifre veya kod yanlış",
"password or code is incorrect, you have %s remaining chances": "şifre veya kod yanlış, %s hakkınız kaldı",
"unsupported password type: %s": "desteklenmeyen şifre türü: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "bu işlem demo modunda izin verilmiyor",
"this operation requires administrator to perform": "bu işlem yönetici tarafından gerçekleştirilmelidir"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "LDAP sunucusu zaten var"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA e-postası etkin ancak e-posta boş",
"MFA phone is enabled but phone number is empty": "MFA telefonu etkin ancak telefon numarası boş",
"New password cannot contain blank space.": "Yeni şifre boşluk içeremez.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "kullanıcının sahibi ve adı boş olmamalıdır"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Не вдалося увійти: %s",
"Invalid token": "Недійсний токен",
"State expected: %s, but got: %s": "Очікувалося стан: %s, але отримано: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Обліковий запис для провайдера: %s та імені користувача: %s (%s) не існує і не дозволяється реєструвати як новий через %%s, використайте інший спосіб",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Обліковий запис для провайдера: %s та імені користувача: %s (%s) не існує і не дозволяється реєструвати як новий через %s, використайте інший спосіб",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Обліковий запис для провайдера: %s та імені користувача: %s (%s) не існує і не дозволяється реєструвати як новий, зверніться до IT-підтримки",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Обліковий запис для провайдера: %s та імені користувача: %s (%s) уже пов’язаний з іншим обліковим записом: %s (%s)",
"The application: %s does not exist": "Додаток: %s не існує",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Ваша IP-адреса: %s заблокована відповідно до конфігурації: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Ditt lösenord har gått ut. Återställ det genom att klicka på \\\"Glömt lösenord\\\"",
"Your region is not allow to signup by phone": "У вашому регіоні реєстрація за телефоном недоступна",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "пароль або код неправильний",
"password or code is incorrect, you have %s remaining chances": "пароль або код неправильний, у вас залишилось %s спроб",
"unsupported password type: %s": "непідтримуваний тип пароля: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "ця операція недоступна в демо-режимі",
"this operation requires administrator to perform": "ця операція потребує прав адміністратора"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Сервер LDAP існує"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA email увімкнено, але email порожній",
"MFA phone is enabled but phone number is empty": "MFA телефон увімкнено, але номер телефону порожній",
"New password cannot contain blank space.": "Новий пароль не може містити пробіли.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "власник ім’я користувача не повинні бути порожніми"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "Đăng nhập không thành công: %s",
"Invalid token": "Mã thông báo không hợp lệ",
"State expected: %s, but got: %s": "Trạng thái dự kiến: %s, nhưng nhận được: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Tài khoản cho nhà cung cấp: %s và tên người dùng: %s (%s) không tồn tại và không được phép đăng ký làm tài khoản mới qua %%s, vui lòng sử dụng cách khác để đăng ký",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "Tài khoản cho nhà cung cấp: %s và tên người dùng: %s (%s) không tồn tại và không được phép đăng ký làm tài khoản mới qua %s, vui lòng sử dụng cách khác để đăng ký",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Tài khoản cho nhà cung cấp: %s và tên người dùng: %s (%s) không tồn tại và không được phép đăng ký như một tài khoản mới, vui lòng liên hệ với bộ phận hỗ trợ công nghệ thông tin của bạn",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Tài khoản cho nhà cung cấp: %s và tên người dùng: %s (%s) đã được liên kết với tài khoản khác: %s (%s)",
"The application: %s does not exist": "Ứng dụng: %s không tồn tại",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "Địa chỉ IP của bạn: %s đã bị cấm theo cấu hình của: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "Mật khẩu của bạn đã hết hạn. Vui lòng đặt lại mật khẩu bằng cách nhấp vào \\\"Quên mật khẩu\\\"",
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
"form type %s already exists": "form type %s already exists",
"password or code is incorrect": "mật khẩu hoặc mã không đúng",
"password or code is incorrect, you have %s remaining chances": "Mật khẩu hoặc mã không chính xác, bạn còn %s lần cơ hội",
"unsupported password type: %s": "Loại mật khẩu không được hỗ trợ: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "thao tác này không được phép trong chế độ demo",
"this operation requires administrator to perform": "thao tác này yêu cầu quản trị viên thực hiện"
},
"invitation": {
"Invitation %s does not exist": "Invitation %s does not exist"
},
"ldap": {
"Ldap server exist": "Máy chủ LDAP tồn tại"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA email đã bật nhưng email trống",
"MFA phone is enabled but phone number is empty": "MFA điện thoại đã bật nhưng số điện thoại trống",
"New password cannot contain blank space.": "Mật khẩu mới không thể chứa dấu trắng.",
"The new password must be different from your current password": "The new password must be different from your current password",
"the user's owner and name should not be empty": "chủ sở hữu và tên người dùng không được để trống"
},
"util": {

View File

@@ -12,7 +12,7 @@
"Failed to login in: %s": "登录失败: %s",
"Invalid token": "无效token",
"State expected: %s, but got: %s": "期望状态为: %s, 实际状态为: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "提供商账户: %s 与用户名: %s (%s) 不存在且 不允许通过 %s 注册新账户, 请使用其他方式注册",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %s, please use another way to sign up": "提供商账户: %s 与用户名: %s (%s) 不存在且 不允许通过 %s 注册新账户, 请使用其他方式注册",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "提供商账户: %s 与用户名: %s (%s) 不存在且 不允许注册新账户, 请联系IT支持",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "提供商账户: %s与用户名: %s (%s)已经与其他账户绑定: %s (%s)",
"The application: %s does not exist": "应用%s不存在",
@@ -86,6 +86,7 @@
"Your IP address: %s has been banned according to the configuration of: ": "您的IP地址%s 根据以下配置已被禁止: ",
"Your password has expired. Please reset your password by clicking \\\"Forgot password\\\"": "您的密码已过期。请点击 \\\"忘记密码\\\" 以重置密码",
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
"form type %s already exists": "表单类型 %s 已经存在",
"password or code is incorrect": "密码错误",
"password or code is incorrect, you have %s remaining chances": "密码错误,您还有 %s 次尝试的机会",
"unsupported password type: %s": "不支持的密码类型: %s"
@@ -106,6 +107,9 @@
"this operation is not allowed in demo mode": "demo模式下不允许该操作",
"this operation requires administrator to perform": "只有管理员才能进行此操作"
},
"invitation": {
"Invitation %s does not exist": "邀请%s不存在"
},
"ldap": {
"Ldap server exist": "LDAP服务器已存在"
},
@@ -164,6 +168,7 @@
"MFA email is enabled but email is empty": "MFA 电子邮件已启用,但电子邮件为空",
"MFA phone is enabled but phone number is empty": "MFA 电话已启用,但电话号码为空",
"New password cannot contain blank space.": "新密码不可以包含空格",
"The new password must be different from your current password": "新密码必须与您当前的密码不同",
"the user's owner and name should not be empty": "用户的组织和名称不能为空"
},
"util": {

View File

@@ -135,6 +135,7 @@ func (idp *LarkIdProvider) GetToken(code string) (*oauth2.Token, error) {
"open_id": "ou-caecc734c2e3328a62489fe0648c4b98779515d3",
"union_id": "on-d89jhsdhjsajkda7828enjdj328ydhhw3u43yjhdj",
"email": "zhangsan@feishu.cn",
"enterprise_email": "zhangsan@company.com",
"user_id": "5d9bdxxx",
"mobile": "+86130002883xx",
"tenant_key": "736588c92lxf175d",
@@ -160,6 +161,7 @@ type LarkUserInfo struct {
OpenId string `json:"open_id"`
UnionId string `json:"union_id"`
Email string `json:"email"`
EnterpriseEmail string `json:"enterprise_email"`
UserId string `json:"user_id"`
Mobile string `json:"mobile"`
TenantKey string `json:"tenant_key"`
@@ -168,7 +170,6 @@ type LarkUserInfo struct {
} `json:"data"`
}
// GetUserInfo use LarkAccessToken gotten before return LinkedInUserInf
// GetUserInfo use LarkAccessToken gotten before return LinkedInUserInfo
// get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context
func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
@@ -207,6 +208,12 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
return nil, err
}
// Use enterprise_email as fallback when email is empty
email := larkUserInfo.Data.Email
if email == "" {
email = larkUserInfo.Data.EnterpriseEmail
}
var phoneNumber string
var countryCode string
if len(larkUserInfo.Data.Mobile) != 0 {
@@ -222,7 +229,7 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
Id: larkUserInfo.Data.OpenId,
DisplayName: larkUserInfo.Data.Name,
Username: larkUserInfo.Data.UserId,
Email: larkUserInfo.Data.Email,
Email: email,
AvatarUrl: larkUserInfo.Data.AvatarUrl,
Phone: phoneNumber,
CountryCode: countryCode,

View File

@@ -60,6 +60,11 @@ type SamlItem struct {
Value string `json:"value"`
}
type JwtItem struct {
Name string `json:"name"`
Value string `json:"value"`
}
type Application struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
@@ -67,6 +72,7 @@ type Application struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Logo string `xorm:"varchar(200)" json:"logo"`
Order int `json:"order"`
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
Description string `xorm:"varchar(100)" json:"description"`
Organization string `xorm:"varchar(100)" json:"organization"`
@@ -96,6 +102,7 @@ type Application struct {
CertPublicKey string `xorm:"-" json:"certPublicKey"`
Tags []string `xorm:"mediumtext" json:"tags"`
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
SamlHashAlgorithm string `xorm:"varchar(20)" json:"samlHashAlgorithm"`
IsShared bool `json:"isShared"`
IpRestriction string `json:"ipRestriction"`
@@ -106,6 +113,7 @@ type Application struct {
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
TokenAttributes []*JwtItem `xorm:"mediumtext" json:"tokenAttributes"`
ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"`
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
@@ -266,6 +274,14 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
Rule: "None",
}
application.SigninItems = append(application.SigninItems, signinItem)
signinItem = &SigninItem{
Name: "Verification code",
Visible: true,
CustomCss: ".verification-code {}\n.verification-code-input{}",
Placeholder: "",
Rule: "None",
}
application.SigninItems = append(application.SigninItems, signinItem)
signinItem = &SigninItem{
Name: "Agreement",
Visible: true,
@@ -550,7 +566,6 @@ func GetMaskedApplication(application *Application, userId string) *Application
application.Providers = providerItems
application.GrantTypes = nil
application.Tags = nil
application.RedirectUris = nil
application.TokenFormat = "***"
application.TokenFields = nil
@@ -626,13 +641,17 @@ func GetAllowedApplications(applications []*Application, userId string, lang str
return res, nil
}
func UpdateApplication(id string, application *Application) (bool, error) {
func UpdateApplication(id string, application *Application, isGlobalAdmin bool) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
oldApplication, err := getApplication(owner, name)
if oldApplication == nil {
return false, err
}
if !isGlobalAdmin && oldApplication.Organization != application.Organization {
return false, fmt.Errorf("auth:Unauthorized operation")
}
if name == "app-built-in" {
application.Name = name
}
@@ -709,7 +728,7 @@ func AddApplication(application *Application) (bool, error) {
}
func deleteApplication(application *Application) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{application.Owner, application.Name}).Delete(&Application{})
affected, err := ormer.Engine.ID(core.PK{application.Owner, application.Name}).Where("organization = ?", application.Organization).Delete(&Application{})
if err != nil {
return false, err
}

View File

@@ -478,7 +478,10 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
continue
}
if !permission.isUserHit(userId) && !permission.isRoleHit(userId) {
userHit := permission.isUserHit(userId)
roleHit := permission.isRoleHit(userId)
if !userHit && !roleHit {
if permission.Effect == "Allow" {
allowPermissionCount += 1
} else {
@@ -493,18 +496,56 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
}
var isAllowed bool
isAllowed, err = enforcer.Enforce(userId, application.Name, "Read")
if err != nil {
return false, err
}
if isAllowed {
if permission.Effect == "Allow" {
allowCount += 1
if userHit {
isAllowed, err = enforcer.Enforce(userId, application.Name, "Read")
if err != nil {
return false, err
}
} else {
if permission.Effect == "Deny" {
denyCount += 1
if isAllowed {
if permission.Effect == "Allow" {
allowCount += 1
}
} else {
if permission.Effect == "Deny" {
denyCount += 1
}
}
}
if roleHit {
targetRoles, err := getRolesByUser(userId)
if err != nil {
return false, err
}
var checkRoleList []*Role
for _, role := range permission.Roles {
if role == "*" {
checkRoleList = targetRoles
break
}
for _, targetRole := range targetRoles {
if role == targetRole.GetId() {
checkRoleList = append(checkRoleList, targetRole)
}
}
}
for _, role := range checkRoleList {
isAllowed, err = enforcer.Enforce(role.GetId(), application.Name, "Read")
if isAllowed {
if permission.Effect == "Allow" {
allowCount += 1
}
} else {
if permission.Effect == "Deny" {
denyCount += 1
}
}
}
}
}

View File

@@ -16,6 +16,8 @@ package object
import (
"regexp"
"github.com/casdoor/casdoor/cred"
)
type ValidatorFunc func(password string) string
@@ -96,3 +98,26 @@ func checkPasswordComplexity(password string, options []string) string {
}
return ""
}
// CheckPasswordNotSameAsCurrent checks if the new password is different from the current password
func CheckPasswordNotSameAsCurrent(user *User, newPassword string, organization *Organization) bool {
if user.Password == "" {
// User doesn't have a password set (e.g., OAuth-only users), allow any password
return true
}
credManager := cred.GetCredManager(organization.PasswordType)
if credManager == nil {
// If no credential manager is available, we can't compare passwords
return true
}
// Check if the new password is the same as the current password
// Try with both organization salt and user salt (like CheckPassword function does)
if credManager.IsPasswordCorrect(newPassword, user.Password, organization.PasswordSalt) ||
credManager.IsPasswordCorrect(newPassword, user.Password, user.PasswordSalt) {
return false
}
return true
}

View File

@@ -30,7 +30,7 @@ func TestSmtpServer(provider *Provider) error {
return nil
}
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
func SendEmail(provider *Provider, title string, content string, dest []string, sender string) error {
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl)
fromAddress := provider.ClientId2

160
object/form.go Normal file
View File

@@ -0,0 +1,160 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type FormItem struct {
Name string `json:"name"`
Label string `json:"label"`
Visible bool `json:"visible"`
Width string `json:"width"`
}
type Form struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Type string `xorm:"varchar(100)" json:"type"`
Tag string `xorm:"varchar(100)" json:"tag"`
FormItems []*FormItem `xorm:"varchar(5000)" json:"formItems"`
}
func GetMaskedForm(form *Form, isMaskEnabled bool) *Form {
if !isMaskEnabled {
return form
}
if form == nil {
return nil
}
return form
}
func GetMaskedForms(forms []*Form, isMaskEnabled bool) []*Form {
if !isMaskEnabled {
return forms
}
for _, form := range forms {
form = GetMaskedForm(form, isMaskEnabled)
}
return forms
}
func GetGlobalForms() ([]*Form, error) {
forms := []*Form{}
err := ormer.Engine.Asc("owner").Desc("created_time").Find(&forms)
if err != nil {
return forms, err
}
return forms, nil
}
func GetForms(owner string) ([]*Form, error) {
forms := []*Form{}
err := ormer.Engine.Desc("created_time").Find(&forms, &Form{Owner: owner})
if err != nil {
return forms, err
}
return forms, nil
}
func getForm(owner string, name string) (*Form, error) {
form := Form{Owner: owner, Name: name}
existed, err := ormer.Engine.Get(&form)
if err != nil {
return &form, err
}
if existed {
return &form, nil
} else {
return nil, nil
}
}
func GetForm(id string) (*Form, error) {
owner, name := util.GetOwnerAndNameFromId(id)
return getForm(owner, name)
}
func UpdateForm(id string, form *Form) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
existingForm, err := getForm(owner, name)
if existingForm == nil {
return false, fmt.Errorf("the form: %s is not found", id)
}
if err != nil {
return false, err
}
if form == nil {
return false, nil
}
_, err = ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(form)
if err != nil {
return false, err
}
return true, nil
}
func AddForm(form *Form) (bool, error) {
affected, err := ormer.Engine.Insert(form)
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteForm(form *Form) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{form.Owner, form.Name}).Delete(&Form{})
if err != nil {
return false, err
}
return affected != 0, nil
}
func (form *Form) GetId() string {
return fmt.Sprintf("%s/%s", form.Owner, form.Name)
}
func GetFormCount(owner string, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Form{})
}
func GetPaginationForms(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Form, error) {
forms := []*Form{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&forms)
if err != nil {
return forms, err
}
return forms, nil
}

View File

@@ -46,6 +46,8 @@ type InitData struct {
Sessions []*Session `json:"sessions"`
Subscriptions []*Subscription `json:"subscriptions"`
Transactions []*Transaction `json:"transactions"`
EnforcerPolicies map[string][][]string `json:"enforcerPolicies"`
}
var initDataNewOnly bool
@@ -85,9 +87,6 @@ func InitFromFile() {
for _, model := range initData.Models {
initDefinedModel(model)
}
for _, permission := range initData.Permissions {
initDefinedPermission(permission)
}
for _, payment := range initData.Payments {
initDefinedPayment(payment)
}
@@ -116,7 +115,11 @@ func InitFromFile() {
initDefinedAdapter(adapter)
}
for _, enforcer := range initData.Enforcers {
initDefinedEnforcer(enforcer)
policies := initData.EnforcerPolicies[enforcer.GetId()]
initDefinedEnforcer(enforcer, policies)
}
for _, permission := range initData.Permissions {
initDefinedPermission(permission)
}
for _, plan := range initData.Plans {
initDefinedPlan(plan)
@@ -175,6 +178,8 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
Sessions: []*Session{},
Subscriptions: []*Subscription{},
Transactions: []*Transaction{},
EnforcerPolicies: map[string][][]string{},
}
err := util.JsonToStruct(s, data)
if err != nil {
@@ -694,7 +699,7 @@ func initDefinedAdapter(adapter *Adapter) {
}
}
func initDefinedEnforcer(enforcer *Enforcer) {
func initDefinedEnforcer(enforcer *Enforcer, policies [][]string) {
existed, err := getEnforcer(enforcer.Owner, enforcer.Name)
if err != nil {
panic(err)
@@ -716,6 +721,27 @@ func initDefinedEnforcer(enforcer *Enforcer) {
if err != nil {
panic(err)
}
err = enforcer.InitEnforcer()
if err != nil {
panic(err)
}
for _, policy := range policies {
if enforcer.HasPolicy(policy) {
continue
}
_, err = enforcer.AddPolicy(policy)
if err != nil {
panic(err)
}
}
err = enforcer.SavePolicy()
if err != nil {
panic(err)
}
}
func initDefinedPlan(plan *Plan) {

View File

@@ -146,6 +146,16 @@ func writeInitDataToFile(filePath string) error {
return err
}
enforcerPolicies := make(map[string][][]string)
for _, enforcer := range enforcers {
err = enforcer.InitEnforcer()
if err != nil {
continue
}
enforcerPolicies[enforcer.GetId()] = enforcer.GetPolicy()
}
data := &InitData{
Organizations: organizations,
Applications: applications,
@@ -172,6 +182,8 @@ func writeInitDataToFile(filePath string) error {
Sessions: sessions,
Subscriptions: subscriptions,
Transactions: transactions,
EnforcerPolicies: enforcerPolicies,
}
text := util.StructToJsonFormatted(data)

View File

@@ -235,3 +235,8 @@ func (invitation *Invitation) IsInvitationCodeValid(application *Application, in
}
return true, ""
}
func (invitation *Invitation) GetInvitationLink(host string, application string) string {
frontEnd, _ := getOriginFromHost(host)
return fmt.Sprintf("%s/signup/%s?invitationCode=%s", frontEnd, application, invitation.Code)
}

View File

@@ -23,18 +23,19 @@ type Ldap struct {
Owner string `xorm:"varchar(100)" json:"owner"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `xorm:"int" json:"port"`
EnableSsl bool `xorm:"bool" json:"enableSsl"`
AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
Username string `xorm:"varchar(100)" json:"username"`
Password string `xorm:"varchar(100)" json:"password"`
BaseDn string `xorm:"varchar(500)" json:"baseDn"`
Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `xorm:"int" json:"port"`
EnableSsl bool `xorm:"bool" json:"enableSsl"`
AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
Username string `xorm:"varchar(100)" json:"username"`
Password string `xorm:"varchar(100)" json:"password"`
BaseDn string `xorm:"varchar(500)" json:"baseDn"`
Filter string `xorm:"varchar(200)" json:"filter"`
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
CustomAttributes map[string]string `json:"customAttributes"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
@@ -151,7 +152,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
}
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert").Update(ldap)
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert", "custom_attributes").Update(ldap)
if err != nil {
return false, nil
}

View File

@@ -58,9 +58,10 @@ type LdapUser struct {
RegisteredAddress string
PostalAddress string
GroupId string `json:"groupId"`
Address string `json:"address"`
MemberOf string `json:"memberOf"`
GroupId string `json:"groupId"`
Address string `json:"address"`
MemberOf string `json:"memberOf"`
Attributes map[string]string `json:"attributes"`
}
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
@@ -158,6 +159,10 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
SearchAttributes = append(SearchAttributes, "uid")
}
for attribute := range ldapServer.CustomAttributes {
SearchAttributes = append(SearchAttributes, attribute)
}
searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
0, 0, false,
ldapServer.Filter, SearchAttributes, nil)
@@ -211,6 +216,13 @@ func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
user.PostalAddress = attribute.Values[0]
case "memberOf":
user.MemberOf = attribute.Values[0]
default:
if propName, ok := ldapServer.CustomAttributes[attribute.Name]; ok {
if user.Attributes == nil {
user.Attributes = make(map[string]string)
}
user.Attributes[propName] = attribute.Values[0]
}
}
}
ldapUsers = append(ldapUsers, user)
@@ -269,6 +281,7 @@ func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
Email: util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
Mobile: util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
Address: util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
Attributes: user.Attributes,
}
}
return res
@@ -345,6 +358,7 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
Tag: tag,
Score: score,
Ldap: syncUser.Uuid,
Properties: syncUser.Attributes,
}
if ldap.DefaultGroup != "" {

View File

@@ -427,4 +427,9 @@ func (a *Ormer) createTable() {
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Form))
if err != nil {
panic(err)
}
}

View File

@@ -49,7 +49,7 @@ type Permission struct {
State string `xorm:"varchar(100)" json:"state"`
}
const builtInAvailableField = 5 // Casdoor built-in adapter, use V5 to filter permission, so has 5 available field
const builtInMaxFields = 6 // Casdoor built-in adapter, use V5 to filter permission, so has 6 max field
func GetPermissionCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")

View File

@@ -410,15 +410,23 @@ m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act`
policyDefinition := strings.Split(cfg.String("policy_definition::p"), ",")
fieldsNum := len(policyDefinition)
if fieldsNum > builtInAvailableField {
return nil, fmt.Errorf("the maximum policy_definition field number cannot exceed %d, got %d", builtInAvailableField, fieldsNum)
if fieldsNum > builtInMaxFields {
return nil, fmt.Errorf("the maximum policy_definition field number cannot exceed %d, got %d", builtInMaxFields, fieldsNum)
}
// filled empty field with "" and V5 with "permissionId"
for i := builtInAvailableField - fieldsNum; i > 0; i-- {
policyDefinition = append(policyDefinition, "")
if fieldsNum == builtInMaxFields {
sixthField := strings.TrimSpace(policyDefinition[builtInMaxFields-1])
if sixthField != "permissionId" {
return nil, fmt.Errorf("when adding policies with permissions, the sixth field of policy_definition must be permissionId, got %s", policyDefinition[builtInMaxFields-1])
}
} else {
needFill := builtInMaxFields - fieldsNum
for i := 0; i < needFill-1; i++ {
policyDefinition = append(policyDefinition, "")
}
policyDefinition = append(policyDefinition, "permissionId")
}
policyDefinition = append(policyDefinition, "permissionId")
m, err := model.NewModelFromString(modelText)
if err != nil {

View File

@@ -43,6 +43,8 @@ type Record struct {
type Response struct {
Status string `json:"status"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
func maskPassword(recordString string) string {
@@ -52,6 +54,10 @@ func maskPassword(recordString string) string {
func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
clientIp := strings.Replace(util.GetClientIpFromRequest(ctx.Request), ": ", "", -1)
action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1)
if strings.HasPrefix(action, "notify-payment") {
action = "notify-payment"
}
requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"})
if len(requestUri) > 1000 {
requestUri = requestUri[0:1000]
@@ -74,6 +80,19 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
return nil, err
}
if action != "buy-product" {
resp.Data = nil
}
dataResp := ""
if resp.Data != nil {
dataByte, err := json.Marshal(resp.Data)
if err != nil {
return nil, err
}
dataResp = fmt.Sprintf(", data:%s", string(dataByte))
}
language := ctx.Request.Header.Get("Accept-Language")
if len(language) > 2 {
language = language[0:2]
@@ -91,7 +110,7 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
Language: languageCode,
Object: object,
StatusCode: 200,
Response: fmt.Sprintf("{status:\"%s\", msg:\"%s\"}", resp.Status, resp.Msg),
Response: fmt.Sprintf("{status:\"%s\", msg:\"%s\"%s}", resp.Status, resp.Msg, dataResp),
IsTriggered: false,
}
return &record, nil

View File

@@ -26,6 +26,7 @@ import (
"errors"
"fmt"
"io"
"net/url"
"strings"
"time"
@@ -70,7 +71,13 @@ func NewSamlResponse(application *Application, user *User, host string, certific
if application.UseEmailAsSamlNameId {
nameIDValue = user.Email
}
subject.CreateElement("saml:NameID").SetText(nameIDValue)
nameId := subject.CreateElement("saml:NameID")
if application.UseEmailAsSamlNameId {
nameId.CreateAttr("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
} else {
nameId.CreateAttr("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")
}
nameId.SetText(nameIDValue)
subjectConfirmation := subject.CreateElement("saml:SubjectConfirmation")
subjectConfirmation.CreateAttr("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer")
subjectConfirmationData := subjectConfirmation.CreateElement("saml:SubjectConfirmationData")
@@ -108,20 +115,28 @@ func NewSamlResponse(application *Application, user *User, host string, certific
displayName.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
displayName.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(user.DisplayName)
err := ExtendUserWithRolesAndPermissions(user)
if err != nil {
return nil, err
}
for _, item := range application.SamlAttributes {
role := attributes.CreateElement("saml:Attribute")
role.CreateAttr("Name", item.Name)
role.CreateAttr("NameFormat", item.NameFormat)
role.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(item.Value)
valueList := replaceAttributeValue(user, item.Value)
for _, value := range valueList {
av := role.CreateElement("saml:AttributeValue")
av.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
av.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
av.CreateAttr("xsi:type", "xs:string").Element().SetText(value)
}
}
roles := attributes.CreateElement("saml:Attribute")
roles.CreateAttr("Name", "Roles")
roles.CreateAttr("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic")
err := ExtendUserWithRolesAndPermissions(user)
if err != nil {
return nil, err
}
for _, role := range user.Roles {
roles.CreateElement("saml:AttributeValue").CreateAttr("xsi:type", "xs:string").Element().SetText(role.Name)
@@ -355,7 +370,13 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
X509Certificate: certificate,
}
ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1
if application.SamlHashAlgorithm == "" || application.SamlHashAlgorithm == "SHA1" {
ctx.Hash = crypto.SHA1
} else if application.SamlHashAlgorithm == "SHA256" {
ctx.Hash = crypto.SHA256
} else if application.SamlHashAlgorithm == "SHA512" {
ctx.Hash = crypto.SHA512
}
if application.EnableSamlC14n10 {
ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
@@ -495,7 +516,14 @@ func NewSamlResponse11(application *Application, user *User, requestID string, h
return samlResponse, nil
}
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string) string {
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string, username string, loginHint string) string {
originF, _ := getOriginFromHost(host)
return fmt.Sprintf("%s/login/saml/authorize/%s/%s?relayState=%s&samlRequest=%s", originF, owner, application, relayState, samlRequest)
baseURL := fmt.Sprintf("%s/login/saml/authorize/%s/%s?relayState=%s&samlRequest=%s", originF, owner, application, relayState, samlRequest)
if username != "" {
baseURL += fmt.Sprintf("&username=%s", url.QueryEscape(username))
}
if loginHint != "" {
baseURL += fmt.Sprintf("&login_hint=%s", url.QueryEscape(loginHint))
}
return baseURL
}

View File

@@ -18,6 +18,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/url"
"regexp"
@@ -152,10 +153,19 @@ func buildSpCertificateStore(provider *Provider, samlResponse string) (certStore
certEncodedData = provider.IdP
}
certData, err := base64.StdEncoding.DecodeString(certEncodedData)
if err != nil {
return dsig.MemoryX509CertificateStore{}, err
var certData []byte
block, _ := pem.Decode([]byte(certEncodedData))
if block != nil {
// this was a PEM file
// block.Bytes are DER encoded so the following code block should happily accept it
certData = block.Bytes
} else {
certData, err = base64.StdEncoding.DecodeString(certEncodedData)
if err != nil {
return dsig.MemoryX509CertificateStore{}, err
}
}
idpCert, err := x509.ParseCertificate(certData)
if err != nil {
return dsig.MemoryX509CertificateStore{}, err

View File

@@ -18,6 +18,7 @@ import (
"bytes"
"fmt"
"net/url"
"path"
"path/filepath"
"strings"
@@ -35,6 +36,7 @@ const (
ProviderTypeTencentCloudCOS = "Tencent Cloud COS"
ProviderTypeAzureBlob = "Azure Blob"
ProviderTypeLocalFileSystem = "Local File System"
ProviderTypeMinIO = "MinIO"
)
func init() {
@@ -211,5 +213,13 @@ func refineObjectKey(provider *Provider, objectKey string) string {
if provider.Type == ProviderTypeGoogleCloudStorage {
return strings.TrimPrefix(objectKey, "/")
}
if provider.Type == ProviderTypeMinIO && provider.PathPrefix != "" {
p := path.Join("/", strings.Trim(provider.PathPrefix, "/"))
if strings.HasPrefix(objectKey, p+"/") {
return strings.TrimPrefix(objectKey, p+"/")
}
}
return objectKey
}

View File

@@ -153,13 +153,15 @@ func GetMaskedSyncers(syncers []*Syncer, errs ...error) ([]*Syncer, error) {
return syncers, nil
}
func UpdateSyncer(id string, syncer *Syncer) (bool, error) {
func UpdateSyncer(id string, syncer *Syncer, isGlobalAdmin bool) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
s, err := getSyncer(owner, name)
if err != nil {
return false, err
} else if s == nil {
return false, nil
} else if !isGlobalAdmin && s.Organization != syncer.Organization {
return false, fmt.Errorf("auth:Unauthorized operation")
}
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
@@ -218,7 +220,7 @@ func AddSyncer(syncer *Syncer) (bool, error) {
}
func DeleteSyncer(syncer *Syncer) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{syncer.Owner, syncer.Name}).Delete(&Syncer{})
affected, err := ormer.Engine.ID(core.PK{syncer.Owner, syncer.Name}).Where("organization = ?", syncer.Organization).Delete(&Syncer{})
if err != nil {
return false, err
}
@@ -273,11 +275,16 @@ func (syncer *Syncer) getKeyColumn() *TableColumn {
return column
}
func (syncer *Syncer) getKey() string {
func (syncer *Syncer) getLocalPrimaryKey() string {
column := syncer.getKeyColumn()
return util.CamelToSnakeCase(column.CasdoorName)
}
func (syncer *Syncer) getTargetTablePrimaryKey() string {
column := syncer.getKeyColumn()
return column.Name
}
func RunSyncer(syncer *Syncer) error {
err := syncer.initAdapter()
if err != nil {

View File

@@ -65,7 +65,7 @@ func (syncer *Syncer) syncUsers() error {
}
}
key := syncer.getKey()
key := syncer.getLocalPrimaryKey()
myUsers := map[string]*User{}
for _, m := range users {

View File

@@ -75,7 +75,7 @@ func (syncer *Syncer) getCasdoorColumns() []string {
}
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
key := syncer.getKey()
key := syncer.getTargetTablePrimaryKey()
m := syncer.getMapFromOriginalUser(user)
pkValue := m[key]
delete(m, key)

View File

@@ -180,12 +180,14 @@ func (token *Token) popularHashes() {
}
}
func UpdateToken(id string, token *Token) (bool, error) {
func UpdateToken(id string, token *Token, isGlobalAdmin bool) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
if t, err := getToken(owner, name); err != nil {
return false, err
} else if t == nil {
return false, nil
} else if !isGlobalAdmin && t.Organization != token.Organization {
return false, nil
}
token.popularHashes()
@@ -210,7 +212,7 @@ func AddToken(token *Token) (bool, error) {
}
func DeleteToken(token *Token) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Delete(&Token{})
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Where("organization = ?", token.Organization).Delete(&Token{})
if err != nil {
return false, err
}

View File

@@ -67,6 +67,14 @@ type CasAttributes struct {
LongTermAuthenticationRequestTokenUsed bool `xml:"cas:longTermAuthenticationRequestTokenUsed"`
IsFromNewLogin bool `xml:"cas:isFromNewLogin"`
MemberOf []string `xml:"cas:memberOf"`
FirstName string `xml:"cas:firstName,omitempty"`
LastName string `xml:"cas:lastName,omitempty"`
Title string `xml:"cas:title,omitempty"`
Email string `xml:"cas:email,omitempty"`
Affiliation string `xml:"cas:affiliation,omitempty"`
Avatar string `xml:"cas:avatar,omitempty"`
Phone string `xml:"cas:phone,omitempty"`
DisplayName string `xml:"cas:displayName,omitempty"`
UserAttributes *CasUserAttributes
ExtraAttributes []*CasAnyAttribute `xml:",any"`
}
@@ -240,6 +248,24 @@ func GenerateCasToken(userId string, service string) (string, error) {
} else {
value = escapedValue
}
switch k {
case "firstName":
authenticationSuccess.Attributes.FirstName = value
case "lastName":
authenticationSuccess.Attributes.LastName = value
case "title":
authenticationSuccess.Attributes.Title = value
case "email":
authenticationSuccess.Attributes.Email = value
case "affiliation":
authenticationSuccess.Attributes.Affiliation = value
case "avatar":
authenticationSuccess.Attributes.Avatar = value
case "phone":
authenticationSuccess.Attributes.Phone = value
case "displayName":
authenticationSuccess.Attributes.DisplayName = value
}
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
Name: k,
Value: value,

View File

@@ -34,6 +34,8 @@ type Claims struct {
// the `azp` (Authorized Party) claim. Optional. See https://openid.net/specs/openid-connect-core-1_0.html#IDToken
Azp string `json:"azp,omitempty"`
Provider string `json:"provider,omitempty"`
SigninMethod string `json:"signinMethod,omitempty"`
jwt.RegisteredClaims
}
@@ -154,6 +156,8 @@ type ClaimsShort struct {
Scope string `json:"scope,omitempty"`
Azp string `json:"azp,omitempty"`
Provider string `json:"provider,omitempty"`
SigninMethod string `json:"signinMethod,omitempty"`
jwt.RegisteredClaims
}
@@ -174,6 +178,8 @@ type ClaimsWithoutThirdIdp struct {
Scope string `json:"scope,omitempty"`
Azp string `json:"azp,omitempty"`
Provider string `json:"provider,omitempty"`
SigninMethod string `json:"signinMethod,omitempty"`
jwt.RegisteredClaims
}
@@ -303,6 +309,7 @@ func getShortClaims(claims Claims) ClaimsShort {
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp,
SigninMethod: claims.SigninMethod,
Provider: claims.Provider,
}
return res
@@ -317,12 +324,13 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
Scope: claims.Scope,
RegisteredClaims: claims.RegisteredClaims,
Azp: claims.Azp,
SigninMethod: claims.SigninMethod,
Provider: claims.Provider,
}
return res
}
func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
func getClaimsCustom(claims Claims, tokenField []string, tokenAttributes []*JwtItem) jwt.MapClaims {
res := make(jwt.MapClaims)
userValue := reflect.ValueOf(claims.User).Elem()
@@ -339,10 +347,11 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
res["tag"] = claims.Tag
res["scope"] = claims.Scope
res["azp"] = claims.Azp
res["signinMethod"] = claims.SigninMethod
res["provider"] = claims.Provider
for _, field := range tokenField {
if strings.HasPrefix(field, "Properties") {
if strings.HasPrefix(field, "Properties.") {
/*
Use selected properties fields as custom claims.
Converts `Properties.my_field` to custom claim with name `my_field`.
@@ -361,6 +370,12 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
res[fieldName] = finalField.Interface()
}
} else if field == "permissionNames" {
permissionNames := []string{}
for _, val := range claims.User.Permissions {
permissionNames = append(permissionNames, val.Name)
}
res[util.SnakeToCamel(util.CamelToSnakeCase(field))] = permissionNames
} else { // Use selected user field as claims.
userField := userValue.FieldByName(field)
if userField.IsValid() {
@@ -370,6 +385,16 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
}
}
for _, item := range tokenAttributes {
valueList := replaceAttributeValue(claims.User, item.Value)
if len(valueList) == 1 {
res[item.Name] = valueList[0]
} else {
res[item.Name] = valueList
}
}
return res
}
@@ -395,7 +420,7 @@ func refineUser(user *User) *User {
return user
}
func generateJwtToken(application *Application, user *User, provider string, nonce string, scope string, host string) (string, string, string, error) {
func generateJwtToken(application *Application, user *User, provider string, signinMethod string, nonce string, scope string, host string) (string, string, string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
@@ -423,10 +448,11 @@ func generateJwtToken(application *Application, user *User, provider string, non
TokenType: "access-token",
Nonce: nonce,
// FIXME: A workaround for custom claim by reusing `tag` in user info
Tag: user.Tag,
Scope: scope,
Azp: application.ClientId,
Provider: provider,
Tag: user.Tag,
Scope: scope,
Azp: application.ClientId,
Provider: provider,
SigninMethod: signinMethod,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: originBackend,
Subject: user.Id,
@@ -481,10 +507,10 @@ func generateJwtToken(application *Application, user *User, provider string, non
claimsShort.TokenType = "refresh-token"
refreshToken = jwt.NewWithClaims(jwtMethod, claimsShort)
} else if application.TokenFormat == "JWT-Custom" {
claimsCustom := getClaimsCustom(claims, application.TokenFields)
claimsCustom := getClaimsCustom(claims, application.TokenFields, application.TokenAttributes)
token = jwt.NewWithClaims(jwtMethod, claimsCustom)
refreshClaims := getClaimsCustom(claims, application.TokenFields)
refreshClaims := getClaimsCustom(claims, application.TokenFields, application.TokenAttributes)
refreshClaims["exp"] = jwt.NewNumericDate(refreshExpireTime)
refreshClaims["TokenType"] = "refresh-token"
refreshToken = jwt.NewWithClaims(jwtMethod, refreshClaims)

View File

@@ -136,7 +136,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return "", application, nil
}
func GetOAuthCode(userId string, clientId string, provider string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
func GetOAuthCode(userId string, clientId string, provider string, signinMethod string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
user, err := GetUser(userId)
if err != nil {
return nil, err
@@ -171,7 +171,7 @@ func GetOAuthCode(userId string, clientId string, provider string, responseType
if err != nil {
return nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, nonce, scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, signinMethod, nonce, scope, host)
if err != nil {
return nil, err
}
@@ -379,7 +379,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
return nil, err
}
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
if err != nil {
return &TokenError{
Error: EndpointError,
@@ -537,6 +537,13 @@ func GetPasswordToken(application *Application, username string, password string
if user.Ldap != "" {
err = CheckLdapUserPassword(user, password, "en")
} else {
// For OAuth users who don't have a password set, they cannot use password grant type
if user.Password == "" {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "OAuth users cannot use password grant type, please use authorization code flow",
}, nil
}
err = CheckPassword(user, password, "en")
}
if err != nil {
@@ -558,7 +565,7 @@ func GetPasswordToken(application *Application, username string, password string
return nil, nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -604,7 +611,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
Type: "application",
}
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", scope, host)
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", "", scope, host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -668,7 +675,7 @@ func GetTokenByUser(application *Application, user *User, scope string, nonce st
return nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", nonce, scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", nonce, scope, host)
if err != nil {
return nil, err
}
@@ -775,7 +782,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
return nil, nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,

View File

@@ -67,7 +67,7 @@ type User struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
Avatar string `xorm:"text" json:"avatar"`
AvatarType string `xorm:"varchar(100)" json:"avatarType"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
@@ -510,6 +510,8 @@ func GetUserByPhone(owner string, phone string) (*User, error) {
return nil, nil
}
phone = util.GetSeperatedPhone(phone)
user := User{Owner: owner, Phone: phone}
existed, err := ormer.Engine.Get(&user)
if err != nil {
@@ -528,6 +530,8 @@ func GetUserByPhoneOnly(phone string) (*User, error) {
return nil, nil
}
phone = util.GetSeperatedPhone(phone)
user := User{Phone: phone}
existed, err := ormer.Engine.Get(&user)
if err != nil {

View File

@@ -15,9 +15,11 @@
package object
import (
"fmt"
"sort"
"strings"
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util"
"github.com/casdoor/casdoor/xlsx"
)
@@ -73,7 +75,7 @@ func parseListItem(lines *[]string, i int) []string {
return trimmedItems
}
func UploadUsers(owner string, path string) (bool, error) {
func UploadUsers(owner string, path string, userObj *User, lang string) (bool, error) {
table := xlsx.ReadXlsxFile(path)
oldUserMap, err := getUserMap(owner)
@@ -81,13 +83,58 @@ func UploadUsers(owner string, path string) (bool, error) {
return false, err
}
transUsers, err := StringArrayToStruct[User](table)
uploadedUsers, err := StringArrayToStruct[User](table)
if err != nil {
return false, err
}
if len(uploadedUsers) == 0 {
return false, fmt.Errorf("no users are provided")
}
organizationName := uploadedUsers[0].Owner
if organizationName == "" || !userObj.IsGlobalAdmin() {
organizationName = owner
}
organization, err := getOrganization("admin", organizationName)
if err != nil {
return false, err
}
if organization == nil {
return false, fmt.Errorf(i18n.Translate(lang, "auth:The organization: %s does not exist"), organizationName)
}
newUsers := []*User{}
for _, user := range transUsers {
for _, user := range uploadedUsers {
if _, ok := oldUserMap[user.GetId()]; !ok {
user.Owner = organizationName
if user.CreatedTime == "" {
user.CreatedTime = util.GetCurrentTime()
}
if user.Id == "" {
user.Id = util.GenerateId()
}
if user.Type == "" {
user.Type = "normal-user"
}
user.PasswordType = "plain"
if user.DisplayName == "" {
user.DisplayName = user.Name
}
user.Avatar = organization.DefaultAvatar
if user.Region == "" {
user.Region = userObj.Region
}
if user.Address == nil {
user.Address = []string{}
}
if user.CountryCode == "" {
user.CountryCode = userObj.CountryCode
}
if user.SignupApplication == "" {
user.SignupApplication = organization.DefaultApplication
}
newUsers = append(newUsers, user)
}
}

View File

@@ -80,7 +80,8 @@ func GetUserByFields(organization string, field string) (*User, error) {
}
// check phone
user, err = GetUserByField(organization, "phone", field)
phone := util.GetSeperatedPhone(field)
user, err = GetUserByField(organization, "phone", phone)
if user != nil || err != nil {
return user, err
}
@@ -247,6 +248,20 @@ func SetUserOAuthProperties(organization *Organization, user *User, providerType
return UpdateUserForAllFields(user.GetId(), user)
}
func getUserRoleNames(user *User) (res []string) {
for _, role := range user.Roles {
res = append(res, role.Name)
}
return res
}
func getUserPermissionNames(user *User) (res []string) {
for _, permission := range user.Permissions {
res = append(res, permission.Name)
}
return res
}
func ClearUserOAuthProperties(user *User, providerType string) (bool, error) {
for k := range user.Properties {
prefix := fmt.Sprintf("oauth_%s_", providerType)
@@ -812,3 +827,49 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
return instances, nil
}
func replaceAttributeValue(user *User, value string) []string {
if user == nil {
return nil
}
valueList := []string{value}
if strings.Contains(value, "$user.roles") {
valueList = replaceAttributeValuesWithList("$user.roles", getUserRoleNames(user), valueList)
}
if strings.Contains(value, "$user.permissions") {
valueList = replaceAttributeValuesWithList("$user.permissions", getUserPermissionNames(user), valueList)
}
if strings.Contains(value, "$user.groups") {
valueList = replaceAttributeValuesWithList("$user.groups", user.Groups, valueList)
}
valueList = replaceAttributeValues("$user.owner", user.Owner, valueList)
valueList = replaceAttributeValues("$user.name", user.Name, valueList)
valueList = replaceAttributeValues("$user.email", user.Email, valueList)
valueList = replaceAttributeValues("$user.id", user.Id, valueList)
valueList = replaceAttributeValues("$user.phone", user.Phone, valueList)
return valueList
}
func replaceAttributeValues(val string, replaceVal string, values []string) []string {
var newValues []string
for _, value := range values {
newValues = append(newValues, strings.ReplaceAll(value, val, replaceVal))
}
return newValues
}
func replaceAttributeValuesWithList(val string, replaceVals []string, values []string) []string {
var newValues []string
for _, value := range values {
for _, rVal := range replaceVals {
newValues = append(newValues, strings.ReplaceAll(value, val, rVal))
}
}
return newValues
}

View File

@@ -129,7 +129,7 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
return err
}
err = SendEmail(provider, title, content, dest, sender)
err = SendEmail(provider, title, content, []string{dest}, sender)
if err != nil {
return err
}

View File

@@ -104,12 +104,14 @@ func GetWebhook(id string) (*Webhook, error) {
return getWebhook(owner, name)
}
func UpdateWebhook(id string, webhook *Webhook) (bool, error) {
func UpdateWebhook(id string, webhook *Webhook, isGlobalAdmin bool) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
if w, err := getWebhook(owner, name); err != nil {
return false, err
} else if w == nil {
return false, nil
} else if !isGlobalAdmin && w.Organization != webhook.Organization {
return false, fmt.Errorf("auth:Unauthorized operation")
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(webhook)
@@ -130,7 +132,7 @@ func AddWebhook(webhook *Webhook) (bool, error) {
}
func DeleteWebhook(webhook *Webhook) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{webhook.Owner, webhook.Name}).Delete(&Webhook{})
affected, err := ormer.Engine.ID(core.PK{webhook.Owner, webhook.Name}).Where("organization = ?", webhook.Organization).Delete(&Webhook{})
if err != nil {
return false, err
}

View File

@@ -34,6 +34,11 @@ type Object struct {
AccessSecret string `json:"accessSecret"`
}
type ObjectWithOrg struct {
Object
Organization string `json:"organization"`
}
func getUsername(ctx *context.Context) (username string) {
username, ok := ctx.Input.Session("username").(string)
if !ok || username == "" {
@@ -91,7 +96,7 @@ func getObject(ctx *context.Context) (string, string, error) {
return "", "", nil
} else {
if path == "/api/add-policy" || path == "/api/remove-policy" || path == "/api/update-policy" {
if path == "/api/add-policy" || path == "/api/remove-policy" || path == "/api/update-policy" || path == "/api/send-invitation" {
id := ctx.Input.Query("id")
if id != "" {
return util.GetOwnerAndNameFromIdWithError(id)
@@ -104,12 +109,27 @@ func getObject(ctx *context.Context) (string, string, error) {
}
var obj Object
if strings.HasSuffix(path, "-application") || strings.HasSuffix(path, "-token") ||
strings.HasSuffix(path, "-syncer") || strings.HasSuffix(path, "-webhook") {
var objWithOrg ObjectWithOrg
err := json.Unmarshal(body, &objWithOrg)
if err != nil {
return "", "", nil
}
return objWithOrg.Organization, objWithOrg.Name, nil
}
err := json.Unmarshal(body, &obj)
if err != nil {
// this is not error
return "", "", nil
}
if strings.HasSuffix(path, "-organization") {
return obj.Name, obj.Name, nil
}
if path == "/api/delete-resource" {
tokens := strings.Split(obj.Name, "/")
if len(tokens) >= 5 {

View File

@@ -102,6 +102,7 @@ func initAPI() {
beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation")
beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation")
beego.Router("/api/verify-invitation", &controllers.ApiController{}, "GET:VerifyInvitation")
beego.Router("/api/send-invitation", &controllers.ApiController{}, "POST:SendInvitation")
beego.Router("/api/get-applications", &controllers.ApiController{}, "GET:GetApplications")
beego.Router("/api/get-application", &controllers.ApiController{}, "GET:GetApplication")
@@ -239,6 +240,13 @@ func initAPI() {
beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")
beego.Handler("/api/metrics", promhttp.Handler())
beego.Router("/api/get-global-forms", &controllers.ApiController{}, "GET:GetGlobalForms")
beego.Router("/api/get-forms", &controllers.ApiController{}, "GET:GetForms")
beego.Router("/api/get-form", &controllers.ApiController{}, "GET:GetForm")
beego.Router("/api/update-form", &controllers.ApiController{}, "POST:UpdateForm")
beego.Router("/api/add-form", &controllers.ApiController{}, "POST:AddForm")
beego.Router("/api/delete-form", &controllers.ApiController{}, "POST:DeleteForm")
beego.Router("/api/get-syncers", &controllers.ApiController{}, "GET:GetSyncers")
beego.Router("/api/get-syncer", &controllers.ApiController{}, "GET:GetSyncer")
beego.Router("/api/update-syncer", &controllers.ApiController{}, "POST:UpdateSyncer")

View File

@@ -89,7 +89,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
return "", nil
}
code, err := object.GetOAuthCode(userId, clientId, "", responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
code, err := object.GetOAuthCode(userId, clientId, "", "autoSignin", responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
if err != nil {
return "", err
} else if code.Message != "" {

View File

@@ -638,7 +638,7 @@
{
"in": "query",
"name": "id",
"description": "The id(organization/application/user) of session",
"description": "The id(organization/user/application) of session",
"required": true,
"type": "string"
},
@@ -1448,7 +1448,7 @@
{
"in": "query",
"name": "id",
"description": "The id(organization/application/user) of session",
"description": "The id(organization/user/application) of session",
"required": true,
"type": "string"
}
@@ -3318,8 +3318,8 @@
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id(organization/application/user) of session",
"name": "sessionPkId",
"description": "The id(organization/user/application) of session",
"required": true,
"type": "string"
}
@@ -4034,8 +4034,8 @@
"parameters": [
{
"in": "query",
"name": "id",
"description": "The id(organization/application/user) of session",
"name": "sessionPkId",
"description": "The id(organization/user/application) of session",
"required": true,
"type": "string"
},
@@ -5457,7 +5457,7 @@
{
"in": "query",
"name": "id",
"description": "The id(organization/application/user) of session",
"description": "The id(organization/user/application) of session",
"required": true,
"type": "string"
}

View File

@@ -413,7 +413,7 @@ paths:
parameters:
- in: query
name: id
description: The id(organization/application/user) of session
description: The id(organization/user/application) of session
required: true
type: string
- in: query
@@ -935,7 +935,7 @@ paths:
parameters:
- in: query
name: id
description: The id(organization/application/user) of session
description: The id(organization/user/application) of session
required: true
type: string
responses:
@@ -2159,8 +2159,8 @@ paths:
operationId: ApiController.GetSingleSession
parameters:
- in: query
name: id
description: The id(organization/application/user) of session
name: sessionPkId
description: The id(organization/user/application) of session
required: true
type: string
responses:
@@ -2629,8 +2629,8 @@ paths:
operationId: ApiController.IsSessionDuplicated
parameters:
- in: query
name: id
description: The id(organization/application/user) of session
name: sessionPkId
description: The id(organization/user/application) of session
required: true
type: string
- in: query
@@ -3567,7 +3567,7 @@ paths:
parameters:
- in: query
name: id
description: The id(organization/application/user) of session
description: The id(organization/user/application) of session
required: true
type: string
responses:

View File

@@ -16,6 +16,7 @@ package util
import (
"fmt"
"net"
"net/http"
"strings"
@@ -28,20 +29,12 @@ func getIpInfo(clientIp string) string {
return ""
}
ips := strings.Split(clientIp, ",")
res := strings.TrimSpace(ips[0])
//res := ""
//for i := range ips {
// ip := strings.TrimSpace(ips[i])
// ipstr := fmt.Sprintf("%s: %s", ip, "")
// if i != len(ips)-1 {
// res += ipstr + " -> "
// } else {
// res += ipstr
// }
//}
first := strings.TrimSpace(strings.Split(clientIp, ",")[0])
if host, _, err := net.SplitHostPort(first); err == nil {
return strings.Trim(host, "[]")
}
return res
return strings.Trim(first, "[]")
}
func GetClientIpFromRequest(req *http.Request) string {

View File

@@ -30,6 +30,7 @@ import (
"unicode"
"github.com/google/uuid"
"github.com/nyaruka/phonenumbers"
)
func ParseInt(s string) int {
@@ -278,6 +279,19 @@ func GetMaskedPhone(phone string) string {
return rePhone.ReplaceAllString(phone, "$1****$2")
}
func GetSeperatedPhone(phone string) string {
if strings.HasPrefix(phone, "+") {
phoneNumberParsed, err := phonenumbers.Parse(phone, "")
if err != nil {
return phone
}
phone = fmt.Sprintf("%d", phoneNumberParsed.GetNationalNumber())
}
return phone
}
func GetMaskedEmail(email string) string {
if email == "" {
return ""

View File

@@ -20,26 +20,34 @@ module.exports = {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/serviceValidate": {
"/cas/**/serviceValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/proxyValidate": {
"/cas/**/proxyValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/proxy": {
"/cas/**/proxy": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/validate": {
"/cas/**/validate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/**/p3/serviceValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/cas/**/p3/proxyValidate": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/scim": {
target: "http://localhost:8000",
changeOrigin: true,
}
},
},
},
plugins: [

View File

@@ -19,6 +19,7 @@
"@web3-onboard/gnosis": "^2.1.10",
"@web3-onboard/infinity-wallet": "^2.0.4",
"@web3-onboard/injected-wallets": "^2.10.4",
"@web3-onboard/phantom": "^2.1.1",
"@web3-onboard/react": "^2.8.10",
"@web3-onboard/sequence": "^2.0.8",
"@web3-onboard/taho": "^2.0.5",

View File

@@ -115,7 +115,7 @@ class App extends Component {
this.setState({selectedMenuKey: "/logs"});
} else if (uri.includes("/products") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions")) {
this.setState({selectedMenuKey: "/business"});
} else if (uri.includes("/sysinfo") || uri.includes("/syncers") || uri.includes("/webhooks")) {
} else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks")) {
this.setState({selectedMenuKey: "/admin"});
} else if (uri.includes("/signup")) {
this.setState({selectedMenuKey: "/signup"});

View File

@@ -37,6 +37,7 @@ import ThemeEditor from "./common/theme/ThemeEditor";
import SigninTable from "./table/SigninTable";
import Editor from "./common/Editor";
import * as GroupBackend from "./backend/GroupBackend";
import TokenAttributeTable from "./table/TokenAttributeTable";
const {Option} = Select;
@@ -116,6 +117,7 @@ class ApplicationEditPage extends React.Component {
providers: [],
uploading: false,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
tokenAttributes: [],
samlAttributes: [],
samlMetadata: null,
isAuthorized: true,
@@ -463,11 +465,36 @@ class ApplicationEditPage extends React.Component {
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>)
{
Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>)
[...Setting.getUserCommonFields(), "permissionNames"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
}
</Select>
</Col>
</Row>
{
this.state.application.tokenFormat === "JWT-Custom" ? (<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Token attributes"), i18next.t("general:Token attributes - Tooltip"))} :
</Col>
<Col span={22} >
<TokenAttributeTable
title={i18next.t("general:Token attributes")}
table={this.state.application.tokenAttributes}
application={this.state.application}
onUpdateTable={(value) => {this.updateApplicationField("tokenAttributes", value);}}
/>
</Col>
</Row>) : null
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Order"), i18next.t("application:Order - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber style={{width: "150px"}} value={this.state.application.order} min={0} step={1} precision={0} addonAfter="" onChange={value => {
this.updateApplicationField("order", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} :
@@ -801,6 +828,26 @@ class ApplicationEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:SAML hash algorithm"), i18next.t("application:SAML hash algorithm - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}}
value={this.state.application.samlHashAlgorithm}
onChange={(value => {
this.updateApplicationField("samlHashAlgorithm", value);
})} >
{
[
{id: "SHA1", name: "SHA1"},
{id: "SHA256", name: "SHA256"},
{id: "SHA512", name: "SHA512"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} :
@@ -1074,7 +1121,7 @@ class ApplicationEditPage extends React.Component {
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => this.updateApplicationField("footerHtml", Setting.getDefaultFooterContent())} >
{i18next.t("provider:Reset to Default HTML")}
{i18next.t("general:Reset to Default")}
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => this.updateApplicationField("footerHtml", Setting.getEmptyFooterContent())} >
{i18next.t("application:Reset to Empty")}

View File

@@ -113,6 +113,34 @@ class ApplicationListPage extends BaseListPage {
});
}
copyApplication(i) {
const original = this.state.data[i];
const randomSuffix = Setting.getRandomName();
const newName = `${original.name}_${randomSuffix}`;
const copiedApplication = {
...original,
name: newName,
createdTime: moment().format(),
displayName: "Copy Application - " + newName,
clientId: "",
clientSecret: "",
};
ApplicationBackend.addApplication(copiedApplication)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/applications/${copiedApplication.organization}/${newName}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully copied"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to copy")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
renderTable(applications) {
const columns = [
{
@@ -237,12 +265,13 @@ class ApplicationListPage extends BaseListPage {
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "170px",
width: "230px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/applications/${record.organization}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.copyApplication(index)}>{i18next.t("general:Copy")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteApplication(index)}
@@ -255,6 +284,7 @@ class ApplicationListPage extends BaseListPage {
},
];
const filteredColumns = Setting.filterTableColumns(columns, this.props.formItems ?? this.state.formItems);
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
@@ -264,7 +294,7 @@ class ApplicationListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={applications} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={filteredColumns} dataSource={applications} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Applications")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -19,6 +19,7 @@ import Highlighter from "react-highlight-words";
import i18next from "i18next";
import * as Setting from "./Setting";
import * as TourConfig from "./TourConfig";
import * as FormBackend from "./backend/FormBackend";
class BaseListPage extends React.Component {
constructor(props) {
@@ -36,16 +37,18 @@ class BaseListPage extends React.Component {
searchedColumn: "",
isAuthorized: true,
isTourVisible: TourConfig.getTourVisible(),
formItems: [],
};
}
handleOrganizationChange = () => {
this.setState({
organizationName: this.props.match?.params.organizationName || Setting.getRequestOrganization(this.props.account),
},
() => {
const {pagination} = this.state;
this.fetch({pagination});
});
const {pagination} = this.state;
this.fetch({pagination});
};
handleTourChange = () => {
@@ -71,6 +74,37 @@ class BaseListPage extends React.Component {
UNSAFE_componentWillMount() {
const {pagination} = this.state;
this.fetch({pagination});
this.getForm();
}
getForm() {
const tag = this.props.account.tag;
const formType = this.props.match?.path?.replace(/^\//, "");
let formName = formType;
if (tag !== "") {
formName = formType + "-tag-" + tag;
FormBackend.getForm(this.props.account.owner, formName)
.then(res => {
if (res.status === "ok" && res.data) {
this.setState({formItems: res.data.formItems});
} else {
this.fetchFormWithoutTag(formType);
}
});
} else {
this.fetchFormWithoutTag(formType);
}
}
fetchFormWithoutTag(formName) {
FormBackend.getForm(this.props.account.owner, formName)
.then(res => {
if (res.status === "ok" && res.data) {
this.setState({formItems: res.data.formItems});
} else {
this.setState({formItems: []});
}
});
}
getColumnSearchProps = (dataIndex, customRender = null) => ({

225
web/src/FormEditPage.js Normal file
View File

@@ -0,0 +1,225 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select} from "antd";
import * as FormBackend from "./backend/FormBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import FormItemTable from "./table/FormItemTable";
import UserListPage from "./UserListPage";
import ApplicationListPage from "./ApplicationListPage";
import ProviderListPage from "./ProviderListPage";
import OrganizationListPage from "./OrganizationListPage";
const {Option} = Select;
class FormEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
formName: props.match.params.formName,
form: null,
formItems: [],
};
}
UNSAFE_componentWillMount() {
this.getForm();
}
getForm() {
FormBackend.getForm(this.props.account.owner, this.state.formName)
.then((res) => {
if (res.status === "ok") {
this.setState({
form: res.data,
});
}
});
}
updateFormField(key, value) {
const form = this.state.form;
form[key] = value;
this.setState({
form: form,
});
}
renderForm() {
return (
<Card size="small" title={
<div>
{i18next.t("form:Edit Form")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitFormEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary"
onClick={() => this.submitFormEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
} style={{marginLeft: "5px"}} type="inner">
<Row style={{marginTop: "10px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22}>
<Input
value={this.state.form.name}
disabled={true}
onChange={e => {this.updateFormField("name", e.target.value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22}>
<Input value={this.state.form.displayName} onChange={e => {
this.updateFormField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
</Col>
<Col span={22}>
<Select
style={{width: "100%"}}
value={this.state.form.type}
onChange={value => {
this.updateFormField("type", value);
this.updateFormField("name", value);
this.updateFormField("displayName", value);
const defaultItems = new FormItemTable({formType: value}).getItems();
this.updateFormField("formItems", defaultItems);
}}
>
{Setting.getFormTypeOptions().map(option => (
<Option key={option.id} value={option.id}>{i18next.t(option.name)}</Option>
))}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={Setting.isMobile() ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
</Col>
<Col span={22}>
<Input value={this.state.form.tag} onChange={e => {
this.updateFormField("tag", e.target.value);
this.updateFormField("name", e.target.value ? `${this.state.form.type}-tag-${e.target.value}` : this.state.form.type);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("form:Form items"), i18next.t("form:Form items - Tooltip"))} :
</Col>
<Col span={22}>
<FormItemTable
title={i18next.t("form:Form items")}
table={this.state.form.formItems}
onUpdateTable={(value) => {
this.updateFormField("formItems", value);
}}
formType={this.state.form.type}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
<Col span={22}>
{
this.renderListPreview()
}
</Col>
</Row>
</Card>
);
}
renderListPreview() {
let listPageComponent = null;
if (this.state.form.type === "users") {
listPageComponent = (<UserListPage {...this.props} formItems={this.state.form.formItems} />);
} else if (this.state.form.type === "applications") {
listPageComponent = (<ApplicationListPage {...this.props} formItems={this.state.form.formItems} />);
} else if (this.state.form.type === "providers") {
listPageComponent = (<ProviderListPage {...this.props} formItems={this.state.form.formItems} />);
} else if (this.state.form.type === "organizations") {
listPageComponent = (<OrganizationListPage {...this.props} formItems={this.state.form.formItems} />);
}
return (
<div style={{position: "relative", border: "1px solid rgb(217,217,217)", height: "600px", cursor: "pointer"}} onClick={(e) => {Setting.openLink(`/${this.state.form.type}`);}}>
<div style={{position: "relative", height: "100%", overflow: "auto"}}>
<div style={{display: "inline-block", position: "relative", zIndex: 1, pointerEvents: "none"}}>
{listPageComponent}
</div>
</div>
<div style={{position: "absolute", top: 0, left: 0, right: 0, bottom: 0, zIndex: 10, background: "rgba(0,0,0,0.4)", pointerEvents: "none"}} />
</div>
);
}
submitFormEdit(exitAfterSave) {
const form = Setting.deepCopy(this.state.form);
FormBackend.updateForm(this.state.form.owner, this.state.formName, form)
.then((res) => {
if (res.status === "ok") {
if (res.data) {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
formName: this.state.form.name,
});
if (exitAfterSave) {
this.props.history.push("/forms");
} else {
this.props.history.push(`/forms/${this.state.form.name}`);
}
} else {
Setting.showMessage("error", i18next.t("general:Failed to save"));
this.updateFormField("name", this.state.formName);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${error}`);
});
}
render() {
return (
<div>
{
this.state.form !== null ? this.renderForm() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitFormEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large"
onClick={() => this.submitFormEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
</div>
</div>
);
}
}
export default FormEditPage;

245
web/src/FormListPage.js Normal file
View File

@@ -0,0 +1,245 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Col, List, Popconfirm, Row, Table} from "antd";
import moment from "moment";
import BaseListPage from "./BaseListPage";
import * as Setting from "./Setting";
import * as FormBackend from "./backend/FormBackend";
import i18next from "i18next";
class FormListPage extends BaseListPage {
constructor(props) {
super(props);
}
newForm() {
const randomName = Setting.getRandomName();
return {
owner: this.props.account.owner,
name: `form_${randomName}`,
createdTime: moment().format(),
displayName: `New Form - ${randomName}`,
formItems: [],
};
}
addForm() {
const newForm = this.newForm();
FormBackend.addForm(newForm)
.then((res) => {
if (res.status === "ok") {
sessionStorage.setItem("formListUrl", window.location.pathname);
this.props.history.push({
pathname: `/forms/${newForm.name}`,
mode: "add",
});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteForm(record) {
FormBackend.deleteForm(record)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: this.state.data.filter((item) => item.name !== record.name),
pagination: {
...this.state.pagination,
total: this.state.pagination.total - 1,
},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${error}`);
});
}
renderTable(forms) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "160px",
sorter: (a, b) => a.name.localeCompare(b.name),
render: (text, record, index) => {
return (
<Link to={`/forms/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "200px",
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
},
{
title: i18next.t("general:Type"),
dataIndex: "type",
key: "type",
width: "120px",
sorter: (a, b) => a.type.localeCompare(b.type),
render: (text, record, index) => {
const typeOption = Setting.getFormTypeOptions().find(option => option.id === text);
return typeOption ? i18next.t(typeOption.name) : text;
},
},
{
title: i18next.t("form:Form items"),
dataIndex: "formItems",
key: "formItems",
...this.getColumnSearchProps("formItems"),
render: (text, record, index) => {
const providers = text;
if (!providers || providers.length === 0) {
return `(${i18next.t("general:empty")})`;
}
const visibleProviders = providers.filter(item => item.visible !== false);
const leftItems = [];
const rightItems = [];
visibleProviders.forEach((item, idx) => {
if (idx % 2 === 0) {
leftItems.push(item);
} else {
rightItems.push(item);
}
});
const getList = (items) => (
<List
size="small"
locale={{emptyText: " "}}
dataSource={items}
renderItem={providerItem => (
<List.Item>
<div style={{display: "inline"}}>{i18next.t(providerItem.label)}</div>
</List.Item>
)}
/>
);
return (
<div>
<Row>
<Col span={12}>{getList(leftItems)}</Col>
<Col span={12}>{getList(rightItems)}</Col>
</Row>
</div>
);
},
},
{
title: i18next.t("general:Action"),
dataIndex: "action",
key: "action",
width: "180px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
type="primary"
onClick={() => this.props.history.push(`/forms/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`${i18next.t("general:Sure to delete")}: ${record.name} ?`}
onConfirm={() => this.deleteForm(record)}
okText={i18next.t("general:OK")}
cancelText={i18next.t("general:Cancel")}
>
<Button style={{marginBottom: "10px"}} type="primary"
danger>{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
);
},
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={forms}
rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered
pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Forms")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small"
onClick={this.addForm.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
const field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
this.setState({loading: true});
FormBackend.getForms(this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
this.setState({
loading: false,
});
if (res.status === "ok") {
this.setState({
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
isAuthorized: false,
});
} else {
Setting.showMessage("error", res.msg);
}
}
});
};
}
export default FormListPage;

View File

@@ -117,7 +117,7 @@ class GroupListPage extends BaseListPage {
return (
<Upload {...props}>
<Button icon={<UploadOutlined />} id="upload-button" type="primary" size="small">
<Button icon={<UploadOutlined />} id="upload-button" size="small">
{i18next.t("group:Upload (.xlsx)")}
</Button>
</Upload>
@@ -268,7 +268,7 @@ class GroupListPage extends BaseListPage {
title={() => (
<div>
{i18next.t("general:Groups")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
<Button style={{marginRight: "15px"}} type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
{
this.renderUpload()
}

View File

@@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
import {Button, Card, Col, Input, InputNumber, Modal, Row, Select, Table} from "antd";
import {CopyOutlined} from "@ant-design/icons";
import * as InvitationBackend from "./backend/InvitationBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
@@ -37,6 +37,7 @@ class InvitationEditPage extends React.Component {
applications: [],
groups: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
sendLoading: false,
};
}
@@ -123,6 +124,35 @@ class InvitationEditPage extends React.Component {
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}
renderSendEmailModal() {
const emailColumns = [
{title: "email", dataIndex: "email"},
];
const emails = this.state.emails?.split("\n")?.filter(email => Setting.isValidEmail(email));
const emailData = emails?.map((email) => {return {email: email};});
return <Modal title={i18next.t("general:Send")}
style={{height: "800px"}}
open={this.state.showSendModal}
closable
footer={[
<Button key={1} loading={this.state.sendLoading} type="primary"
onClick={() => {
this.setState({sendLoading: true});
InvitationBackend.sendInvitation(this.state.invitation, emails).then(() => {
this.setState({sendLoading: false});
Setting.showMessage("success", i18next.t("general:Successfully sent"));
}).catch(err => Setting.showMessage("success", err.message));
}}>{i18next.t("general:Send")}</Button>,
]}
onCancel={() => {this.setState({showSendModal: false});}}>
<div >
<p>You will send invitation email to:</p>
<Table showHeader={false} columns={emailColumns} dataSource={emailData} size={"small"}></Table>
</div>
</Modal>;
}
renderInvitation() {
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
return (
@@ -199,6 +229,17 @@ class InvitationEditPage extends React.Component {
</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Send")}
</Col>
<Col span={22} >
<Input.TextArea autoSize={{minRows: 3, maxRows: 10}} value={this.state.emails} onChange={(value) => {
this.setState({emails: value.target.value});
}}></Input.TextArea>
<Button type="primary" style={{marginTop: "20px"}} onClick={() => this.setState({showSendModal: true})}>{i18next.t("general:Send")}</Button>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} :
@@ -338,6 +379,7 @@ class InvitationEditPage extends React.Component {
render() {
return (
<div>
{this.state.showSendModal ? this.renderSendEmailModal() : null}
{
this.state.invitation !== null ? this.renderInvitation() : null
}

View File

@@ -20,6 +20,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as GroupBackend from "./backend/GroupBackend";
import AttributesMapperTable from "./table/AttributesMapperTable";
const {Option} = Select;
@@ -278,6 +279,14 @@ class LdapEditPage extends React.Component {
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Custom attributes"), i18next.t("ldap:Custom attributes - Tooltip"))} :
</Col>
<Col span={21} >
<AttributesMapperTable customAttributes={this.state.ldap.customAttributes} onUpdateTable={(value) => {this.updateLdapField("customAttributes", value);}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} :

View File

@@ -73,6 +73,8 @@ import PricingEditPage from "./PricingEditPage";
import SubscriptionListPage from "./SubscriptionListPage";
import SubscriptionEditPage from "./SubscriptionEditPage";
import SystemInfo from "./SystemInfo";
import FormListPage from "./FormListPage";
import FormEditPage from "./FormEditPage";
import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import WebhookListPage from "./WebhookListPage";
@@ -330,11 +332,13 @@ function ManagementPage(props) {
if (Setting.isAdminUser(props.account)) {
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
} else {
res.push(Setting.getItem(<Link style={{color: textColor}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks")]));
}
@@ -434,6 +438,8 @@ function ManagementPage(props) {
<Route exact path="/subscriptions" render={(props) => renderLoginIfNotLoggedIn(<SubscriptionListPage account={account} {...props} />)} />
<Route exact path="/subscriptions/:organizationName/:subscriptionName" render={(props) => renderLoginIfNotLoggedIn(<SubscriptionEditPage account={account} {...props} />)} />
<Route exact path="/sysinfo" render={(props) => renderLoginIfNotLoggedIn(<SystemInfo account={account} {...props} />)} />
<Route exact path="/forms" render={(props) => renderLoginIfNotLoggedIn(<FormListPage account={account} {...props} />)} />
<Route exact path="/forms/:formName" render={(props) => renderLoginIfNotLoggedIn(<FormEditPage account={account} {...props} />)} />
<Route exact path="/syncers" render={(props) => renderLoginIfNotLoggedIn(<SyncerListPage account={account} {...props} />)} />
<Route exact path="/syncers/:syncerName" render={(props) => renderLoginIfNotLoggedIn(<SyncerEditPage account={account} {...props} />)} />
<Route exact path="/transactions" render={(props) => renderLoginIfNotLoggedIn(<TransactionListPage account={account} {...props} />)} />

View File

@@ -271,6 +271,7 @@ class OrganizationListPage extends BaseListPage {
},
];
const filteredColumns = Setting.filterTableColumns(columns, this.props.formItems ?? this.state.formItems);
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
@@ -280,7 +281,7 @@ class OrganizationListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={organizations} rowKey="name" size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={filteredColumns} dataSource={organizations} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Organizations")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -99,6 +99,7 @@ class PermissionListPage extends BaseListPage {
Setting.showMessage("error", "File failed to upload");
}
}
renderPermissionUpload() {
const props = {
name: "file",
@@ -113,7 +114,7 @@ class PermissionListPage extends BaseListPage {
return (
<Upload {...props}>
<Button icon={<UploadOutlined />} id="upload-button" type="primary" size="small">
<Button icon={<UploadOutlined />} id="upload-button" size="small">
{i18next.t("user:Upload (.xlsx)")}
</Button></Upload>
);
@@ -390,7 +391,7 @@ class PermissionListPage extends BaseListPage {
title={() => (
<div>
{i18next.t("general:Permissions")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button id="add-button" style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addPermission.bind(this)}>{i18next.t("general:Add")}</Button>
<Button id="add-button" style={{marginRight: "15px"}} type="primary" size="small" onClick={this.addPermission.bind(this)}>{i18next.t("general:Add")}</Button>
{
this.renderPermissionUpload()
}

View File

@@ -367,6 +367,19 @@ class ProviderEditPage extends React.Component {
}
}
getDomainLabel(provider) {
switch (provider.category) {
case "OAuth":
if (provider.type === "AzureAD" || provider.type === "AzureADB2C") {
return Setting.getLabel(i18next.t("provider:Tenant ID"), i18next.t("provider:Tenant ID - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
}
default:
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
}
}
getProviderSubTypeOptions(type) {
if (type === "WeCom" || type === "Infoflow") {
return (
@@ -597,6 +610,7 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("disableSsl", false);
this.updateProviderField("title", "Casdoor Verification Code");
this.updateProviderField("content", Setting.getDefaultHtmlEmailContent());
this.updateProviderField("metadata", Setting.getDefaultInvitationHtmlEmailContent());
this.updateProviderField("receiver", this.props.account.email);
} else if (value === "SMS") {
this.updateProviderField("type", "Twilio SMS");
@@ -962,7 +976,7 @@ class ProviderEditPage extends React.Component {
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.category !== "Storage") && this.state.provider.type !== "Okta" && this.state.provider.type !== "Nextcloud" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
{this.getDomainLabel(this.state.provider)} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.provider.domain} onChange={e => {
@@ -1242,10 +1256,10 @@ class ProviderEditPage extends React.Component {
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes. <reset-link>Or click %link to reset</reset-link>")} >
{i18next.t("provider:Reset to Default Text")}
{i18next.t("general:Reset to Default")}
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => this.updateProviderField("content", Setting.getDefaultHtmlEmailContent())} >
{i18next.t("provider:Reset to Default HTML")}
{i18next.t("general:Reset to Default")}
</Button>
</Row>
<Row>
@@ -1271,6 +1285,42 @@ class ProviderEditPage extends React.Component {
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(`${i18next.t("provider:Email content")}-${i18next.t("general:Invitations")}`, i18next.t("provider:Email content - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: "20px"}} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => this.updateProviderField("metadata", "You have invited to join Casdoor. Here is your invitation code: %s, please enter in 5 minutes. Or click %link to signup")} >
{i18next.t("general:Reset to Default")}
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => this.updateProviderField("metadata", Setting.getDefaultInvitationHtmlEmailContent())} >
{i18next.t("general:Reset to Default")}
</Button>
</Row>
<Row>
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{height: "300px", margin: "10px"}}>
<Editor
value={this.state.provider.metadata}
fillHeight
dark
lang="html"
onChange={value => {
this.updateProviderField("metadata", value);
}}
/>
</div>
</Col>
<Col span={1} />
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{margin: "10px"}}>
<div dangerouslySetInnerHTML={{__html: this.state.provider.metadata.replace("%code", "123456")}} />
</div>
</Col>
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Test Email"), i18next.t("provider:Test Email - Tooltip"))} :

View File

@@ -230,6 +230,7 @@ class ProviderListPage extends BaseListPage {
},
];
const filteredColumns = Setting.filterTableColumns(columns, this.props.formItems ?? this.state.formItems);
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
@@ -239,7 +240,7 @@ class ProviderListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={providers} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={filteredColumns} dataSource={providers} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Providers")}&nbsp;&nbsp;&nbsp;&nbsp;

Some files were not shown because too many files have changed in this diff Show More