mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-23 22:53:31 +08:00
Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
383bf44391 | |||
36f5de3203 | |||
eae69c41d7 | |||
91057f54f3 | |||
daa7b79915 | |||
d3a5539dae | |||
7d1c614452 | |||
e2eafa909b | |||
56bcef0592 | |||
0860cbf343 | |||
2f4180b1b6 | |||
e3d5619b25 | |||
019fd87b92 | |||
5c41c6c4a5 | |||
b7fafcc62b | |||
493ceddcd9 | |||
fc618b9bd5 | |||
a00900e405 | |||
77ef5828dd | |||
c11f013e04 | |||
b3bafe8402 | |||
f04a431d85 | |||
952538916d | |||
18bb445e71 | |||
cca88e2cb0 | |||
86c10fe0ab | |||
c1b3bf0f45 | |||
62bda61af5 | |||
b6f943e326 | |||
2cc5e82d91 | |||
e55cd94298 | |||
08f7a05e61 | |||
4bee21f4a3 | |||
5417a90223 | |||
131820e34e | |||
2fcbf7cf6c | |||
14ade8b7e4 | |||
a11fe59704 | |||
af55d0547f | |||
81102f8298 | |||
141372cb86 | |||
15a037ca74 | |||
73c680d56f | |||
aafc16e4f4 | |||
7be026dd1f | |||
3e7938e5f6 | |||
30789138e2 | |||
9610ce5b8c | |||
a39a311d2f | |||
08e41ab762 | |||
85ca318e2f | |||
9032865e60 | |||
5692522ee0 | |||
cb1882e589 | |||
41d9422687 | |||
3297db688b | |||
cc82d292f0 | |||
f2e3037bc5 | |||
d986a4a9e0 | |||
2df3878c15 | |||
24ab8880cc | |||
f26b4853c5 | |||
d78e8e9776 | |||
d61f9a1856 | |||
aa52af02b3 | |||
2a5722e45b | |||
26718bc4a1 | |||
f8d44e2dca | |||
26eea501be | |||
63b8e857bc | |||
81b336b37a | |||
9c39179849 | |||
37d93a5eea | |||
e926a07c58 | |||
9c46344e68 |
@ -1,10 +1,10 @@
|
|||||||
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
||||||
WORKDIR /web
|
WORKDIR /web
|
||||||
COPY ./web .
|
COPY ./web .
|
||||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
|
RUN yarn install --frozen-lockfile --network-timeout 1000000 && NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.20.12 AS BACK
|
FROM --platform=$BUILDPLATFORM golang:1.21.13 AS BACK
|
||||||
WORKDIR /go/src/casdoor
|
WORKDIR /go/src/casdoor
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN ./build.sh
|
RUN ./build.sh
|
||||||
|
@ -47,6 +47,7 @@ p, *, *, GET, /api/get-app-login, *, *
|
|||||||
p, *, *, POST, /api/logout, *, *
|
p, *, *, POST, /api/logout, *, *
|
||||||
p, *, *, GET, /api/logout, *, *
|
p, *, *, GET, /api/logout, *, *
|
||||||
p, *, *, POST, /api/callback, *, *
|
p, *, *, POST, /api/callback, *, *
|
||||||
|
p, *, *, POST, /api/device-auth, *, *
|
||||||
p, *, *, GET, /api/get-account, *, *
|
p, *, *, GET, /api/get-account, *, *
|
||||||
p, *, *, GET, /api/userinfo, *, *
|
p, *, *, GET, /api/userinfo, *, *
|
||||||
p, *, *, GET, /api/user, *, *
|
p, *, *, GET, /api/user, *, *
|
||||||
|
@ -32,6 +32,7 @@ const (
|
|||||||
ResponseTypeIdToken = "id_token"
|
ResponseTypeIdToken = "id_token"
|
||||||
ResponseTypeSaml = "saml"
|
ResponseTypeSaml = "saml"
|
||||||
ResponseTypeCas = "cas"
|
ResponseTypeCas = "cas"
|
||||||
|
ResponseTypeDevice = "device"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
@ -139,6 +140,8 @@ func (c *ApiController) Signup() {
|
|||||||
invitationName = invitation.Name
|
invitationName = invitation.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userEmailVerified := false
|
||||||
|
|
||||||
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
|
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
|
||||||
var checkResult *object.VerifyResult
|
var checkResult *object.VerifyResult
|
||||||
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
|
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
|
||||||
@ -150,6 +153,8 @@ func (c *ApiController) Signup() {
|
|||||||
c.ResponseError(checkResult.Msg)
|
c.ResponseError(checkResult.Msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userEmailVerified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkPhone string
|
var checkPhone string
|
||||||
@ -228,6 +233,7 @@ func (c *ApiController) Signup() {
|
|||||||
Karma: 0,
|
Karma: 0,
|
||||||
Invitation: invitationName,
|
Invitation: invitationName,
|
||||||
InvitationCode: authForm.InvitationCode,
|
InvitationCode: authForm.InvitationCode,
|
||||||
|
EmailVerified: userEmailVerified,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(organization.Tags) > 0 {
|
if len(organization.Tags) > 0 {
|
||||||
@ -249,6 +255,10 @@ func (c *ApiController) Signup() {
|
|||||||
user.Groups = []string{invitation.SignupGroup}
|
user.Groups = []string{invitation.SignupGroup}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if application.DefaultGroup != "" && user.Groups == nil {
|
||||||
|
user.Groups = []string{application.DefaultGroup}
|
||||||
|
}
|
||||||
|
|
||||||
affected, err := object.AddUser(user)
|
affected, err := object.AddUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
@ -458,6 +468,10 @@ func (c *ApiController) GetAccount() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if organization != nil && len(organization.CountryCodes) == 1 && u != nil && u.CountryCode == "" {
|
||||||
|
u.CountryCode = organization.CountryCodes[0]
|
||||||
|
}
|
||||||
|
|
||||||
accessToken := c.GetSessionToken()
|
accessToken := c.GetSessionToken()
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)
|
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)
|
||||||
|
@ -25,10 +25,12 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/captcha"
|
"github.com/casdoor/casdoor/captcha"
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/casdoor/form"
|
"github.com/casdoor/casdoor/form"
|
||||||
|
"github.com/casdoor/casdoor/i18n"
|
||||||
"github.com/casdoor/casdoor/idp"
|
"github.com/casdoor/casdoor/idp"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
"github.com/casdoor/casdoor/proxy"
|
"github.com/casdoor/casdoor/proxy"
|
||||||
@ -54,6 +56,11 @@ func tokenToResponse(token *object.Token) *Response {
|
|||||||
|
|
||||||
// HandleLoggedIn ...
|
// HandleLoggedIn ...
|
||||||
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
||||||
|
if user.IsForbidden {
|
||||||
|
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userId := user.GetId()
|
userId := user.GetId()
|
||||||
|
|
||||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
@ -163,6 +170,32 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
|||||||
|
|
||||||
resp.Data2 = user.NeedUpdatePassword
|
resp.Data2 = user.NeedUpdatePassword
|
||||||
}
|
}
|
||||||
|
} else if form.Type == ResponseTypeDevice {
|
||||||
|
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
|
||||||
|
if !ok {
|
||||||
|
c.ResponseError(c.T("auth:UserCode Expired"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authCacheCast := authCache.(object.DeviceAuthCache)
|
||||||
|
if authCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
|
||||||
|
c.ResponseError(c.T("auth:UserCode Expired"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceAuthCacheDeviceCode, ok := object.DeviceAuthMap.Load(authCacheCast.UserName)
|
||||||
|
if !ok {
|
||||||
|
c.ResponseError(c.T("auth:DeviceCode Invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceAuthCacheDeviceCodeCast := deviceAuthCacheDeviceCode.(object.DeviceAuthCache)
|
||||||
|
deviceAuthCacheDeviceCodeCast.UserName = user.Name
|
||||||
|
deviceAuthCacheDeviceCodeCast.UserSignIn = true
|
||||||
|
|
||||||
|
object.DeviceAuthMap.Store(authCacheCast.UserName, deviceAuthCacheDeviceCodeCast)
|
||||||
|
|
||||||
|
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
|
||||||
} else if form.Type == ResponseTypeSaml { // saml flow
|
} else if form.Type == ResponseTypeSaml { // saml flow
|
||||||
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
|
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -236,6 +269,7 @@ func (c *ApiController) GetApplicationLogin() {
|
|||||||
state := c.Input().Get("state")
|
state := c.Input().Get("state")
|
||||||
id := c.Input().Get("id")
|
id := c.Input().Get("id")
|
||||||
loginType := c.Input().Get("type")
|
loginType := c.Input().Get("type")
|
||||||
|
userCode := c.Input().Get("userCode")
|
||||||
|
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
var msg string
|
var msg string
|
||||||
@ -262,6 +296,19 @@ func (c *ApiController) GetApplicationLogin() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else if loginType == "device" {
|
||||||
|
deviceAuthCache, ok := object.DeviceAuthMap.Load(userCode)
|
||||||
|
if !ok {
|
||||||
|
c.ResponseError(c.T("auth:UserCode Invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
|
||||||
|
application, err = object.GetApplication(deviceAuthCacheCast.ApplicationId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
@ -397,11 +444,27 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
faceIdProvider, err := object.GetFaceIdProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if faceIdProvider == nil {
|
||||||
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ok, err := user.CheckUserFace(authForm.FaceIdImage, faceIdProvider)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
c.ResponseError(i18n.Translate(c.GetAcceptLanguage(), "check:Face data does not exist, cannot log in"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if authForm.Password == "" {
|
} else if authForm.Password == "" {
|
||||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
@ -461,6 +524,14 @@ func (c *ApiController) Login() {
|
|||||||
verificationType = "sms"
|
verificationType = "sms"
|
||||||
} else {
|
} else {
|
||||||
verificationType = "email"
|
verificationType = "email"
|
||||||
|
if !user.EmailVerified {
|
||||||
|
user.EmailVerified = true
|
||||||
|
_, err = object.UpdateUser(user.GetId(), user, []string{"email_verified"}, false)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
@ -593,6 +664,9 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if provider == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), authForm.Provider))
|
||||||
|
}
|
||||||
|
|
||||||
providerItem := application.GetProviderItem(provider.Name)
|
providerItem := application.GetProviderItem(provider.Name)
|
||||||
if !providerItem.IsProviderVisible() {
|
if !providerItem.IsProviderVisible() {
|
||||||
@ -678,10 +752,6 @@ func (c *ApiController) Login() {
|
|||||||
|
|
||||||
if user != nil && !user.IsDeleted {
|
if user != nil && !user.IsDeleted {
|
||||||
// Sign in via OAuth (want to sign up but already have account)
|
// Sign in via OAuth (want to sign up but already have account)
|
||||||
|
|
||||||
if user.IsForbidden {
|
|
||||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
|
||||||
}
|
|
||||||
// sync info from 3rd-party if possible
|
// sync info from 3rd-party if possible
|
||||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -985,6 +1055,18 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if authForm.Language != "" {
|
||||||
|
user := c.getCurrentUser()
|
||||||
|
if user != nil {
|
||||||
|
user.Language = authForm.Language
|
||||||
|
_, err = object.UpdateUser(user.GetId(), user, []string{"language"}, user.IsAdmin)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = resp
|
c.Data["json"] = resp
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
@ -1174,3 +1256,75 @@ func (c *ApiController) Callback() {
|
|||||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
|
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
|
||||||
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
|
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeviceAuth
|
||||||
|
// @Title DeviceAuth
|
||||||
|
// @Tag Device Authorization Endpoint
|
||||||
|
// @Description Endpoint for the device authorization flow
|
||||||
|
// @router /device-auth [post]
|
||||||
|
// @Success 200 {object} object.DeviceAuthResponse The Response object
|
||||||
|
func (c *ApiController) DeviceAuth() {
|
||||||
|
clientId := c.Input().Get("client_id")
|
||||||
|
scope := c.Input().Get("scope")
|
||||||
|
application, err := object.GetApplicationByClientId(clientId)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = object.TokenError{
|
||||||
|
Error: err.Error(),
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if application == nil {
|
||||||
|
c.Data["json"] = object.TokenError{
|
||||||
|
Error: c.T("token:Invalid client_id"),
|
||||||
|
ErrorDescription: c.T("token:Invalid client_id"),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceCode := util.GenerateId()
|
||||||
|
userCode := util.GetRandomName()
|
||||||
|
|
||||||
|
generateTime := 0
|
||||||
|
for {
|
||||||
|
if generateTime > 5 {
|
||||||
|
c.Data["json"] = object.TokenError{
|
||||||
|
Error: "userCode gen",
|
||||||
|
ErrorDescription: c.T("token:Invalid client_id"),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, ok := object.DeviceAuthMap.Load(userCode)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
generateTime++
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceAuthCache := object.DeviceAuthCache{
|
||||||
|
UserSignIn: false,
|
||||||
|
UserName: "",
|
||||||
|
Scope: scope,
|
||||||
|
ApplicationId: application.GetId(),
|
||||||
|
RequestAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuthCache := object.DeviceAuthCache{
|
||||||
|
UserSignIn: false,
|
||||||
|
UserName: deviceCode,
|
||||||
|
Scope: scope,
|
||||||
|
ApplicationId: application.GetId(),
|
||||||
|
RequestAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
object.DeviceAuthMap.Store(deviceCode, deviceAuthCache)
|
||||||
|
object.DeviceAuthMap.Store(userCode, userAuthCache)
|
||||||
|
|
||||||
|
c.Data["json"] = object.GetDeviceAuthResponse(deviceCode, userCode, c.Ctx.Request.Host)
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
@ -27,10 +27,10 @@ type LdapResp struct {
|
|||||||
ExistUuids []string `json:"existUuids"`
|
ExistUuids []string `json:"existUuids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//type LdapRespGroup struct {
|
// type LdapRespGroup struct {
|
||||||
// GroupId string
|
// GroupId string
|
||||||
// GroupName string
|
// GroupName string
|
||||||
//}
|
// }
|
||||||
|
|
||||||
type LdapSyncResp struct {
|
type LdapSyncResp struct {
|
||||||
Exist []object.LdapUser `json:"exist"`
|
Exist []object.LdapUser `json:"exist"`
|
||||||
@ -61,18 +61,18 @@ func (c *ApiController) GetLdapUsers() {
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
|
// groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
|
||||||
//if err != nil {
|
// if err != nil {
|
||||||
// c.ResponseError(err.Error())
|
// c.ResponseError(err.Error())
|
||||||
// return
|
// return
|
||||||
//}
|
// }
|
||||||
|
|
||||||
//for _, group := range groupsMap {
|
// for _, group := range groupsMap {
|
||||||
// resp.Groups = append(resp.Groups, LdapRespGroup{
|
// resp.Groups = append(resp.Groups, LdapRespGroup{
|
||||||
// GroupId: group.GidNumber,
|
// GroupId: group.GidNumber,
|
||||||
// GroupName: group.Cn,
|
// GroupName: group.Cn,
|
||||||
// })
|
// })
|
||||||
//}
|
// }
|
||||||
|
|
||||||
users, err := conn.GetLdapUsers(ldapServer)
|
users, err := conn.GetLdapUsers(ldapServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -269,7 +269,11 @@ func (c *ApiController) SyncLdapUsers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exist, failed, _ := object.SyncLdapUsers(owner, users, ldapId)
|
exist, failed, err := object.SyncLdapUsers(owner, users, ldapId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.ResponseOk(&LdapSyncResp{
|
c.ResponseOk(&LdapSyncResp{
|
||||||
Exist: exist,
|
Exist: exist,
|
||||||
|
@ -16,6 +16,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/beego/beego/utils/pagination"
|
"github.com/beego/beego/utils/pagination"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
@ -170,12 +171,17 @@ func (c *ApiController) GetOAuthToken() {
|
|||||||
tag := c.Input().Get("tag")
|
tag := c.Input().Get("tag")
|
||||||
avatar := c.Input().Get("avatar")
|
avatar := c.Input().Get("avatar")
|
||||||
refreshToken := c.Input().Get("refresh_token")
|
refreshToken := c.Input().Get("refresh_token")
|
||||||
|
deviceCode := c.Input().Get("device_code")
|
||||||
|
|
||||||
if clientId == "" && clientSecret == "" {
|
if clientId == "" && clientSecret == "" {
|
||||||
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.Ctx.Input.RequestBody) != 0 {
|
if grantType == "urn:ietf:params:oauth:grant-type:device_code" {
|
||||||
|
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Ctx.Input.RequestBody) != 0 && grantType != "urn:ietf:params:oauth:grant-type:device_code" {
|
||||||
// If clientId is empty, try to read data from RequestBody
|
// If clientId is empty, try to read data from RequestBody
|
||||||
var tokenRequest TokenRequest
|
var tokenRequest TokenRequest
|
||||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
|
||||||
@ -219,6 +225,40 @@ func (c *ApiController) GetOAuthToken() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if deviceCode != "" {
|
||||||
|
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
|
||||||
|
if !ok {
|
||||||
|
c.Data["json"] = object.TokenError{
|
||||||
|
Error: "expired_token",
|
||||||
|
ErrorDescription: "token is expired",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
|
||||||
|
if !deviceAuthCacheCast.UserSignIn {
|
||||||
|
c.Data["json"] = object.TokenError{
|
||||||
|
Error: "authorization_pending",
|
||||||
|
ErrorDescription: "authorization pending",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if deviceAuthCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
|
||||||
|
c.Data["json"] = object.TokenError{
|
||||||
|
Error: "expired_token",
|
||||||
|
ErrorDescription: "token is expired",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
object.DeviceAuthMap.Delete(deviceCode)
|
||||||
|
|
||||||
|
username = deviceAuthCacheCast.UserName
|
||||||
|
}
|
||||||
|
|
||||||
host := c.Ctx.Request.Host
|
host := c.Ctx.Request.Host
|
||||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
|
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -321,6 +361,11 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
respondWithInactiveToken := func() {
|
||||||
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||||
var token *object.Token
|
var token *object.Token
|
||||||
if tokenTypeHint != "" {
|
if tokenTypeHint != "" {
|
||||||
@ -329,7 +374,12 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
c.ResponseTokenError(err.Error())
|
c.ResponseTokenError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if token == nil {
|
if token == nil || token.ExpiresIn <= 0 {
|
||||||
|
respondWithInactiveToken()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.ExpiresIn <= 0 {
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
return
|
return
|
||||||
@ -340,12 +390,11 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
|
|
||||||
if application.TokenFormat == "JWT-Standard" {
|
if application.TokenFormat == "JWT-Standard" {
|
||||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||||
if err != nil || jwtToken.Valid() != nil {
|
if err != nil {
|
||||||
// and token revoked case. but we not implement
|
// and token revoked case. but we not implement
|
||||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||||
// refs: https://tools.ietf.org/html/rfc7009
|
// refs: https://tools.ietf.org/html/rfc7009
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
respondWithInactiveToken()
|
||||||
c.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,21 +414,17 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||||
if err != nil || jwtToken.Valid() != nil {
|
if err != nil {
|
||||||
// and token revoked case. but we not implement
|
// and token revoked case. but we not implement
|
||||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||||
// refs: https://tools.ietf.org/html/rfc7009
|
// refs: https://tools.ietf.org/html/rfc7009
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
respondWithInactiveToken()
|
||||||
c.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
introspectionResponse = object.IntrospectionResponse{
|
introspectionResponse = object.IntrospectionResponse{
|
||||||
Active: true,
|
Active: true,
|
||||||
Scope: jwtToken.Scope,
|
|
||||||
ClientId: clientId,
|
ClientId: clientId,
|
||||||
Username: jwtToken.Name,
|
|
||||||
TokenType: jwtToken.TokenType,
|
|
||||||
Exp: jwtToken.ExpiresAt.Unix(),
|
Exp: jwtToken.ExpiresAt.Unix(),
|
||||||
Iat: jwtToken.IssuedAt.Unix(),
|
Iat: jwtToken.IssuedAt.Unix(),
|
||||||
Nbf: jwtToken.NotBefore.Unix(),
|
Nbf: jwtToken.NotBefore.Unix(),
|
||||||
@ -388,6 +433,16 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
Iss: jwtToken.Issuer,
|
Iss: jwtToken.Issuer,
|
||||||
Jti: jwtToken.ID,
|
Jti: jwtToken.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jwtToken.Scope != "" {
|
||||||
|
introspectionResponse.Scope = jwtToken.Scope
|
||||||
|
}
|
||||||
|
if jwtToken.Name != "" {
|
||||||
|
introspectionResponse.Username = jwtToken.Name
|
||||||
|
}
|
||||||
|
if jwtToken.TokenType != "" {
|
||||||
|
introspectionResponse.TokenType = jwtToken.TokenType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenTypeHint == "" {
|
if tokenTypeHint == "" {
|
||||||
@ -396,13 +451,15 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
c.ResponseTokenError(err.Error())
|
c.ResponseTokenError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if token == nil {
|
if token == nil || token.ExpiresIn <= 0 {
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
respondWithInactiveToken()
|
||||||
c.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if token != nil {
|
||||||
introspectionResponse.TokenType = token.TokenType
|
introspectionResponse.TokenType = token.TokenType
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = introspectionResponse
|
c.Data["json"] = introspectionResponse
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
|
@ -457,10 +457,10 @@ func (c *ApiController) SetPassword() {
|
|||||||
newPassword := c.Ctx.Request.Form.Get("newPassword")
|
newPassword := c.Ctx.Request.Form.Get("newPassword")
|
||||||
code := c.Ctx.Request.Form.Get("code")
|
code := c.Ctx.Request.Form.Get("code")
|
||||||
|
|
||||||
//if userOwner == "built-in" && userName == "admin" {
|
// if userOwner == "built-in" && userName == "admin" {
|
||||||
// c.ResponseError(c.T("auth:Unauthorized operation"))
|
// c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||||
// return
|
// return
|
||||||
//}
|
// }
|
||||||
|
|
||||||
if strings.Contains(newPassword, " ") {
|
if strings.Contains(newPassword, " ") {
|
||||||
c.ResponseError(c.T("user:New password cannot contain blank space."))
|
c.ResponseError(c.T("user:New password cannot contain blank space."))
|
||||||
@ -602,7 +602,11 @@ func (c *ApiController) CheckUserPassword() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
|
/*
|
||||||
|
* Verified password with user as subject, if field ldap not empty,
|
||||||
|
* then `isPasswordWithLdapEnabled` is true
|
||||||
|
*/
|
||||||
|
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage(), false, false, user.Ldap != "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
@ -242,7 +242,7 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
} else if vform.Method == ResetVerification {
|
} else if vform.Method == ResetVerification {
|
||||||
user = c.getCurrentUser()
|
user = c.getCurrentUser()
|
||||||
} else if vform.Method == MfaAuthVerification {
|
} else if vform.Method == MfaAuthVerification {
|
||||||
mfaProps := user.GetPreferredMfaProps(false)
|
mfaProps := user.GetMfaProps(object.EmailType, false)
|
||||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||||
vform.Dest = mfaProps.Secret
|
vform.Dest = mfaProps.Secret
|
||||||
}
|
}
|
||||||
@ -281,7 +281,7 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if vform.Method == MfaAuthVerification {
|
} else if vform.Method == MfaAuthVerification {
|
||||||
mfaProps := user.GetPreferredMfaProps(false)
|
mfaProps := user.GetMfaProps(object.SmsType, false)
|
||||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||||
vform.Dest = mfaProps.Secret
|
vform.Dest = mfaProps.Secret
|
||||||
}
|
}
|
||||||
@ -436,7 +436,8 @@ func (c *ApiController) ResetEmailOrPhone() {
|
|||||||
switch destType {
|
switch destType {
|
||||||
case object.VerifyTypeEmail:
|
case object.VerifyTypeEmail:
|
||||||
user.Email = dest
|
user.Email = dest
|
||||||
_, err = object.SetUserField(user, "email", user.Email)
|
user.EmailVerified = true
|
||||||
|
_, err = object.UpdateUser(user.GetId(), user, []string{"email", "email_verified"}, false)
|
||||||
case object.VerifyTypePhone:
|
case object.VerifyTypePhone:
|
||||||
user.Phone = dest
|
user.Phone = dest
|
||||||
_, err = object.SetUserField(user, "phone", user.Phone)
|
_, err = object.SetUserField(user, "phone", user.Phone)
|
||||||
|
@ -16,7 +16,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/form"
|
"github.com/casdoor/casdoor/form"
|
||||||
@ -118,24 +118,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userOwner := c.Input().Get("owner")
|
options, sessionData, err := webauthnObj.BeginDiscoverableLogin()
|
||||||
userName := c.Input().Get("name")
|
|
||||||
user, err := object.GetUserByFields(userOwner, userName)
|
|
||||||
if err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(userOwner, userName)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(user.WebauthnCredentials) == 0 {
|
|
||||||
c.ResponseError(c.T("webauthn:Found no credentials for this user"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options, sessionData, err := webauthnObj.BeginLogin(user)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
@ -168,20 +151,23 @@ func (c *ApiController) WebAuthnSigninFinish() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||||
userId := string(sessionData.UserID)
|
|
||||||
user, err := object.GetUser(userId)
|
var user *object.User
|
||||||
|
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
|
||||||
|
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
return nil, err
|
||||||
return
|
}
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.SetSessionUsername(userId)
|
c.SetSessionUsername(user.GetId())
|
||||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
util.LogInfo(c.Ctx, "API: [%s] signed in", user.GetId())
|
||||||
|
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
|
|
||||||
|
@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
|
|||||||
return NewPbkdf2SaltCredManager()
|
return NewPbkdf2SaltCredManager()
|
||||||
} else if passwordType == "argon2id" {
|
} else if passwordType == "argon2id" {
|
||||||
return NewArgon2idCredManager()
|
return NewArgon2idCredManager()
|
||||||
|
} else if passwordType == "pbkdf2-django" {
|
||||||
|
return NewPbkdf2DjangoCredManager()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
71
cred/pbkdf2_django.go
Normal file
71
cred/pbkdf2_django.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// 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 cred
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// password type: pbkdf2-django
|
||||||
|
|
||||||
|
type Pbkdf2DjangoCredManager struct{}
|
||||||
|
|
||||||
|
func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
|
||||||
|
cm := &Pbkdf2DjangoCredManager{}
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||||
|
iterations := 260000
|
||||||
|
salt := userSalt
|
||||||
|
if salt == "" {
|
||||||
|
salt = organizationSalt
|
||||||
|
}
|
||||||
|
|
||||||
|
saltBytes := []byte(salt)
|
||||||
|
passwordBytes := []byte(password)
|
||||||
|
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iterations, sha256.Size, sha256.New)
|
||||||
|
hashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||||
|
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool {
|
||||||
|
parts := strings.Split(passwordHash, "$")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm, iterations, salt, hash := parts[0], parts[1], parts[2], parts[3]
|
||||||
|
if algorithm != "pbkdf2_sha256" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
iter, err := strconv.Atoi(iterations)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
saltBytes := []byte(salt)
|
||||||
|
passwordBytes := []byte(password)
|
||||||
|
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iter, sha256.Size, sha256.New)
|
||||||
|
computedHashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||||
|
|
||||||
|
return computedHashBase64 == hash
|
||||||
|
}
|
@ -15,6 +15,8 @@
|
|||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -26,12 +28,22 @@ import (
|
|||||||
type HttpEmailProvider struct {
|
type HttpEmailProvider struct {
|
||||||
endpoint string
|
endpoint string
|
||||||
method string
|
method string
|
||||||
|
httpHeaders map[string]string
|
||||||
|
bodyMapping map[string]string
|
||||||
|
contentType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
|
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) *HttpEmailProvider {
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
|
||||||
client := &HttpEmailProvider{
|
client := &HttpEmailProvider{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
method: method,
|
method: method,
|
||||||
|
httpHeaders: httpHeaders,
|
||||||
|
bodyMapping: bodyMapping,
|
||||||
|
contentType: contentType,
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
@ -39,18 +51,52 @@ func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
|
|||||||
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 req *http.Request
|
||||||
var err error
|
var err error
|
||||||
if c.method == "POST" {
|
|
||||||
|
fromNameField := "fromName"
|
||||||
|
toAddressField := "toAddress"
|
||||||
|
subjectField := "subject"
|
||||||
|
contentField := "content"
|
||||||
|
|
||||||
|
for k, v := range c.bodyMapping {
|
||||||
|
switch k {
|
||||||
|
case "fromName":
|
||||||
|
fromNameField = v
|
||||||
|
case "toAddress":
|
||||||
|
toAddressField = v
|
||||||
|
case "subject":
|
||||||
|
subjectField = v
|
||||||
|
case "content":
|
||||||
|
contentField = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
var fromValueBytes []byte
|
||||||
|
if c.contentType == "application/json" {
|
||||||
|
fromValueBytes, err = json.Marshal(bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes))
|
||||||
|
} else {
|
||||||
formValues := url.Values{}
|
formValues := url.Values{}
|
||||||
formValues.Set("fromName", fromName)
|
for k, v := range bodyMap {
|
||||||
formValues.Set("toAddress", toAddress)
|
formValues.Add(k, v)
|
||||||
formValues.Set("subject", subject)
|
}
|
||||||
formValues.Set("content", content)
|
|
||||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", c.contentType)
|
||||||
} else if c.method == "GET" {
|
} else if c.method == "GET" {
|
||||||
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -58,15 +104,19 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
q.Add("fromName", fromName)
|
q.Add(fromNameField, fromName)
|
||||||
q.Add("toAddress", toAddress)
|
q.Add(toAddressField, toAddress)
|
||||||
q.Add("subject", subject)
|
q.Add(subjectField, subject)
|
||||||
q.Add("content", content)
|
q.Add(contentField, content)
|
||||||
req.URL.RawQuery = q.Encode()
|
req.URL.RawQuery = q.Encode()
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, v := range c.httpHeaders {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
httpClient := proxy.DefaultHttpClient
|
httpClient := proxy.DefaultHttpClient
|
||||||
resp, err := httpClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -18,13 +18,13 @@ type EmailProvider interface {
|
|||||||
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
|
Send(fromAddress string, fromName, 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) EmailProvider {
|
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 {
|
||||||
if typ == "Azure ACS" {
|
if typ == "Azure ACS" {
|
||||||
return NewAzureACSEmailProvider(clientSecret, host)
|
return NewAzureACSEmailProvider(clientSecret, host)
|
||||||
} else if typ == "Custom HTTP Email" {
|
} else if typ == "Custom HTTP Email" {
|
||||||
return NewHttpEmailProvider(endpoint, method)
|
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||||
} else if typ == "SendGrid" {
|
} else if typ == "SendGrid" {
|
||||||
return NewSendgridEmailProvider(clientSecret)
|
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||||
} else {
|
} else {
|
||||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
|
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ package email
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sendgrid/sendgrid-go"
|
"github.com/sendgrid/sendgrid-go"
|
||||||
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
||||||
@ -25,6 +25,8 @@ import (
|
|||||||
|
|
||||||
type SendgridEmailProvider struct {
|
type SendgridEmailProvider struct {
|
||||||
ApiKey string
|
ApiKey string
|
||||||
|
Host string
|
||||||
|
Endpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendgridResponseBody struct {
|
type SendgridResponseBody struct {
|
||||||
@ -35,23 +37,25 @@ type SendgridResponseBody struct {
|
|||||||
} `json:"errors"`
|
} `json:"errors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
|
func NewSendgridEmailProvider(apiKey string, host string, endpoint string) *SendgridEmailProvider {
|
||||||
return &SendgridEmailProvider{ApiKey: apiKey}
|
return &SendgridEmailProvider{ApiKey: apiKey, Host: host, Endpoint: endpoint}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress string, subject string, content string) error {
|
func (s *SendgridEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||||
|
client := s.initSendgridClient()
|
||||||
|
|
||||||
from := mail.NewEmail(fromName, fromAddress)
|
from := mail.NewEmail(fromName, fromAddress)
|
||||||
to := mail.NewEmail("", toAddress)
|
to := mail.NewEmail("", toAddress)
|
||||||
message := mail.NewSingleEmail(from, subject, to, "", content)
|
message := mail.NewSingleEmail(from, subject, to, "", content)
|
||||||
client := sendgrid.NewSendClient(s.ApiKey)
|
|
||||||
response, err := client.Send(message)
|
resp, err := client.Send(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode >= 300 {
|
if resp.StatusCode >= 300 {
|
||||||
var responseBody SendgridResponseBody
|
var responseBody SendgridResponseBody
|
||||||
err = json.Unmarshal([]byte(response.Body), &responseBody)
|
err = json.Unmarshal([]byte(resp.Body), &responseBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -61,8 +65,23 @@ func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress str
|
|||||||
messages = append(messages, sendgridError.Message)
|
messages = append(messages, sendgridError.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("SendGrid status code: %d, error message: %s", response.StatusCode, strings.Join(messages, " | "))
|
return fmt.Errorf("status code: %d, error message: %s", resp.StatusCode, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
|
return fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SendgridEmailProvider) initSendgridClient() *sendgrid.Client {
|
||||||
|
if s.Host == "" || s.Endpoint == "" {
|
||||||
|
return sendgrid.NewSendClient(s.ApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := sendgrid.GetRequest(s.ApiKey, s.Endpoint, s.Host)
|
||||||
|
request.Method = "POST"
|
||||||
|
|
||||||
|
return &sendgrid.Client{Request: request}
|
||||||
|
}
|
||||||
|
81
faceId/aliyun.go
Normal file
81
faceId/aliyun.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// 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 faceId
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||||
|
facebody20191230 "github.com/alibabacloud-go/facebody-20191230/v5/client"
|
||||||
|
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||||
|
"github.com/alibabacloud-go/tea/tea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AliyunFaceIdProvider struct {
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
|
||||||
|
Endpoint string
|
||||||
|
QualityScoreThreshold float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAliyunFaceIdProvider(accessKey string, accessSecret string, endPoint string) *AliyunFaceIdProvider {
|
||||||
|
return &AliyunFaceIdProvider{
|
||||||
|
AccessKey: accessKey,
|
||||||
|
AccessSecret: accessSecret,
|
||||||
|
Endpoint: endPoint,
|
||||||
|
QualityScoreThreshold: 0.65,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *AliyunFaceIdProvider) Check(base64ImageA string, base64ImageB string) (bool, error) {
|
||||||
|
config := openapi.Config{
|
||||||
|
AccessKeyId: tea.String(provider.AccessKey),
|
||||||
|
AccessKeySecret: tea.String(provider.AccessSecret),
|
||||||
|
}
|
||||||
|
config.Endpoint = tea.String(provider.Endpoint)
|
||||||
|
client, err := facebody20191230.NewClient(&config)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
compareFaceRequest := &facebody20191230.CompareFaceRequest{
|
||||||
|
QualityScoreThreshold: tea.Float32(provider.QualityScoreThreshold),
|
||||||
|
ImageDataA: tea.String(strings.Replace(base64ImageA, "data:image/png;base64,", "", -1)),
|
||||||
|
ImageDataB: tea.String(strings.Replace(base64ImageB, "data:image/png;base64,", "", -1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime := &util.RuntimeOptions{}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := tea.Recover(recover()); r != nil {
|
||||||
|
err = r
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
result, err := client.CompareFaceWithOptions(compareFaceRequest, runtime)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if *result.Body.Data.Thresholds[0] < *result.Body.Data.Confidence {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
23
faceId/provider.go
Normal file
23
faceId/provider.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// 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 faceId
|
||||||
|
|
||||||
|
type FaceIdProvider interface {
|
||||||
|
Check(base64ImageA string, base64ImageB string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFaceIdProvider(typ string, clientId string, clientSecret string, endPoint string) FaceIdProvider {
|
||||||
|
return NewAliyunFaceIdProvider(clientId, clientSecret, endPoint)
|
||||||
|
}
|
@ -34,6 +34,7 @@ type AuthForm struct {
|
|||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
Affiliation string `json:"affiliation"`
|
Affiliation string `json:"affiliation"`
|
||||||
IdCard string `json:"idCard"`
|
IdCard string `json:"idCard"`
|
||||||
|
Language string `json:"language"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
InvitationCode string `json:"invitationCode"`
|
InvitationCode string `json:"invitationCode"`
|
||||||
|
|
||||||
@ -68,6 +69,8 @@ type AuthForm struct {
|
|||||||
Pricing string `json:"pricing"`
|
Pricing string `json:"pricing"`
|
||||||
|
|
||||||
FaceId []float64 `json:"faceId"`
|
FaceId []float64 `json:"faceId"`
|
||||||
|
FaceIdImage []string `json:"faceIdImage"`
|
||||||
|
UserCode string `json:"userCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
||||||
|
194
go.mod
194
go.mod
@ -1,10 +1,14 @@
|
|||||||
module github.com/casdoor/casdoor
|
module github.com/casdoor/casdoor
|
||||||
|
|
||||||
go 1.16
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/squirrel v1.5.3
|
github.com/Masterminds/squirrel v1.5.3
|
||||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||||
|
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
|
||||||
|
github.com/alibabacloud-go/tea v1.3.2
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||||
github.com/aws/aws-sdk-go v1.45.5
|
github.com/aws/aws-sdk-go v1.45.5
|
||||||
github.com/beego/beego v1.12.12
|
github.com/beego/beego v1.12.12
|
||||||
github.com/beevik/etree v1.1.0
|
github.com/beevik/etree v1.1.0
|
||||||
@ -18,7 +22,6 @@ require (
|
|||||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||||
github.com/denisenkom/go-mssqldb v0.9.0
|
github.com/denisenkom/go-mssqldb v0.9.0
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
|
||||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||||
github.com/fogleman/gg v1.3.0
|
github.com/fogleman/gg v1.3.0
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||||
@ -28,8 +31,8 @@ require (
|
|||||||
github.com/go-pay/gopay v1.5.72
|
github.com/go-pay/gopay v1.5.72
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||||
github.com/go-webauthn/webauthn v0.6.0
|
github.com/go-webauthn/webauthn v0.10.2
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/lestrrat-go/jwx v1.2.29
|
github.com/lestrrat-go/jwx v1.2.29
|
||||||
@ -46,7 +49,6 @@ require (
|
|||||||
github.com/russellhaering/gosaml2 v0.9.0
|
github.com/russellhaering/gosaml2 v0.9.0
|
||||||
github.com/russellhaering/goxmldsig v1.2.0
|
github.com/russellhaering/goxmldsig v1.2.0
|
||||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
||||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
@ -54,20 +56,194 @@ require (
|
|||||||
github.com/stripe/stripe-go/v74 v74.29.0
|
github.com/stripe/stripe-go/v74 v74.29.0
|
||||||
github.com/tealeg/xlsx v1.0.5
|
github.com/tealeg/xlsx v1.0.5
|
||||||
github.com/thanhpk/randstr v1.0.4
|
github.com/thanhpk/randstr v1.0.4
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
|
||||||
github.com/xorm-io/builder v0.3.13
|
github.com/xorm-io/builder v0.3.13
|
||||||
github.com/xorm-io/core v0.7.4
|
github.com/xorm-io/core v0.7.4
|
||||||
github.com/xorm-io/xorm v1.1.6
|
github.com/xorm-io/xorm v1.1.6
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/net v0.34.0
|
golang.org/x/net v0.34.0
|
||||||
golang.org/x/oauth2 v0.17.0
|
golang.org/x/oauth2 v0.17.0
|
||||||
golang.org/x/text v0.21.0
|
golang.org/x/text v0.21.0
|
||||||
google.golang.org/api v0.150.0
|
google.golang.org/api v0.150.0
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
||||||
maunium.net/go/mautrix v0.16.0
|
maunium.net/go/mautrix v0.16.0
|
||||||
modernc.org/sqlite v1.18.2
|
modernc.org/sqlite v1.18.2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go v0.110.8 // indirect
|
||||||
|
cloud.google.com/go/compute v1.23.1 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
cloud.google.com/go/iam v1.1.3 // indirect
|
||||||
|
cloud.google.com/go/storage v1.35.1 // indirect
|
||||||
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
|
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||||
|
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
|
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||||
|
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||||
|
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 // indirect
|
||||||
|
github.com/SherClockHolmes/webpush-go v1.2.0 // indirect
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||||
|
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||||
|
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||||
|
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||||
|
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||||
|
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||||
|
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||||
|
github.com/atc0005/go-teams-notify/v2 v2.6.1 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
|
github.com/bwmarrin/discordgo v0.27.1 // indirect
|
||||||
|
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||||
|
github.com/casdoor/go-reddit/v2 v2.1.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
|
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
|
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||||
|
github.com/dghubble/sling v1.4.0 // indirect
|
||||||
|
github.com/di-wu/parser v0.2.2 // indirect
|
||||||
|
github.com/di-wu/xsd-datetime v1.0.0 // indirect
|
||||||
|
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||||
|
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||||
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||||
|
github.com/go-lark/lark v1.9.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/go-webauthn/x v0.1.9 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/google/go-tpm v0.9.0 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/gregdel/pushover v1.2.1 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
|
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
|
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||||
|
github.com/markbates/going v1.0.0 // indirect
|
||||||
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
|
github.com/mileusna/viber v1.0.1 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
|
||||||
|
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
|
||||||
|
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/common v0.30.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
|
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
|
github.com/rs/zerolog v1.30.0 // indirect
|
||||||
|
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||||
|
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||||
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
|
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||||
|
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
|
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||||
|
github.com/slack-go/slack v0.12.3 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||||
|
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
|
||||||
|
github.com/tidwall/gjson v1.16.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||||
|
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||||
|
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||||
|
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||||
|
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
|
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776 // indirect
|
||||||
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
|
go.uber.org/zap v1.19.1 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||||
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
|
golang.org/x/time v0.3.0 // indirect
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||||
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
lukechampine.com/uint128 v1.1.1 // indirect
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||||
|
modernc.org/cc/v3 v3.37.0 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||||
|
modernc.org/libc v1.18.0 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.3.0 // indirect
|
||||||
|
modernc.org/opt v0.1.1 // indirect
|
||||||
|
modernc.org/strutil v1.1.3 // indirect
|
||||||
|
modernc.org/token v1.0.1 // indirect
|
||||||
|
)
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
|
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
|
||||||
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
|
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
|
||||||
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
|
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
|
||||||
"Username is too long (maximum is 39 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 39 znaků).",
|
"Username is too long (maximum is 255 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 255 znaků).",
|
||||||
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
|
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
|
||||||
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
|
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
|
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
|
||||||
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
|
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
|
||||||
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen",
|
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen",
|
||||||
"Username is too long (maximum is 39 characters).": "Benutzername ist zu lang (das Maximum beträgt 39 Zeichen).",
|
"Username is too long (maximum is 255 characters).": "Benutzername ist zu lang (das Maximum beträgt 255 Zeichen).",
|
||||||
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
|
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
|
||||||
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
|
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
|
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
|
||||||
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
|
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
|
||||||
"Username cannot start with a digit": "El nombre de usuario no puede empezar con un dígito",
|
"Username cannot start with a digit": "El nombre de usuario no puede empezar con un dígito",
|
||||||
"Username is too long (maximum is 39 characters).": "El nombre de usuario es demasiado largo (el máximo es de 39 caracteres).",
|
"Username is too long (maximum is 255 characters).": "El nombre de usuario es demasiado largo (el máximo es de 255 caracteres).",
|
||||||
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
|
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
|
||||||
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
|
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
||||||
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
||||||
"Username cannot start with a digit": "نام کاربری نمیتواند با یک رقم شروع شود",
|
"Username cannot start with a digit": "نام کاربری نمیتواند با یک رقم شروع شود",
|
||||||
"Username is too long (maximum is 39 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
|
"Username is too long (maximum is 255 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
|
||||||
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
|
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کردهاید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کردهاید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
|
||||||
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
|
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
|
||||||
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
|
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
|
||||||
"Username cannot start with a digit": "Nom d'utilisateur ne peut pas commencer par un chiffre",
|
"Username cannot start with a digit": "Nom d'utilisateur ne peut pas commencer par un chiffre",
|
||||||
"Username is too long (maximum is 39 characters).": "Nom d'utilisateur est trop long (maximum de 39 caractères).",
|
"Username is too long (maximum is 255 characters).": "Nom d'utilisateur est trop long (maximum de 255 caractères).",
|
||||||
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
|
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
|
||||||
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
|
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
|
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
|
||||||
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
|
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
|
||||||
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
|
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
|
||||||
"Username is too long (maximum is 39 characters).": "Nama pengguna terlalu panjang (maksimum 39 karakter).",
|
"Username is too long (maximum is 255 characters).": "Nama pengguna terlalu panjang (maksimum 255 karakter).",
|
||||||
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
|
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
|
||||||
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
|
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
|
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
|
||||||
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
|
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
|
||||||
"Username cannot start with a digit": "ユーザー名は数字で始めることはできません",
|
"Username cannot start with a digit": "ユーザー名は数字で始めることはできません",
|
||||||
"Username is too long (maximum is 39 characters).": "ユーザー名が長すぎます(最大39文字)。",
|
"Username is too long (maximum is 255 characters).": "ユーザー名が長すぎます(最大255文字)。",
|
||||||
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
|
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
|
||||||
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
|
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
|
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
|
||||||
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
|
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
|
||||||
"Username cannot start with a digit": "사용자 이름은 숫자로 시작할 수 없습니다",
|
"Username cannot start with a digit": "사용자 이름은 숫자로 시작할 수 없습니다",
|
||||||
"Username is too long (maximum is 39 characters).": "사용자 이름이 너무 깁니다 (최대 39자).",
|
"Username is too long (maximum is 255 characters).": "사용자 이름이 너무 깁니다 (최대 255자).",
|
||||||
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
|
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
|
||||||
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
|
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "O nome de usuário não pode começar com um dígito",
|
"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 39 characters).": "Nome de usuário é muito longo (máximo é 39 caracteres).",
|
"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 must have at least 2 characters": "Nome de usuário deve ter pelo menos 2 caracteres",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
||||||
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
||||||
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
|
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
|
||||||
"Username is too long (maximum is 39 characters).": "Имя пользователя слишком длинное (максимальная длина - 39 символов).",
|
"Username is too long (maximum is 255 characters).": "Имя пользователя слишком длинное (максимальная длина - 255 символов).",
|
||||||
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
|
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
|
||||||
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
|
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
|
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
|
||||||
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
|
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
|
||||||
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
|
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
|
||||||
"Username is too long (maximum is 39 characters).": "Používateľské meno je príliš dlhé (maximum je 39 znakov).",
|
"Username is too long (maximum is 255 characters).": "Používateľské meno je príliš dlhé (maximum je 255 znakov).",
|
||||||
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
|
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
|
||||||
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
|
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Kullanıcı adı bir e-mail adresi olamaz",
|
"Username cannot be an email address": "Kullanıcı adı bir e-mail adresi olamaz",
|
||||||
"Username cannot contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
|
"Username cannot contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
|
||||||
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz",
|
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz",
|
||||||
"Username is too long (maximum is 39 characters).": "Kullanıcı adı çok uzun (en fazla 39 karakter olmalı).",
|
"Username is too long (maximum is 255 characters).": "Kullanıcı adı çok uzun (en fazla 255 karakter olmalı).",
|
||||||
"Username must have at least 2 characters": "Kullanıcı adı en az iki karakterden oluşmalı",
|
"Username must have at least 2 characters": "Kullanıcı adı en az iki karakterden oluşmalı",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre denemesi yaptınız. %d dakika kadar bekleyip yeniden giriş yapmayı deneyebilirsiniz.",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre denemesi yaptınız. %d dakika kadar bekleyip yeniden giriş yapmayı deneyebilirsiniz.",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
|
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
|
||||||
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
|
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
|
||||||
"Username cannot start with a digit": "Tên người dùng không thể bắt đầu bằng chữ số",
|
"Username cannot start with a digit": "Tên người dùng không thể bắt đầu bằng chữ số",
|
||||||
"Username is too long (maximum is 39 characters).": "Tên đăng nhập quá dài (tối đa là 39 ký tự).",
|
"Username is too long (maximum is 255 characters).": "Tên đăng nhập quá dài (tối đa là 255 ký tự).",
|
||||||
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
|
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
|
||||||
"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",
|
"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",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "用户名不可以是邮箱地址",
|
"Username cannot be an email address": "用户名不可以是邮箱地址",
|
||||||
"Username cannot contain white spaces": "用户名禁止包含空格",
|
"Username cannot contain white spaces": "用户名禁止包含空格",
|
||||||
"Username cannot start with a digit": "用户名禁止使用数字开头",
|
"Username cannot start with a digit": "用户名禁止使用数字开头",
|
||||||
"Username is too long (maximum is 39 characters).": "用户名过长(最大允许长度为39个字符)",
|
"Username is too long (maximum is 255 characters).": "用户名过长(最大允许长度为255个字符)",
|
||||||
"Username must have at least 2 characters": "用户名至少要有2个字符",
|
"Username must have at least 2 characters": "用户名至少要有2个字符",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
|
||||||
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
|
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
|
||||||
|
@ -136,12 +136,12 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
|||||||
dtUserInfo := &DingTalkUserResponse{}
|
dtUserInfo := &DingTalkUserResponse{}
|
||||||
accessToken := token.AccessToken
|
accessToken := token.AccessToken
|
||||||
|
|
||||||
reqest, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
|
request, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
reqest.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
request.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
||||||
resp, err := idp.Client.Do(reqest)
|
resp, err := idp.Client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -299,12 +299,12 @@ func GetWechatOfficialAccountQRCode(clientId string, clientSecret string, provid
|
|||||||
params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
|
params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
|
||||||
|
|
||||||
bodyData := bytes.NewReader([]byte(params))
|
bodyData := bytes.NewReader([]byte(params))
|
||||||
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
request, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(requeset)
|
resp, err := client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
@ -434,7 +434,7 @@
|
|||||||
"isTopGroup": true,
|
"isTopGroup": true,
|
||||||
"title": "",
|
"title": "",
|
||||||
"key": "",
|
"key": "",
|
||||||
"children": "",
|
"children": [],
|
||||||
"isEnabled": true
|
"isEnabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -185,12 +185,9 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
|
|||||||
attr := string(f.AttributeDesc())
|
attr := string(f.AttributeDesc())
|
||||||
|
|
||||||
if attr == ldapMemberOfAttr {
|
if attr == ldapMemberOfAttr {
|
||||||
groupId := string(f.AssertionValue())
|
|
||||||
users, err := object.GetGroupUsers(groupId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var names []string
|
var names []string
|
||||||
|
groupId := string(f.AssertionValue())
|
||||||
|
users := object.GetGroupUsersWithoutError(groupId)
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
names = append(names, user.Name)
|
names = append(names, user.Name)
|
||||||
}
|
}
|
||||||
@ -249,7 +246,7 @@ func buildSafeCondition(filter interface{}) builder.Cond {
|
|||||||
condition, err := buildUserFilterCondition(filter)
|
condition, err := buildUserFilterCondition(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("err = %v", err.Error())
|
log.Printf("err = %v", err.Error())
|
||||||
return nil
|
return builder.And(builder.Expr("1 != 1"))
|
||||||
}
|
}
|
||||||
return condition
|
return condition
|
||||||
}
|
}
|
||||||
|
@ -191,12 +191,7 @@ func (adapter *Adapter) InitAdapter() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tableName string
|
tableName := adapter.Table
|
||||||
if driverName == "mssql" {
|
|
||||||
tableName = fmt.Sprintf("[%s]", adapter.Table)
|
|
||||||
} else {
|
|
||||||
tableName = adapter.Table
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
|
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -71,6 +71,7 @@ type Application struct {
|
|||||||
Description string `xorm:"varchar(100)" json:"description"`
|
Description string `xorm:"varchar(100)" json:"description"`
|
||||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||||
|
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||||
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
||||||
EnablePassword bool `json:"enablePassword"`
|
EnablePassword bool `json:"enablePassword"`
|
||||||
EnableSignUp bool `json:"enableSignUp"`
|
EnableSignUp bool `json:"enableSignUp"`
|
||||||
@ -100,6 +101,7 @@ type Application struct {
|
|||||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||||
|
ForcedRedirectOrigin string `xorm:"varchar(100)" json:"forcedRedirectOrigin"`
|
||||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||||
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
||||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||||
@ -120,6 +122,7 @@ type Application struct {
|
|||||||
FormOffset int `json:"formOffset"`
|
FormOffset int `json:"formOffset"`
|
||||||
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
|
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
|
||||||
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
||||||
|
FormBackgroundUrlMobile string `xorm:"varchar(200)" json:"formBackgroundUrlMobile"`
|
||||||
|
|
||||||
FailedSigninLimit int `json:"failedSigninLimit"`
|
FailedSigninLimit int `json:"failedSigninLimit"`
|
||||||
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
||||||
@ -539,7 +542,7 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
|||||||
|
|
||||||
providerItems := []*ProviderItem{}
|
providerItems := []*ProviderItem{}
|
||||||
for _, providerItem := range application.Providers {
|
for _, providerItem := range application.Providers {
|
||||||
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML") {
|
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML" || providerItem.Provider.Category == "Face ID") {
|
||||||
providerItems = append(providerItems, providerItem)
|
providerItems = append(providerItems, providerItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,11 @@ func GetCertCount(owner, field, value string) (int64, error) {
|
|||||||
|
|
||||||
func GetCerts(owner string) ([]*Cert, error) {
|
func GetCerts(owner string) ([]*Cert, error) {
|
||||||
certs := []*Cert{}
|
certs := []*Cert{}
|
||||||
err := ormer.Engine.Where("owner = ? or owner = ? ", "admin", owner).Desc("created_time").Find(&certs, &Cert{})
|
db := ormer.Engine.NewSession()
|
||||||
|
if owner != "" {
|
||||||
|
db = db.Where("owner = ? or owner = ? ", "admin", owner)
|
||||||
|
}
|
||||||
|
err := db.Desc("created_time").Find(&certs, &Cert{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return certs, err
|
return certs, err
|
||||||
}
|
}
|
||||||
@ -146,7 +150,12 @@ func getCertByName(name string) (*Cert, error) {
|
|||||||
|
|
||||||
func GetCert(id string) (*Cert, error) {
|
func GetCert(id string) (*Cert, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name := util.GetOwnerAndNameFromId(id)
|
||||||
return getCert(owner, name)
|
cert, err := getCert(owner, name)
|
||||||
|
if cert == nil && owner != "admin" {
|
||||||
|
return getCert("admin", name)
|
||||||
|
} else {
|
||||||
|
return cert, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateCert(id string, cert *Cert) (bool, error) {
|
func UpdateCert(id string, cert *Cert) (bool, error) {
|
||||||
|
@ -252,7 +252,7 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
|||||||
credManager := cred.GetCredManager(passwordType)
|
credManager := cred.GetCredManager(passwordType)
|
||||||
if credManager != nil {
|
if credManager != nil {
|
||||||
if organization.MasterPassword != "" {
|
if organization.MasterPassword != "" {
|
||||||
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
|
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
|
||||||
return resetUserSigninErrorTimes(user)
|
return resetUserSigninErrorTimes(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -517,8 +517,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
|
|||||||
func CheckUsername(username string, lang string) string {
|
func CheckUsername(username string, lang string) string {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return i18n.Translate(lang, "check:Empty username.")
|
return i18n.Translate(lang, "check:Empty username.")
|
||||||
} else if len(username) > 39 {
|
} else if len(username) > 255 {
|
||||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||||
@ -533,8 +533,8 @@ func CheckUsername(username string, lang string) string {
|
|||||||
func CheckUsernameWithEmail(username string, lang string) string {
|
func CheckUsernameWithEmail(username string, lang string) string {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return i18n.Translate(lang, "check:Empty username.")
|
return i18n.Translate(lang, "check:Empty username.")
|
||||||
} else if len(username) > 39 {
|
} else if len(username) > 255 {
|
||||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||||
|
@ -74,7 +74,7 @@ func checkPasswordComplexity(password string, options []string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(options) == 0 {
|
if len(options) == 0 {
|
||||||
options = []string{"AtLeast6"}
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
checkers := map[string]ValidatorFunc{
|
checkers := map[string]ValidatorFunc{
|
||||||
|
@ -31,7 +31,7 @@ func TestSmtpServer(provider *Provider) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
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
|
fromAddress := provider.ClientId2
|
||||||
if fromAddress == "" {
|
if fromAddress == "" {
|
||||||
|
@ -83,19 +83,23 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
|
|||||||
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
|
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
|
||||||
groupsHaveChildren := []*Group{}
|
groupsHaveChildren := []*Group{}
|
||||||
resultMap := make(map[string]*Group)
|
resultMap := make(map[string]*Group)
|
||||||
|
groupMap := map[string]*Group{}
|
||||||
|
|
||||||
groupIds := []string{}
|
groupIds := []string{}
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
|
groupMap[group.Name] = group
|
||||||
groupIds = append(groupIds, group.Name)
|
groupIds = append(groupIds, group.Name)
|
||||||
|
if !group.IsTopGroup {
|
||||||
groupIds = append(groupIds, group.ParentId)
|
groupIds = append(groupIds, group.ParentId)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
|
err := ormer.Engine.Cols("owner", "name", "parent_id", "display_name").Distinct("parent_id").In("parent_id", groupIds).Find(&groupsHaveChildren)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, group := range groups {
|
for _, group := range groupsHaveChildren {
|
||||||
resultMap[group.Name] = group
|
resultMap[group.ParentId] = groupMap[group.ParentId]
|
||||||
}
|
}
|
||||||
return resultMap, nil
|
return resultMap, nil
|
||||||
}
|
}
|
||||||
@ -302,7 +306,10 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
|
|||||||
|
|
||||||
func GetGroupUsers(groupId string) ([]*User, error) {
|
func GetGroupUsers(groupId string) ([]*User, error) {
|
||||||
users := []*User{}
|
users := []*User{}
|
||||||
owner, _ := util.GetOwnerAndNameFromId(groupId)
|
owner, _, err := util.GetOwnerAndNameFromIdWithError(groupId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
|
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -314,6 +321,11 @@ func GetGroupUsers(groupId string) ([]*User, error) {
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetGroupUsersWithoutError(groupId string) []*User {
|
||||||
|
users, _ := GetGroupUsers(groupId)
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
func ExtendGroupWithUsers(group *Group) error {
|
func ExtendGroupWithUsers(group *Group) error {
|
||||||
if group == nil {
|
if group == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -70,12 +70,12 @@ func InitFromFile() {
|
|||||||
for _, provider := range initData.Providers {
|
for _, provider := range initData.Providers {
|
||||||
initDefinedProvider(provider)
|
initDefinedProvider(provider)
|
||||||
}
|
}
|
||||||
for _, user := range initData.Users {
|
|
||||||
initDefinedUser(user)
|
|
||||||
}
|
|
||||||
for _, application := range initData.Applications {
|
for _, application := range initData.Applications {
|
||||||
initDefinedApplication(application)
|
initDefinedApplication(application)
|
||||||
}
|
}
|
||||||
|
for _, user := range initData.Users {
|
||||||
|
initDefinedUser(user)
|
||||||
|
}
|
||||||
for _, cert := range initData.Certs {
|
for _, cert := range initData.Certs {
|
||||||
initDefinedCert(cert)
|
initDefinedCert(cert)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ type Ldap struct {
|
|||||||
Host string `xorm:"varchar(100)" json:"host"`
|
Host string `xorm:"varchar(100)" json:"host"`
|
||||||
Port int `xorm:"int" json:"port"`
|
Port int `xorm:"int" json:"port"`
|
||||||
EnableSsl bool `xorm:"bool" json:"enableSsl"`
|
EnableSsl bool `xorm:"bool" json:"enableSsl"`
|
||||||
|
AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
|
||||||
Username string `xorm:"varchar(100)" json:"username"`
|
Username string `xorm:"varchar(100)" json:"username"`
|
||||||
Password string `xorm:"varchar(100)" json:"password"`
|
Password string `xorm:"varchar(100)" json:"password"`
|
||||||
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
|
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
|
||||||
@ -150,7 +151,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
||||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").Update(ldap)
|
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert").Update(ldap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -106,6 +106,12 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
existed, failed, err := SyncLdapUsers(ldap.Owner, AutoAdjustLdapUser(users), ldap.Id)
|
existed, failed, err := SyncLdapUsers(ldap.Owner, AutoAdjustLdapUser(users), ldap.Id)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if len(failed) != 0 {
|
if len(failed) != 0 {
|
||||||
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
|
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
|
||||||
logs.Warning(err.Error())
|
logs.Warning(err.Error())
|
||||||
|
@ -16,6 +16,7 @@ package object
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -64,8 +65,11 @@ type LdapUser struct {
|
|||||||
|
|
||||||
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
||||||
var conn *goldap.Conn
|
var conn *goldap.Conn
|
||||||
|
tlsConfig := tls.Config{
|
||||||
|
InsecureSkipVerify: ldap.AllowSelfSignedCert,
|
||||||
|
}
|
||||||
if ldap.EnableSsl {
|
if ldap.EnableSsl {
|
||||||
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), nil)
|
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), &tlsConfig)
|
||||||
} else {
|
} else {
|
||||||
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
|
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,8 @@ func (mfa *SmsMfa) Enable(user *User) error {
|
|||||||
columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
|
columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
|
||||||
} else if mfa.MfaType == EmailType {
|
} else if mfa.MfaType == EmailType {
|
||||||
user.MfaEmailEnabled = true
|
user.MfaEmailEnabled = true
|
||||||
columns = append(columns, "mfa_email_enabled", "email")
|
user.EmailVerified = true
|
||||||
|
columns = append(columns, "mfa_email_enabled", "email", "email_verified")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := UpdateUser(user.GetId(), user, columns, false)
|
_, err := UpdateUser(user.GetId(), user, columns, false)
|
||||||
|
@ -30,6 +30,7 @@ type OidcDiscovery struct {
|
|||||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
TokenEndpoint string `json:"token_endpoint"`
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||||
|
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
|
||||||
JwksUri string `json:"jwks_uri"`
|
JwksUri string `json:"jwks_uri"`
|
||||||
IntrospectionEndpoint string `json:"introspection_endpoint"`
|
IntrospectionEndpoint string `json:"introspection_endpoint"`
|
||||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
@ -77,6 +78,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
|||||||
return origin, origin
|
return origin, origin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDev := conf.GetConfigString("runmode") == "dev"
|
||||||
// "door.casdoor.com"
|
// "door.casdoor.com"
|
||||||
protocol := "https://"
|
protocol := "https://"
|
||||||
if !strings.Contains(host, ".") {
|
if !strings.Contains(host, ".") {
|
||||||
@ -87,7 +89,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
|||||||
protocol = "http://"
|
protocol = "http://"
|
||||||
}
|
}
|
||||||
|
|
||||||
if host == "localhost:8000" {
|
if host == "localhost:8000" && isDev {
|
||||||
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
|
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
|
||||||
} else {
|
} else {
|
||||||
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
|
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
|
||||||
@ -118,6 +120,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
|||||||
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
|
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
|
||||||
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
|
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
|
||||||
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
|
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
|
||||||
|
DeviceAuthorizationEndpoint: fmt.Sprintf("%s/api/device-auth", originBackend),
|
||||||
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
|
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
|
||||||
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
|
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
|
||||||
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
|
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
|
||||||
@ -137,7 +140,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
|||||||
|
|
||||||
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
|
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
|
||||||
jwks := jose.JSONWebKeySet{}
|
jwks := jose.JSONWebKeySet{}
|
||||||
certs, err := GetCerts("admin")
|
certs, err := GetCerts("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jwks, err
|
return jwks, err
|
||||||
}
|
}
|
||||||
@ -212,3 +215,14 @@ func GetWebFinger(resource string, rels []string, host string) (WebFinger, error
|
|||||||
|
|
||||||
return wf, nil
|
return wf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDeviceAuthResponse(deviceCode string, userCode string, host string) DeviceAuthResponse {
|
||||||
|
originFrontend, _ := getOriginFromHost(host)
|
||||||
|
|
||||||
|
return DeviceAuthResponse{
|
||||||
|
DeviceCode: deviceCode,
|
||||||
|
UserCode: userCode,
|
||||||
|
VerificationUri: fmt.Sprintf("%s/login/oauth/device/%s", originFrontend, userCode),
|
||||||
|
ExpiresIn: 120,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -63,7 +63,7 @@ type Organization struct {
|
|||||||
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
||||||
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
||||||
PasswordExpireDays int `json:"passwordExpireDays"`
|
PasswordExpireDays int `json:"passwordExpireDays"`
|
||||||
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
CountryCodes []string `xorm:"mediumtext" json:"countryCodes"`
|
||||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||||
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
|
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
|
||||||
@ -80,7 +80,8 @@ type Organization struct {
|
|||||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||||
EnableTour bool `json:"enableTour"`
|
EnableTour bool `json:"enableTour"`
|
||||||
IpRestriction string `json:"ipRestriction"`
|
IpRestriction string `json:"ipRestriction"`
|
||||||
NavItems []string `xorm:"varchar(500)" json:"navItems"`
|
NavItems []string `xorm:"varchar(1000)" json:"navItems"`
|
||||||
|
WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"`
|
||||||
|
|
||||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||||
@ -227,6 +228,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
|
|||||||
|
|
||||||
if !isGlobalAdmin {
|
if !isGlobalAdmin {
|
||||||
organization.NavItems = org.NavItems
|
organization.NavItems = org.NavItems
|
||||||
|
organization.WidgetItems = org.WidgetItems
|
||||||
}
|
}
|
||||||
|
|
||||||
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||||
|
@ -157,7 +157,7 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) (*Ormer
|
|||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAdapterFromdb is the constructor for Ormer.
|
// NewAdapterFromDb is the constructor for Ormer.
|
||||||
func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, db *sql.DB) (*Ormer, error) {
|
func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, db *sql.DB) (*Ormer, error) {
|
||||||
a := &Ormer{}
|
a := &Ormer{}
|
||||||
a.driverName = driverName
|
a.driverName = driverName
|
||||||
|
@ -148,7 +148,7 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if permission.ResourceType == "Application" && permission.Model != "" {
|
if permission.ResourceType == "Application" && permission.Model != "" {
|
||||||
model, err := GetModelEx(util.GetId(owner, permission.Model))
|
model, err := GetModelEx(util.GetId(permission.Owner, permission.Model))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if model == nil {
|
} else if model == nil {
|
||||||
|
@ -48,6 +48,7 @@ type Provider struct {
|
|||||||
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
||||||
Scopes string `xorm:"varchar(100)" json:"scopes"`
|
Scopes string `xorm:"varchar(100)" json:"scopes"`
|
||||||
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"`
|
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"`
|
||||||
|
HttpHeaders map[string]string `xorm:"varchar(500)" json:"httpHeaders"`
|
||||||
|
|
||||||
Host string `xorm:"varchar(100)" json:"host"`
|
Host string `xorm:"varchar(100)" json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
@ -384,6 +385,44 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFaceIdProviderByOwnerName(applicationId, lang string) (*Provider, error) {
|
||||||
|
owner, name := util.GetOwnerAndNameFromId(applicationId)
|
||||||
|
provider := Provider{Owner: owner, Name: name, Category: "Face ID"}
|
||||||
|
existed, err := ormer.Engine.Get(&provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !existed {
|
||||||
|
return nil, fmt.Errorf(i18n.Translate(lang, "provider:the provider: %s does not exist"), applicationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFaceIdProviderByApplication(applicationId, isCurrentProvider, lang string) (*Provider, error) {
|
||||||
|
if isCurrentProvider == "true" {
|
||||||
|
return GetFaceIdProviderByOwnerName(applicationId, lang)
|
||||||
|
}
|
||||||
|
application, err := GetApplication(applicationId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if application == nil || len(application.Providers) == 0 {
|
||||||
|
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
|
||||||
|
}
|
||||||
|
for _, provider := range application.Providers {
|
||||||
|
if provider.Provider == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if provider.Provider.Category == "Face ID" {
|
||||||
|
return GetFaceIdProviderByOwnerName(util.GetId(provider.Provider.Owner, provider.Provider.Name), lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func providerChangeTrigger(oldName string, newName string) error {
|
func providerChangeTrigger(oldName string, newName string) error {
|
||||||
session := ormer.Engine.NewSession()
|
session := ormer.Engine.NewSession()
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
@ -263,6 +263,27 @@ func addWebhookRecord(webhook *Webhook, record *casvisorsdk.Record, statusCode i
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterRecordObject(object string, objectFields []string) string {
|
||||||
|
var rawObject map[string]interface{}
|
||||||
|
_ = json.Unmarshal([]byte(object), &rawObject)
|
||||||
|
|
||||||
|
if rawObject == nil {
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredObject := make(map[string]interface{})
|
||||||
|
|
||||||
|
for _, field := range objectFields {
|
||||||
|
fieldValue, ok := rawObject[field]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredObject[field] = fieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.StructToJson(filteredObject)
|
||||||
|
}
|
||||||
|
|
||||||
func SendWebhooks(record *casvisorsdk.Record) error {
|
func SendWebhooks(record *casvisorsdk.Record) error {
|
||||||
webhooks, err := getWebhooksByOrganization("")
|
webhooks, err := getWebhooksByOrganization("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -271,7 +292,14 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
|||||||
|
|
||||||
errs := []error{}
|
errs := []error{}
|
||||||
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
||||||
|
|
||||||
|
record2 := *record
|
||||||
for _, webhook := range webhooks {
|
for _, webhook := range webhooks {
|
||||||
|
|
||||||
|
if len(webhook.ObjectFields) != 0 && webhook.ObjectFields[0] != "All" {
|
||||||
|
record2.Object = filterRecordObject(record.Object, webhook.ObjectFields)
|
||||||
|
}
|
||||||
|
|
||||||
var user *User
|
var user *User
|
||||||
if webhook.IsUserExtended {
|
if webhook.IsUserExtended {
|
||||||
user, err = getUser(record.Organization, record.User)
|
user, err = getUser(record.Organization, record.User)
|
||||||
@ -287,12 +315,12 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusCode, respBody, err := sendWebhook(webhook, record, user)
|
statusCode, respBody, err := sendWebhook(webhook, &record2, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addWebhookRecord(webhook, record, statusCode, respBody, err)
|
err = addWebhookRecord(webhook, &record2, statusCode, respBody, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
saml "github.com/russellhaering/gosaml2"
|
saml "github.com/russellhaering/gosaml2"
|
||||||
dsig "github.com/russellhaering/goxmldsig"
|
dsig "github.com/russellhaering/goxmldsig"
|
||||||
|
@ -21,7 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/i18n"
|
"github.com/casdoor/casdoor/i18n"
|
||||||
@ -37,6 +38,8 @@ const (
|
|||||||
EndpointError = "endpoint_error"
|
EndpointError = "endpoint_error"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var DeviceAuthMap = sync.Map{}
|
||||||
|
|
||||||
type Code struct {
|
type Code struct {
|
||||||
Message string `xorm:"varchar(100)" json:"message"`
|
Message string `xorm:"varchar(100)" json:"message"`
|
||||||
Code string `xorm:"varchar(100)" json:"code"`
|
Code string `xorm:"varchar(100)" json:"code"`
|
||||||
@ -71,6 +74,22 @@ type IntrospectionResponse struct {
|
|||||||
Jti string `json:"jti,omitempty"`
|
Jti string `json:"jti,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeviceAuthCache struct {
|
||||||
|
UserSignIn bool
|
||||||
|
UserName string
|
||||||
|
ApplicationId string
|
||||||
|
Scope string
|
||||||
|
RequestAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceAuthResponse struct {
|
||||||
|
DeviceCode string `json:"device_code"`
|
||||||
|
UserCode string `json:"user_code"`
|
||||||
|
VerificationUri string `json:"verification_uri"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Interval int `json:"interval"`
|
||||||
|
}
|
||||||
|
|
||||||
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
|
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
|
||||||
token, err := GetTokenByAccessToken(accessToken)
|
token, err := GetTokenByAccessToken(accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -222,6 +241,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
|||||||
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||||
case "token", "id_token": // Implicit Grant
|
case "token", "id_token": // Implicit Grant
|
||||||
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||||
|
case "urn:ietf:params:oauth:grant-type:device_code":
|
||||||
|
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
|
||||||
case "refresh_token":
|
case "refresh_token":
|
||||||
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -19,7 +19,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClaimsStandard struct {
|
type ClaimsStandard struct {
|
||||||
|
@ -15,13 +15,17 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
|
"github.com/casdoor/casdoor/faceId"
|
||||||
|
"github.com/casdoor/casdoor/proxy"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/xorm-io/builder"
|
"github.com/xorm-io/builder"
|
||||||
@ -48,7 +52,7 @@ func InitUserManager() {
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(255) notnull pk" json:"name"`
|
||||||
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
|
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
|
||||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||||
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
|
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
|
||||||
@ -244,6 +248,7 @@ type MfaAccount struct {
|
|||||||
type FaceId struct {
|
type FaceId struct {
|
||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||||
FaceIdData []float64 `json:"faceIdData"`
|
FaceIdData []float64 `json:"faceIdData"`
|
||||||
|
ImageUrl string `json:"ImageUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
|
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
|
||||||
@ -454,6 +459,31 @@ func GetUserByEmail(owner string, email string) (*User, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserByWebauthID(webauthId string) (*User, error) {
|
||||||
|
user := User{}
|
||||||
|
existed := false
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if ormer.driverName == "postgres" {
|
||||||
|
existed, err = ormer.Engine.Where(builder.Like{"\"webauthnCredentials\"", webauthId}).Get(&user)
|
||||||
|
} else if ormer.driverName == "mssql" {
|
||||||
|
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS VARCHAR(MAX)) like ?", "%"+webauthId+"%").Get(&user)
|
||||||
|
} else if ormer.driverName == "sqlite" {
|
||||||
|
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS text) like ?", "%"+webauthId+"%").Get(&user)
|
||||||
|
} else {
|
||||||
|
existed, err = ormer.Engine.Where("webauthnCredentials like ?", "%"+webauthId+"%").Get(&user)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !existed {
|
||||||
|
return nil, fmt.Errorf("user not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, err
|
||||||
|
}
|
||||||
|
|
||||||
func GetUserByEmailOnly(email string) (*User, error) {
|
func GetUserByEmailOnly(email string) (*User, error) {
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -807,6 +837,10 @@ func AddUser(user *User) (bool, error) {
|
|||||||
return false, fmt.Errorf("the user's owner and name should not be empty")
|
return false, fmt.Errorf("the user's owner and name should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CheckUsername(user.Name, "en") != "" {
|
||||||
|
user.Name = util.GetRandomName()
|
||||||
|
}
|
||||||
|
|
||||||
organization, err := GetOrganizationByUser(user)
|
organization, err := GetOrganizationByUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -815,6 +849,16 @@ func AddUser(user *User) (bool, error) {
|
|||||||
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
|
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Owner != "built-in" {
|
||||||
|
applicationCount, err := GetOrganizationApplicationCount(organization.Owner, organization.Name, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if applicationCount == 0 {
|
||||||
|
return false, fmt.Errorf("The organization: %s should have one application at least", organization.Owner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if organization.DefaultPassword != "" && user.Password == "123" {
|
if organization.DefaultPassword != "" && user.Password == "123" {
|
||||||
user.Password = organization.DefaultPassword
|
user.Password = organization.DefaultPassword
|
||||||
}
|
}
|
||||||
@ -1179,6 +1223,40 @@ func (user *User) IsGlobalAdmin() bool {
|
|||||||
return user.Owner == "built-in"
|
return user.Owner == "built-in"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) CheckUserFace(faceIdImage []string, provider *Provider) (bool, error) {
|
||||||
|
faceIdChecker := faceId.GetFaceIdProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Endpoint)
|
||||||
|
httpClient := proxy.DefaultHttpClient
|
||||||
|
errList := []error{}
|
||||||
|
for _, userFaceId := range user.FaceIds {
|
||||||
|
if userFaceId.ImageUrl != "" {
|
||||||
|
imgResp, err := httpClient.Get(userFaceId.ImageUrl)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imgByte, err := io.ReadAll(imgResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Img := base64.StdEncoding.EncodeToString(imgByte)
|
||||||
|
for _, imgBase64 := range faceIdImage {
|
||||||
|
isSuccess, err := faceIdChecker.Check(imgBase64, base64Img)
|
||||||
|
if err != nil {
|
||||||
|
errList = append(errList, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isSuccess {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errList) > 0 {
|
||||||
|
return false, errList[0]
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GenerateIdForNewUser(application *Application) (string, error) {
|
func GenerateIdForNewUser(application *Application) (string, error) {
|
||||||
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
|
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
|
||||||
return util.GenerateId(), nil
|
return util.GenerateId(), nil
|
||||||
|
@ -86,9 +86,9 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
|
|||||||
title := provider.Title
|
title := provider.Title
|
||||||
|
|
||||||
code := getRandomCode(6)
|
code := getRandomCode(6)
|
||||||
if organization.MasterVerificationCode != "" {
|
// if organization.MasterVerificationCode != "" {
|
||||||
code = organization.MasterVerificationCode
|
// code = organization.MasterVerificationCode
|
||||||
}
|
// }
|
||||||
|
|
||||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||||
content := strings.Replace(provider.Content, "%s", code, 1)
|
content := strings.Replace(provider.Content, "%s", code, 1)
|
||||||
@ -124,9 +124,9 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
|
|||||||
}
|
}
|
||||||
|
|
||||||
code := getRandomCode(6)
|
code := getRandomCode(6)
|
||||||
if organization.MasterVerificationCode != "" {
|
// if organization.MasterVerificationCode != "" {
|
||||||
code = organization.MasterVerificationCode
|
// code = organization.MasterVerificationCode
|
||||||
}
|
// }
|
||||||
|
|
||||||
err = SendSms(provider, code, dest)
|
err = SendSms(provider, code, dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -38,6 +38,8 @@ type Webhook struct {
|
|||||||
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
||||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||||
Events []string `xorm:"varchar(1000)" json:"events"`
|
Events []string `xorm:"varchar(1000)" json:"events"`
|
||||||
|
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||||
|
ObjectFields []string `xorm:"varchar(1000)" json:"objectFields"`
|
||||||
IsUserExtended bool `json:"isUserExtended"`
|
IsUserExtended bool `json:"isUserExtended"`
|
||||||
SingleOrgOnly bool `json:"singleOrgOnly"`
|
SingleOrgOnly bool `json:"singleOrgOnly"`
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
|
@ -17,6 +17,7 @@ package object
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
@ -25,7 +26,32 @@ import (
|
|||||||
|
|
||||||
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
|
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
userMap := make(map[string]interface{})
|
||||||
|
var body io.Reader
|
||||||
|
|
||||||
|
if webhook.TokenFields != nil && len(webhook.TokenFields) > 0 && extendedUser != nil {
|
||||||
|
userValue := reflect.ValueOf(extendedUser).Elem()
|
||||||
|
|
||||||
|
for _, field := range webhook.TokenFields {
|
||||||
|
userField := userValue.FieldByName(field)
|
||||||
|
if userField.IsValid() {
|
||||||
|
newfield := util.SnakeToCamel(util.CamelToSnakeCase(field))
|
||||||
|
userMap[newfield] = userField.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordEx struct {
|
||||||
|
casvisorsdk.Record
|
||||||
|
ExtendedUser map[string]interface{} `json:"extendedUser"`
|
||||||
|
}
|
||||||
|
|
||||||
|
recordEx := &RecordEx{
|
||||||
|
Record: *record,
|
||||||
|
ExtendedUser: userMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
body = strings.NewReader(util.StructToJson(recordEx))
|
||||||
|
} else {
|
||||||
type RecordEx struct {
|
type RecordEx struct {
|
||||||
casvisorsdk.Record
|
casvisorsdk.Record
|
||||||
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
||||||
@ -35,7 +61,8 @@ func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *Use
|
|||||||
ExtendedUser: extendedUser,
|
ExtendedUser: extendedUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
body := strings.NewReader(util.StructToJson(recordEx))
|
body = strings.NewReader(util.StructToJson(recordEx))
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
|
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@ -180,7 +181,11 @@ func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(body)
|
b, _ := json.Marshal(body)
|
||||||
req, _ := http.NewRequest(method, url, bytes.NewBuffer(b))
|
var reqBody io.Reader
|
||||||
|
if method != "GET" {
|
||||||
|
reqBody = bytes.NewBuffer(b)
|
||||||
|
}
|
||||||
|
req, _ := http.NewRequest(method, url, reqBody)
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
|
@ -66,6 +66,7 @@ func initAPI() {
|
|||||||
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
|
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
|
||||||
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
|
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
|
||||||
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
|
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
|
||||||
|
beego.Router("/api/device-auth", &controllers.ApiController{}, "POST:DeviceAuth")
|
||||||
|
|
||||||
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
|
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
|
||||||
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
|
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
|
||||||
|
@ -106,11 +106,14 @@ func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
organizationThemeCookie := &OrganizationThemeCookie{
|
organizationThemeCookie := &OrganizationThemeCookie{
|
||||||
application.ThemeData,
|
ThemeData: application.ThemeData,
|
||||||
application.Logo,
|
LogoUrl: application.Logo,
|
||||||
application.FooterHtml,
|
FooterHtml: application.FooterHtml,
|
||||||
organization.Favicon,
|
}
|
||||||
organization.DisplayName,
|
|
||||||
|
if organization != nil {
|
||||||
|
organizationThemeCookie.Favicon = organization.Favicon
|
||||||
|
organizationThemeCookie.DisplayName = organization.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
return organizationThemeCookie, nil
|
return organizationThemeCookie, nil
|
||||||
|
@ -3,13 +3,16 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/cssinjs": "^1.10.1",
|
"@ant-design/cssinjs": "^1.23.0",
|
||||||
"@ant-design/icons": "^4.7.0",
|
"@ant-design/icons": "^5.6.1",
|
||||||
"@craco/craco": "^6.4.5",
|
"@craco/craco": "^6.4.5",
|
||||||
"@crowdin/cli": "^3.7.10",
|
"@crowdin/cli": "^3.7.10",
|
||||||
"@ctrl/tinycolor": "^3.5.0",
|
"@ctrl/tinycolor": "^3.5.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@metamask/eth-sig-util": "^6.0.0",
|
"@metamask/eth-sig-util": "^6.0.0",
|
||||||
|
"@uiw/codemirror-extensions-langs": "^4.23.8",
|
||||||
|
"@uiw/codemirror-theme-material": "^4.23.8",
|
||||||
|
"@uiw/react-codemirror": "^4.23.8",
|
||||||
"@web3-onboard/coinbase": "^2.2.5",
|
"@web3-onboard/coinbase": "^2.2.5",
|
||||||
"@web3-onboard/core": "^2.20.5",
|
"@web3-onboard/core": "^2.20.5",
|
||||||
"@web3-onboard/frontier": "^2.0.4",
|
"@web3-onboard/frontier": "^2.0.4",
|
||||||
@ -20,10 +23,10 @@
|
|||||||
"@web3-onboard/sequence": "^2.0.8",
|
"@web3-onboard/sequence": "^2.0.8",
|
||||||
"@web3-onboard/taho": "^2.0.5",
|
"@web3-onboard/taho": "^2.0.5",
|
||||||
"@web3-onboard/trust": "^2.0.4",
|
"@web3-onboard/trust": "^2.0.4",
|
||||||
"antd": "5.2.3",
|
"antd": "5.24.1",
|
||||||
"antd-token-previewer": "^1.1.0-22",
|
"antd-token-previewer": "^2.0.8",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"codemirror": "^5.61.1",
|
"codemirror": "^6.0.1",
|
||||||
"copy-to-clipboard": "^3.3.1",
|
"copy-to-clipboard": "^3.3.1",
|
||||||
"core-js": "^3.25.0",
|
"core-js": "^3.25.0",
|
||||||
"craco-less": "^2.0.0",
|
"craco-less": "^2.0.0",
|
||||||
@ -40,7 +43,6 @@
|
|||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-polyfill": "^3.0.0",
|
"react-app-polyfill": "^3.0.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
|
||||||
"react-cropper": "^2.1.7",
|
"react-cropper": "^2.1.7",
|
||||||
"react-device-detect": "^2.2.2",
|
"react-device-detect": "^2.2.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -327,7 +327,7 @@ class App extends Component {
|
|||||||
isAiAssistantOpen: false,
|
isAiAssistantOpen: false,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
visible={this.state.isAiAssistantOpen}
|
open={this.state.isAiAssistantOpen}
|
||||||
>
|
>
|
||||||
<iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
<iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -58,6 +58,16 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.org-select {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(50%);
|
||||||
|
margin: 0 10px !important;
|
||||||
|
float: right;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.rightDropDown {
|
.rightDropDown {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
|
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
|
||||||
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
|
import {CopyOutlined, HolderOutlined, LinkOutlined, UploadOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||||
import * as CertBackend from "./backend/CertBackend";
|
import * as CertBackend from "./backend/CertBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
@ -34,14 +34,9 @@ import PromptPage from "./auth/PromptPage";
|
|||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||||
|
|
||||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
|
||||||
import "codemirror/lib/codemirror.css";
|
|
||||||
import SigninTable from "./table/SigninTable";
|
import SigninTable from "./table/SigninTable";
|
||||||
|
import Editor from "./common/Editor";
|
||||||
require("codemirror/theme/material-darker.css");
|
import * as GroupBackend from "./backend/GroupBackend";
|
||||||
require("codemirror/mode/htmlmixed/htmlmixed");
|
|
||||||
require("codemirror/mode/xml/xml");
|
|
||||||
require("codemirror/mode/css/css");
|
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -58,6 +53,14 @@ const template = `<style>
|
|||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
|
box-shadow: 0 0 30px 20px rgba(255, 255, 255, 0.20);
|
||||||
}
|
}
|
||||||
|
.forget-content {
|
||||||
|
padding: 10px 100px 20px;
|
||||||
|
margin: 30px auto;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 7px;
|
||||||
|
background-color: rgb(255 255 255);
|
||||||
|
box-shadow: 0 0 20px rgb(0 0 0 / 20%);
|
||||||
|
}
|
||||||
</style>`;
|
</style>`;
|
||||||
|
|
||||||
const previewGrid = Setting.isMobile() ? 22 : 11;
|
const previewGrid = Setting.isMobile() ? 22 : 11;
|
||||||
@ -91,11 +94,11 @@ const sideTemplate = `<style>
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="left-model">
|
<div class="left-model">
|
||||||
<span class="side-logo"> <img src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png" alt="Casdoor" style="width: 120px">
|
<span class="side-logo"> <img src="${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png" alt="Casdoor" style="width: 120px">
|
||||||
<span>SSO</span>
|
<span>SSO</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="img">
|
<div class="img">
|
||||||
<img src="https://cdn.casbin.org/img/casbin.svg" alt="Casdoor"/>
|
<img src="${Setting.StaticBaseUrl}/img/casbin.svg" alt="Casdoor"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -122,6 +125,7 @@ class ApplicationEditPage extends React.Component {
|
|||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
this.getApplication();
|
this.getApplication();
|
||||||
this.getOrganizations();
|
this.getOrganizations();
|
||||||
|
this.getGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplication() {
|
getApplication() {
|
||||||
@ -173,6 +177,17 @@ class ApplicationEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGroups() {
|
||||||
|
GroupBackend.getGroups(this.state.organizationName)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === "ok") {
|
||||||
|
this.setState({
|
||||||
|
groups: res.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getCerts(application) {
|
getCerts(application) {
|
||||||
let owner = application.organization;
|
let owner = application.organization;
|
||||||
if (application.isShared) {
|
if (application.isShared) {
|
||||||
@ -403,6 +418,16 @@ class ApplicationEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("application:Forced redirect origin"), i18next.t("general:Forced redirect origin - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input prefix={<LinkOutlined />} value={this.state.application.forcedRedirectOrigin} onChange={e => {
|
||||||
|
this.updateApplicationField("forcedRedirectOrigin", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
|
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
|
||||||
@ -475,6 +500,31 @@ class ApplicationEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("ldap:Default group"), i18next.t("ldap:Default group - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22}>
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.application.defaultGroup ?? []} onChange={(value => {
|
||||||
|
this.updateApplicationField("defaultGroup", value);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Option key={""} value={""}>
|
||||||
|
<Space>
|
||||||
|
{i18next.t("general:Default")}
|
||||||
|
</Space>
|
||||||
|
</Option>
|
||||||
|
{
|
||||||
|
this.state.groups?.map((group) => <Option key={group.name} value={`${group.owner}/${group.name}`}>
|
||||||
|
<Space>
|
||||||
|
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
|
||||||
|
{group.displayName}
|
||||||
|
</Space>
|
||||||
|
</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||||
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
|
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
|
||||||
@ -629,13 +679,9 @@ class ApplicationEditPage extends React.Component {
|
|||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Popover placement="right" content={
|
<Popover placement="right" content={
|
||||||
<div style={{width: "900px", height: "300px"}} >
|
<div style={{width: "900px", height: "300px"}} >
|
||||||
<CodeMirror
|
<Editor value={this.state.application.signupHtml} lang="html" fillHeight dark onChange={value => {
|
||||||
value={this.state.application.signupHtml}
|
|
||||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
|
||||||
onBeforeChange={(editor, data, value) => {
|
|
||||||
this.updateApplicationField("signupHtml", value);
|
this.updateApplicationField("signupHtml", value);
|
||||||
}}
|
}} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
} title={i18next.t("provider:Signup HTML - Edit")} trigger="click">
|
} title={i18next.t("provider:Signup HTML - Edit")} trigger="click">
|
||||||
<Input value={this.state.application.signupHtml} style={{marginBottom: "10px"}} onChange={e => {
|
<Input value={this.state.application.signupHtml} style={{marginBottom: "10px"}} onChange={e => {
|
||||||
@ -651,13 +697,9 @@ class ApplicationEditPage extends React.Component {
|
|||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Popover placement="right" content={
|
<Popover placement="right" content={
|
||||||
<div style={{width: "900px", height: "300px"}} >
|
<div style={{width: "900px", height: "300px"}} >
|
||||||
<CodeMirror
|
<Editor value={this.state.application.signinHtml} lang="html" fillHeight dark onChange={value => {
|
||||||
value={this.state.application.signinHtml}
|
|
||||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
|
||||||
onBeforeChange={(editor, data, value) => {
|
|
||||||
this.updateApplicationField("signinHtml", value);
|
this.updateApplicationField("signinHtml", value);
|
||||||
}}
|
}} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
} title={i18next.t("provider:Signin HTML - Edit")} trigger="click">
|
} title={i18next.t("provider:Signin HTML - Edit")} trigger="click">
|
||||||
<Input value={this.state.application.signinHtml} style={{marginBottom: "10px"}} onChange={e => {
|
<Input value={this.state.application.signinHtml} style={{marginBottom: "10px"}} onChange={e => {
|
||||||
@ -684,6 +726,7 @@ class ApplicationEditPage extends React.Component {
|
|||||||
{id: "token", name: "Token"},
|
{id: "token", name: "Token"},
|
||||||
{id: "id_token", name: "ID Token"},
|
{id: "id_token", name: "ID Token"},
|
||||||
{id: "refresh_token", name: "Refresh Token"},
|
{id: "refresh_token", name: "Refresh Token"},
|
||||||
|
{id: "urn:ietf:params:oauth:grant-type:device_code", name: "Device Code"},
|
||||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||||
}
|
}
|
||||||
</Select>
|
</Select>
|
||||||
@ -758,11 +801,7 @@ class ApplicationEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
|
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22}>
|
<Col span={22}>
|
||||||
<CodeMirror
|
<Editor value={this.state.samlMetadata?.toString() ?? ""} lang="xml" readOnly />
|
||||||
value={this.state.samlMetadata}
|
|
||||||
options={{mode: "xml", theme: "default"}}
|
|
||||||
onBeforeChange={(editor, data, value) => {}}
|
|
||||||
/>
|
|
||||||
<br />
|
<br />
|
||||||
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
||||||
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
|
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&enablePostBinding=${this.state.application.enableSamlPostBinding}`);
|
||||||
@ -822,6 +861,33 @@ class ApplicationEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("application:Background URL Mobile"), i18next.t("application:Background URL Mobile - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} style={(Setting.isMobile()) ? {maxWidth: "100%"} : {}}>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input prefix={<LinkOutlined />} value={this.state.application.formBackgroundUrlMobile} onChange={e => {
|
||||||
|
this.updateApplicationField("formBackgroundUrlMobile", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{i18next.t("general:Preview")}:
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<a target="_blank" rel="noreferrer" href={this.state.application.formBackgroundUrlMobile}>
|
||||||
|
<img src={this.state.application.formBackgroundUrlMobile} alt={this.state.application.formBackgroundUrlMobile} height={90} style={{marginBottom: "20px"}} />
|
||||||
|
</a>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("application:Custom CSS"), i18next.t("application:Custom CSS - Tooltip"))} :
|
{Setting.getLabel(i18next.t("application:Custom CSS"), i18next.t("application:Custom CSS - Tooltip"))} :
|
||||||
@ -829,9 +895,12 @@ class ApplicationEditPage extends React.Component {
|
|||||||
<Col span={22}>
|
<Col span={22}>
|
||||||
<Popover placement="right" content={
|
<Popover placement="right" content={
|
||||||
<div style={{width: "900px", height: "300px"}} >
|
<div style={{width: "900px", height: "300px"}} >
|
||||||
<CodeMirror value={this.state.application.formCss === "" ? template : this.state.application.formCss}
|
<Editor
|
||||||
options={{mode: "css", theme: "material-darker"}}
|
value={this.state.application.formCss === "" ? template : this.state.application.formCss}
|
||||||
onBeforeChange={(editor, data, value) => {
|
lang="css"
|
||||||
|
fillHeight
|
||||||
|
dark
|
||||||
|
onChange={value => {
|
||||||
this.updateApplicationField("formCss", value);
|
this.updateApplicationField("formCss", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -850,9 +919,12 @@ class ApplicationEditPage extends React.Component {
|
|||||||
<Col span={22}>
|
<Col span={22}>
|
||||||
<Popover placement="right" content={
|
<Popover placement="right" content={
|
||||||
<div style={{width: "900px", height: "300px"}} >
|
<div style={{width: "900px", height: "300px"}} >
|
||||||
<CodeMirror value={this.state.application.formCssMobile === "" ? template : this.state.application.formCssMobile}
|
<Editor
|
||||||
options={{mode: "css", theme: "material-darker"}}
|
value={this.state.application.formCssMobile === "" ? template : this.state.application.formCssMobile}
|
||||||
onBeforeChange={(editor, data, value) => {
|
lang="css"
|
||||||
|
fillHeight
|
||||||
|
dark
|
||||||
|
onChange={value => {
|
||||||
this.updateApplicationField("formCssMobile", value);
|
this.updateApplicationField("formCssMobile", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -887,9 +959,12 @@ class ApplicationEditPage extends React.Component {
|
|||||||
<Col span={21} >
|
<Col span={21} >
|
||||||
<Popover placement="right" content={
|
<Popover placement="right" content={
|
||||||
<div style={{width: "900px", height: "300px"}} >
|
<div style={{width: "900px", height: "300px"}} >
|
||||||
<CodeMirror value={this.state.application.formSideHtml === "" ? sideTemplate : this.state.application.formSideHtml}
|
<Editor
|
||||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
value={this.state.application.formSideHtml === "" ? sideTemplate : this.state.application.formSideHtml}
|
||||||
onBeforeChange={(editor, data, value) => {
|
lang="html"
|
||||||
|
fillHeight
|
||||||
|
dark
|
||||||
|
onChange={value => {
|
||||||
this.updateApplicationField("formSideHtml", value);
|
this.updateApplicationField("formSideHtml", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -936,10 +1011,12 @@ class ApplicationEditPage extends React.Component {
|
|||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Popover placement="right" content={
|
<Popover placement="right" content={
|
||||||
<div style={{width: "900px", height: "300px"}} >
|
<div style={{width: "900px", height: "300px"}} >
|
||||||
<CodeMirror
|
<Editor
|
||||||
value={this.state.application.headerHtml}
|
value={this.state.application.headerHtml}
|
||||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
lang="html"
|
||||||
onBeforeChange={(editor, data, value) => {
|
fillHeight
|
||||||
|
dark
|
||||||
|
onChange={value => {
|
||||||
this.updateApplicationField("headerHtml", value);
|
this.updateApplicationField("headerHtml", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -958,10 +1035,12 @@ class ApplicationEditPage extends React.Component {
|
|||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Popover placement="right" content={
|
<Popover placement="right" content={
|
||||||
<div style={{width: "900px", height: "300px"}} >
|
<div style={{width: "900px", height: "300px"}} >
|
||||||
<CodeMirror
|
<Editor
|
||||||
value={this.state.application.footerHtml}
|
value={this.state.application.footerHtml}
|
||||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
lang="html"
|
||||||
onBeforeChange={(editor, data, value) => {
|
fillHeight
|
||||||
|
dark
|
||||||
|
onChange={value => {
|
||||||
this.updateApplicationField("footerHtml", value);
|
this.updateApplicationField("footerHtml", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -73,7 +73,7 @@ class BaseListPage extends React.Component {
|
|||||||
this.fetch({pagination});
|
this.fetch({pagination});
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumnSearchProps = dataIndex => ({
|
getColumnSearchProps = (dataIndex, customRender = null) => ({
|
||||||
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
|
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
|
||||||
<div style={{padding: 8}}>
|
<div style={{padding: 8}}>
|
||||||
<Input
|
<Input
|
||||||
@ -121,13 +121,15 @@ class BaseListPage extends React.Component {
|
|||||||
record[dataIndex]
|
record[dataIndex]
|
||||||
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
|
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
|
||||||
: "",
|
: "",
|
||||||
onFilterDropdownOpenChange: visible => {
|
filterDropdownProps: {
|
||||||
|
onOpenChange: visible => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setTimeout(() => this.searchInput.select(), 100);
|
setTimeout(() => this.searchInput.select(), 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render: text =>
|
},
|
||||||
this.state.searchedColumn === dataIndex ? (
|
render: (text, record, index) => {
|
||||||
|
const highlightContent = this.state.searchedColumn === dataIndex ? (
|
||||||
<Highlighter
|
<Highlighter
|
||||||
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
|
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
|
||||||
searchWords={[this.state.searchText]}
|
searchWords={[this.state.searchText]}
|
||||||
@ -136,7 +138,10 @@ class BaseListPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
text
|
text
|
||||||
),
|
);
|
||||||
|
|
||||||
|
return customRender ? customRender({text, record, index}, highlightContent) : highlightContent;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSearch = (selectedKeys, confirm, dataIndex) => {
|
handleSearch = (selectedKeys, confirm, dataIndex) => {
|
||||||
@ -170,7 +175,7 @@ class BaseListPage extends React.Component {
|
|||||||
const steps = TourConfig.getSteps();
|
const steps = TourConfig.getSteps();
|
||||||
steps.map((item, index) => {
|
steps.map((item, index) => {
|
||||||
if (!index) {
|
if (!index) {
|
||||||
item.target = () => document.querySelector("table");
|
item.target = () => document.querySelector(".ant-table");
|
||||||
} else {
|
} else {
|
||||||
item.target = () => document.getElementById(item.id) || null;
|
item.target = () => document.getElementById(item.id) || null;
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useRef, useState} from "react";
|
import React, {useCallback, useEffect, useRef, useState} from "react";
|
||||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
|
||||||
import "codemirror/lib/codemirror.css";
|
|
||||||
import "codemirror/mode/properties/properties";
|
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import IframeEditor from "./IframeEditor";
|
import IframeEditor from "./IframeEditor";
|
||||||
import {Tabs} from "antd";
|
import {Tabs} from "antd";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import Editor from "./common/Editor";
|
||||||
const {TabPane} = Tabs;
|
|
||||||
|
|
||||||
const CasbinEditor = ({model, onModelTextChange}) => {
|
const CasbinEditor = ({model, onModelTextChange}) => {
|
||||||
const [activeKey, setActiveKey] = useState("advanced");
|
const [activeKey, setActiveKey] = useState("advanced");
|
||||||
@ -68,10 +64,15 @@ const CasbinEditor = ({model, onModelTextChange}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
|
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
|
||||||
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
|
<Tabs
|
||||||
<TabPane tab={i18next.t("model:Basic Editor")} key="basic" />
|
activeKey={activeKey}
|
||||||
<TabPane tab={i18next.t("model:Advanced Editor")} key="advanced" />
|
onChange={handleTabChange}
|
||||||
</Tabs>
|
style={{flex: "0 0 auto", marginTop: "-10px"}}
|
||||||
|
items={[
|
||||||
|
{key: "basic", label: i18next.t("model:Basic Editor")},
|
||||||
|
{key: "advanced", label: i18next.t("model:Advanced Editor")},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
|
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
|
||||||
{activeKey === "advanced" ? (
|
{activeKey === "advanced" ? (
|
||||||
<IframeEditor
|
<IframeEditor
|
||||||
@ -81,11 +82,10 @@ const CasbinEditor = ({model, onModelTextChange}) => {
|
|||||||
style={{width: "100%", height: "100%"}}
|
style={{width: "100%", height: "100%"}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CodeMirror
|
<Editor
|
||||||
value={localModelText}
|
value={localModelText}
|
||||||
className="full-height-editor no-horizontal-scroll-editor"
|
readOnly={Setting.builtInObject(model)}
|
||||||
options={{mode: "properties", theme: "default"}}
|
onChange={value => {
|
||||||
onBeforeChange={(editor, data, value) => {
|
|
||||||
handleModelTextChange(value);
|
handleModelTextChange(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -109,7 +109,7 @@ class EntryPage extends React.Component {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<CustomHead headerHtml={this.state.application?.headerHtml} />
|
<CustomHead headerHtml={this.state.application?.headerHtml} />
|
||||||
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
<div className={`${isDarkMode ? "loginBackgroundDark" : "loginBackground"}`}
|
||||||
style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
|
style={{backgroundImage: Setting.inIframe() ? null : (Setting.isMobile() ? `url(${this.state.application?.formBackgroundUrlMobile})` : `url(${this.state.application?.formBackgroundUrl})`)}}>
|
||||||
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
<Spin size="large" spinning={this.state.application === undefined && this.state.pricing === undefined} tip={i18next.t("login:Loading")}
|
||||||
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
|
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
|
||||||
<Switch>
|
<Switch>
|
||||||
@ -119,6 +119,7 @@ class EntryPage extends React.Component {
|
|||||||
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
|
||||||
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||||
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||||
|
<Route exact path="/login/oauth/device/:userCode" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"device"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||||
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||||
<Route exact path="/forget" render={(props) => <SelfForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
<Route exact path="/forget" render={(props) => <SelfForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||||
<Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
<Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Button, Table} from "antd";
|
import {Button, Table, Tooltip} from "antd";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import * as GroupBackend from "./backend/GroupBackend";
|
import * as GroupBackend from "./backend/GroupBackend";
|
||||||
@ -202,12 +202,16 @@ class GroupListPage extends BaseListPage {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||||
|
{
|
||||||
|
record.haveChildren ? <Tooltip placement="topLeft" title={i18next.t("group:You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page")}>
|
||||||
|
<Button disabled type="primary" danger>{i18next.t("general:Delete")}</Button>
|
||||||
|
</Tooltip> :
|
||||||
<PopconfirmModal
|
<PopconfirmModal
|
||||||
disabled={record.haveChildren}
|
|
||||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||||
onConfirm={() => this.deleteGroup(index)}
|
onConfirm={() => this.deleteGroup(index)}
|
||||||
>
|
>
|
||||||
</PopconfirmModal>
|
</PopconfirmModal>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -114,7 +114,7 @@ class InvitationEditPage extends React.Component {
|
|||||||
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
||||||
defaultApplication = selectedOrganization.defaultApplication;
|
defaultApplication = selectedOrganization.defaultApplication;
|
||||||
if (!defaultApplication) {
|
if (!defaultApplication) {
|
||||||
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
|
Setting.showMessage("error", i18next.t("invitation:You need to first specify a default application for organization: ") + selectedOrganization.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,6 +170,16 @@ class LdapEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
|
{Setting.getLabel(i18next.t("ldap:Allow self-signed certificate"), i18next.t("ldap:Allow self-signed certificate - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={21} >
|
||||||
|
<Switch checked={this.state.ldap.allowSelfSignedCert} onChange={checked => {
|
||||||
|
this.updateLdapField("allowSelfSignedCert", checked);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}}>
|
<Row style={{marginTop: "20px"}}>
|
||||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||||
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} :
|
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} :
|
||||||
|
@ -95,8 +95,9 @@ import TransactionEditPage from "./TransactionEditPage";
|
|||||||
import VerificationListPage from "./VerificationListPage";
|
import VerificationListPage from "./VerificationListPage";
|
||||||
|
|
||||||
function ManagementPage(props) {
|
function ManagementPage(props) {
|
||||||
|
|
||||||
const [menuVisible, setMenuVisible] = useState(false);
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
|
const navItems = props.account?.organization?.navItems;
|
||||||
|
const widgetItems = props.account?.organization?.widgetItems;
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
AuthBackend.logout()
|
AuthBackend.logout()
|
||||||
@ -175,6 +176,35 @@ function ManagementPage(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function navItemsIsAll() {
|
||||||
|
return !Array.isArray(navItems) || !!navItems?.includes("all");
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetItemsIsAll() {
|
||||||
|
return !Array.isArray(widgetItems) || !!widgetItems?.includes("all");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWidgets() {
|
||||||
|
const widgets = [
|
||||||
|
Setting.getItem(<ThemeSelect themeAlgorithm={props.themeAlgorithm} onChange={props.setLogoAndThemeAlgorithm} />, "theme"),
|
||||||
|
Setting.getItem(<LanguageSelect languages={props.account.organization.languages} />, "language"),
|
||||||
|
Setting.getItem(Conf.AiAssistantUrl?.trim() && (
|
||||||
|
<Tooltip title="Click to open AI assistant">
|
||||||
|
<div className="select-box" onClick={props.openAiAssistant}>
|
||||||
|
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
), "ai-assistant"),
|
||||||
|
Setting.getItem(<OpenTour />, "tour"),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (widgetItemsIsAll()) {
|
||||||
|
return widgets.map(item => item.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widgets.filter(item => widgetItems.includes(item.key)).map(item => item.label);
|
||||||
|
}
|
||||||
|
|
||||||
function renderAccountMenu() {
|
function renderAccountMenu() {
|
||||||
if (props.account === undefined) {
|
if (props.account === undefined) {
|
||||||
return null;
|
return null;
|
||||||
@ -188,29 +218,16 @@ function ManagementPage(props) {
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{renderRightDropdown()}
|
{renderRightDropdown()}
|
||||||
<ThemeSelect
|
{renderWidgets()}
|
||||||
themeAlgorithm={props.themeAlgorithm}
|
|
||||||
onChange={props.setLogoAndThemeAlgorithm} />
|
|
||||||
<LanguageSelect languages={props.account.organization.languages} />
|
|
||||||
{
|
|
||||||
Conf.AiAssistantUrl?.trim() && (
|
|
||||||
<Tooltip title="Click to open AI assistant">
|
|
||||||
<div className="select-box" onClick={props.openAiAssistant}>
|
|
||||||
<DeploymentUnitOutlined style={{fontSize: "24px"}} />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<OpenTour />
|
|
||||||
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
||||||
<OrganizationSelect
|
<OrganizationSelect
|
||||||
initValue={Setting.getOrganization()}
|
initValue={Setting.getOrganization()}
|
||||||
withAll={true}
|
withAll={true}
|
||||||
style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
|
className="org-select"
|
||||||
|
style={{display: Setting.isMobile() ? "none" : "flex"}}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
Setting.setOrganization(value);
|
Setting.setOrganization(value);
|
||||||
}}
|
}}
|
||||||
className="select-box"
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@ -323,13 +340,7 @@ function ManagementPage(props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = props.account.organization.navItems;
|
if (navItemsIsAll()) {
|
||||||
|
|
||||||
if (!Array.isArray(navItems)) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navItems.includes("all")) {
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,8 +454,6 @@ function ManagementPage(props) {
|
|||||||
return Setting.isMobile() || window.location.pathname.startsWith("/trees");
|
return Setting.isMobile() || window.location.pathname.startsWith("/trees");
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuStyleRight = Setting.isAdminUser(props.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "320px";
|
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setMenuVisible(false);
|
setMenuVisible(false);
|
||||||
};
|
};
|
||||||
@ -456,10 +465,11 @@ function ManagementPage(props) {
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<EnableMfaNotification account={props.account} />
|
<EnableMfaNotification account={props.account} />
|
||||||
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
|
<Header style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0", marginBottom: "4px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
|
||||||
{props.requiredEnableMfa || (Setting.isMobile() ?
|
{
|
||||||
|
props.requiredEnableMfa || (Setting.isMobile() ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Drawer title={i18next.t("general:Close")} placement="left" visible={menuVisible} onClose={onClose}>
|
<Drawer title={i18next.t("general:Close")} placement="left" open={menuVisible} onClose={onClose}>
|
||||||
<Menu
|
<Menu
|
||||||
items={getMenuItems()}
|
items={getMenuItems()}
|
||||||
mode={"inline"}
|
mode={"inline"}
|
||||||
@ -472,18 +482,23 @@ function ManagementPage(props) {
|
|||||||
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
|
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
|
||||||
{i18next.t("general:Menu")}
|
{i18next.t("general:Menu")}
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment> :
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
// Padding 1px for Menu Item Highlight border
|
||||||
|
<div style={{flex: 1, overflow: "hidden", paddingBottom: "1px"}}>
|
||||||
<Menu
|
<Menu
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
items={getMenuItems()}
|
items={getMenuItems()}
|
||||||
mode={"horizontal"}
|
mode={"horizontal"}
|
||||||
selectedKeys={[props.selectedMenuKey]}
|
selectedKeys={[props.selectedMenuKey]}
|
||||||
style={{position: "absolute", left: 0, right: menuStyleRight, backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
|
style={{backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
{
|
))
|
||||||
renderAccountMenu()
|
|
||||||
}
|
}
|
||||||
|
<div style={{flexShrink: 0}}>
|
||||||
|
{renderAccountMenu()}
|
||||||
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{display: "flex", flexDirection: "column"}} >
|
<Content style={{display: "flex", flexDirection: "column"}} >
|
||||||
{isWithoutCard() ?
|
{isWithoutCard() ?
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Button, Popover, Table} from "antd";
|
import {Button, Popover, Table} from "antd";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@ -22,6 +22,7 @@ import * as ModelBackend from "./backend/ModelBackend";
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import BaseListPage from "./BaseListPage";
|
import BaseListPage from "./BaseListPage";
|
||||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||||
|
import Editor from "./common/Editor";
|
||||||
|
|
||||||
const rbacModel = `[request_definition]
|
const rbacModel = `[request_definition]
|
||||||
r = sub, obj, act
|
r = sub, obj, act
|
||||||
@ -148,11 +149,7 @@ class ModelListPage extends BaseListPage {
|
|||||||
return (
|
return (
|
||||||
<Popover placement="topRight" content={() => {
|
<Popover placement="topRight" content={() => {
|
||||||
return (
|
return (
|
||||||
<CodeMirror
|
<Editor value={text} />
|
||||||
value={text}
|
|
||||||
options={{mode: "properties", theme: "default"}}
|
|
||||||
onBeforeChange={(editor, data, value) => {}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}} title="" trigger="hover">
|
}} title="" trigger="hover">
|
||||||
{
|
{
|
||||||
|
@ -27,6 +27,7 @@ import AccountTable from "./table/AccountTable";
|
|||||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||||
import MfaTable from "./table/MfaTable";
|
import MfaTable from "./table/MfaTable";
|
||||||
import {NavItemTree} from "./common/NavItemTree";
|
import {NavItemTree} from "./common/NavItemTree";
|
||||||
|
import {WidgetItemTree} from "./common/WidgetItemTree";
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -275,7 +276,7 @@ class OrganizationEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
|
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
|
||||||
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))}
|
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id", "pbkdf2-django"].map(item => Setting.getOption(item, item))}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -537,7 +538,7 @@ class OrganizationEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Navbar items"), i18next.t("general:Navbar items - Tooltip"))} :
|
{Setting.getLabel(i18next.t("organization:Navbar items"), i18next.t("organization:Navbar items - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<NavItemTree
|
<NavItemTree
|
||||||
@ -550,6 +551,21 @@ class OrganizationEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("organization:Widget items"), i18next.t("organization:Widget items - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<WidgetItemTree
|
||||||
|
disabled={!Setting.isAdminUser(this.props.account)}
|
||||||
|
checkedKeys={this.state.organization.widgetItems ?? ["all"]}
|
||||||
|
defaultExpandedKeys={["all"]}
|
||||||
|
onCheck={(checked, _) => {
|
||||||
|
this.updateOrganizationField("widgetItems", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
||||||
|
@ -34,7 +34,7 @@ class OrganizationListPage extends BaseListPage {
|
|||||||
favicon: `${Setting.StaticBaseUrl}/img/favicon.png`,
|
favicon: `${Setting.StaticBaseUrl}/img/favicon.png`,
|
||||||
passwordType: "plain",
|
passwordType: "plain",
|
||||||
PasswordSalt: "",
|
PasswordSalt: "",
|
||||||
passwordOptions: [],
|
passwordOptions: ["AtLeast6"],
|
||||||
passwordObfuscatorType: "Plain",
|
passwordObfuscatorType: "Plain",
|
||||||
passwordObfuscatorKey: "",
|
passwordObfuscatorKey: "",
|
||||||
passwordExpireDays: 0,
|
passwordExpireDays: 0,
|
||||||
|
@ -113,8 +113,8 @@ class PermissionListPage extends BaseListPage {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Upload {...props}>
|
<Upload {...props}>
|
||||||
<Button id="upload-button" type="primary" size="small">
|
<Button icon={<UploadOutlined />} id="upload-button" type="primary" size="small">
|
||||||
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
{i18next.t("user:Upload (.xlsx)")}
|
||||||
</Button></Upload>
|
</Button></Upload>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -232,6 +232,15 @@ class PlanEditPage extends React.Component {
|
|||||||
[
|
[
|
||||||
{id: "USD", name: "USD"},
|
{id: "USD", name: "USD"},
|
||||||
{id: "CNY", name: "CNY"},
|
{id: "CNY", name: "CNY"},
|
||||||
|
{id: "EUR", name: "EUR"},
|
||||||
|
{id: "JPY", name: "JPY"},
|
||||||
|
{id: "GBP", name: "GBP"},
|
||||||
|
{id: "AUD", name: "AUD"},
|
||||||
|
{id: "CAD", name: "CAD"},
|
||||||
|
{id: "CHF", name: "CHF"},
|
||||||
|
{id: "HKD", name: "HKD"},
|
||||||
|
{id: "SGD", name: "SGD"},
|
||||||
|
{id: "BRL", name: "BRL"},
|
||||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||||
}
|
}
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -139,6 +139,8 @@ class ProductBuyPage extends React.Component {
|
|||||||
return "HK$";
|
return "HK$";
|
||||||
} else if (product?.currency === "SGD") {
|
} else if (product?.currency === "SGD") {
|
||||||
return "S$";
|
return "S$";
|
||||||
|
} else if (product?.currency === "BRL") {
|
||||||
|
return "R$";
|
||||||
} else {
|
} else {
|
||||||
return "(Unknown currency)";
|
return "(Unknown currency)";
|
||||||
}
|
}
|
||||||
|
@ -217,6 +217,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{id: "CHF", name: "CHF"},
|
{id: "CHF", name: "CHF"},
|
||||||
{id: "HKD", name: "HKD"},
|
{id: "HKD", name: "HKD"},
|
||||||
{id: "SGD", name: "SGD"},
|
{id: "SGD", name: "SGD"},
|
||||||
|
{id: "BRL", name: "BRL"},
|
||||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||||
}
|
}
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -28,14 +28,8 @@ import copy from "copy-to-clipboard";
|
|||||||
import {CaptchaPreview} from "./common/CaptchaPreview";
|
import {CaptchaPreview} from "./common/CaptchaPreview";
|
||||||
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
|
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
|
||||||
import * as Web3Auth from "./auth/Web3Auth";
|
import * as Web3Auth from "./auth/Web3Auth";
|
||||||
|
import Editor from "./common/Editor";
|
||||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
import HttpHeaderTable from "./table/HttpHeaderTable";
|
||||||
import "codemirror/lib/codemirror.css";
|
|
||||||
|
|
||||||
require("codemirror/theme/material-darker.css");
|
|
||||||
require("codemirror/mode/htmlmixed/htmlmixed");
|
|
||||||
require("codemirror/mode/xml/xml");
|
|
||||||
require("codemirror/mode/css/css");
|
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
const {TextArea} = Input;
|
const {TextArea} = Input;
|
||||||
@ -48,6 +42,13 @@ const defaultUserMapping = {
|
|||||||
avatarUrl: "avatarUrl",
|
avatarUrl: "avatarUrl",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultEmailMapping = {
|
||||||
|
fromName: "fromName",
|
||||||
|
toAddress: "toAddress",
|
||||||
|
subject: "subject",
|
||||||
|
content: "content",
|
||||||
|
};
|
||||||
|
|
||||||
class ProviderEditPage extends React.Component {
|
class ProviderEditPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -78,7 +79,16 @@ class ProviderEditPage extends React.Component {
|
|||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const provider = res.data;
|
const provider = res.data;
|
||||||
|
if (provider.type === "Custom HTTP Email") {
|
||||||
|
if (!provider.userMapping) {
|
||||||
|
provider.userMapping = provider.userMapping || defaultEmailMapping;
|
||||||
|
}
|
||||||
|
if (!provider.userMapping?.fromName) {
|
||||||
|
provider.userMapping = defaultEmailMapping;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
provider.userMapping = provider.userMapping || defaultUserMapping;
|
provider.userMapping = provider.userMapping || defaultUserMapping;
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
provider: provider,
|
provider: provider,
|
||||||
});
|
});
|
||||||
@ -152,10 +162,17 @@ class ProviderEditPage extends React.Component {
|
|||||||
const requiredKeys = ["id", "username", "displayName"];
|
const requiredKeys = ["id", "username", "displayName"];
|
||||||
const provider = this.state.provider;
|
const provider = this.state.provider;
|
||||||
|
|
||||||
|
if (provider.type === "Custom HTTP Email") {
|
||||||
|
if (value === "") {
|
||||||
|
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (value === "" && requiredKeys.includes(key)) {
|
if (value === "" && requiredKeys.includes(key)) {
|
||||||
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
Setting.showMessage("error", i18next.t("provider:This field is required"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
provider.userMapping[key] = value;
|
provider.userMapping[key] = value;
|
||||||
|
|
||||||
@ -190,6 +207,30 @@ class ProviderEditPage extends React.Component {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderEmailMappingInput() {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"))} :
|
||||||
|
<Input value={this.state.provider.userMapping.fromName} onChange={e => {
|
||||||
|
this.updateUserMappingField("fromName", e.target.value);
|
||||||
|
}} />
|
||||||
|
{Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"))} :
|
||||||
|
<Input value={this.state.provider.userMapping.toAddress} onChange={e => {
|
||||||
|
this.updateUserMappingField("toAddress", e.target.value);
|
||||||
|
}} />
|
||||||
|
{Setting.getLabel(i18next.t("provider:Subject"), i18next.t("provider:Subject - Tooltip"))} :
|
||||||
|
<Input value={this.state.provider.userMapping.subject} onChange={e => {
|
||||||
|
this.updateUserMappingField("subject", e.target.value);
|
||||||
|
}} />
|
||||||
|
{Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} :
|
||||||
|
<Input value={this.state.provider.userMapping.content} onChange={e => {
|
||||||
|
this.updateUserMappingField("content", e.target.value);
|
||||||
|
}} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getClientIdLabel(provider) {
|
getClientIdLabel(provider) {
|
||||||
switch (provider.category) {
|
switch (provider.category) {
|
||||||
case "OAuth":
|
case "OAuth":
|
||||||
@ -295,10 +336,8 @@ class ProviderEditPage extends React.Component {
|
|||||||
default:
|
default:
|
||||||
if (provider.type === "Aliyun Captcha") {
|
if (provider.type === "Aliyun Captcha") {
|
||||||
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip"));
|
||||||
} else if (provider.type === "WeChat Pay") {
|
} else if (provider.type === "WeChat Pay" || provider.type === "CUCloud") {
|
||||||
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:App ID"), i18next.t("provider:App ID - Tooltip"));
|
||||||
} else if (provider.type === "CUCloud") {
|
|
||||||
return Setting.getLabel(i18next.t("provider:Account ID"), i18next.t("provider:Account ID - Tooltip"));
|
|
||||||
} else {
|
} else {
|
||||||
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
return Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"));
|
||||||
}
|
}
|
||||||
@ -396,8 +435,8 @@ class ProviderEditPage extends React.Component {
|
|||||||
text = i18next.t("provider:App Key");
|
text = i18next.t("provider:App Key");
|
||||||
tooltip = i18next.t("provider:App Key - Tooltip");
|
tooltip = i18next.t("provider:App Key - Tooltip");
|
||||||
} else if (provider.type === "CUCloud") {
|
} else if (provider.type === "CUCloud") {
|
||||||
text = i18next.t("provider:Topic name");
|
text = "Topic name";
|
||||||
tooltip = i18next.t("provider:Topic name - Tooltip");
|
tooltip = "Topic name - Tooltip";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -574,6 +613,8 @@ class ProviderEditPage extends React.Component {
|
|||||||
this.updateProviderField("type", "MetaMask");
|
this.updateProviderField("type", "MetaMask");
|
||||||
} else if (value === "Notification") {
|
} else if (value === "Notification") {
|
||||||
this.updateProviderField("type", "Telegram");
|
this.updateProviderField("type", "Telegram");
|
||||||
|
} else if (value === "Face ID") {
|
||||||
|
this.updateProviderField("type", "Alibaba Cloud Facebody");
|
||||||
}
|
}
|
||||||
})}>
|
})}>
|
||||||
{
|
{
|
||||||
@ -587,6 +628,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
{id: "SMS", name: "SMS"},
|
{id: "SMS", name: "SMS"},
|
||||||
{id: "Storage", name: "Storage"},
|
{id: "Storage", name: "Storage"},
|
||||||
{id: "Web3", name: "Web3"},
|
{id: "Web3", name: "Web3"},
|
||||||
|
{id: "Face ID", name: "Face ID"},
|
||||||
]
|
]
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
|
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
|
||||||
@ -777,6 +819,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
(this.state.provider.category === "Web3") ||
|
(this.state.provider.category === "Web3") ||
|
||||||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
|
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
|
||||||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
|
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
|
||||||
|
(this.state.provider.category === "Email" && this.state.provider.type === "Custom HTTP Email") ||
|
||||||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
|
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{
|
{
|
||||||
@ -822,7 +865,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{
|
{
|
||||||
(this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
|
(this.state.provider.type === "WeChat Pay" || this.state.provider.type === "CUCloud") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS")) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{this.getClientSecret2Label(this.state.provider)} :
|
{this.getClientSecret2Label(this.state.provider)} :
|
||||||
@ -908,7 +951,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email", "CUCloud"].includes(this.state.provider.type) ? (
|
{["Face ID", "Storage"].includes(this.state.provider.category) || ["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud"].includes(this.state.provider.type) ? (
|
||||||
<div>
|
<div>
|
||||||
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
{["Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
@ -922,7 +965,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology", "Casdoor", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
|
||||||
@ -934,7 +977,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Local File System", "CUCloud"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Local File System", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{["Casdoor"].includes(this.state.provider.type) ?
|
{["Casdoor"].includes(this.state.provider.type) ?
|
||||||
@ -948,7 +991,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "CUCloud"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
|
||||||
@ -960,7 +1003,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "Synology", "Casdoor", "CUCloud"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Custom HTTP Email", "SendGrid", "Synology", "Casdoor", "CUCloud", "Alibaba Cloud Facebody"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||||
@ -1067,7 +1110,6 @@ class ProviderEditPage extends React.Component {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : this.state.provider.category === "Email" ? (
|
) : this.state.provider.category === "Email" ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{["SendGrid"].includes(this.state.provider.type) ? null : (
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||||
@ -1078,7 +1120,6 @@ class ProviderEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
|
||||||
{["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
|
{["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
@ -1103,6 +1144,66 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
{
|
||||||
|
!["Custom HTTP Email"].includes(this.state.provider.type) ? null : (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
|
||||||
|
this.updateProviderField("method", value);
|
||||||
|
}}>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{id: "GET", name: "GET"},
|
||||||
|
{id: "POST", name: "POST"},
|
||||||
|
{id: "PUT", name: "PUT"},
|
||||||
|
{id: "DELETE", name: "DELETE"},
|
||||||
|
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{
|
||||||
|
this.state.provider.method !== "GET" ? (<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.issuerUrl === "" ? "application/x-www-form-urlencoded" : this.state.provider.issuerUrl} onChange={value => {
|
||||||
|
this.updateProviderField("issuerUrl", value);
|
||||||
|
}}>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{id: "application/json", name: "application/json"},
|
||||||
|
{id: "application/x-www-form-urlencoded", name: "application/x-www-form-urlencoded"},
|
||||||
|
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>) : null
|
||||||
|
}
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<HttpHeaderTable httpHeaders={this.state.provider.httpHeaders} onUpdateTable={(value) => {this.updateProviderField("httpHeaders", value);}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{this.state.provider.method !== "GET" ? <Row style={{marginTop: "20px"}}>
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22}>
|
||||||
|
{this.renderEmailMappingInput()}
|
||||||
|
</Col>
|
||||||
|
</Row> : null}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
|
||||||
@ -1129,10 +1230,12 @@ class ProviderEditPage extends React.Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col span={Setting.isMobile() ? 22 : 11}>
|
<Col span={Setting.isMobile() ? 22 : 11}>
|
||||||
<div style={{height: "300px", margin: "10px"}}>
|
<div style={{height: "300px", margin: "10px"}}>
|
||||||
<CodeMirror
|
<Editor
|
||||||
value={this.state.provider.content}
|
value={this.state.provider.content}
|
||||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
fillHeight
|
||||||
onBeforeChange={(editor, data, value) => {
|
dark
|
||||||
|
lang="html"
|
||||||
|
onChange={value => {
|
||||||
this.updateProviderField("content", value);
|
this.updateProviderField("content", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -1169,7 +1272,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : this.state.provider.category === "SMS" ? (
|
) : ["SMS"].includes(this.state.provider.category) ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
|
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
|
||||||
null :
|
null :
|
||||||
@ -1287,7 +1390,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={16} >
|
<Col span={16} >
|
||||||
<Button type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
|
<Button style={{marginLeft: "10px"}} type="primary" loading={this.state.metadataLoading} onClick={() => {this.fetchSamlMetadata();}}>{i18next.t("general:Request")}</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Switch, Table} from "antd";
|
import {Button, Descriptions, Drawer, Switch, Table, Tooltip} from "antd";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import * as RecordBackend from "./backend/RecordBackend";
|
import * as RecordBackend from "./backend/RecordBackend";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import moment from "moment";
|
|
||||||
import BaseListPage from "./BaseListPage";
|
import BaseListPage from "./BaseListPage";
|
||||||
|
import Editor from "./common/Editor";
|
||||||
|
|
||||||
class RecordListPage extends BaseListPage {
|
class RecordListPage extends BaseListPage {
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
@ -28,21 +28,6 @@ class RecordListPage extends BaseListPage {
|
|||||||
this.fetch({pagination});
|
this.fetch({pagination});
|
||||||
}
|
}
|
||||||
|
|
||||||
newRecord() {
|
|
||||||
return {
|
|
||||||
owner: "built-in",
|
|
||||||
name: "1234",
|
|
||||||
id: "1234",
|
|
||||||
clientIp: "::1",
|
|
||||||
timestamp: moment().format(),
|
|
||||||
organization: "built-in",
|
|
||||||
username: "admin",
|
|
||||||
requestUri: "/api/get-account",
|
|
||||||
action: "login",
|
|
||||||
isTriggered: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTable(records) {
|
renderTable(records) {
|
||||||
let columns = [
|
let columns = [
|
||||||
{
|
{
|
||||||
@ -65,16 +50,13 @@ class RecordListPage extends BaseListPage {
|
|||||||
title: i18next.t("general:Client IP"),
|
title: i18next.t("general:Client IP"),
|
||||||
dataIndex: "clientIp",
|
dataIndex: "clientIp",
|
||||||
key: "clientIp",
|
key: "clientIp",
|
||||||
width: "100px",
|
width: "120px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("clientIp"),
|
...this.getColumnSearchProps("clientIp", (row, highlightContent) => (
|
||||||
render: (text, record, index) => {
|
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${row.text}`}>
|
||||||
return (
|
{highlightContent}
|
||||||
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${text}`}>
|
|
||||||
{text}
|
|
||||||
</a>
|
</a>
|
||||||
);
|
)),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Timestamp"),
|
title: i18next.t("general:Timestamp"),
|
||||||
@ -120,28 +102,28 @@ class RecordListPage extends BaseListPage {
|
|||||||
title: i18next.t("general:Method"),
|
title: i18next.t("general:Method"),
|
||||||
dataIndex: "method",
|
dataIndex: "method",
|
||||||
key: "method",
|
key: "method",
|
||||||
width: "110px",
|
width: "100px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
filterMultiple: false,
|
filterMultiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{text: "GET", value: "GET"},
|
"GET", "HEAD", "POST", "PUT", "DELETE",
|
||||||
{text: "HEAD", value: "HEAD"},
|
"CONNECT", "OPTIONS", "TRACE", "PATCH",
|
||||||
{text: "POST", value: "POST"},
|
].map(el => ({text: el, value: el})),
|
||||||
{text: "PUT", value: "PUT"},
|
|
||||||
{text: "DELETE", value: "DELETE"},
|
|
||||||
{text: "CONNECT", value: "CONNECT"},
|
|
||||||
{text: "OPTIONS", value: "OPTIONS"},
|
|
||||||
{text: "TRACE", value: "TRACE"},
|
|
||||||
{text: "PATCH", value: "PATCH"},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Request URI"),
|
title: i18next.t("general:Request URI"),
|
||||||
dataIndex: "requestUri",
|
dataIndex: "requestUri",
|
||||||
key: "requestUri",
|
key: "requestUri",
|
||||||
// width: "300px",
|
width: "200px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("requestUri"),
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
...this.getColumnSearchProps("requestUri", (row, highlightContent) => (
|
||||||
|
<Tooltip placement="topLeft" title={row.text}>
|
||||||
|
{highlightContent}
|
||||||
|
</Tooltip>
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("user:Language"),
|
title: i18next.t("user:Language"),
|
||||||
@ -155,7 +137,7 @@ class RecordListPage extends BaseListPage {
|
|||||||
title: i18next.t("record:Status code"),
|
title: i18next.t("record:Status code"),
|
||||||
dataIndex: "statusCode",
|
dataIndex: "statusCode",
|
||||||
key: "statusCode",
|
key: "statusCode",
|
||||||
width: "90px",
|
width: "120px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("statusCode"),
|
...this.getColumnSearchProps("statusCode"),
|
||||||
},
|
},
|
||||||
@ -163,16 +145,26 @@ class RecordListPage extends BaseListPage {
|
|||||||
title: i18next.t("record:Response"),
|
title: i18next.t("record:Response"),
|
||||||
dataIndex: "response",
|
dataIndex: "response",
|
||||||
key: "response",
|
key: "response",
|
||||||
width: "90px",
|
width: "220px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("response"),
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
...this.getColumnSearchProps("response", (row, highlightContent) => (
|
||||||
|
<Tooltip placement="topLeft" title={row.text}>
|
||||||
|
{highlightContent}
|
||||||
|
</Tooltip>
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("record:Object"),
|
title: i18next.t("record:Object"),
|
||||||
dataIndex: "object",
|
dataIndex: "object",
|
||||||
key: "object",
|
key: "object",
|
||||||
width: "90px",
|
width: "200px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
...this.getColumnSearchProps("object"),
|
...this.getColumnSearchProps("object"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -191,7 +183,7 @@ class RecordListPage extends BaseListPage {
|
|||||||
title: i18next.t("record:Is triggered"),
|
title: i18next.t("record:Is triggered"),
|
||||||
dataIndex: "isTriggered",
|
dataIndex: "isTriggered",
|
||||||
key: "isTriggered",
|
key: "isTriggered",
|
||||||
width: "140px",
|
width: "120px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@ -204,6 +196,24 @@ class RecordListPage extends BaseListPage {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("general:Action"),
|
||||||
|
dataIndex: "action",
|
||||||
|
key: "action",
|
||||||
|
width: "80px",
|
||||||
|
sorter: true,
|
||||||
|
fixed: "right",
|
||||||
|
render: (text, record, index) => (
|
||||||
|
<Button type="link" onClick={() => {
|
||||||
|
this.setState({
|
||||||
|
detailRecord: record,
|
||||||
|
detailShow: true,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
{i18next.t("general:Detail")}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (Setting.isLocalAdminUser(this.props.account)) {
|
if (Setting.isLocalAdminUser(this.props.account)) {
|
||||||
@ -220,7 +230,7 @@ class RecordListPage extends BaseListPage {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={records} rowKey="id" size="middle" bordered pagination={paginationProps}
|
<Table scroll={{x: "100%"}} columns={columns} dataSource={records} rowKey="id" size="middle" bordered pagination={paginationProps}
|
||||||
title={() => (
|
title={() => (
|
||||||
<div>
|
<div>
|
||||||
{i18next.t("general:Records")}
|
{i18next.t("general:Records")}
|
||||||
@ -229,10 +239,79 @@ class RecordListPage extends BaseListPage {
|
|||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
onChange={this.handleTableChange}
|
onChange={this.handleTableChange}
|
||||||
/>
|
/>
|
||||||
|
{/* TODO: Should be packaged as a component after confirm it run correctly.*/}
|
||||||
|
<Drawer
|
||||||
|
title={i18next.t("general:Detail")}
|
||||||
|
width={Setting.isMobile() ? "100%" : 640}
|
||||||
|
placement="right"
|
||||||
|
destroyOnClose
|
||||||
|
onClose={() => this.setState({detailShow: false})}
|
||||||
|
open={this.state.detailShow}
|
||||||
|
>
|
||||||
|
<Descriptions bordered size="small" column={1} layout={Setting.isMobile() ? "vertical" : "horizontal"} style={{padding: "12px", height: "100%", overflowY: "auto"}}>
|
||||||
|
<Descriptions.Item label={i18next.t("general:ID")}>{this.getDetailField("id")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("general:Client IP")}>{this.getDetailField("clientIp")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("general:Timestamp")}>{this.getDetailField("createdTime")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("general:Organization")}>
|
||||||
|
<Link to={`/organizations/${this.getDetailField("organization")}`}>
|
||||||
|
{this.getDetailField("organization")}
|
||||||
|
</Link>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("general:User")}>
|
||||||
|
<Link to={`/users/${this.getDetailField("organization")}/${this.getDetailField("user")}`}>
|
||||||
|
{this.getDetailField("user")}
|
||||||
|
</Link>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("general:Method")}>{this.getDetailField("method")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("general:Request URI")}>{this.getDetailField("requestUri")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("user:Language")}>{this.getDetailField("language")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("record:Status code")}>{this.getDetailField("statusCode")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("general:Action")}>{this.getDetailField("action")}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("record:Response")}>
|
||||||
|
<Editor
|
||||||
|
value={this.getDetailField("response")}
|
||||||
|
fillHeight
|
||||||
|
fillWidth
|
||||||
|
maxWidth={this.getEditorMaxWidth()}
|
||||||
|
dark
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label={i18next.t("record:Object")}>
|
||||||
|
<Editor
|
||||||
|
value={this.jsonStrFormatter(this.getDetailField("object"))}
|
||||||
|
lang="json"
|
||||||
|
fillHeight
|
||||||
|
fillWidth
|
||||||
|
maxWidth={this.getEditorMaxWidth()}
|
||||||
|
dark
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEditorMaxWidth = () => {
|
||||||
|
return Setting.isMobile() ? window.innerWidth - 60 : 475;
|
||||||
|
};
|
||||||
|
|
||||||
|
jsonStrFormatter = str => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(str), null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getDetailField = dataIndex => {
|
||||||
|
return this.state.detailRecord ? this.state.detailRecord?.[dataIndex] ?? "" : "";
|
||||||
|
};
|
||||||
|
|
||||||
fetch = (params = {}) => {
|
fetch = (params = {}) => {
|
||||||
let field = params.searchedColumn, value = params.searchText;
|
let field = params.searchedColumn, value = params.searchText;
|
||||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||||
@ -255,6 +334,8 @@ class RecordListPage extends BaseListPage {
|
|||||||
},
|
},
|
||||||
searchText: params.searchText,
|
searchText: params.searchText,
|
||||||
searchedColumn: params.searchedColumn,
|
searchedColumn: params.searchedColumn,
|
||||||
|
detailShow: false,
|
||||||
|
detailRecord: null,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (res.data.includes("Please login first")) {
|
if (res.data.includes("Please login first")) {
|
||||||
|
@ -106,8 +106,8 @@ class RoleListPage extends BaseListPage {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Upload {...props}>
|
<Upload {...props}>
|
||||||
<Button type="primary" size="small">
|
<Button icon={<UploadOutlined />} type="primary" size="small">
|
||||||
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
{i18next.t("user:Upload (.xlsx)")}
|
||||||
</Button>
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
);
|
);
|
||||||
|
@ -416,6 +416,12 @@ export const OtherProviderInfo = {
|
|||||||
url: "https://www.cucloud.cn/",
|
url: "https://www.cucloud.cn/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"Face ID": {
|
||||||
|
"Alibaba Cloud Facebody": {
|
||||||
|
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||||
|
url: "https://vision.aliyun.com/facebody",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function initCountries() {
|
export function initCountries() {
|
||||||
@ -1150,6 +1156,10 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Viber", name: "Viber"},
|
{id: "Viber", name: "Viber"},
|
||||||
{id: "CUCloud", name: "CUCloud"},
|
{id: "CUCloud", name: "CUCloud"},
|
||||||
]);
|
]);
|
||||||
|
} else if (category === "Face ID") {
|
||||||
|
return ([
|
||||||
|
{id: "Alibaba Cloud Facebody", name: "Alibaba Cloud Facebody"},
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -1522,7 +1532,7 @@ export function getUserCommonFields() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultFooterContent() {
|
export function getDefaultFooterContent() {
|
||||||
return "Powered by <a target=\"_blank\" href=\"https://casdoor.org\" rel=\"noreferrer\"><img style=\"padding-bottom: 3px\" height=\"20\" alt=\"Casdoor\" src=\"https://cdn.casbin.org/img/casdoor-logo_1185x256.png\"/></a>";
|
return `Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style="padding-bottom: 3px" height="20" alt="Casdoor" src="${StaticBaseUrl}/img/casdoor-logo_1185x256.png"/></a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEmptyFooterContent() {
|
export function getEmptyFooterContent() {
|
||||||
@ -1554,7 +1564,7 @@ export function getDefaultHtmlEmailContent() {
|
|||||||
<div class="email-container">
|
<div class="email-container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h3>Casbin Organization</h3>
|
<h3>Casbin Organization</h3>
|
||||||
<img src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png" alt="Casdoor Logo" width="300">
|
<img src="${StaticBaseUrl}/img/casdoor-logo_1185x256.png" alt="Casdoor Logo" width="300">
|
||||||
</div>
|
</div>
|
||||||
<p><strong>%{user.friendlyName}</strong>, here is your verification code</p>
|
<p><strong>%{user.friendlyName}</strong>, here is your verification code</p>
|
||||||
<p>Use this code for your transaction. It's valid for 5 minutes</p>
|
<p>Use this code for your transaction. It's valid for 5 minutes</p>
|
||||||
@ -1593,6 +1603,8 @@ export function getCurrencyText(product) {
|
|||||||
return i18next.t("currency:HKD");
|
return i18next.t("currency:HKD");
|
||||||
} else if (product?.currency === "SGD") {
|
} else if (product?.currency === "SGD") {
|
||||||
return i18next.t("currency:SGD");
|
return i18next.t("currency:SGD");
|
||||||
|
} else if (product?.currency === "BRL") {
|
||||||
|
return i18next.t("currency:BRL");
|
||||||
} else {
|
} else {
|
||||||
return "(Unknown currency)";
|
return "(Unknown currency)";
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,8 @@ import * as Setting from "./Setting";
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import SyncerTableColumnTable from "./table/SyncerTableColumnTable";
|
import SyncerTableColumnTable from "./table/SyncerTableColumnTable";
|
||||||
|
|
||||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
|
||||||
import "codemirror/lib/codemirror.css";
|
|
||||||
import * as CertBackend from "./backend/CertBackend";
|
import * as CertBackend from "./backend/CertBackend";
|
||||||
require("codemirror/theme/material-darker.css");
|
import Editor from "./common/Editor";
|
||||||
require("codemirror/mode/javascript/javascript");
|
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -512,10 +509,13 @@ class SyncerEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<div style={{width: "100%", height: "300px"}} >
|
<div style={{width: "100%", height: "300px"}} >
|
||||||
<CodeMirror
|
<Editor
|
||||||
value={this.state.syncer.errorText}
|
value={this.state.syncer.errorText}
|
||||||
options={{mode: "javascript", theme: "material-darker"}}
|
fillHeight
|
||||||
onBeforeChange={(editor, data, value) => {
|
readOnly
|
||||||
|
dark
|
||||||
|
lang="js"
|
||||||
|
onChange={value => {
|
||||||
this.updateSyncerField("errorText", value);
|
this.updateSyncerField("errorText", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user