mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-14 16:13:24 +08:00
Compare commits
98 Commits
Author | SHA1 | Date | |
---|---|---|---|
0ad4d82d9c | |||
731daf5204 | |||
b6b77da7cf | |||
8b4637aa3a | |||
87506b84e3 | |||
fed9332246 | |||
33afc52a0b | |||
9035ca365a | |||
b97ae72179 | |||
9190db1099 | |||
1173f75794 | |||
086859d1ce | |||
9afaf5d695 | |||
521f90a603 | |||
4260efcfd0 | |||
d772b0b7a8 | |||
702b390da1 | |||
b15b3b9335 | |||
f8f864c5b9 | |||
90e790f83c | |||
58413246f3 | |||
8f307dd907 | |||
fe42b5e0ba | |||
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
|
||||
WORKDIR /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
|
||||
COPY . .
|
||||
RUN ./build.sh
|
||||
|
@ -47,6 +47,7 @@ p, *, *, GET, /api/get-app-login, *, *
|
||||
p, *, *, POST, /api/logout, *, *
|
||||
p, *, *, GET, /api/logout, *, *
|
||||
p, *, *, POST, /api/callback, *, *
|
||||
p, *, *, POST, /api/device-auth, *, *
|
||||
p, *, *, GET, /api/get-account, *, *
|
||||
p, *, *, GET, /api/userinfo, *, *
|
||||
p, *, *, GET, /api/user, *, *
|
||||
|
@ -31,7 +31,7 @@ radiusServerPort = 1812
|
||||
radiusDefaultOrganization = "built-in"
|
||||
radiusSecret = "secret"
|
||||
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
|
||||
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
logConfig = {"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}
|
||||
initDataNewOnly = false
|
||||
initDataFile = "./init_data.json"
|
||||
frontendBaseDir = "../cc_0"
|
@ -115,7 +115,7 @@ func TestGetConfigLogs(t *testing.T) {
|
||||
description string
|
||||
expected string
|
||||
}{
|
||||
{"Default log config", `{"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
|
||||
{"Default log config", `{"adapter":"file", "filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}`},
|
||||
}
|
||||
|
||||
err := beego.LoadAppConfig("ini", "app.conf")
|
||||
|
@ -32,6 +32,7 @@ const (
|
||||
ResponseTypeIdToken = "id_token"
|
||||
ResponseTypeSaml = "saml"
|
||||
ResponseTypeCas = "cas"
|
||||
ResponseTypeDevice = "device"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
@ -139,6 +140,8 @@ func (c *ApiController) Signup() {
|
||||
invitationName = invitation.Name
|
||||
}
|
||||
|
||||
userEmailVerified := false
|
||||
|
||||
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
|
||||
var checkResult *object.VerifyResult
|
||||
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
|
||||
@ -150,6 +153,8 @@ func (c *ApiController) Signup() {
|
||||
c.ResponseError(checkResult.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
userEmailVerified = true
|
||||
}
|
||||
|
||||
var checkPhone string
|
||||
@ -228,6 +233,7 @@ func (c *ApiController) Signup() {
|
||||
Karma: 0,
|
||||
Invitation: invitationName,
|
||||
InvitationCode: authForm.InvitationCode,
|
||||
EmailVerified: userEmailVerified,
|
||||
}
|
||||
|
||||
if len(organization.Tags) > 0 {
|
||||
@ -249,6 +255,10 @@ func (c *ApiController) Signup() {
|
||||
user.Groups = []string{invitation.SignupGroup}
|
||||
}
|
||||
|
||||
if application.DefaultGroup != "" && user.Groups == nil {
|
||||
user.Groups = []string{application.DefaultGroup}
|
||||
}
|
||||
|
||||
affected, err := object.AddUser(user)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
@ -458,6 +468,10 @@ func (c *ApiController) GetAccount() {
|
||||
return
|
||||
}
|
||||
|
||||
if organization != nil && len(organization.CountryCodes) == 1 && u != nil && u.CountryCode == "" {
|
||||
u.CountryCode = organization.CountryCodes[0]
|
||||
}
|
||||
|
||||
accessToken := c.GetSessionToken()
|
||||
if accessToken == "" {
|
||||
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)
|
||||
|
@ -25,10 +25,12 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/captcha"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/form"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
@ -54,6 +56,11 @@ func tokenToResponse(token *object.Token) *Response {
|
||||
|
||||
// HandleLoggedIn ...
|
||||
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()
|
||||
|
||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||
@ -140,7 +147,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
c.ResponseError(c.T("auth:Challenge method should be S256"))
|
||||
return
|
||||
}
|
||||
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
code, err := object.GetOAuthCode(userId, clientId, form.Provider, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
return
|
||||
@ -163,6 +170,32 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
|
||||
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
|
||||
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
|
||||
if err != nil {
|
||||
@ -236,6 +269,7 @@ func (c *ApiController) GetApplicationLogin() {
|
||||
state := c.Input().Get("state")
|
||||
id := c.Input().Get("id")
|
||||
loginType := c.Input().Get("type")
|
||||
userCode := c.Input().Get("userCode")
|
||||
|
||||
var application *object.Application
|
||||
var msg string
|
||||
@ -262,6 +296,19 @@ func (c *ApiController) GetApplicationLogin() {
|
||||
c.ResponseError(err.Error())
|
||||
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)
|
||||
@ -397,11 +444,27 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
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 {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
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 == "" {
|
||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||
c.ResponseError(err.Error(), nil)
|
||||
@ -461,6 +524,14 @@ func (c *ApiController) Login() {
|
||||
verificationType = "sms"
|
||||
} else {
|
||||
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 {
|
||||
var application *object.Application
|
||||
@ -593,6 +664,9 @@ func (c *ApiController) Login() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if provider == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), authForm.Provider))
|
||||
}
|
||||
|
||||
providerItem := application.GetProviderItem(provider.Name)
|
||||
if !providerItem.IsProviderVisible() {
|
||||
@ -678,10 +752,6 @@ func (c *ApiController) Login() {
|
||||
|
||||
if user != nil && !user.IsDeleted {
|
||||
// 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
|
||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
||||
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.ServeJSON()
|
||||
}
|
||||
@ -1174,3 +1256,75 @@ func (c *ApiController) Callback() {
|
||||
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
|
||||
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()
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@ -78,12 +79,12 @@ func (c *ApiController) GetGroups() {
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
_, ok := groupsHaveChildrenMap[group.Name]
|
||||
_, ok := groupsHaveChildrenMap[group.GetId()]
|
||||
if ok {
|
||||
group.HaveChildren = true
|
||||
}
|
||||
|
||||
parent, ok := groupsHaveChildrenMap[group.ParentId]
|
||||
parent, ok := groupsHaveChildrenMap[fmt.Sprintf("%s/%s", group.Owner, group.ParentId)]
|
||||
if ok {
|
||||
group.ParentName = parent.DisplayName
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ type LdapResp struct {
|
||||
ExistUuids []string `json:"existUuids"`
|
||||
}
|
||||
|
||||
//type LdapRespGroup struct {
|
||||
// type LdapRespGroup struct {
|
||||
// GroupId string
|
||||
// GroupName string
|
||||
//}
|
||||
// }
|
||||
|
||||
type LdapSyncResp struct {
|
||||
Exist []object.LdapUser `json:"exist"`
|
||||
@ -61,18 +61,18 @@ func (c *ApiController) GetLdapUsers() {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
|
||||
//if err != nil {
|
||||
// groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
|
||||
// if err != nil {
|
||||
// c.ResponseError(err.Error())
|
||||
// return
|
||||
//}
|
||||
// }
|
||||
|
||||
//for _, group := range groupsMap {
|
||||
// for _, group := range groupsMap {
|
||||
// resp.Groups = append(resp.Groups, LdapRespGroup{
|
||||
// GroupId: group.GidNumber,
|
||||
// GroupName: group.Cn,
|
||||
// })
|
||||
//}
|
||||
// }
|
||||
|
||||
users, err := conn.GetLdapUsers(ldapServer)
|
||||
if err != nil {
|
||||
@ -269,7 +269,11 @@ func (c *ApiController) SyncLdapUsers() {
|
||||
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{
|
||||
Exist: exist,
|
||||
|
@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@ -170,12 +171,13 @@ func (c *ApiController) GetOAuthToken() {
|
||||
tag := c.Input().Get("tag")
|
||||
avatar := c.Input().Get("avatar")
|
||||
refreshToken := c.Input().Get("refresh_token")
|
||||
deviceCode := c.Input().Get("device_code")
|
||||
|
||||
if clientId == "" && clientSecret == "" {
|
||||
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
|
||||
}
|
||||
|
||||
if len(c.Ctx.Input.RequestBody) != 0 {
|
||||
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
|
||||
var tokenRequest TokenRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
|
||||
@ -219,6 +221,46 @@ 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.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
|
||||
if !deviceAuthCacheCast.UserSignIn {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "authorization_pending",
|
||||
ErrorDescription: "authorization pending",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
|
||||
if deviceAuthCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
|
||||
c.Data["json"] = &object.TokenError{
|
||||
Error: "expired_token",
|
||||
ErrorDescription: "token is expired",
|
||||
}
|
||||
c.SetTokenErrorHttpStatus()
|
||||
c.ServeJSON()
|
||||
c.SetTokenErrorHttpStatus()
|
||||
return
|
||||
}
|
||||
object.DeviceAuthMap.Delete(deviceCode)
|
||||
|
||||
username = deviceAuthCacheCast.UserName
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
|
||||
if err != nil {
|
||||
@ -321,6 +363,11 @@ func (c *ApiController) IntrospectToken() {
|
||||
return
|
||||
}
|
||||
|
||||
respondWithInactiveToken := func() {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||
var token *object.Token
|
||||
if tokenTypeHint != "" {
|
||||
@ -329,7 +376,12 @@ func (c *ApiController) IntrospectToken() {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
if token.ExpiresIn <= 0 {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
@ -340,12 +392,11 @@ func (c *ApiController) IntrospectToken() {
|
||||
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
if err != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
@ -365,28 +416,34 @@ func (c *ApiController) IntrospectToken() {
|
||||
}
|
||||
} else {
|
||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
if err != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
|
||||
introspectionResponse = object.IntrospectionResponse{
|
||||
Active: true,
|
||||
Scope: jwtToken.Scope,
|
||||
ClientId: clientId,
|
||||
Username: jwtToken.Name,
|
||||
TokenType: jwtToken.TokenType,
|
||||
Exp: jwtToken.ExpiresAt.Unix(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
Sub: jwtToken.Subject,
|
||||
Aud: jwtToken.Audience,
|
||||
Iss: jwtToken.Issuer,
|
||||
Jti: jwtToken.ID,
|
||||
Active: true,
|
||||
ClientId: clientId,
|
||||
Exp: jwtToken.ExpiresAt.Unix(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
Sub: jwtToken.Subject,
|
||||
Aud: jwtToken.Audience,
|
||||
Iss: jwtToken.Issuer,
|
||||
Jti: jwtToken.ID,
|
||||
}
|
||||
|
||||
if jwtToken.Scope != "" {
|
||||
introspectionResponse.Scope = jwtToken.Scope
|
||||
}
|
||||
if jwtToken.Name != "" {
|
||||
introspectionResponse.Username = jwtToken.Name
|
||||
}
|
||||
if jwtToken.TokenType != "" {
|
||||
introspectionResponse.TokenType = jwtToken.TokenType
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,13 +453,15 @@ func (c *ApiController) IntrospectToken() {
|
||||
c.ResponseTokenError(err.Error())
|
||||
return
|
||||
}
|
||||
if token == nil {
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
if token == nil || token.ExpiresIn <= 0 {
|
||||
respondWithInactiveToken()
|
||||
return
|
||||
}
|
||||
}
|
||||
introspectionResponse.TokenType = token.TokenType
|
||||
|
||||
if token != nil {
|
||||
introspectionResponse.TokenType = token.TokenType
|
||||
}
|
||||
|
||||
c.Data["json"] = introspectionResponse
|
||||
c.ServeJSON()
|
||||
|
@ -457,10 +457,10 @@ func (c *ApiController) SetPassword() {
|
||||
newPassword := c.Ctx.Request.Form.Get("newPassword")
|
||||
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"))
|
||||
// return
|
||||
//}
|
||||
// }
|
||||
|
||||
if strings.Contains(newPassword, " ") {
|
||||
c.ResponseError(c.T("user:New password cannot contain blank space."))
|
||||
@ -602,7 +602,11 @@ func (c *ApiController) CheckUserPassword() {
|
||||
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 {
|
||||
c.ResponseError(err.Error())
|
||||
} else {
|
||||
|
@ -242,7 +242,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
} else if vform.Method == ResetVerification {
|
||||
user = c.getCurrentUser()
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
mfaProps := user.GetMfaProps(object.EmailType, false)
|
||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
@ -281,7 +281,7 @@ func (c *ApiController) SendVerificationCode() {
|
||||
}
|
||||
}
|
||||
} else if vform.Method == MfaAuthVerification {
|
||||
mfaProps := user.GetPreferredMfaProps(false)
|
||||
mfaProps := user.GetMfaProps(object.SmsType, false)
|
||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||
vform.Dest = mfaProps.Secret
|
||||
}
|
||||
@ -436,7 +436,8 @@ func (c *ApiController) ResetEmailOrPhone() {
|
||||
switch destType {
|
||||
case object.VerifyTypeEmail:
|
||||
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:
|
||||
user.Phone = dest
|
||||
_, err = object.SetUserField(user, "phone", user.Phone)
|
||||
|
@ -16,7 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/casdoor/casdoor/form"
|
||||
@ -118,24 +118,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
||||
return
|
||||
}
|
||||
|
||||
userOwner := c.Input().Get("owner")
|
||||
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)
|
||||
options, sessionData, err := webauthnObj.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -168,20 +151,23 @@ func (c *ApiController) WebAuthnSigninFinish() {
|
||||
return
|
||||
}
|
||||
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||
userId := string(sessionData.UserID)
|
||||
user, err := object.GetUser(userId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
var user *object.User
|
||||
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
|
||||
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
||||
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.SetSessionUsername(userId)
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||
c.SetSessionUsername(user.GetId())
|
||||
util.LogInfo(c.Ctx, "API: [%s] signed in", user.GetId())
|
||||
|
||||
var application *object.Application
|
||||
|
||||
|
@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
} else if passwordType == "argon2id" {
|
||||
return NewArgon2idCredManager()
|
||||
} else if passwordType == "pbkdf2-django" {
|
||||
return NewPbkdf2DjangoCredManager()
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -24,14 +26,24 @@ import (
|
||||
)
|
||||
|
||||
type HttpEmailProvider struct {
|
||||
endpoint string
|
||||
method string
|
||||
endpoint 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{
|
||||
endpoint: endpoint,
|
||||
method: method,
|
||||
endpoint: endpoint,
|
||||
method: method,
|
||||
httpHeaders: httpHeaders,
|
||||
bodyMapping: bodyMapping,
|
||||
contentType: contentType,
|
||||
}
|
||||
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 {
|
||||
var req *http.Request
|
||||
var err error
|
||||
if c.method == "POST" {
|
||||
formValues := url.Values{}
|
||||
formValues.Set("fromName", fromName)
|
||||
formValues.Set("toAddress", toAddress)
|
||||
formValues.Set("subject", subject)
|
||||
formValues.Set("content", content)
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
|
||||
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{}
|
||||
for k, v := range bodyMap {
|
||||
formValues.Add(k, v)
|
||||
}
|
||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", c.contentType)
|
||||
} else if c.method == "GET" {
|
||||
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
||||
if err != nil {
|
||||
@ -58,15 +104,19 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("fromName", fromName)
|
||||
q.Add("toAddress", toAddress)
|
||||
q.Add("subject", subject)
|
||||
q.Add("content", content)
|
||||
q.Add(fromNameField, fromName)
|
||||
q.Add(toAddressField, toAddress)
|
||||
q.Add(subjectField, subject)
|
||||
q.Add(contentField, content)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
} else {
|
||||
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
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
@ -18,13 +18,13 @@ type EmailProvider interface {
|
||||
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" {
|
||||
return NewAzureACSEmailProvider(clientSecret, host)
|
||||
} else if typ == "Custom HTTP Email" {
|
||||
return NewHttpEmailProvider(endpoint, method)
|
||||
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||
} else if typ == "SendGrid" {
|
||||
return NewSendgridEmailProvider(clientSecret)
|
||||
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||
} else {
|
||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
|
||||
}
|
||||
|
@ -17,14 +17,16 @@ package email
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"net/http"
|
||||
|
||||
"github.com/sendgrid/sendgrid-go"
|
||||
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
||||
)
|
||||
|
||||
type SendgridEmailProvider struct {
|
||||
ApiKey string
|
||||
ApiKey string
|
||||
Host string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
type SendgridResponseBody struct {
|
||||
@ -35,23 +37,25 @@ type SendgridResponseBody struct {
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
|
||||
return &SendgridEmailProvider{ApiKey: apiKey}
|
||||
func NewSendgridEmailProvider(apiKey string, host string, endpoint string) *SendgridEmailProvider {
|
||||
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)
|
||||
to := mail.NewEmail("", toAddress)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.StatusCode >= 300 {
|
||||
if resp.StatusCode >= 300 {
|
||||
var responseBody SendgridResponseBody
|
||||
err = json.Unmarshal([]byte(response.Body), &responseBody)
|
||||
err = json.Unmarshal([]byte(resp.Body), &responseBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -61,8 +65,23 @@ func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress str
|
||||
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
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
@ -27,8 +27,7 @@ type SmtpEmailProvider struct {
|
||||
}
|
||||
|
||||
func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool) *SmtpEmailProvider {
|
||||
dialer := &gomail.Dialer{}
|
||||
dialer = gomail.NewDialer(host, port, userName, password)
|
||||
dialer := gomail.NewDialer(host, port, userName, password)
|
||||
if typ == "SUBMAIL" {
|
||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
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"`
|
||||
Affiliation string `json:"affiliation"`
|
||||
IdCard string `json:"idCard"`
|
||||
Language string `json:"language"`
|
||||
Region string `json:"region"`
|
||||
InvitationCode string `json:"invitationCode"`
|
||||
|
||||
@ -67,7 +68,9 @@ type AuthForm struct {
|
||||
Plan string `json:"plan"`
|
||||
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) {
|
||||
|
196
go.mod
196
go.mod
@ -1,10 +1,14 @@
|
||||
module github.com/casdoor/casdoor
|
||||
|
||||
go 1.16
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
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/beego/beego v1.12.12
|
||||
github.com/beevik/etree v1.1.0
|
||||
@ -12,13 +16,12 @@ require (
|
||||
github.com/casdoor/go-sms-sender v0.25.0
|
||||
github.com/casdoor/gomail/v2 v2.1.0
|
||||
github.com/casdoor/ldapserver v1.2.0
|
||||
github.com/casdoor/notify v1.0.0
|
||||
github.com/casdoor/notify v1.0.1
|
||||
github.com/casdoor/oss v1.8.0
|
||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
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/fogleman/gg v1.3.0
|
||||
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-sql-driver/mysql v1.6.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/go-webauthn/webauthn v0.6.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lestrrat-go/jwx v1.2.29
|
||||
@ -46,7 +49,6 @@ require (
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
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/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
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/tealeg/xlsx v1.0.5
|
||||
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/core v0.7.4
|
||||
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/net v0.34.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/text v0.21.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
|
||||
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
||||
maunium.net/go/mautrix v0.16.0
|
||||
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.13.0 // 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 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 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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"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 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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"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 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",
|
||||
"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",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -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 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 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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
||||
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
||||
"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": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
|
||||
"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": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"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 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",
|
||||
"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",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"account": {
|
||||
"Failed to add user": "Gagal menambahkan pengguna",
|
||||
"Get init score failed, error: %w": "Gagal mendapatkan nilai init, kesalahan: %w",
|
||||
"Get init score failed, error: %w": "Gagal mendapatkan nilai inisiasi, kesalahan: %w",
|
||||
"Please sign out first": "Silakan keluar terlebih dahulu",
|
||||
"The application does not allow to sign up new account": "Aplikasi tidak memperbolehkan untuk mendaftar akun baru"
|
||||
"The application does not allow to sign up new account": "Aplikasi tidak memperbolehkan pendaftaran akun baru"
|
||||
},
|
||||
"auth": {
|
||||
"Challenge method should be S256": "Metode tantangan harus S256",
|
||||
@ -13,17 +13,17 @@
|
||||
"State expected: %s, but got: %s": "Diharapkan: %s, tapi diperoleh: %s",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru melalui %%s, silakan gunakan cara lain untuk mendaftar",
|
||||
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "Akun untuk penyedia: %s dan nama pengguna: %s (%s) tidak ada dan tidak diizinkan untuk mendaftar sebagai akun baru, silakan hubungi dukungan IT Anda",
|
||||
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Akun untuk provider: %s dan username: %s (%s) sudah terhubung dengan akun lain: %s (%s)",
|
||||
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "Akun untuk penyedia: %s dan username: %s (%s) sudah terhubung dengan akun lain: %s (%s)",
|
||||
"The application: %s does not exist": "Aplikasi: %s tidak ada",
|
||||
"The login method: login with LDAP is not enabled for the application": "The login method: login with LDAP is not enabled for the application",
|
||||
"The login method: login with SMS is not enabled for the application": "The login method: login with SMS is not enabled for the application",
|
||||
"The login method: login with email is not enabled for the application": "The login method: login with email is not enabled for the application",
|
||||
"The login method: login with face is not enabled for the application": "The login method: login with face is not enabled for the application",
|
||||
"The login method: login with password is not enabled for the application": "Metode login: login dengan kata sandi tidak diaktifkan untuk aplikasi tersebut",
|
||||
"The login method: login with password is not enabled for the application": "Metode login: login dengan sandi tidak diaktifkan untuk aplikasi tersebut",
|
||||
"The organization: %s does not exist": "The organization: %s does not exist",
|
||||
"The provider: %s is not enabled for the application": "Penyedia: %s tidak diaktifkan untuk aplikasi ini",
|
||||
"Unauthorized operation": "Operasi tidak sah",
|
||||
"Unknown authentication type (not password or provider), form = %s": "Jenis otentikasi tidak diketahui (bukan kata sandi atau pemberi), formulir = %s",
|
||||
"Unknown authentication type (not password or provider), form = %s": "Jenis otentikasi tidak diketahui (bukan sandi atau penyedia), formulir = %s",
|
||||
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
|
||||
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
|
||||
},
|
||||
@ -39,59 +39,59 @@
|
||||
"Email cannot be empty": "Email tidak boleh kosong",
|
||||
"Email is invalid": "Email tidak valid",
|
||||
"Empty username.": "Nama pengguna kosong.",
|
||||
"Face data does not exist, cannot log in": "Face data does not exist, cannot log in",
|
||||
"Face data mismatch": "Face data mismatch",
|
||||
"Face data does not exist, cannot log in": "Data wajah tidak ada, tidak bisa login",
|
||||
"Face data mismatch": "Ketidakcocokan data wajah",
|
||||
"FirstName cannot be blank": "Nama depan tidak boleh kosong",
|
||||
"Invitation code cannot be blank": "Invitation code cannot be blank",
|
||||
"Invitation code exhausted": "Invitation code exhausted",
|
||||
"Invitation code is invalid": "Invitation code is invalid",
|
||||
"Invitation code suspended": "Invitation code suspended",
|
||||
"LDAP user name or password incorrect": "Nama pengguna atau kata sandi Ldap salah",
|
||||
"Invitation code cannot be blank": "Kode undangan tidak boleh kosong",
|
||||
"Invitation code exhausted": "Kode undangan habis",
|
||||
"Invitation code is invalid": "Kode undangan tidak valid",
|
||||
"Invitation code suspended": "Kode undangan ditangguhkan",
|
||||
"LDAP user name or password incorrect": "Nama pengguna atau sandi LDAP salah",
|
||||
"LastName cannot be blank": "Nama belakang tidak boleh kosong",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Beberapa akun dengan uid yang sama, harap periksa server ldap Anda",
|
||||
"Multiple accounts with same uid, please check your ldap server": "Beberapa akun dengan uid yang sama, harap periksa server LDAP Anda",
|
||||
"Organization does not exist": "Organisasi tidak ada",
|
||||
"Phone already exists": "Telepon sudah ada",
|
||||
"Phone cannot be empty": "Telepon tidak boleh kosong",
|
||||
"Phone number is invalid": "Nomor telepon tidak valid",
|
||||
"Please register using the email corresponding to the invitation code": "Please register using the email corresponding to the invitation code",
|
||||
"Please register using the phone corresponding to the invitation code": "Please register using the phone corresponding to the invitation code",
|
||||
"Please register using the username corresponding to the invitation code": "Please register using the username corresponding to the invitation code",
|
||||
"Session outdated, please login again": "Sesi kedaluwarsa, silakan masuk lagi",
|
||||
"The invitation code has already been used": "The invitation code has already been used",
|
||||
"Please register using the email corresponding to the invitation code": "Silakan mendaftar menggunakan email yang sesuai dengan kode undangan",
|
||||
"Please register using the phone corresponding to the invitation code": "Silakan mendaftar menggunakan email yang sesuai dengan kode undangan",
|
||||
"Please register using the username corresponding to the invitation code": "Silakan mendaftar menggunakan username yang sesuai dengan kode undangan",
|
||||
"Session outdated, please login again": "Sesi kadaluwarsa, silakan masuk lagi",
|
||||
"The invitation code has already been used": "Kode undangan sudah digunakan",
|
||||
"The user is forbidden to sign in, please contact the administrator": "Pengguna dilarang masuk, silakan hubungi administrator",
|
||||
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
|
||||
"The user: %s doesn't exist in LDAP server": "Pengguna: %s tidak ada di server LDAP",
|
||||
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "Nama pengguna hanya bisa menggunakan karakter alfanumerik, garis bawah atau tanda hubung, tidak boleh memiliki dua tanda hubung atau garis bawah berurutan, dan tidak boleh diawali atau diakhiri dengan tanda hubung atau garis bawah.",
|
||||
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex",
|
||||
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"",
|
||||
"The value \\\"%s\\\" for account field \\\"%s\\\" doesn't match the account item regex": "Nilai \\\"%s\\\" pada bidang akun \\\"%s\\\" tidak cocok dengan ketentuan",
|
||||
"The value \\\"%s\\\" for signup field \\\"%s\\\" doesn't match the signup item regex of the application \\\"%s\\\"": "Nilai \\\"%s\\\" pada bidang pendaftaran \\\"%s\\\" tidak cocok dengan ketentuan aplikasi \\\"%s\\\"",
|
||||
"Username already exists": "Nama pengguna sudah ada",
|
||||
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
|
||||
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
|
||||
"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",
|
||||
"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 sandi atau kode yang salah terlalu sering, mohon tunggu selama %d menit lalu coba kembali",
|
||||
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
|
||||
"password or code is incorrect": "password or code is incorrect",
|
||||
"password or code is incorrect, you have %d remaining chances": "Kata sandi atau kode salah, Anda memiliki %d kesempatan tersisa",
|
||||
"password or code is incorrect": "kata sandi atau kode salah",
|
||||
"password or code is incorrect, you have %d remaining chances": "Sandi atau kode salah, Anda memiliki %d kesempatan tersisa",
|
||||
"unsupported password type: %s": "jenis sandi tidak didukung: %s"
|
||||
},
|
||||
"general": {
|
||||
"Missing parameter": "Parameter hilang",
|
||||
"Please login first": "Silahkan login terlebih dahulu",
|
||||
"The organization: %s should have one application at least": "The organization: %s should have one application at least",
|
||||
"The organization: %s should have one application at least": "Organisasi: %s setidaknya harus memiliki satu aplikasi",
|
||||
"The user: %s doesn't exist": "Pengguna: %s tidak ada",
|
||||
"don't support captchaProvider: ": "Jangan mendukung captchaProvider:",
|
||||
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode",
|
||||
"this operation requires administrator to perform": "this operation requires administrator to perform"
|
||||
"this operation is not allowed in demo mode": "tindakan ini tidak diizinkan pada mode demo",
|
||||
"this operation requires administrator to perform": "tindakan ini membutuhkan peran administrator"
|
||||
},
|
||||
"ldap": {
|
||||
"Ldap server exist": "Server ldap ada"
|
||||
},
|
||||
"link": {
|
||||
"Please link first": "Tolong tautkan terlebih dahulu",
|
||||
"Please link first": "Silahkan tautkan terlebih dahulu",
|
||||
"This application has no providers": "Aplikasi ini tidak memiliki penyedia",
|
||||
"This application has no providers of type": " Aplikasi ini tidak memiliki penyedia tipe ",
|
||||
"This provider can't be unlinked": "Pemberi layanan ini tidak dapat dipisahkan",
|
||||
"This provider can't be unlinked": "Penyedia layanan ini tidak dapat dipisahkan",
|
||||
"You are not the global admin, you can't unlink other users": "Anda bukan admin global, Anda tidak dapat memutuskan tautan pengguna lain",
|
||||
"You can't unlink yourself, you are not a member of any application": "Anda tidak dapat memutuskan tautan diri sendiri, karena Anda bukan anggota dari aplikasi apa pun"
|
||||
},
|
||||
@ -101,11 +101,11 @@
|
||||
"Unknown modify rule %s.": "Aturan modifikasi tidak diketahui %s."
|
||||
},
|
||||
"permission": {
|
||||
"The permission: \\\"%s\\\" doesn't exist": "The permission: \\\"%s\\\" doesn't exist"
|
||||
"The permission: \\\"%s\\\" doesn't exist": "Izin: \\\"%s\\\" tidak ada"
|
||||
},
|
||||
"provider": {
|
||||
"Invalid application id": "ID aplikasi tidak valid",
|
||||
"the provider: %s does not exist": "provider: %s tidak ada"
|
||||
"the provider: %s does not exist": "penyedia: %s tidak ada"
|
||||
},
|
||||
"resource": {
|
||||
"User is nil for tag: avatar": "Pengguna kosong untuk tag: avatar",
|
||||
@ -129,13 +129,13 @@
|
||||
"token": {
|
||||
"Grant_type: %s is not supported in this application": "Jenis grant (grant_type) %s tidak didukung dalam aplikasi ini",
|
||||
"Invalid application or wrong clientSecret": "Aplikasi tidak valid atau clientSecret salah",
|
||||
"Invalid client_id": "Invalid client_id = ID klien tidak valid",
|
||||
"Invalid client_id": "ID klien tidak valid",
|
||||
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "URI pengalihan: %s tidak ada dalam daftar URI Pengalihan yang diizinkan",
|
||||
"Token not found, invalid accessToken": "Token tidak ditemukan, accessToken tidak valid"
|
||||
},
|
||||
"user": {
|
||||
"Display name cannot be empty": "Nama tampilan tidak boleh kosong",
|
||||
"New password cannot contain blank space.": "Kata sandi baru tidak boleh mengandung spasi kosong."
|
||||
"New password cannot contain blank space.": "Sandi baru tidak boleh mengandung spasi kosong."
|
||||
},
|
||||
"user_upload": {
|
||||
"Failed to import users": "Gagal mengimpor pengguna"
|
||||
@ -148,16 +148,16 @@
|
||||
"verification": {
|
||||
"Invalid captcha provider.": "Penyedia captcha tidak valid.",
|
||||
"Phone number is invalid in your region %s": "Nomor telepon tidak valid di wilayah anda %s",
|
||||
"The verification code has not been sent yet!": "The verification code has not been sent yet!",
|
||||
"The verification code has not been sent yet, or has already been used!": "The verification code has not been sent yet, or has already been used!",
|
||||
"The verification code has not been sent yet!": "Kode verifikasi belum terkirim!",
|
||||
"The verification code has not been sent yet, or has already been used!": "Kode verifikasi belum dikirim atau telah digunakan!",
|
||||
"Turing test failed.": "Tes Turing gagal.",
|
||||
"Unable to get the email modify rule.": "Tidak dapat memperoleh aturan modifikasi email.",
|
||||
"Unable to get the phone modify rule.": "Tidak dapat memodifikasi aturan telepon.",
|
||||
"Unknown type": "Tipe tidak diketahui",
|
||||
"Wrong verification code!": "Kode verifikasi salah!",
|
||||
"You should verify your code in %d min!": "Anda harus memverifikasi kode Anda dalam %d menit!",
|
||||
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "please add a SMS provider to the \\\"Providers\\\" list for the application: %s",
|
||||
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "please add an Email provider to the \\\"Providers\\\" list for the application: %s",
|
||||
"please add a SMS provider to the \\\"Providers\\\" list for the application: %s": "silahkan tambahkan penyedia SMS ke daftar \\\"Penyedia\\\" untuk aplikasi: %s",
|
||||
"please add an Email provider to the \\\"Providers\\\" list for the application: %s": "silahkan tambahkan penyedia Email ke daftar \\\"Penyedia\\\" untuk aplikasi: %s",
|
||||
"the user does not exist, please sign up first": "Pengguna tidak ada, silakan daftar terlebih dahulu"
|
||||
},
|
||||
"webauthn": {
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
|
||||
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
|
||||
"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文字必要です",
|
||||
"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": "あなたの地域は電話でサインアップすることができません",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
|
||||
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
|
||||
"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개의 문자가 있어야 합니다",
|
||||
"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": "당신의 지역은 전화로 가입할 수 없습니다",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -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": "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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
||||
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
||||
"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 символов",
|
||||
"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": "Ваш регион не разрешает регистрацию по телефону",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"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 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",
|
||||
"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",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"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 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ı",
|
||||
"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",
|
||||
|
@ -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 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",
|
||||
"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",
|
||||
|
@ -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 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 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ự",
|
||||
"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",
|
||||
|
@ -67,7 +67,7 @@
|
||||
"Username cannot be an email address": "用户名不可以是邮箱地址",
|
||||
"Username cannot contain white spaces": "用户名禁止包含空格",
|
||||
"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个字符",
|
||||
"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": "所在地区不支持手机号注册",
|
||||
|
@ -136,12 +136,12 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
dtUserInfo := &DingTalkUserResponse{}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
reqest.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
||||
resp, err := idp.Client.Do(reqest)
|
||||
request.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
||||
resp, err := idp.Client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
13
idp/goth.go
13
idp/goth.go
@ -278,9 +278,16 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
|
||||
Session: &naver.Session{},
|
||||
}
|
||||
case "Nextcloud":
|
||||
idp = GothIdProvider{
|
||||
Provider: nextcloud.New(clientId, clientSecret, redirectUrl),
|
||||
Session: &nextcloud.Session{},
|
||||
if hostUrl != "" {
|
||||
idp = GothIdProvider{
|
||||
Provider: nextcloud.NewCustomisedDNS(clientId, clientSecret, redirectUrl, hostUrl),
|
||||
Session: &nextcloud.Session{},
|
||||
}
|
||||
} else {
|
||||
idp = GothIdProvider{
|
||||
Provider: nextcloud.New(clientId, clientSecret, redirectUrl),
|
||||
Session: &nextcloud.Session{},
|
||||
}
|
||||
}
|
||||
case "OneDrive":
|
||||
idp = GothIdProvider{
|
||||
|
@ -44,6 +44,7 @@ type ProviderInfo struct {
|
||||
AppId string
|
||||
HostUrl string
|
||||
RedirectUrl string
|
||||
DisableSsl bool
|
||||
|
||||
TokenURL string
|
||||
AuthURL string
|
||||
@ -79,9 +80,9 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
|
||||
return NewLinkedInIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
case "WeCom":
|
||||
if idpInfo.SubType == "Internal" {
|
||||
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
return NewWeComInternalIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
|
||||
} else if idpInfo.SubType == "Third-party" {
|
||||
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
|
||||
return NewWeComIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.DisableSsl), nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("WeCom provider subType: %s is not supported", idpInfo.SubType)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
bodyData := bytes.NewReader([]byte(params))
|
||||
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
||||
request, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
resp, err := client.Do(requeset)
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
@ -29,13 +29,16 @@ import (
|
||||
type WeComInternalIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
|
||||
UseIdAsName bool
|
||||
}
|
||||
|
||||
func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComInternalIdProvider {
|
||||
func NewWeComInternalIdProvider(clientId string, clientSecret string, redirectUrl string, useIdAsName bool) *WeComInternalIdProvider {
|
||||
idp := &WeComInternalIdProvider{}
|
||||
|
||||
config := idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||
idp.Config = config
|
||||
idp.UseIdAsName = useIdAsName
|
||||
|
||||
return idp
|
||||
}
|
||||
@ -169,5 +172,9 @@ func (idp *WeComInternalIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo,
|
||||
userInfo.Id = userInfo.Username
|
||||
}
|
||||
|
||||
if idp.UseIdAsName {
|
||||
userInfo.Username = userInfo.Id
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
@ -28,13 +28,16 @@ import (
|
||||
type WeComIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
|
||||
UseIdAsName bool
|
||||
}
|
||||
|
||||
func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string) *WeComIdProvider {
|
||||
func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string, useIdAsName bool) *WeComIdProvider {
|
||||
idp := &WeComIdProvider{}
|
||||
|
||||
config := idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||
idp.Config = config
|
||||
idp.UseIdAsName = useIdAsName
|
||||
|
||||
return idp
|
||||
}
|
||||
@ -183,6 +186,10 @@ func (idp *WeComIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
|
||||
DisplayName: wecomUserInfo.UserInfo.Name,
|
||||
AvatarUrl: wecomUserInfo.UserInfo.Avatar,
|
||||
}
|
||||
|
||||
if idp.UseIdAsName {
|
||||
userInfo.Username = userInfo.Id
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
|
@ -434,7 +434,7 @@
|
||||
"isTopGroup": true,
|
||||
"title": "",
|
||||
"key": "",
|
||||
"children": "",
|
||||
"children": [],
|
||||
"isEnabled": true
|
||||
}
|
||||
],
|
||||
|
@ -185,12 +185,9 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
|
||||
attr := string(f.AttributeDesc())
|
||||
|
||||
if attr == ldapMemberOfAttr {
|
||||
groupId := string(f.AssertionValue())
|
||||
users, err := object.GetGroupUsers(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
groupId := string(f.AssertionValue())
|
||||
users := object.GetGroupUsersWithoutError(groupId)
|
||||
for _, user := range users {
|
||||
names = append(names, user.Name)
|
||||
}
|
||||
@ -249,7 +246,7 @@ func buildSafeCondition(filter interface{}) builder.Cond {
|
||||
condition, err := buildUserFilterCondition(filter)
|
||||
if err != nil {
|
||||
log.Printf("err = %v", err.Error())
|
||||
return nil
|
||||
return builder.And(builder.Expr("1 != 1"))
|
||||
}
|
||||
return condition
|
||||
}
|
||||
|
19
main.go
19
main.go
@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego"
|
||||
@ -77,10 +78,26 @@ func main() {
|
||||
beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 30
|
||||
// beego.BConfig.WebConfig.Session.SessionCookieSameSite = http.SameSiteNoneMode
|
||||
|
||||
err := logs.SetLogger(logs.AdapterFile, conf.GetConfigString("logConfig"))
|
||||
var logAdapter string
|
||||
logConfigMap := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(conf.GetConfigString("logConfig")), &logConfigMap)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, ok := logConfigMap["adapter"]
|
||||
if !ok {
|
||||
logAdapter = "file"
|
||||
} else {
|
||||
logAdapter = logConfigMap["adapter"].(string)
|
||||
}
|
||||
if logAdapter == "console" {
|
||||
logs.Reset()
|
||||
}
|
||||
err = logs.SetLogger(logAdapter, conf.GetConfigString("logConfig"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
port := beego.AppConfig.DefaultInt("httpport", 8000)
|
||||
// logs.SetLevel(logs.LevelInformational)
|
||||
logs.SetLogFuncCall(false)
|
||||
|
@ -191,12 +191,7 @@ func (adapter *Adapter) InitAdapter() error {
|
||||
}
|
||||
}
|
||||
|
||||
var tableName string
|
||||
if driverName == "mssql" {
|
||||
tableName = fmt.Sprintf("[%s]", adapter.Table)
|
||||
} else {
|
||||
tableName = adapter.Table
|
||||
}
|
||||
tableName := adapter.Table
|
||||
|
||||
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
|
||||
if err != nil {
|
||||
|
@ -71,6 +71,7 @@ type Application struct {
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
||||
EnablePassword bool `json:"enablePassword"`
|
||||
EnableSignUp bool `json:"enableSignUp"`
|
||||
@ -84,7 +85,7 @@ type Application struct {
|
||||
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
|
||||
OrgChoiceMode string `json:"orgChoiceMode"`
|
||||
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
|
||||
SamlReplyUrl string `xorm:"varchar(500)" json:"samlReplyUrl"`
|
||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
|
||||
SignupItems []*SignupItem `xorm:"varchar(3000)" json:"signupItems"`
|
||||
@ -97,29 +98,31 @@ type Application struct {
|
||||
IsShared bool `json:"isShared"`
|
||||
IpRestriction string `json:"ipRestriction"`
|
||||
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||
ExpireInHours int `json:"expireInHours"`
|
||||
RefreshExpireInHours int `json:"refreshExpireInHours"`
|
||||
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
|
||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
||||
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
||||
FooterHtml string `xorm:"mediumtext" json:"footerHtml"`
|
||||
FormCss string `xorm:"text" json:"formCss"`
|
||||
FormCssMobile string `xorm:"text" json:"formCssMobile"`
|
||||
FormOffset int `json:"formOffset"`
|
||||
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
|
||||
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||
ForcedRedirectOrigin string `xorm:"varchar(100)" json:"forcedRedirectOrigin"`
|
||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||
ExpireInHours int `json:"expireInHours"`
|
||||
RefreshExpireInHours int `json:"refreshExpireInHours"`
|
||||
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
|
||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
||||
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
||||
FooterHtml string `xorm:"mediumtext" json:"footerHtml"`
|
||||
FormCss string `xorm:"text" json:"formCss"`
|
||||
FormCssMobile string `xorm:"text" json:"formCssMobile"`
|
||||
FormOffset int `json:"formOffset"`
|
||||
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
|
||||
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
||||
FormBackgroundUrlMobile string `xorm:"varchar(200)" json:"formBackgroundUrlMobile"`
|
||||
|
||||
FailedSigninLimit int `json:"failedSigninLimit"`
|
||||
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
||||
@ -539,7 +542,7 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
||||
|
||||
providerItems := []*ProviderItem{}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,11 @@ func GetCertCount(owner, field, value string) (int64, error) {
|
||||
|
||||
func GetCerts(owner string) ([]*Cert, error) {
|
||||
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 {
|
||||
return certs, err
|
||||
}
|
||||
@ -146,7 +150,12 @@ func getCertByName(name string) (*Cert, error) {
|
||||
|
||||
func GetCert(id string) (*Cert, error) {
|
||||
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) {
|
||||
|
@ -252,7 +252,7 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
||||
credManager := cred.GetCredManager(passwordType)
|
||||
if credManager != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -517,8 +517,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
|
||||
func CheckUsername(username string, lang string) string {
|
||||
if username == "" {
|
||||
return i18n.Translate(lang, "check:Empty username.")
|
||||
} else if len(username) > 39 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
||||
} else if len(username) > 255 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if username == "" {
|
||||
return i18n.Translate(lang, "check:Empty username.")
|
||||
} else if len(username) > 39 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
||||
} else if len(username) > 255 {
|
||||
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
options = []string{"AtLeast6"}
|
||||
return ""
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
if fromAddress == "" {
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -83,19 +84,24 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
|
||||
func GetGroupsHaveChildrenMap(groups []*Group) (map[string]*Group, error) {
|
||||
groupsHaveChildren := []*Group{}
|
||||
resultMap := make(map[string]*Group)
|
||||
groupMap := map[string]*Group{}
|
||||
|
||||
groupIds := []string{}
|
||||
for _, group := range groups {
|
||||
groupMap[group.Name] = group
|
||||
groupIds = append(groupIds, group.Name)
|
||||
groupIds = append(groupIds, group.ParentId)
|
||||
if !group.IsTopGroup {
|
||||
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("name").In("name", groupIds).Find(&groupsHaveChildren)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, group := range groups {
|
||||
resultMap[group.Name] = group
|
||||
|
||||
for _, group := range groupsHaveChildren {
|
||||
resultMap[group.GetId()] = group
|
||||
}
|
||||
return resultMap, nil
|
||||
}
|
||||
@ -206,6 +212,12 @@ func DeleteGroup(group *Group) (bool, error) {
|
||||
}
|
||||
|
||||
func checkGroupName(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("group name can't be empty")
|
||||
}
|
||||
if strings.Contains(name, "/") {
|
||||
return errors.New("group name can't contain \"/\"")
|
||||
}
|
||||
exist, err := ormer.Engine.Exist(&Organization{Owner: "admin", Name: name})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -302,7 +314,10 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
|
||||
|
||||
func GetGroupUsers(groupId string) ([]*User, error) {
|
||||
users := []*User{}
|
||||
owner, _ := util.GetOwnerAndNameFromId(groupId)
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -314,6 +329,11 @@ func GetGroupUsers(groupId string) ([]*User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetGroupUsersWithoutError(groupId string) []*User {
|
||||
users, _ := GetGroupUsers(groupId)
|
||||
return users
|
||||
}
|
||||
|
||||
func ExtendGroupWithUsers(group *Group) error {
|
||||
if group == nil {
|
||||
return nil
|
||||
|
@ -70,12 +70,12 @@ func InitFromFile() {
|
||||
for _, provider := range initData.Providers {
|
||||
initDefinedProvider(provider)
|
||||
}
|
||||
for _, user := range initData.Users {
|
||||
initDefinedUser(user)
|
||||
}
|
||||
for _, application := range initData.Applications {
|
||||
initDefinedApplication(application)
|
||||
}
|
||||
for _, user := range initData.Users {
|
||||
initDefinedUser(user)
|
||||
}
|
||||
for _, cert := range initData.Certs {
|
||||
initDefinedCert(cert)
|
||||
}
|
||||
|
@ -23,17 +23,18 @@ type Ldap struct {
|
||||
Owner string `xorm:"varchar(100)" json:"owner"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
ServerName string `xorm:"varchar(100)" json:"serverName"`
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `xorm:"int" json:"port"`
|
||||
EnableSsl bool `xorm:"bool" json:"enableSsl"`
|
||||
Username string `xorm:"varchar(100)" json:"username"`
|
||||
Password string `xorm:"varchar(100)" json:"password"`
|
||||
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
|
||||
Filter string `xorm:"varchar(200)" json:"filter"`
|
||||
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||
ServerName string `xorm:"varchar(100)" json:"serverName"`
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `xorm:"int" json:"port"`
|
||||
EnableSsl bool `xorm:"bool" json:"enableSsl"`
|
||||
AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
|
||||
Username string `xorm:"varchar(100)" json:"username"`
|
||||
Password string `xorm:"varchar(100)" json:"password"`
|
||||
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
|
||||
Filter string `xorm:"varchar(200)" json:"filter"`
|
||||
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||
|
||||
AutoSync int `json:"autoSync"`
|
||||
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
||||
@ -150,7 +151,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type").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 {
|
||||
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)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
|
||||
continue
|
||||
}
|
||||
|
||||
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(err.Error())
|
||||
|
@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -64,8 +65,11 @@ type LdapUser struct {
|
||||
|
||||
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
||||
var conn *goldap.Conn
|
||||
tlsConfig := tls.Config{
|
||||
InsecureSkipVerify: ldap.AllowSelfSignedCert,
|
||||
}
|
||||
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 {
|
||||
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")
|
||||
} else if mfa.MfaType == EmailType {
|
||||
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)
|
||||
|
@ -30,6 +30,7 @@ type OidcDiscovery struct {
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
|
||||
JwksUri string `json:"jwks_uri"`
|
||||
IntrospectionEndpoint string `json:"introspection_endpoint"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
@ -77,6 +78,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
||||
return origin, origin
|
||||
}
|
||||
|
||||
isDev := conf.GetConfigString("runmode") == "dev"
|
||||
// "door.casdoor.com"
|
||||
protocol := "https://"
|
||||
if !strings.Contains(host, ".") {
|
||||
@ -87,7 +89,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
||||
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")
|
||||
} else {
|
||||
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),
|
||||
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
|
||||
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
|
||||
DeviceAuthorizationEndpoint: fmt.Sprintf("%s/api/device-auth", originBackend),
|
||||
JwksUri: fmt.Sprintf("%s/.well-known/jwks", 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"},
|
||||
@ -137,7 +140,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
||||
|
||||
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
|
||||
jwks := jose.JSONWebKeySet{}
|
||||
certs, err := GetCerts("admin")
|
||||
certs, err := GetCerts("")
|
||||
if err != nil {
|
||||
return jwks, err
|
||||
}
|
||||
@ -212,3 +215,14 @@ func GetWebFinger(resource string, rels []string, host string) (WebFinger, error
|
||||
|
||||
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"`
|
||||
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
||||
PasswordExpireDays int `json:"passwordExpireDays"`
|
||||
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
||||
CountryCodes []string `xorm:"mediumtext" json:"countryCodes"`
|
||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
|
||||
@ -80,7 +80,8 @@ type Organization struct {
|
||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||
EnableTour bool `json:"enableTour"`
|
||||
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"`
|
||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||
@ -227,6 +228,7 @@ func UpdateOrganization(id string, organization *Organization, isGlobalAdmin boo
|
||||
|
||||
if !isGlobalAdmin {
|
||||
organization.NavItems = org.NavItems
|
||||
organization.WidgetItems = org.WidgetItems
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
a := &Ormer{}
|
||||
a.driverName = driverName
|
||||
@ -179,7 +179,7 @@ func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, d
|
||||
|
||||
func refineDataSourceNameForPostgres(dataSourceName string) string {
|
||||
reg := regexp.MustCompile(`dbname=[^ ]+\s*`)
|
||||
return reg.ReplaceAllString(dataSourceName, "")
|
||||
return reg.ReplaceAllString(dataSourceName, "dbname=postgres")
|
||||
}
|
||||
|
||||
func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error {
|
||||
@ -190,7 +190,7 @@ func createDatabaseForPostgres(driverName string, dataSourceName string, dbName
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName))
|
||||
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE \"%s\";", dbName))
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "already exists") {
|
||||
return err
|
||||
|
@ -148,7 +148,7 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
return false, err
|
||||
} else if model == nil {
|
||||
|
@ -48,6 +48,7 @@ type Provider struct {
|
||||
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
||||
Scopes string `xorm:"varchar(100)" json:"scopes"`
|
||||
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"`
|
||||
Port int `json:"port"`
|
||||
@ -384,6 +385,44 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
|
||||
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 {
|
||||
session := ormer.Engine.NewSession()
|
||||
defer session.Close()
|
||||
@ -436,6 +475,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
|
||||
AuthURL: provider.CustomAuthUrl,
|
||||
UserInfoURL: provider.CustomUserInfoUrl,
|
||||
UserMapping: provider.UserMapping,
|
||||
DisableSsl: provider.DisableSsl,
|
||||
}
|
||||
|
||||
if provider.Type == "WeChat" {
|
||||
|
@ -263,6 +263,27 @@ func addWebhookRecord(webhook *Webhook, record *casvisorsdk.Record, statusCode i
|
||||
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 {
|
||||
webhooks, err := getWebhooksByOrganization("")
|
||||
if err != nil {
|
||||
@ -271,7 +292,14 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
|
||||
errs := []error{}
|
||||
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
||||
|
||||
record2 := *record
|
||||
for _, webhook := range webhooks {
|
||||
|
||||
if len(webhook.ObjectFields) != 0 && webhook.ObjectFields[0] != "All" {
|
||||
record2.Object = filterRecordObject(record.Object, webhook.ObjectFields)
|
||||
}
|
||||
|
||||
var user *User
|
||||
if webhook.IsUserExtended {
|
||||
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 {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = addWebhookRecord(webhook, record, statusCode, respBody, err)
|
||||
err = addWebhookRecord(webhook, &record2, statusCode, respBody, err)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
saml "github.com/russellhaering/gosaml2"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
@ -31,7 +31,8 @@ type Claims struct {
|
||||
Tag string `json:"tag"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// the `azp` (Authorized Party) claim. Optional. See https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -46,6 +47,17 @@ type UserShort struct {
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
}
|
||||
|
||||
type UserStandard struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"preferred_username,omitempty"`
|
||||
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"name,omitempty"`
|
||||
Avatar string `xorm:"varchar(500)" json:"picture,omitempty"`
|
||||
Email string `xorm:"varchar(100) index" json:"email,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone,omitempty"`
|
||||
}
|
||||
|
||||
type UserWithoutThirdIdp struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
@ -140,6 +152,7 @@ type ClaimsShort struct {
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -159,6 +172,7 @@ type ClaimsWithoutThirdIdp struct {
|
||||
Tag string `json:"tag"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@ -176,6 +190,20 @@ func getShortUser(user *User) *UserShort {
|
||||
return res
|
||||
}
|
||||
|
||||
func getStandardUser(user *User) *UserStandard {
|
||||
res := &UserStandard{
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
|
||||
res := &UserWithoutThirdIdp{
|
||||
Owner: user.Owner,
|
||||
@ -274,6 +302,7 @@ func getShortClaims(claims Claims) ClaimsShort {
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
Provider: claims.Provider,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -287,6 +316,7 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
Provider: claims.Provider,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -308,6 +338,7 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
|
||||
res["tag"] = claims.Tag
|
||||
res["scope"] = claims.Scope
|
||||
res["azp"] = claims.Azp
|
||||
res["provider"] = claims.Provider
|
||||
|
||||
for _, field := range tokenField {
|
||||
userField := userValue.FieldByName(field)
|
||||
@ -342,7 +373,7 @@ func refineUser(user *User) *User {
|
||||
return user
|
||||
}
|
||||
|
||||
func generateJwtToken(application *Application, user *User, nonce string, scope string, host string) (string, string, string, error) {
|
||||
func generateJwtToken(application *Application, user *User, provider string, nonce string, scope string, host string) (string, string, string, error) {
|
||||
nowTime := time.Now()
|
||||
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
|
||||
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
|
||||
@ -362,9 +393,10 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
||||
TokenType: "access-token",
|
||||
Nonce: nonce,
|
||||
// FIXME: A workaround for custom claim by reusing `tag` in user info
|
||||
Tag: user.Tag,
|
||||
Scope: scope,
|
||||
Azp: application.ClientId,
|
||||
Tag: user.Tag,
|
||||
Scope: scope,
|
||||
Azp: application.ClientId,
|
||||
Provider: provider,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: originBackend,
|
||||
Subject: user.Id,
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
@ -37,6 +38,8 @@ const (
|
||||
EndpointError = "endpoint_error"
|
||||
)
|
||||
|
||||
var DeviceAuthMap = sync.Map{}
|
||||
|
||||
type Code struct {
|
||||
Message string `xorm:"varchar(100)" json:"message"`
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
@ -71,6 +74,22 @@ type IntrospectionResponse struct {
|
||||
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) {
|
||||
token, err := GetTokenByAccessToken(accessToken)
|
||||
if err != nil {
|
||||
@ -117,7 +136,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
|
||||
return "", application, nil
|
||||
}
|
||||
|
||||
func GetOAuthCode(userId string, clientId string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
|
||||
func GetOAuthCode(userId string, clientId string, provider string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
|
||||
user, err := GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -152,7 +171,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -222,6 +241,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
|
||||
case "token", "id_token": // Implicit Grant
|
||||
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":
|
||||
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
|
||||
if err != nil {
|
||||
@ -358,7 +379,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: EndpointError,
|
||||
@ -537,7 +558,7 @@ func GetPasswordToken(application *Application, username string, password string
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
@ -583,7 +604,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
Type: "application",
|
||||
}
|
||||
|
||||
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", scope, host)
|
||||
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", scope, host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
@ -647,7 +668,7 @@ func GetTokenByUser(application *Application, user *User, scope string, nonce st
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, nonce, scope, host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", nonce, scope, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -754,7 +775,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", host)
|
||||
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", host)
|
||||
if err != nil {
|
||||
return nil, &TokenError{
|
||||
Error: EndpointError,
|
||||
|
@ -19,11 +19,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type ClaimsStandard struct {
|
||||
*UserShort
|
||||
*UserStandard
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
|
||||
@ -33,6 +33,7 @@ type ClaimsStandard struct {
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Address OIDCAddress `json:"address,omitempty"`
|
||||
Azp string `json:"azp,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@ -47,13 +48,14 @@ func getStreetAddress(user *User) string {
|
||||
|
||||
func getStandardClaims(claims Claims) ClaimsStandard {
|
||||
res := ClaimsStandard{
|
||||
UserShort: getShortUser(claims.User),
|
||||
UserStandard: getStandardUser(claims.User),
|
||||
EmailVerified: claims.User.EmailVerified,
|
||||
TokenType: claims.TokenType,
|
||||
Nonce: claims.Nonce,
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
Azp: claims.Azp,
|
||||
Provider: claims.Provider,
|
||||
}
|
||||
|
||||
res.Phone = ""
|
||||
|
@ -15,13 +15,17 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/faceId"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/xorm-io/builder"
|
||||
@ -48,7 +52,7 @@ func InitUserManager() {
|
||||
|
||||
type User struct {
|
||||
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"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
|
||||
@ -244,6 +248,7 @@ type MfaAccount struct {
|
||||
type FaceId struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
FaceIdData []float64 `json:"faceIdData"`
|
||||
ImageUrl string `json:"ImageUrl"`
|
||||
}
|
||||
|
||||
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) {
|
||||
if email == "" {
|
||||
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")
|
||||
}
|
||||
|
||||
if CheckUsernameWithEmail(user.Name, "en") != "" {
|
||||
user.Name = util.GetRandomName()
|
||||
}
|
||||
|
||||
organization, err := GetOrganizationByUser(user)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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" {
|
||||
user.Password = organization.DefaultPassword
|
||||
}
|
||||
@ -1179,6 +1223,40 @@ func (user *User) IsGlobalAdmin() bool {
|
||||
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) {
|
||||
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
|
||||
return util.GenerateId(), nil
|
||||
|
@ -81,62 +81,12 @@ func UploadUsers(owner string, path string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
transUsers, err := StringArrayToUser(table)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
newUsers := []*User{}
|
||||
for index, line := range table {
|
||||
line := line
|
||||
if index == 0 || parseLineItem(&line, 0) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Owner: parseLineItem(&line, 0),
|
||||
Name: parseLineItem(&line, 1),
|
||||
CreatedTime: parseLineItem(&line, 2),
|
||||
UpdatedTime: parseLineItem(&line, 3),
|
||||
Id: parseLineItem(&line, 4),
|
||||
Type: parseLineItem(&line, 5),
|
||||
Password: parseLineItem(&line, 6),
|
||||
PasswordSalt: parseLineItem(&line, 7),
|
||||
DisplayName: parseLineItem(&line, 8),
|
||||
FirstName: parseLineItem(&line, 9),
|
||||
LastName: parseLineItem(&line, 10),
|
||||
Avatar: parseLineItem(&line, 11),
|
||||
PermanentAvatar: "",
|
||||
Email: parseLineItem(&line, 12),
|
||||
Phone: parseLineItem(&line, 13),
|
||||
Location: parseLineItem(&line, 14),
|
||||
Address: []string{parseLineItem(&line, 15)},
|
||||
Affiliation: parseLineItem(&line, 16),
|
||||
Title: parseLineItem(&line, 17),
|
||||
IdCardType: parseLineItem(&line, 18),
|
||||
IdCard: parseLineItem(&line, 19),
|
||||
Homepage: parseLineItem(&line, 20),
|
||||
Bio: parseLineItem(&line, 21),
|
||||
Tag: parseLineItem(&line, 22),
|
||||
Region: parseLineItem(&line, 23),
|
||||
Language: parseLineItem(&line, 24),
|
||||
Gender: parseLineItem(&line, 25),
|
||||
Birthday: parseLineItem(&line, 26),
|
||||
Education: parseLineItem(&line, 27),
|
||||
Score: parseLineItemInt(&line, 28),
|
||||
Karma: parseLineItemInt(&line, 29),
|
||||
Ranking: parseLineItemInt(&line, 30),
|
||||
IsDefaultAvatar: false,
|
||||
IsOnline: parseLineItemBool(&line, 31),
|
||||
IsAdmin: parseLineItemBool(&line, 32),
|
||||
IsForbidden: parseLineItemBool(&line, 33),
|
||||
IsDeleted: parseLineItemBool(&line, 34),
|
||||
SignupApplication: parseLineItem(&line, 35),
|
||||
Hash: "",
|
||||
PreHash: "",
|
||||
CreatedIp: parseLineItem(&line, 36),
|
||||
LastSigninTime: parseLineItem(&line, 37),
|
||||
LastSigninIp: parseLineItem(&line, 38),
|
||||
Ldap: "",
|
||||
Properties: map[string]string{},
|
||||
DeletedTime: parseLineItem(&line, 39),
|
||||
}
|
||||
|
||||
for _, user := range transUsers {
|
||||
if _, ok := oldUserMap[user.GetId()]; !ok {
|
||||
newUsers = append(newUsers, user)
|
||||
}
|
||||
|
@ -19,12 +19,14 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
@ -689,3 +691,103 @@ func IsAppUser(userId string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setReflectAttr[T any](fieldValue *reflect.Value, fieldString string) error {
|
||||
unmarshalValue := new(T)
|
||||
err := json.Unmarshal([]byte(fieldString), unmarshalValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fvElem := fieldValue
|
||||
fvElem.Set(reflect.ValueOf(*unmarshalValue))
|
||||
return nil
|
||||
}
|
||||
|
||||
func StringArrayToUser(stringArray [][]string) ([]*User, error) {
|
||||
fieldNames := stringArray[0]
|
||||
excelMap := []map[string]string{}
|
||||
userFieldMap := map[string]int{}
|
||||
|
||||
reflectedUser := reflect.TypeOf(User{})
|
||||
for i := 0; i < reflectedUser.NumField(); i++ {
|
||||
userFieldMap[strings.ToLower(reflectedUser.Field(i).Name)] = i
|
||||
}
|
||||
|
||||
for idx, field := range stringArray {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tempMap := map[string]string{}
|
||||
for idx, val := range field {
|
||||
tempMap[fieldNames[idx]] = val
|
||||
}
|
||||
excelMap = append(excelMap, tempMap)
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
var err error
|
||||
|
||||
for _, u := range excelMap {
|
||||
user := User{}
|
||||
reflectedUser := reflect.ValueOf(&user).Elem()
|
||||
for k, v := range u {
|
||||
if v == "" || v == "null" || v == "[]" || v == "{}" {
|
||||
continue
|
||||
}
|
||||
fName := strings.ToLower(strings.ReplaceAll(k, "_", ""))
|
||||
fieldIdx, ok := userFieldMap[fName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fv := reflectedUser.Field(fieldIdx)
|
||||
if !fv.IsValid() {
|
||||
continue
|
||||
}
|
||||
switch fv.Kind() {
|
||||
case reflect.String:
|
||||
fv.SetString(v)
|
||||
continue
|
||||
case reflect.Bool:
|
||||
fv.SetBool(v == "1")
|
||||
continue
|
||||
case reflect.Int:
|
||||
intVal, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fv.SetInt(int64(intVal))
|
||||
continue
|
||||
}
|
||||
|
||||
switch fv.Type() {
|
||||
case reflect.TypeOf([]string{}):
|
||||
err = setReflectAttr[[]string](&fv, v)
|
||||
case reflect.TypeOf([]*string{}):
|
||||
err = setReflectAttr[[]*string](&fv, v)
|
||||
case reflect.TypeOf([]*FaceId{}):
|
||||
err = setReflectAttr[[]*FaceId](&fv, v)
|
||||
case reflect.TypeOf([]*MfaProps{}):
|
||||
err = setReflectAttr[[]*MfaProps](&fv, v)
|
||||
case reflect.TypeOf([]*Role{}):
|
||||
err = setReflectAttr[[]*Role](&fv, v)
|
||||
case reflect.TypeOf([]*Permission{}):
|
||||
err = setReflectAttr[[]*Permission](&fv, v)
|
||||
case reflect.TypeOf([]ManagedAccount{}):
|
||||
err = setReflectAttr[[]ManagedAccount](&fv, v)
|
||||
case reflect.TypeOf([]MfaAccount{}):
|
||||
err = setReflectAttr[[]MfaAccount](&fv, v)
|
||||
case reflect.TypeOf([]webauthn.Credential{}):
|
||||
err = setReflectAttr[[]webauthn.Credential](&fv, v)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
users = append(users, &user)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
@ -86,9 +86,9 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
|
||||
title := provider.Title
|
||||
|
||||
code := getRandomCode(6)
|
||||
if organization.MasterVerificationCode != "" {
|
||||
code = organization.MasterVerificationCode
|
||||
}
|
||||
// if organization.MasterVerificationCode != "" {
|
||||
// code = organization.MasterVerificationCode
|
||||
// }
|
||||
|
||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||
content := strings.Replace(provider.Content, "%s", code, 1)
|
||||
@ -124,9 +124,9 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
|
||||
}
|
||||
|
||||
code := getRandomCode(6)
|
||||
if organization.MasterVerificationCode != "" {
|
||||
code = organization.MasterVerificationCode
|
||||
}
|
||||
// if organization.MasterVerificationCode != "" {
|
||||
// code = organization.MasterVerificationCode
|
||||
// }
|
||||
|
||||
err = SendSms(provider, code, dest)
|
||||
if err != nil {
|
||||
|
@ -38,6 +38,8 @@ type Webhook struct {
|
||||
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||
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"`
|
||||
SingleOrgOnly bool `json:"singleOrgOnly"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -25,17 +26,43 @@ import (
|
||||
|
||||
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
|
||||
client := &http.Client{}
|
||||
userMap := make(map[string]interface{})
|
||||
var body io.Reader
|
||||
|
||||
type RecordEx struct {
|
||||
casvisorsdk.Record
|
||||
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
||||
}
|
||||
recordEx := &RecordEx{
|
||||
Record: *record,
|
||||
ExtendedUser: extendedUser,
|
||||
}
|
||||
if webhook.TokenFields != nil && len(webhook.TokenFields) > 0 && extendedUser != nil {
|
||||
userValue := reflect.ValueOf(extendedUser).Elem()
|
||||
|
||||
body := strings.NewReader(util.StructToJson(recordEx))
|
||||
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 {
|
||||
casvisorsdk.Record
|
||||
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
||||
}
|
||||
recordEx := &RecordEx{
|
||||
Record: *record,
|
||||
ExtendedUser: extendedUser,
|
||||
}
|
||||
|
||||
body = strings.NewReader(util.StructToJson(recordEx))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
|
||||
if err != nil {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@ -180,7 +181,11 @@ func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map
|
||||
return nil, err
|
||||
}
|
||||
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("Content-Type", "application/json")
|
||||
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-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
|
||||
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-organization", &controllers.ApiController{}, "GET:GetOrganization")
|
||||
|
@ -89,7 +89,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
code, err := object.GetOAuthCode(userId, clientId, responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
||||
code, err := object.GetOAuthCode(userId, clientId, "", responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if code.Message != "" {
|
||||
|
@ -106,11 +106,14 @@ func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string)
|
||||
}
|
||||
|
||||
organizationThemeCookie := &OrganizationThemeCookie{
|
||||
application.ThemeData,
|
||||
application.Logo,
|
||||
application.FooterHtml,
|
||||
organization.Favicon,
|
||||
organization.DisplayName,
|
||||
ThemeData: application.ThemeData,
|
||||
LogoUrl: application.Logo,
|
||||
FooterHtml: application.FooterHtml,
|
||||
}
|
||||
|
||||
if organization != nil {
|
||||
organizationThemeCookie.Favicon = organization.Favicon
|
||||
organizationThemeCookie.DisplayName = organization.DisplayName
|
||||
}
|
||||
|
||||
return organizationThemeCookie, nil
|
||||
|
@ -3,13 +3,16 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^1.10.1",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/cssinjs": "^1.23.0",
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@craco/craco": "^6.4.5",
|
||||
"@crowdin/cli": "^3.7.10",
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@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/core": "^2.20.5",
|
||||
"@web3-onboard/frontier": "^2.0.4",
|
||||
@ -20,10 +23,10 @@
|
||||
"@web3-onboard/sequence": "^2.0.8",
|
||||
"@web3-onboard/taho": "^2.0.5",
|
||||
"@web3-onboard/trust": "^2.0.4",
|
||||
"antd": "5.2.3",
|
||||
"antd-token-previewer": "^1.1.0-22",
|
||||
"antd": "5.24.1",
|
||||
"antd-token-previewer": "^2.0.8",
|
||||
"buffer": "^6.0.3",
|
||||
"codemirror": "^5.61.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"core-js": "^3.25.0",
|
||||
"craco-less": "^2.0.0",
|
||||
@ -40,7 +43,6 @@
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-cropper": "^2.1.7",
|
||||
"react-device-detect": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -327,7 +327,7 @@ class App extends Component {
|
||||
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" />
|
||||
</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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
|
||||
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import {Button, Card, Col, ConfigProvider, Input, InputNumber, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
|
||||
import {CopyOutlined, HolderOutlined, LinkOutlined, UploadOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as CertBackend from "./backend/CertBackend";
|
||||
import * as Setting from "./Setting";
|
||||
@ -34,14 +34,9 @@ import PromptPage from "./auth/PromptPage";
|
||||
import copy from "copy-to-clipboard";
|
||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||
|
||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import SigninTable from "./table/SigninTable";
|
||||
|
||||
require("codemirror/theme/material-darker.css");
|
||||
require("codemirror/mode/htmlmixed/htmlmixed");
|
||||
require("codemirror/mode/xml/xml");
|
||||
require("codemirror/mode/css/css");
|
||||
import Editor from "./common/Editor";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -58,6 +53,14 @@ const template = `<style>
|
||||
background-color: #333333;
|
||||
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>`;
|
||||
|
||||
const previewGrid = Setting.isMobile() ? 22 : 11;
|
||||
@ -91,11 +94,11 @@ const sideTemplate = `<style>
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
<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>
|
||||
`;
|
||||
@ -122,6 +125,7 @@ class ApplicationEditPage extends React.Component {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getApplication();
|
||||
this.getOrganizations();
|
||||
this.getGroups();
|
||||
}
|
||||
|
||||
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) {
|
||||
let owner = application.organization;
|
||||
if (application.isShared) {
|
||||
@ -403,6 +418,16 @@ class ApplicationEditPage extends React.Component {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application: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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
|
||||
@ -429,6 +454,7 @@ class ApplicationEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
|
||||
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>)
|
||||
{
|
||||
Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
@ -475,6 +501,31 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</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"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable signup"), i18next.t("application:Enable signup - Tooltip"))} :
|
||||
@ -629,13 +680,9 @@ class ApplicationEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror
|
||||
value={this.state.application.signupHtml}
|
||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
this.updateApplicationField("signupHtml", value);
|
||||
}}
|
||||
/>
|
||||
<Editor value={this.state.application.signupHtml} lang="html" fillHeight dark onChange={value => {
|
||||
this.updateApplicationField("signupHtml", value);
|
||||
}} />
|
||||
</div>
|
||||
} title={i18next.t("provider:Signup HTML - Edit")} trigger="click">
|
||||
<Input value={this.state.application.signupHtml} style={{marginBottom: "10px"}} onChange={e => {
|
||||
@ -651,13 +698,9 @@ class ApplicationEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror
|
||||
value={this.state.application.signinHtml}
|
||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
this.updateApplicationField("signinHtml", value);
|
||||
}}
|
||||
/>
|
||||
<Editor value={this.state.application.signinHtml} lang="html" fillHeight dark onChange={value => {
|
||||
this.updateApplicationField("signinHtml", value);
|
||||
}} />
|
||||
</div>
|
||||
} title={i18next.t("provider:Signin HTML - Edit")} trigger="click">
|
||||
<Input value={this.state.application.signinHtml} style={{marginBottom: "10px"}} onChange={e => {
|
||||
@ -684,6 +727,7 @@ class ApplicationEditPage extends React.Component {
|
||||
{id: "token", name: "Token"},
|
||||
{id: "id_token", name: "ID 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>)
|
||||
}
|
||||
</Select>
|
||||
@ -758,11 +802,7 @@ class ApplicationEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<CodeMirror
|
||||
value={this.state.samlMetadata}
|
||||
options={{mode: "xml", theme: "default"}}
|
||||
onBeforeChange={(editor, data, value) => {}}
|
||||
/>
|
||||
<Editor value={this.state.samlMetadata?.toString() ?? ""} lang="xml" readOnly />
|
||||
<br />
|
||||
<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}`);
|
||||
@ -822,6 +862,33 @@ class ApplicationEditPage extends React.Component {
|
||||
</Row>
|
||||
</Col>
|
||||
</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>
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Custom CSS"), i18next.t("application:Custom CSS - Tooltip"))} :
|
||||
@ -829,9 +896,12 @@ class ApplicationEditPage extends React.Component {
|
||||
<Col span={22}>
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror value={this.state.application.formCss === "" ? template : this.state.application.formCss}
|
||||
options={{mode: "css", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
<Editor
|
||||
value={this.state.application.formCss === "" ? template : this.state.application.formCss}
|
||||
lang="css"
|
||||
fillHeight
|
||||
dark
|
||||
onChange={value => {
|
||||
this.updateApplicationField("formCss", value);
|
||||
}}
|
||||
/>
|
||||
@ -850,9 +920,12 @@ class ApplicationEditPage extends React.Component {
|
||||
<Col span={22}>
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror value={this.state.application.formCssMobile === "" ? template : this.state.application.formCssMobile}
|
||||
options={{mode: "css", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
<Editor
|
||||
value={this.state.application.formCssMobile === "" ? template : this.state.application.formCssMobile}
|
||||
lang="css"
|
||||
fillHeight
|
||||
dark
|
||||
onChange={value => {
|
||||
this.updateApplicationField("formCssMobile", value);
|
||||
}}
|
||||
/>
|
||||
@ -887,9 +960,12 @@ class ApplicationEditPage extends React.Component {
|
||||
<Col span={21} >
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror value={this.state.application.formSideHtml === "" ? sideTemplate : this.state.application.formSideHtml}
|
||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
<Editor
|
||||
value={this.state.application.formSideHtml === "" ? sideTemplate : this.state.application.formSideHtml}
|
||||
lang="html"
|
||||
fillHeight
|
||||
dark
|
||||
onChange={value => {
|
||||
this.updateApplicationField("formSideHtml", value);
|
||||
}}
|
||||
/>
|
||||
@ -936,10 +1012,12 @@ class ApplicationEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror
|
||||
<Editor
|
||||
value={this.state.application.headerHtml}
|
||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
lang="html"
|
||||
fillHeight
|
||||
dark
|
||||
onChange={value => {
|
||||
this.updateApplicationField("headerHtml", value);
|
||||
}}
|
||||
/>
|
||||
@ -958,10 +1036,12 @@ class ApplicationEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<CodeMirror
|
||||
<Editor
|
||||
value={this.state.application.footerHtml}
|
||||
options={{mode: "htmlmixed", theme: "material-darker"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
lang="html"
|
||||
fillHeight
|
||||
dark
|
||||
onChange={value => {
|
||||
this.updateApplicationField("footerHtml", value);
|
||||
}}
|
||||
/>
|
||||
|
@ -73,7 +73,7 @@ class BaseListPage extends React.Component {
|
||||
this.fetch({pagination});
|
||||
}
|
||||
|
||||
getColumnSearchProps = dataIndex => ({
|
||||
getColumnSearchProps = (dataIndex, customRender = null) => ({
|
||||
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => (
|
||||
<div style={{padding: 8}}>
|
||||
<Input
|
||||
@ -121,13 +121,15 @@ class BaseListPage extends React.Component {
|
||||
record[dataIndex]
|
||||
? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase())
|
||||
: "",
|
||||
onFilterDropdownOpenChange: visible => {
|
||||
if (visible) {
|
||||
setTimeout(() => this.searchInput.select(), 100);
|
||||
}
|
||||
filterDropdownProps: {
|
||||
onOpenChange: visible => {
|
||||
if (visible) {
|
||||
setTimeout(() => this.searchInput.select(), 100);
|
||||
}
|
||||
},
|
||||
},
|
||||
render: text =>
|
||||
this.state.searchedColumn === dataIndex ? (
|
||||
render: (text, record, index) => {
|
||||
const highlightContent = this.state.searchedColumn === dataIndex ? (
|
||||
<Highlighter
|
||||
highlightStyle={{backgroundColor: "#ffc069", padding: 0}}
|
||||
searchWords={[this.state.searchText]}
|
||||
@ -136,7 +138,10 @@ class BaseListPage extends React.Component {
|
||||
/>
|
||||
) : (
|
||||
text
|
||||
),
|
||||
);
|
||||
|
||||
return customRender ? customRender({text, record, index}, highlightContent) : highlightContent;
|
||||
},
|
||||
});
|
||||
|
||||
handleSearch = (selectedKeys, confirm, dataIndex) => {
|
||||
@ -170,7 +175,7 @@ class BaseListPage extends React.Component {
|
||||
const steps = TourConfig.getSteps();
|
||||
steps.map((item, index) => {
|
||||
if (!index) {
|
||||
item.target = () => document.querySelector("table");
|
||||
item.target = () => document.querySelector(".ant-table");
|
||||
} else {
|
||||
item.target = () => document.getElementById(item.id) || null;
|
||||
}
|
||||
|
@ -13,15 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
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 IframeEditor from "./IframeEditor";
|
||||
import {Tabs} from "antd";
|
||||
import i18next from "i18next";
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
import Editor from "./common/Editor";
|
||||
|
||||
const CasbinEditor = ({model, onModelTextChange}) => {
|
||||
const [activeKey, setActiveKey] = useState("advanced");
|
||||
@ -68,10 +64,15 @@ const CasbinEditor = ({model, onModelTextChange}) => {
|
||||
|
||||
return (
|
||||
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
|
||||
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
|
||||
<TabPane tab={i18next.t("model:Basic Editor")} key="basic" />
|
||||
<TabPane tab={i18next.t("model:Advanced Editor")} key="advanced" />
|
||||
</Tabs>
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={handleTabChange}
|
||||
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"}}>
|
||||
{activeKey === "advanced" ? (
|
||||
<IframeEditor
|
||||
@ -81,11 +82,10 @@ const CasbinEditor = ({model, onModelTextChange}) => {
|
||||
style={{width: "100%", height: "100%"}}
|
||||
/>
|
||||
) : (
|
||||
<CodeMirror
|
||||
<Editor
|
||||
value={localModelText}
|
||||
className="full-height-editor no-horizontal-scroll-editor"
|
||||
options={{mode: "properties", theme: "default"}}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
readOnly={Setting.builtInObject(model)}
|
||||
onChange={value => {
|
||||
handleModelTextChange(value);
|
||||
}}
|
||||
/>
|
||||
|
@ -109,7 +109,7 @@ class EntryPage extends React.Component {
|
||||
<React.Fragment>
|
||||
<CustomHead headerHtml={this.state.application?.headerHtml} />
|
||||
<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")}
|
||||
style={{width: "100%", margin: "0 auto", position: "absolute"}} />
|
||||
<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="/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/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="/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} />} />
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import {Button, Table, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
@ -202,12 +202,16 @@ class GroupListPage extends BaseListPage {
|
||||
return (
|
||||
<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>
|
||||
<PopconfirmModal
|
||||
disabled={record.haveChildren}
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteGroup(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
{
|
||||
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
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteGroup(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -114,7 +114,7 @@ class InvitationEditPage extends React.Component {
|
||||
const selectedOrganization = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner);
|
||||
defaultApplication = selectedOrganization.defaultApplication;
|
||||
if (!defaultApplication) {
|
||||
Setting.showMessage("error", i18next.t("invitation:You need to specify a default application for ") + selectedOrganization.name);
|
||||
Setting.showMessage("error", i18next.t("invitation:You need to first specify a default application for organization: ") + selectedOrganization.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +170,16 @@ class LdapEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</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"}}>
|
||||
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
|
||||
{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";
|
||||
|
||||
function ManagementPage(props) {
|
||||
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const navItems = props.account?.organization?.navItems;
|
||||
const widgetItems = props.account?.organization?.widgetItems;
|
||||
|
||||
function 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() {
|
||||
if (props.account === undefined) {
|
||||
return null;
|
||||
@ -188,29 +218,16 @@ function ManagementPage(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{renderRightDropdown()}
|
||||
<ThemeSelect
|
||||
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 />
|
||||
{renderWidgets()}
|
||||
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
||||
<OrganizationSelect
|
||||
initValue={Setting.getOrganization()}
|
||||
withAll={true}
|
||||
style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
|
||||
className="org-select"
|
||||
style={{display: Setting.isMobile() ? "none" : "flex"}}
|
||||
onChange={(value) => {
|
||||
Setting.setOrganization(value);
|
||||
}}
|
||||
className="select-box"
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
@ -323,13 +340,7 @@ function ManagementPage(props) {
|
||||
}
|
||||
}
|
||||
|
||||
const navItems = props.account.organization.navItems;
|
||||
|
||||
if (!Array.isArray(navItems)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (navItems.includes("all")) {
|
||||
if (navItemsIsAll()) {
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -443,8 +454,6 @@ function ManagementPage(props) {
|
||||
return Setting.isMobile() || window.location.pathname.startsWith("/trees");
|
||||
}
|
||||
|
||||
const menuStyleRight = Setting.isAdminUser(props.account) && !Setting.isMobile() ? "calc(180px + 280px)" : "320px";
|
||||
|
||||
const onClose = () => {
|
||||
setMenuVisible(false);
|
||||
};
|
||||
@ -456,34 +465,40 @@ function ManagementPage(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EnableMfaNotification account={props.account} />
|
||||
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}} >
|
||||
{props.requiredEnableMfa || (Setting.isMobile() ?
|
||||
<React.Fragment>
|
||||
<Drawer title={i18next.t("general:Close")} placement="left" visible={menuVisible} onClose={onClose}>
|
||||
<Menu
|
||||
items={getMenuItems()}
|
||||
mode={"inline"}
|
||||
selectedKeys={[props.selectedMenuKey]}
|
||||
style={{lineHeight: "64px"}}
|
||||
onClick={onClose}
|
||||
>
|
||||
</Menu>
|
||||
</Drawer>
|
||||
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
|
||||
{i18next.t("general:Menu")}
|
||||
</Button>
|
||||
</React.Fragment> :
|
||||
<Menu
|
||||
onClick={onClose}
|
||||
items={getMenuItems()}
|
||||
mode={"horizontal"}
|
||||
selectedKeys={[props.selectedMenuKey]}
|
||||
style={{position: "absolute", left: 0, right: menuStyleRight, 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"}} >
|
||||
{
|
||||
renderAccountMenu()
|
||||
props.requiredEnableMfa || (Setting.isMobile() ? (
|
||||
<React.Fragment>
|
||||
<Drawer title={i18next.t("general:Close")} placement="left" open={menuVisible} onClose={onClose}>
|
||||
<Menu
|
||||
items={getMenuItems()}
|
||||
mode={"inline"}
|
||||
selectedKeys={[props.selectedMenuKey]}
|
||||
style={{lineHeight: "64px"}}
|
||||
onClick={onClose}
|
||||
>
|
||||
</Menu>
|
||||
</Drawer>
|
||||
<Button icon={<BarsOutlined />} onClick={showMenu} type="text">
|
||||
{i18next.t("general:Menu")}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
// Padding 1px for Menu Item Highlight border
|
||||
<div style={{flex: 1, overflow: "hidden", paddingBottom: "1px"}}>
|
||||
<Menu
|
||||
onClick={onClose}
|
||||
items={getMenuItems()}
|
||||
mode={"horizontal"}
|
||||
selectedKeys={[props.selectedMenuKey]}
|
||||
style={{backgroundColor: props.themeAlgorithm.includes("dark") ? "black" : "white"}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<div style={{flexShrink: 0}}>
|
||||
{renderAccountMenu()}
|
||||
</div>
|
||||
</Header>
|
||||
<Content style={{display: "flex", flexDirection: "column"}} >
|
||||
{isWithoutCard() ?
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user