mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-09 01:13:41 +08:00
Compare commits
117 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
c0ec73dfd3 | |||
b1b6ebe692 | |||
a0931e4597 | |||
c181006661 | |||
2e83e49492 | |||
5661942175 | |||
7f9f7c6468 | |||
b7a818e2d3 | |||
1a8cfe4ee6 | |||
b3526de675 | |||
3b9e08b70d | |||
cfc6015aca | |||
1600a6799a | |||
ca60cc3a33 | |||
df295717f0 | |||
e3001671a2 | |||
bbe2162e27 | |||
92b5ce3722 | |||
bad21fb6bb | |||
5a78dcf06d | |||
558b168477 | |||
802b6812a9 | |||
a5a627f92e | |||
9701818a6e | |||
06986fbd41 | |||
3d12ac8dc2 | |||
f01839123f | |||
e1b3b0ac6a | |||
4b0a2fdbfc | |||
db551eb24a | |||
18b49bb731 | |||
17653888a3 | |||
ee16616df4 | |||
ea450005e0 | |||
4c5ad14f6b | |||
49dda2aea5 | |||
a74a004540 | |||
2b89f6b37b | |||
c699e35e6b | |||
e28d90d0aa | |||
4fc7600865 | |||
19f62a461b | |||
7ddc2778c0 | |||
b96fa2a995 | |||
fcfb73af6e |
@ -1,10 +1,10 @@
|
|||||||
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
||||||
WORKDIR /web
|
WORKDIR /web
|
||||||
COPY ./web .
|
COPY ./web .
|
||||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
|
RUN yarn install --frozen-lockfile --network-timeout 1000000 && NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.20.12 AS BACK
|
FROM --platform=$BUILDPLATFORM golang:1.21.13 AS BACK
|
||||||
WORKDIR /go/src/casdoor
|
WORKDIR /go/src/casdoor
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN ./build.sh
|
RUN ./build.sh
|
||||||
|
@ -99,6 +99,7 @@ p, *, *, GET, /api/get-all-objects, *, *
|
|||||||
p, *, *, GET, /api/get-all-actions, *, *
|
p, *, *, GET, /api/get-all-actions, *, *
|
||||||
p, *, *, GET, /api/get-all-roles, *, *
|
p, *, *, GET, /api/get-all-roles, *, *
|
||||||
p, *, *, GET, /api/run-casbin-command, *, *
|
p, *, *, GET, /api/run-casbin-command, *, *
|
||||||
|
p, *, *, POST, /api/refresh-engines, *, *
|
||||||
p, *, *, GET, /api/get-invitation-info, *, *
|
p, *, *, GET, /api/get-invitation-info, *, *
|
||||||
p, *, *, GET, /api/faceid-signin-begin, *, *
|
p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||||
`
|
`
|
||||||
@ -156,7 +157,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
|||||||
|
|
||||||
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||||
if method == "POST" {
|
if method == "POST" {
|
||||||
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" {
|
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" {
|
||||||
return true
|
return true
|
||||||
} else if urlPath == "/api/update-user" {
|
} else if urlPath == "/api/update-user" {
|
||||||
// Allow ordinary users to update their own information
|
// Allow ordinary users to update their own information
|
||||||
|
@ -139,6 +139,8 @@ func (c *ApiController) Signup() {
|
|||||||
invitationName = invitation.Name
|
invitationName = invitation.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userEmailVerified := false
|
||||||
|
|
||||||
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
|
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && authForm.Email != "" {
|
||||||
var checkResult *object.VerifyResult
|
var checkResult *object.VerifyResult
|
||||||
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
|
checkResult, err = object.CheckVerificationCode(authForm.Email, authForm.EmailCode, c.GetAcceptLanguage())
|
||||||
@ -150,6 +152,8 @@ func (c *ApiController) Signup() {
|
|||||||
c.ResponseError(checkResult.Msg)
|
c.ResponseError(checkResult.Msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userEmailVerified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkPhone string
|
var checkPhone string
|
||||||
@ -228,6 +232,7 @@ func (c *ApiController) Signup() {
|
|||||||
Karma: 0,
|
Karma: 0,
|
||||||
Invitation: invitationName,
|
Invitation: invitationName,
|
||||||
InvitationCode: authForm.InvitationCode,
|
InvitationCode: authForm.InvitationCode,
|
||||||
|
EmailVerified: userEmailVerified,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(organization.Tags) > 0 {
|
if len(organization.Tags) > 0 {
|
||||||
@ -249,6 +254,10 @@ func (c *ApiController) Signup() {
|
|||||||
user.Groups = []string{invitation.SignupGroup}
|
user.Groups = []string{invitation.SignupGroup}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if application.DefaultGroup != "" && user.Groups == nil {
|
||||||
|
user.Groups = []string{application.DefaultGroup}
|
||||||
|
}
|
||||||
|
|
||||||
affected, err := object.AddUser(user)
|
affected, err := object.AddUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
@ -458,6 +467,10 @@ func (c *ApiController) GetAccount() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if organization != nil && len(organization.CountryCodes) == 1 && u != nil && u.CountryCode == "" {
|
||||||
|
u.CountryCode = organization.CountryCodes[0]
|
||||||
|
}
|
||||||
|
|
||||||
accessToken := c.GetSessionToken()
|
accessToken := c.GetSessionToken()
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)
|
accessToken, err = object.GetAccessTokenByUser(user, c.Ctx.Request.Host)
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/casdoor/casdoor/captcha"
|
"github.com/casdoor/casdoor/captcha"
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/casdoor/form"
|
"github.com/casdoor/casdoor/form"
|
||||||
|
"github.com/casdoor/casdoor/i18n"
|
||||||
"github.com/casdoor/casdoor/idp"
|
"github.com/casdoor/casdoor/idp"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
"github.com/casdoor/casdoor/proxy"
|
"github.com/casdoor/casdoor/proxy"
|
||||||
@ -54,6 +55,11 @@ func tokenToResponse(token *object.Token) *Response {
|
|||||||
|
|
||||||
// HandleLoggedIn ...
|
// HandleLoggedIn ...
|
||||||
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *form.AuthForm) (resp *Response) {
|
||||||
|
if user.IsForbidden {
|
||||||
|
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userId := user.GetId()
|
userId := user.GetId()
|
||||||
|
|
||||||
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
|
||||||
@ -306,6 +312,35 @@ func isProxyProviderType(providerType string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
|
||||||
|
if object.IsNeedPromptMfa(organization, user) {
|
||||||
|
// The prompt page needs the user to be srigned in
|
||||||
|
c.SetSessionUsername(user.GetId())
|
||||||
|
c.ResponseOk(object.RequiredMfa)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.IsMfaEnabled() {
|
||||||
|
c.setMfaUserSession(user.GetId())
|
||||||
|
mfaList := object.GetAllMfaProps(user, true)
|
||||||
|
mfaAllowList := []*object.MfaProps{}
|
||||||
|
for _, prop := range mfaList {
|
||||||
|
if prop.MfaType == verificationType || !prop.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mfaAllowList = append(mfaAllowList, prop)
|
||||||
|
}
|
||||||
|
if len(mfaAllowList) >= 1 {
|
||||||
|
c.SetSession("verificationCodeType", verificationType)
|
||||||
|
c.Ctx.Input.CruSession.SessionRelease(c.Ctx.ResponseWriter)
|
||||||
|
c.ResponseOk(object.NextMfa, mfaAllowList)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Login ...
|
// Login ...
|
||||||
// @Title Login
|
// @Title Login
|
||||||
// @Tag Login API
|
// @Tag Login API
|
||||||
@ -331,6 +366,8 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verificationType := ""
|
||||||
|
|
||||||
if authForm.Username != "" {
|
if authForm.Username != "" {
|
||||||
if authForm.Type == ResponseTypeLogin {
|
if authForm.Type == ResponseTypeLogin {
|
||||||
if c.GetSessionUsername() != "" {
|
if c.GetSessionUsername() != "" {
|
||||||
@ -366,11 +403,27 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
faceIdProvider, err := object.GetFaceIdProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if faceIdProvider == nil {
|
||||||
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
if err := object.CheckFaceId(user, authForm.FaceId, c.GetAcceptLanguage()); err != nil {
|
||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ok, err := user.CheckUserFace(authForm.FaceIdImage, faceIdProvider)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
c.ResponseError(i18n.Translate(c.GetAcceptLanguage(), "check:Face data does not exist, cannot log in"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if authForm.Password == "" {
|
} else if authForm.Password == "" {
|
||||||
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
if user, err = object.GetUserByFields(authForm.Organization, authForm.Username); err != nil {
|
||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
@ -425,6 +478,20 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error(), nil)
|
c.ResponseError(err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if verificationCodeType == object.VerifyTypePhone {
|
||||||
|
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 {
|
} else {
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||||
@ -515,16 +582,7 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if object.IsNeedPromptMfa(organization, user) {
|
if checkMfaEnable(c, user, organization, verificationType) {
|
||||||
// The prompt page needs the user to be signed in
|
|
||||||
c.SetSessionUsername(user.GetId())
|
|
||||||
c.ResponseOk(object.RequiredMfa)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.IsMfaEnabled() {
|
|
||||||
c.setMfaUserSession(user.GetId())
|
|
||||||
c.ResponseOk(object.NextMfa, user.GetPreferredMfaProps(true))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,6 +623,9 @@ func (c *ApiController) Login() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if provider == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("auth:The provider: %s does not exist"), authForm.Provider))
|
||||||
|
}
|
||||||
|
|
||||||
providerItem := application.GetProviderItem(provider.Name)
|
providerItem := application.GetProviderItem(provider.Name)
|
||||||
if !providerItem.IsProviderVisible() {
|
if !providerItem.IsProviderVisible() {
|
||||||
@ -650,16 +711,17 @@ func (c *ApiController) Login() {
|
|||||||
|
|
||||||
if user != nil && !user.IsDeleted {
|
if user != nil && !user.IsDeleted {
|
||||||
// Sign in via OAuth (want to sign up but already have account)
|
// Sign in via OAuth (want to sign up but already have account)
|
||||||
|
|
||||||
if user.IsForbidden {
|
|
||||||
c.ResponseError(c.T("check:The user is forbidden to sign in, please contact the administrator"))
|
|
||||||
}
|
|
||||||
// sync info from 3rd-party if possible
|
// sync info from 3rd-party if possible
|
||||||
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
_, err = object.SetUserOAuthProperties(organization, user, provider.Type, userInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if checkMfaEnable(c, user, organization, verificationType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||||
|
|
||||||
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
c.Ctx.Input.SetParam("recordUserId", user.GetId())
|
||||||
@ -866,18 +928,32 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if authForm.Passcode != "" {
|
if authForm.Passcode != "" {
|
||||||
|
if authForm.MfaType == c.GetSession("verificationCodeType") {
|
||||||
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
|
return
|
||||||
|
}
|
||||||
user.CountryCode = user.GetCountryCode(user.CountryCode)
|
user.CountryCode = user.GetCountryCode(user.CountryCode)
|
||||||
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetPreferredMfaProps(false))
|
mfaUtil := object.GetMfaUtil(authForm.MfaType, user.GetMfaProps(authForm.MfaType, false))
|
||||||
if mfaUtil == nil {
|
if mfaUtil == nil {
|
||||||
c.ResponseError("Invalid multi-factor authentication type")
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Passcode)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !passed {
|
||||||
err = mfaUtil.Verify(authForm.Passcode)
|
err = mfaUtil.Verify(authForm.Passcode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetSession("verificationCodeType", "")
|
||||||
} else if authForm.RecoveryCode != "" {
|
} else if authForm.RecoveryCode != "" {
|
||||||
err = object.MfaRecover(user, authForm.RecoveryCode)
|
err = object.MfaRecover(user, authForm.RecoveryCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -890,7 +966,11 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
|
if authForm.ClientId == "" {
|
||||||
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
|
||||||
|
} else {
|
||||||
|
application, err = object.GetApplicationByClientId(authForm.ClientId)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
@ -920,6 +1000,10 @@ func (c *ApiController) Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if authForm.Provider == "" {
|
||||||
|
authForm.Provider = authForm.ProviderBack
|
||||||
|
}
|
||||||
|
|
||||||
user := c.getCurrentUser()
|
user := c.getCurrentUser()
|
||||||
resp = c.HandleLoggedIn(application, user, &authForm)
|
resp = c.HandleLoggedIn(application, user, &authForm)
|
||||||
|
|
||||||
@ -930,6 +1014,18 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if authForm.Language != "" {
|
||||||
|
user := c.getCurrentUser()
|
||||||
|
if user != nil {
|
||||||
|
user.Language = authForm.Language
|
||||||
|
_, err = object.UpdateUser(user.GetId(), user, []string{"language"}, user.IsAdmin)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = resp
|
c.Data["json"] = resp
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,76 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CLIVersionInfo struct {
|
||||||
|
Version string
|
||||||
|
BinaryPath string
|
||||||
|
BinaryTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cliVersionCache = make(map[string]*CLIVersionInfo)
|
||||||
|
cliVersionMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// getCLIVersion
|
||||||
|
// @Title getCLIVersion
|
||||||
|
// @Description Get CLI version with cache mechanism
|
||||||
|
// @Param language string The language of CLI (go/java/rust etc.)
|
||||||
|
// @Return string The version string of CLI
|
||||||
|
// @Return error Error if CLI execution fails
|
||||||
|
func getCLIVersion(language string) (string, error) {
|
||||||
|
binaryName := fmt.Sprintf("casbin-%s-cli", language)
|
||||||
|
|
||||||
|
binaryPath, err := exec.LookPath(binaryName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("executable file not found: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get binary info: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cliVersionMutex.RLock()
|
||||||
|
if info, exists := cliVersionCache[language]; exists {
|
||||||
|
if info.BinaryPath == binaryPath && info.BinaryTime == fileInfo.ModTime() {
|
||||||
|
cliVersionMutex.RUnlock()
|
||||||
|
return info.Version, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cliVersionMutex.RUnlock()
|
||||||
|
|
||||||
|
cmd := exec.Command(binaryName, "--version")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get CLI version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
version := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
cliVersionMutex.Lock()
|
||||||
|
cliVersionCache[language] = &CLIVersionInfo{
|
||||||
|
Version: version,
|
||||||
|
BinaryPath: binaryPath,
|
||||||
|
BinaryTime: fileInfo.ModTime(),
|
||||||
|
}
|
||||||
|
cliVersionMutex.Unlock()
|
||||||
|
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
||||||
tempFiles := []string{}
|
tempFiles := []string{}
|
||||||
newArgs := []string{}
|
newArgs := []string{}
|
||||||
@ -57,6 +120,11 @@ func processArgsToTempFiles(args []string) ([]string, []string, error) {
|
|||||||
// @Success 200 {object} controllers.Response The Response object
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
// @router /run-casbin-command [get]
|
// @router /run-casbin-command [get]
|
||||||
func (c *ApiController) RunCasbinCommand() {
|
func (c *ApiController) RunCasbinCommand() {
|
||||||
|
if err := validateIdentifier(c); err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
language := c.Input().Get("language")
|
language := c.Input().Get("language")
|
||||||
argString := c.Input().Get("args")
|
argString := c.Input().Get("args")
|
||||||
|
|
||||||
@ -84,6 +152,16 @@ func (c *ApiController) RunCasbinCommand() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 && args[0] == "--version" {
|
||||||
|
version, err := getCLIVersion(language)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ResponseOk(version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tempFiles, processedArgs, err := processArgsToTempFiles(args)
|
tempFiles, processedArgs, err := processArgsToTempFiles(args)
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, file := range tempFiles {
|
for _, file := range tempFiles {
|
||||||
@ -112,3 +190,58 @@ func (c *ApiController) RunCasbinCommand() {
|
|||||||
output = strings.TrimSuffix(output, "\n")
|
output = strings.TrimSuffix(output, "\n")
|
||||||
c.ResponseOk(output)
|
c.ResponseOk(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateIdentifier
|
||||||
|
// @Title validateIdentifier
|
||||||
|
// @Description Validate the request hash and timestamp
|
||||||
|
// @Param hash string The SHA-256 hash string
|
||||||
|
// @Return error Returns error if validation fails, nil if successful
|
||||||
|
func validateIdentifier(c *ApiController) error {
|
||||||
|
language := c.Input().Get("language")
|
||||||
|
args := c.Input().Get("args")
|
||||||
|
hash := c.Input().Get("m")
|
||||||
|
timestamp := c.Input().Get("t")
|
||||||
|
|
||||||
|
if hash == "" || timestamp == "" || language == "" || args == "" {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
timeDiff := time.Since(requestTime)
|
||||||
|
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]string{
|
||||||
|
"language": language,
|
||||||
|
"args": args,
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k := range params {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var paramParts []string
|
||||||
|
for _, k := range keys {
|
||||||
|
paramParts = append(paramParts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||||
|
}
|
||||||
|
paramString := strings.Join(paramParts, "&")
|
||||||
|
|
||||||
|
version := "casbin-editor-v1"
|
||||||
|
rawString := fmt.Sprintf("%s|%s|%s", version, timestamp, paramString)
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write([]byte(rawString))
|
||||||
|
|
||||||
|
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
||||||
|
if calculatedHash != strings.ToLower(hash) {
|
||||||
|
return fmt.Errorf("invalid identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
519
controllers/cli_downloader.go
Normal file
519
controllers/cli_downloader.go
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beego/beego"
|
||||||
|
"github.com/casdoor/casdoor/proxy"
|
||||||
|
"github.com/casdoor/casdoor/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
javaCliRepo = "https://api.github.com/repos/jcasbin/casbin-java-cli/releases/latest"
|
||||||
|
goCliRepo = "https://api.github.com/repos/casbin/casbin-go-cli/releases/latest"
|
||||||
|
rustCliRepo = "https://api.github.com/repos/casbin-rs/casbin-rust-cli/releases/latest"
|
||||||
|
downloadFolder = "bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReleaseInfo struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Assets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"browser_download_url"`
|
||||||
|
} `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title getBinaryNames
|
||||||
|
// @Description Get binary names for different platforms and architectures
|
||||||
|
// @Success 200 {map[string]string} map[string]string "Binary names map"
|
||||||
|
func getBinaryNames() map[string]string {
|
||||||
|
const (
|
||||||
|
golang = "go"
|
||||||
|
java = "java"
|
||||||
|
rust = "rust"
|
||||||
|
)
|
||||||
|
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
archMap := map[string]struct{ goArch, rustArch string }{
|
||||||
|
"amd64": {"x86_64", "x86_64"},
|
||||||
|
"arm64": {"arm64", "aarch64"},
|
||||||
|
}
|
||||||
|
|
||||||
|
archNames, ok := archMap[arch]
|
||||||
|
if !ok {
|
||||||
|
archNames = struct{ goArch, rustArch string }{arch, arch}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return map[string]string{
|
||||||
|
golang: fmt.Sprintf("casbin-go-cli_Windows_%s.zip", archNames.goArch),
|
||||||
|
java: "casbin-java-cli.jar",
|
||||||
|
rust: fmt.Sprintf("casbin-rust-cli-%s-pc-windows-gnu", archNames.rustArch),
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
return map[string]string{
|
||||||
|
golang: fmt.Sprintf("casbin-go-cli_Darwin_%s.tar.gz", archNames.goArch),
|
||||||
|
java: "casbin-java-cli.jar",
|
||||||
|
rust: fmt.Sprintf("casbin-rust-cli-%s-apple-darwin", archNames.rustArch),
|
||||||
|
}
|
||||||
|
case "linux":
|
||||||
|
return map[string]string{
|
||||||
|
golang: fmt.Sprintf("casbin-go-cli_Linux_%s.tar.gz", archNames.goArch),
|
||||||
|
java: "casbin-java-cli.jar",
|
||||||
|
rust: fmt.Sprintf("casbin-rust-cli-%s-unknown-linux-gnu", archNames.rustArch),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title getFinalBinaryName
|
||||||
|
// @Description Get final binary name for specific language
|
||||||
|
// @Param lang string true "Language type (go/java/rust)"
|
||||||
|
// @Success 200 {string} string "Final binary name"
|
||||||
|
func getFinalBinaryName(lang string) string {
|
||||||
|
switch lang {
|
||||||
|
case "go":
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "casbin-go-cli.exe"
|
||||||
|
}
|
||||||
|
return "casbin-go-cli"
|
||||||
|
case "java":
|
||||||
|
return "casbin-java-cli.jar"
|
||||||
|
case "rust":
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "casbin-rust-cli.exe"
|
||||||
|
}
|
||||||
|
return "casbin-rust-cli"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title getLatestCLIURL
|
||||||
|
// @Description Get latest CLI download URL from GitHub
|
||||||
|
// @Param repoURL string true "GitHub repository URL"
|
||||||
|
// @Param language string true "Language type"
|
||||||
|
// @Success 200 {string} string "Download URL and version"
|
||||||
|
func getLatestCLIURL(repoURL string, language string) (string, string, error) {
|
||||||
|
client := proxy.GetHttpClient(repoURL)
|
||||||
|
resp, err := client.Get(repoURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to fetch release info: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var release ReleaseInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryNames := getBinaryNames()
|
||||||
|
if binaryNames == nil {
|
||||||
|
return "", "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryName := binaryNames[language]
|
||||||
|
for _, asset := range release.Assets {
|
||||||
|
if asset.Name == binaryName {
|
||||||
|
return asset.URL, release.TagName, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("no suitable binary found for OS: %s, language: %s", runtime.GOOS, language)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title extractGoCliFile
|
||||||
|
// @Description Extract the Go CLI file
|
||||||
|
// @Param filePath string true "The file path"
|
||||||
|
// @Success 200 {string} string "The extracted file path"
|
||||||
|
// @router /extractGoCliFile [post]
|
||||||
|
func extractGoCliFile(filePath string) error {
|
||||||
|
tempDir := filepath.Join(downloadFolder, "temp")
|
||||||
|
if err := os.MkdirAll(tempDir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
if err := unzipFile(filePath, tempDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := untarFile(filePath, tempDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execName := "casbin-go-cli"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
execName += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
var execPath string
|
||||||
|
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if info.Name() == execName {
|
||||||
|
execPath = path
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPath := filepath.Join(downloadFolder, execName)
|
||||||
|
if err := os.Rename(execPath, finalPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Remove(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title unzipFile
|
||||||
|
// @Description Unzip the file
|
||||||
|
// @Param zipPath string true "The zip file path"
|
||||||
|
// @Param destDir string true "The destination directory"
|
||||||
|
// @Success 200 {string} string "The extracted file path"
|
||||||
|
// @router /unzipFile [post]
|
||||||
|
func unzipFile(zipPath, destDir string) error {
|
||||||
|
r, err := zip.OpenReader(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
fpath := filepath.Join(destDir, f.Name)
|
||||||
|
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
os.MkdirAll(fpath, os.ModePerm)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
outFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(outFile, rc)
|
||||||
|
outFile.Close()
|
||||||
|
rc.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title untarFile
|
||||||
|
// @Description Untar the file
|
||||||
|
// @Param tarPath string true "The tar file path"
|
||||||
|
// @Param destDir string true "The destination directory"
|
||||||
|
// @Success 200 {string} string "The extracted file path"
|
||||||
|
// @router /untarFile [post]
|
||||||
|
func untarFile(tarPath, destDir string) error {
|
||||||
|
file, err := os.Open(tarPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
gzr, err := gzip.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(destDir, header.Name)
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeReg:
|
||||||
|
outFile, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(outFile, tr); err != nil {
|
||||||
|
outFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outFile.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title createJavaCliWrapper
|
||||||
|
// @Description Create the Java CLI wrapper
|
||||||
|
// @Param binPath string true "The binary path"
|
||||||
|
// @Success 200 {string} string "The created file path"
|
||||||
|
// @router /createJavaCliWrapper [post]
|
||||||
|
func createJavaCliWrapper(binPath string) error {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Create a Windows CMD file
|
||||||
|
cmdPath := filepath.Join(binPath, "casbin-java-cli.cmd")
|
||||||
|
cmdContent := fmt.Sprintf(`@echo off
|
||||||
|
java -jar "%s\casbin-java-cli.jar" %%*`, binPath)
|
||||||
|
|
||||||
|
err := os.WriteFile(cmdPath, []byte(cmdContent), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create Unix shell script
|
||||||
|
shPath := filepath.Join(binPath, "casbin-java-cli")
|
||||||
|
shContent := fmt.Sprintf(`#!/bin/sh
|
||||||
|
java -jar "%s/casbin-java-cli.jar" "$@"`, binPath)
|
||||||
|
|
||||||
|
err := os.WriteFile(shPath, []byte(shContent), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title downloadCLI
|
||||||
|
// @Description Download and setup CLI tools
|
||||||
|
// @Success 200 {error} error "Error if any"
|
||||||
|
func downloadCLI() error {
|
||||||
|
pathEnv := os.Getenv("PATH")
|
||||||
|
binPath, err := filepath.Abs(downloadFolder)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path to download directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(pathEnv, binPath) {
|
||||||
|
newPath := fmt.Sprintf("%s%s%s", binPath, string(os.PathListSeparator), pathEnv)
|
||||||
|
if err := os.Setenv("PATH", newPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to update PATH environment variable: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(downloadFolder, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create download directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repos := map[string]string{
|
||||||
|
"java": javaCliRepo,
|
||||||
|
"go": goCliRepo,
|
||||||
|
"rust": rustCliRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
for lang, repo := range repos {
|
||||||
|
cliURL, version, err := getLatestCLIURL(repo, lang)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to get %s CLI URL: %v\n", lang, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang])
|
||||||
|
fmt.Printf("downloading %s CLI: %s\n", lang, cliURL)
|
||||||
|
|
||||||
|
client := proxy.GetHttpClient(cliURL)
|
||||||
|
resp, err := client.Get(cliURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(originalPath), 0o755); err != nil {
|
||||||
|
fmt.Printf("failed to create directory for %s CLI: %v\n", lang, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := originalPath + ".tmp"
|
||||||
|
out, err := os.Create(tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to create or write %s CLI: %v\n", lang, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
out.Close()
|
||||||
|
os.Remove(tmpFile)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err = io.Copy(out, resp.Body); err != nil ||
|
||||||
|
out.Close() != nil ||
|
||||||
|
os.Rename(tmpFile, originalPath) != nil {
|
||||||
|
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if lang == "go" {
|
||||||
|
if err := extractGoCliFile(originalPath); err != nil {
|
||||||
|
fmt.Printf("failed to extract Go CLI: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
||||||
|
if err := os.Rename(originalPath, finalPath); err != nil {
|
||||||
|
fmt.Printf("failed to rename %s CLI: %v\n", lang, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
execPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
||||||
|
if err := os.Chmod(execPath, 0o755); err != nil {
|
||||||
|
fmt.Printf("failed to set %s CLI execution permission: %v\n", lang, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("downloaded %s CLI version: %s\n", lang, version)
|
||||||
|
|
||||||
|
if lang == "java" {
|
||||||
|
if err := createJavaCliWrapper(binPath); err != nil {
|
||||||
|
fmt.Printf("failed to create Java CLI wrapper: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title RefreshEngines
|
||||||
|
// @Tag CLI API
|
||||||
|
// @Description Refresh all CLI engines
|
||||||
|
// @Param m query string true "Hash for request validation"
|
||||||
|
// @Param t query string true "Timestamp for request validation"
|
||||||
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
|
// @router /refresh-engines [post]
|
||||||
|
func (c *ApiController) RefreshEngines() {
|
||||||
|
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||||
|
c.ResponseError("refresh engines is only available in demo mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := c.Input().Get("m")
|
||||||
|
timestamp := c.Input().Get("t")
|
||||||
|
|
||||||
|
if hash == "" || timestamp == "" {
|
||||||
|
c.ResponseError("invalid identifier")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError("invalid identifier")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timeDiff := time.Since(requestTime)
|
||||||
|
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
||||||
|
c.ResponseError("invalid identifier")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
version := "casbin-editor-v1"
|
||||||
|
rawString := fmt.Sprintf("%s|%s", version, timestamp)
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write([]byte(rawString))
|
||||||
|
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
||||||
|
|
||||||
|
if calculatedHash != strings.ToLower(hash) {
|
||||||
|
c.ResponseError("invalid identifier")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = downloadCLI()
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(fmt.Sprintf("failed to refresh engines: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ResponseOk(map[string]string{
|
||||||
|
"status": "success",
|
||||||
|
"message": "CLI engines updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title ScheduleCLIUpdater
|
||||||
|
// @Description Start periodic CLI update scheduler
|
||||||
|
func ScheduleCLIUpdater() {
|
||||||
|
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
err := downloadCLI()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to update CLI: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("CLI updated successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title DownloadCLI
|
||||||
|
// @Description Download the CLI
|
||||||
|
// @Success 200 {string} string "The downloaded file path"
|
||||||
|
// @router /downloadCLI [post]
|
||||||
|
func DownloadCLI() error {
|
||||||
|
return downloadCLI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title InitCLIDownloader
|
||||||
|
// @Description Initialize CLI downloader and start update scheduler
|
||||||
|
func InitCLIDownloader() {
|
||||||
|
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.SafeGoroutine(func() {
|
||||||
|
err := DownloadCLI()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleCLIUpdater()
|
||||||
|
})
|
||||||
|
}
|
@ -70,7 +70,25 @@ func (c *ApiController) GetGroups() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
|
groupsHaveChildrenMap, err := object.GetGroupsHaveChildrenMap(groups)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
_, ok := groupsHaveChildrenMap[group.Name]
|
||||||
|
if ok {
|
||||||
|
group.HaveChildren = true
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, ok := groupsHaveChildrenMap[group.ParentId]
|
||||||
|
if ok {
|
||||||
|
group.ParentName = parent.DisplayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = object.ExtendGroupsWithUsers(groups)
|
err = object.ExtendGroupsWithUsers(groups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
@ -78,7 +96,7 @@ func (c *ApiController) GetGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.ResponseOk(groups, paginator.Nums())
|
c.ResponseOk(groups, paginator.Nums())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +269,11 @@ func (c *ApiController) SyncLdapUsers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exist, failed, _ := object.SyncLdapUsers(owner, users, ldapId)
|
exist, failed, err := object.SyncLdapUsers(owner, users, ldapId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.ResponseOk(&LdapSyncResp{
|
c.ResponseOk(&LdapSyncResp{
|
||||||
Exist: exist,
|
Exist: exist,
|
||||||
|
@ -124,7 +124,9 @@ func (c *ApiController) UpdateOrganization() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization))
|
isGlobalAdmin, _ := c.isGlobalAdmin()
|
||||||
|
|
||||||
|
c.Data["json"] = wrapActionResponse(object.UpdateOrganization(id, &organization, isGlobalAdmin))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *RootController) HandleScim() {
|
func (c *RootController) HandleScim() {
|
||||||
|
_, ok := c.RequireAdmin()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path := c.Ctx.Request.URL.Path
|
path := c.Ctx.Request.URL.Path
|
||||||
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
c.Ctx.Request.URL.Path = strings.TrimPrefix(path, "/scim")
|
||||||
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
scim.Server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request)
|
||||||
|
@ -93,7 +93,7 @@ func (c *ApiController) SendEmail() {
|
|||||||
|
|
||||||
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
// when receiver is the reserved keyword: "TestSmtpServer", it means to test the SMTP server instead of sending a real Email
|
||||||
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
if len(emailForm.Receivers) == 1 && emailForm.Receivers[0] == "TestSmtpServer" {
|
||||||
err = object.DailSmtpServer(provider)
|
err = object.TestSmtpServer(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -321,6 +321,11 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
respondWithInactiveToken := func() {
|
||||||
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
tokenTypeHint := c.Input().Get("token_type_hint")
|
tokenTypeHint := c.Input().Get("token_type_hint")
|
||||||
var token *object.Token
|
var token *object.Token
|
||||||
if tokenTypeHint != "" {
|
if tokenTypeHint != "" {
|
||||||
@ -329,7 +334,12 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
c.ResponseTokenError(err.Error())
|
c.ResponseTokenError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if token == nil {
|
if token == nil || token.ExpiresIn <= 0 {
|
||||||
|
respondWithInactiveToken()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.ExpiresIn <= 0 {
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
return
|
return
|
||||||
@ -340,12 +350,11 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
|
|
||||||
if application.TokenFormat == "JWT-Standard" {
|
if application.TokenFormat == "JWT-Standard" {
|
||||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||||
if err != nil || jwtToken.Valid() != nil {
|
if err != nil {
|
||||||
// and token revoked case. but we not implement
|
// and token revoked case. but we not implement
|
||||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||||
// refs: https://tools.ietf.org/html/rfc7009
|
// refs: https://tools.ietf.org/html/rfc7009
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
respondWithInactiveToken()
|
||||||
c.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,21 +374,17 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||||
if err != nil || jwtToken.Valid() != nil {
|
if err != nil {
|
||||||
// and token revoked case. but we not implement
|
// and token revoked case. but we not implement
|
||||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||||
// refs: https://tools.ietf.org/html/rfc7009
|
// refs: https://tools.ietf.org/html/rfc7009
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
respondWithInactiveToken()
|
||||||
c.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
introspectionResponse = object.IntrospectionResponse{
|
introspectionResponse = object.IntrospectionResponse{
|
||||||
Active: true,
|
Active: true,
|
||||||
Scope: jwtToken.Scope,
|
|
||||||
ClientId: clientId,
|
ClientId: clientId,
|
||||||
Username: jwtToken.Name,
|
|
||||||
TokenType: jwtToken.TokenType,
|
|
||||||
Exp: jwtToken.ExpiresAt.Unix(),
|
Exp: jwtToken.ExpiresAt.Unix(),
|
||||||
Iat: jwtToken.IssuedAt.Unix(),
|
Iat: jwtToken.IssuedAt.Unix(),
|
||||||
Nbf: jwtToken.NotBefore.Unix(),
|
Nbf: jwtToken.NotBefore.Unix(),
|
||||||
@ -388,6 +393,16 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
Iss: jwtToken.Issuer,
|
Iss: jwtToken.Issuer,
|
||||||
Jti: jwtToken.ID,
|
Jti: jwtToken.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jwtToken.Scope != "" {
|
||||||
|
introspectionResponse.Scope = jwtToken.Scope
|
||||||
|
}
|
||||||
|
if jwtToken.Name != "" {
|
||||||
|
introspectionResponse.Username = jwtToken.Name
|
||||||
|
}
|
||||||
|
if jwtToken.TokenType != "" {
|
||||||
|
introspectionResponse.TokenType = jwtToken.TokenType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenTypeHint == "" {
|
if tokenTypeHint == "" {
|
||||||
@ -396,13 +411,15 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
c.ResponseTokenError(err.Error())
|
c.ResponseTokenError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if token == nil {
|
if token == nil || token.ExpiresIn <= 0 {
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
respondWithInactiveToken()
|
||||||
c.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if token != nil {
|
||||||
introspectionResponse.TokenType = token.TokenType
|
introspectionResponse.TokenType = token.TokenType
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["json"] = introspectionResponse
|
c.Data["json"] = introspectionResponse
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
|
@ -353,13 +353,7 @@ func (c *ApiController) AddUser() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := object.GetUserCount("", "", "", "")
|
if err := checkQuotaForUser(); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkQuotaForUser(int(count)); err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -580,7 +574,11 @@ func (c *ApiController) SetPassword() {
|
|||||||
if user.Ldap == "" {
|
if user.Ldap == "" {
|
||||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
||||||
} else {
|
} else {
|
||||||
err = object.ResetLdapPassword(targetUser, newPassword, c.GetAcceptLanguage())
|
if isAdmin {
|
||||||
|
err = object.ResetLdapPassword(targetUser, "", newPassword, c.GetAcceptLanguage())
|
||||||
|
} else {
|
||||||
|
err = object.ResetLdapPassword(targetUser, oldPassword, newPassword, c.GetAcceptLanguage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -604,7 +602,11 @@ func (c *ApiController) CheckUserPassword() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage())
|
/*
|
||||||
|
* Verified password with user as subject, if field ldap not empty,
|
||||||
|
* then `isPasswordWithLdapEnabled` is true
|
||||||
|
*/
|
||||||
|
_, err = object.CheckUserPassword(user.Owner, user.Name, user.Password, c.GetAcceptLanguage(), false, false, user.Ldap != "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
@ -294,12 +294,18 @@ func checkQuotaForProvider(count int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkQuotaForUser(count int) error {
|
func checkQuotaForUser() error {
|
||||||
quota := conf.GetConfigQuota().User
|
quota := conf.GetConfigQuota().User
|
||||||
if quota == -1 {
|
if quota == -1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if count >= quota {
|
|
||||||
|
count, err := object.GetUserCount("", "", "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(count) >= quota {
|
||||||
return fmt.Errorf("user quota is exceeded")
|
return fmt.Errorf("user quota is exceeded")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -242,7 +242,7 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
} else if vform.Method == ResetVerification {
|
} else if vform.Method == ResetVerification {
|
||||||
user = c.getCurrentUser()
|
user = c.getCurrentUser()
|
||||||
} else if vform.Method == MfaAuthVerification {
|
} else if vform.Method == MfaAuthVerification {
|
||||||
mfaProps := user.GetPreferredMfaProps(false)
|
mfaProps := user.GetMfaProps(object.EmailType, false)
|
||||||
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
|
||||||
vform.Dest = mfaProps.Secret
|
vform.Dest = mfaProps.Secret
|
||||||
}
|
}
|
||||||
@ -281,7 +281,7 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if vform.Method == MfaAuthVerification {
|
} else if vform.Method == MfaAuthVerification {
|
||||||
mfaProps := user.GetPreferredMfaProps(false)
|
mfaProps := user.GetMfaProps(object.SmsType, false)
|
||||||
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {
|
||||||
vform.Dest = mfaProps.Secret
|
vform.Dest = mfaProps.Secret
|
||||||
}
|
}
|
||||||
@ -436,7 +436,8 @@ func (c *ApiController) ResetEmailOrPhone() {
|
|||||||
switch destType {
|
switch destType {
|
||||||
case object.VerifyTypeEmail:
|
case object.VerifyTypeEmail:
|
||||||
user.Email = dest
|
user.Email = dest
|
||||||
_, err = object.SetUserField(user, "email", user.Email)
|
user.EmailVerified = true
|
||||||
|
_, err = object.UpdateUser(user.GetId(), user, []string{"email", "email_verified"}, false)
|
||||||
case object.VerifyTypePhone:
|
case object.VerifyTypePhone:
|
||||||
user.Phone = dest
|
user.Phone = dest
|
||||||
_, err = object.SetUserField(user, "phone", user.Phone)
|
_, err = object.SetUserField(user, "phone", user.Phone)
|
||||||
@ -510,11 +511,18 @@ func (c *ApiController) VerifyCode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
passed, err := c.checkOrgMasterVerificationCode(user, authForm.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(c.T(err.Error()))
|
c.ResponseError(c.T(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !passed {
|
||||||
|
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
if result.Code != object.VerificationSuccess {
|
if result.Code != object.VerificationSuccess {
|
||||||
c.ResponseError(result.Msg)
|
c.ResponseError(result.Msg)
|
||||||
return
|
return
|
||||||
@ -525,6 +533,7 @@ func (c *ApiController) VerifyCode() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.SetSession("verifiedCode", authForm.Code)
|
c.SetSession("verifiedCode", authForm.Code)
|
||||||
c.SetSession("verifiedUserId", user.GetId())
|
c.SetSession("verifiedUserId", user.GetId())
|
||||||
|
36
controllers/verification_util.go
Normal file
36
controllers/verification_util.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *ApiController) checkOrgMasterVerificationCode(user *object.User, code string) (bool, error) {
|
||||||
|
organization, err := object.GetOrganizationByUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if organization == nil {
|
||||||
|
return false, fmt.Errorf("The organization: %s does not exist", user.Owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization.MasterVerificationCode != "" && organization.MasterVerificationCode == code {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
@ -16,7 +16,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/form"
|
"github.com/casdoor/casdoor/form"
|
||||||
@ -118,24 +118,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userOwner := c.Input().Get("owner")
|
options, sessionData, err := webauthnObj.BeginDiscoverableLogin()
|
||||||
userName := c.Input().Get("name")
|
|
||||||
user, err := object.GetUserByFields(userOwner, userName)
|
|
||||||
if err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(userOwner, userName)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(user.WebauthnCredentials) == 0 {
|
|
||||||
c.ResponseError(c.T("webauthn:Found no credentials for this user"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options, sessionData, err := webauthnObj.BeginLogin(user)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
@ -168,20 +151,23 @@ func (c *ApiController) WebAuthnSigninFinish() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
c.Ctx.Request.Body = io.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||||
userId := string(sessionData.UserID)
|
|
||||||
user, err := object.GetUser(userId)
|
var user *object.User
|
||||||
|
handler := func(rawID, userHandle []byte) (webauthn.User, error) {
|
||||||
|
user, err = object.GetUserByWebauthID(base64.StdEncoding.EncodeToString(rawID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
return nil, err
|
||||||
return
|
}
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
_, err = webauthnObj.FinishDiscoverableLogin(handler, sessionData, c.Ctx.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.SetSessionUsername(userId)
|
c.SetSessionUsername(user.GetId())
|
||||||
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
util.LogInfo(c.Ctx, "API: [%s] signed in", user.GetId())
|
||||||
|
|
||||||
var application *object.Application
|
var application *object.Application
|
||||||
|
|
||||||
|
@ -34,6 +34,8 @@ func GetCredManager(passwordType string) CredManager {
|
|||||||
return NewPbkdf2SaltCredManager()
|
return NewPbkdf2SaltCredManager()
|
||||||
} else if passwordType == "argon2id" {
|
} else if passwordType == "argon2id" {
|
||||||
return NewArgon2idCredManager()
|
return NewArgon2idCredManager()
|
||||||
|
} else if passwordType == "pbkdf2-django" {
|
||||||
|
return NewPbkdf2DjangoCredManager()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
71
cred/pbkdf2_django.go
Normal file
71
cred/pbkdf2_django.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package cred
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// password type: pbkdf2-django
|
||||||
|
|
||||||
|
type Pbkdf2DjangoCredManager struct{}
|
||||||
|
|
||||||
|
func NewPbkdf2DjangoCredManager() *Pbkdf2DjangoCredManager {
|
||||||
|
cm := &Pbkdf2DjangoCredManager{}
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Pbkdf2DjangoCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||||
|
iterations := 260000
|
||||||
|
salt := userSalt
|
||||||
|
if salt == "" {
|
||||||
|
salt = organizationSalt
|
||||||
|
}
|
||||||
|
|
||||||
|
saltBytes := []byte(salt)
|
||||||
|
passwordBytes := []byte(password)
|
||||||
|
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iterations, sha256.Size, sha256.New)
|
||||||
|
hashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||||
|
return "pbkdf2_sha256$" + strconv.Itoa(iterations) + "$" + salt + "$" + hashBase64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Pbkdf2DjangoCredManager) IsPasswordCorrect(password string, passwordHash string, userSalt string, organizationSalt string) bool {
|
||||||
|
parts := strings.Split(passwordHash, "$")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm, iterations, salt, hash := parts[0], parts[1], parts[2], parts[3]
|
||||||
|
if algorithm != "pbkdf2_sha256" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
iter, err := strconv.Atoi(iterations)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
saltBytes := []byte(salt)
|
||||||
|
passwordBytes := []byte(password)
|
||||||
|
computedHash := pbkdf2.Key(passwordBytes, saltBytes, iter, sha256.Size, sha256.New)
|
||||||
|
computedHashBase64 := base64.StdEncoding.EncodeToString(computedHash)
|
||||||
|
|
||||||
|
return computedHashBase64 == hash
|
||||||
|
}
|
@ -15,6 +15,8 @@
|
|||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -26,12 +28,22 @@ import (
|
|||||||
type HttpEmailProvider struct {
|
type HttpEmailProvider struct {
|
||||||
endpoint string
|
endpoint string
|
||||||
method string
|
method string
|
||||||
|
httpHeaders map[string]string
|
||||||
|
bodyMapping map[string]string
|
||||||
|
contentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) *HttpEmailProvider {
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/x-www-form-urlencoded"
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
|
|
||||||
client := &HttpEmailProvider{
|
client := &HttpEmailProvider{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
method: method,
|
method: method,
|
||||||
|
httpHeaders: httpHeaders,
|
||||||
|
bodyMapping: bodyMapping,
|
||||||
|
contentType: contentType,
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
@ -39,18 +51,52 @@ func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
|
|||||||
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
var err error
|
var err error
|
||||||
if c.method == "POST" {
|
|
||||||
|
fromNameField := "fromName"
|
||||||
|
toAddressField := "toAddress"
|
||||||
|
subjectField := "subject"
|
||||||
|
contentField := "content"
|
||||||
|
|
||||||
|
for k, v := range c.bodyMapping {
|
||||||
|
switch k {
|
||||||
|
case "fromName":
|
||||||
|
fromNameField = v
|
||||||
|
case "toAddress":
|
||||||
|
toAddressField = v
|
||||||
|
case "subject":
|
||||||
|
subjectField = v
|
||||||
|
case "content":
|
||||||
|
contentField = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" {
|
||||||
|
bodyMap := make(map[string]string)
|
||||||
|
bodyMap[fromNameField] = fromName
|
||||||
|
bodyMap[toAddressField] = toAddress
|
||||||
|
bodyMap[subjectField] = subject
|
||||||
|
bodyMap[contentField] = content
|
||||||
|
|
||||||
|
var fromValueBytes []byte
|
||||||
|
if c.contentType == "application/json" {
|
||||||
|
fromValueBytes, err = json.Marshal(bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes))
|
||||||
|
} else {
|
||||||
formValues := url.Values{}
|
formValues := url.Values{}
|
||||||
formValues.Set("fromName", fromName)
|
for k, v := range bodyMap {
|
||||||
formValues.Set("toAddress", toAddress)
|
formValues.Add(k, v)
|
||||||
formValues.Set("subject", subject)
|
}
|
||||||
formValues.Set("content", content)
|
|
||||||
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", c.contentType)
|
||||||
} else if c.method == "GET" {
|
} else if c.method == "GET" {
|
||||||
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
req, err = http.NewRequest(c.method, c.endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -58,15 +104,19 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
q.Add("fromName", fromName)
|
q.Add(fromNameField, fromName)
|
||||||
q.Add("toAddress", toAddress)
|
q.Add(toAddressField, toAddress)
|
||||||
q.Add("subject", subject)
|
q.Add(subjectField, subject)
|
||||||
q.Add("content", content)
|
q.Add(contentField, content)
|
||||||
req.URL.RawQuery = q.Encode()
|
req.URL.RawQuery = q.Encode()
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, v := range c.httpHeaders {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
httpClient := proxy.DefaultHttpClient
|
httpClient := proxy.DefaultHttpClient
|
||||||
resp, err := httpClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -18,13 +18,13 @@ type EmailProvider interface {
|
|||||||
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
|
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string) EmailProvider {
|
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) EmailProvider {
|
||||||
if typ == "Azure ACS" {
|
if typ == "Azure ACS" {
|
||||||
return NewAzureACSEmailProvider(clientSecret, host)
|
return NewAzureACSEmailProvider(clientSecret, host)
|
||||||
} else if typ == "Custom HTTP Email" {
|
} else if typ == "Custom HTTP Email" {
|
||||||
return NewHttpEmailProvider(endpoint, method)
|
return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType)
|
||||||
} else if typ == "SendGrid" {
|
} else if typ == "SendGrid" {
|
||||||
return NewSendgridEmailProvider(clientSecret)
|
return NewSendgridEmailProvider(clientSecret, host, endpoint)
|
||||||
} else {
|
} else {
|
||||||
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
|
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ package email
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sendgrid/sendgrid-go"
|
"github.com/sendgrid/sendgrid-go"
|
||||||
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
||||||
@ -25,6 +25,8 @@ import (
|
|||||||
|
|
||||||
type SendgridEmailProvider struct {
|
type SendgridEmailProvider struct {
|
||||||
ApiKey string
|
ApiKey string
|
||||||
|
Host string
|
||||||
|
Endpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendgridResponseBody struct {
|
type SendgridResponseBody struct {
|
||||||
@ -35,23 +37,25 @@ type SendgridResponseBody struct {
|
|||||||
} `json:"errors"`
|
} `json:"errors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
|
func NewSendgridEmailProvider(apiKey string, host string, endpoint string) *SendgridEmailProvider {
|
||||||
return &SendgridEmailProvider{ApiKey: apiKey}
|
return &SendgridEmailProvider{ApiKey: apiKey, Host: host, Endpoint: endpoint}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress string, subject string, content string) error {
|
func (s *SendgridEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
|
||||||
|
client := s.initSendgridClient()
|
||||||
|
|
||||||
from := mail.NewEmail(fromName, fromAddress)
|
from := mail.NewEmail(fromName, fromAddress)
|
||||||
to := mail.NewEmail("", toAddress)
|
to := mail.NewEmail("", toAddress)
|
||||||
message := mail.NewSingleEmail(from, subject, to, "", content)
|
message := mail.NewSingleEmail(from, subject, to, "", content)
|
||||||
client := sendgrid.NewSendClient(s.ApiKey)
|
|
||||||
response, err := client.Send(message)
|
resp, err := client.Send(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode >= 300 {
|
if resp.StatusCode >= 300 {
|
||||||
var responseBody SendgridResponseBody
|
var responseBody SendgridResponseBody
|
||||||
err = json.Unmarshal([]byte(response.Body), &responseBody)
|
err = json.Unmarshal([]byte(resp.Body), &responseBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -61,8 +65,23 @@ func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress str
|
|||||||
messages = append(messages, sendgridError.Message)
|
messages = append(messages, sendgridError.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("SendGrid status code: %d, error message: %s", response.StatusCode, strings.Join(messages, " | "))
|
return fmt.Errorf("status code: %d, error message: %s", resp.StatusCode, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
|
return fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SendgridEmailProvider) initSendgridClient() *sendgrid.Client {
|
||||||
|
if s.Host == "" || s.Endpoint == "" {
|
||||||
|
return sendgrid.NewSendClient(s.ApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := sendgrid.GetRequest(s.ApiKey, s.Endpoint, s.Host)
|
||||||
|
request.Method = "POST"
|
||||||
|
|
||||||
|
return &sendgrid.Client{Request: request}
|
||||||
|
}
|
||||||
|
@ -16,7 +16,9 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/gomail/v2"
|
"github.com/casdoor/gomail/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,6 +35,13 @@ func NewSmtpEmailProvider(userName string, password string, host string, port in
|
|||||||
|
|
||||||
dialer.SSL = !disableSsl
|
dialer.SSL = !disableSsl
|
||||||
|
|
||||||
|
if strings.HasSuffix(host, ".amazonaws.com") {
|
||||||
|
socks5Proxy := conf.GetConfigString("socks5Proxy")
|
||||||
|
if socks5Proxy != "" {
|
||||||
|
dialer.SetSocks5Proxy(socks5Proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &SmtpEmailProvider{Dialer: dialer}
|
return &SmtpEmailProvider{Dialer: dialer}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,12 +34,14 @@ type AuthForm struct {
|
|||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
Affiliation string `json:"affiliation"`
|
Affiliation string `json:"affiliation"`
|
||||||
IdCard string `json:"idCard"`
|
IdCard string `json:"idCard"`
|
||||||
|
Language string `json:"language"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
InvitationCode string `json:"invitationCode"`
|
InvitationCode string `json:"invitationCode"`
|
||||||
|
|
||||||
Application string `json:"application"`
|
Application string `json:"application"`
|
||||||
ClientId string `json:"clientId"`
|
ClientId string `json:"clientId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
ProviderBack string `json:"providerBack"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
RedirectUri string `json:"redirectUri"`
|
RedirectUri string `json:"redirectUri"`
|
||||||
@ -67,6 +69,7 @@ type AuthForm struct {
|
|||||||
Pricing string `json:"pricing"`
|
Pricing string `json:"pricing"`
|
||||||
|
|
||||||
FaceId []float64 `json:"faceId"`
|
FaceId []float64 `json:"faceId"`
|
||||||
|
FaceIdImage []string `json:"faceIdImage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {
|
||||||
|
204
go.mod
204
go.mod
@ -1,24 +1,27 @@
|
|||||||
module github.com/casdoor/casdoor
|
module github.com/casdoor/casdoor
|
||||||
|
|
||||||
go 1.16
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/squirrel v1.5.3
|
github.com/Masterminds/squirrel v1.5.3
|
||||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||||
|
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2
|
||||||
|
github.com/alibabacloud-go/tea v1.3.2
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||||
github.com/aws/aws-sdk-go v1.45.5
|
github.com/aws/aws-sdk-go v1.45.5
|
||||||
github.com/beego/beego v1.12.12
|
github.com/beego/beego v1.12.12
|
||||||
github.com/beevik/etree v1.1.0
|
github.com/beevik/etree v1.1.0
|
||||||
github.com/casbin/casbin/v2 v2.77.2
|
github.com/casbin/casbin/v2 v2.77.2
|
||||||
github.com/casdoor/go-sms-sender v0.25.0
|
github.com/casdoor/go-sms-sender v0.25.0
|
||||||
github.com/casdoor/gomail/v2 v2.0.1
|
github.com/casdoor/gomail/v2 v2.1.0
|
||||||
github.com/casdoor/ldapserver v1.2.0
|
github.com/casdoor/ldapserver v1.2.0
|
||||||
github.com/casdoor/notify v0.45.0
|
github.com/casdoor/notify v1.0.0
|
||||||
github.com/casdoor/oss v1.8.0
|
github.com/casdoor/oss v1.8.0
|
||||||
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
github.com/casdoor/xorm-adapter/v3 v3.1.0
|
||||||
github.com/casvisor/casvisor-go-sdk v1.4.0
|
github.com/casvisor/casvisor-go-sdk v1.4.0
|
||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||||
github.com/denisenkom/go-mssqldb v0.9.0
|
github.com/denisenkom/go-mssqldb v0.9.0
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
|
||||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
|
||||||
github.com/fogleman/gg v1.3.0
|
github.com/fogleman/gg v1.3.0
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.5
|
github.com/go-asn1-ber/asn1-ber v1.5.5
|
||||||
@ -28,8 +31,8 @@ require (
|
|||||||
github.com/go-pay/gopay v1.5.72
|
github.com/go-pay/gopay v1.5.72
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||||
github.com/go-webauthn/webauthn v0.6.0
|
github.com/go-webauthn/webauthn v0.10.2
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/lestrrat-go/jwx v1.2.29
|
github.com/lestrrat-go/jwx v1.2.29
|
||||||
@ -46,7 +49,6 @@ require (
|
|||||||
github.com/russellhaering/gosaml2 v0.9.0
|
github.com/russellhaering/gosaml2 v0.9.0
|
||||||
github.com/russellhaering/goxmldsig v1.2.0
|
github.com/russellhaering/goxmldsig v1.2.0
|
||||||
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
|
||||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
@ -54,20 +56,194 @@ require (
|
|||||||
github.com/stripe/stripe-go/v74 v74.29.0
|
github.com/stripe/stripe-go/v74 v74.29.0
|
||||||
github.com/tealeg/xlsx v1.0.5
|
github.com/tealeg/xlsx v1.0.5
|
||||||
github.com/thanhpk/randstr v1.0.4
|
github.com/thanhpk/randstr v1.0.4
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
|
||||||
github.com/xorm-io/builder v0.3.13
|
github.com/xorm-io/builder v0.3.13
|
||||||
github.com/xorm-io/core v0.7.4
|
github.com/xorm-io/core v0.7.4
|
||||||
github.com/xorm-io/xorm v1.1.6
|
github.com/xorm-io/xorm v1.1.6
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/crypto v0.21.0
|
golang.org/x/net v0.34.0
|
||||||
golang.org/x/net v0.21.0
|
|
||||||
golang.org/x/oauth2 v0.17.0
|
golang.org/x/oauth2 v0.17.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.21.0
|
||||||
google.golang.org/api v0.150.0
|
google.golang.org/api v0.150.0
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
||||||
maunium.net/go/mautrix v0.16.0
|
maunium.net/go/mautrix v0.16.0
|
||||||
modernc.org/sqlite v1.18.2
|
modernc.org/sqlite v1.18.2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go v0.110.8 // indirect
|
||||||
|
cloud.google.com/go/compute v1.23.1 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
cloud.google.com/go/iam v1.1.3 // indirect
|
||||||
|
cloud.google.com/go/storage v1.35.1 // indirect
|
||||||
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
|
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||||
|
github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
|
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||||
|
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||||
|
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 // indirect
|
||||||
|
github.com/SherClockHolmes/webpush-go v1.2.0 // indirect
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||||
|
github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||||
|
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||||
|
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||||
|
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||||
|
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||||
|
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||||
|
github.com/atc0005/go-teams-notify/v2 v2.6.1 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
|
github.com/bwmarrin/discordgo v0.27.1 // indirect
|
||||||
|
github.com/casdoor/casdoor-go-sdk v0.50.0 // indirect
|
||||||
|
github.com/casdoor/go-reddit/v2 v2.1.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
|
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
|
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||||
|
github.com/dghubble/sling v1.4.0 // indirect
|
||||||
|
github.com/di-wu/parser v0.2.2 // indirect
|
||||||
|
github.com/di-wu/xsd-datetime v1.0.0 // indirect
|
||||||
|
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||||
|
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||||
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||||
|
github.com/go-lark/lark v1.9.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/go-webauthn/x v0.1.9 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/google/go-tpm v0.9.0 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/gregdel/pushover v1.2.1 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
|
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
|
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||||
|
github.com/markbates/going v1.0.0 // indirect
|
||||||
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
|
github.com/mileusna/viber v1.0.1 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
|
||||||
|
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
|
||||||
|
github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/common v0.30.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
|
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
|
github.com/rs/zerolog v1.30.0 // indirect
|
||||||
|
github.com/scim2/filter-parser/v2 v2.2.0 // indirect
|
||||||
|
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||||
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
|
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
|
||||||
|
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
|
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||||
|
github.com/slack-go/slack v0.12.3 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||||
|
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.744 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
|
||||||
|
github.com/tidwall/gjson v1.16.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||||
|
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||||
|
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||||
|
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||||
|
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
|
go.mau.fi/util v0.0.0-20230805171708-199bf3eec776 // indirect
|
||||||
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
|
go.uber.org/zap v1.19.1 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||||
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
|
golang.org/x/time v0.3.0 // indirect
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||||
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
lukechampine.com/uint128 v1.1.1 // indirect
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||||
|
modernc.org/cc/v3 v3.37.0 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||||
|
modernc.org/libc v1.18.0 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.3.0 // indirect
|
||||||
|
modernc.org/opt v0.1.1 // indirect
|
||||||
|
modernc.org/strutil v1.1.3 // indirect
|
||||||
|
modernc.org/token v1.0.1 // indirect
|
||||||
|
)
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
|
"Username cannot be an email address": "Uživatelské jméno nemůže být emailová adresa",
|
||||||
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
|
"Username cannot contain white spaces": "Uživatelské jméno nemůže obsahovat mezery",
|
||||||
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
|
"Username cannot start with a digit": "Uživatelské jméno nemůže začínat číslicí",
|
||||||
"Username is too long (maximum is 39 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 39 znaků).",
|
"Username is too long (maximum is 255 characters).": "Uživatelské jméno je příliš dlouhé (maximálně 255 znaků).",
|
||||||
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
|
"Username must have at least 2 characters": "Uživatelské jméno musí mít alespoň 2 znaky",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali jste špatné heslo nebo kód příliš mnohokrát, prosím počkejte %d minut a zkuste to znovu",
|
||||||
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
|
"Your region is not allow to signup by phone": "Vaše oblast neumožňuje registraci pomocí telefonu",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
|
"Username cannot be an email address": "Benutzername kann keine E-Mail-Adresse sein",
|
||||||
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
|
"Username cannot contain white spaces": "Benutzername darf keine Leerzeichen enthalten",
|
||||||
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen",
|
"Username cannot start with a digit": "Benutzername darf nicht mit einer Ziffer beginnen",
|
||||||
"Username is too long (maximum is 39 characters).": "Benutzername ist zu lang (das Maximum beträgt 39 Zeichen).",
|
"Username is too long (maximum is 255 characters).": "Benutzername ist zu lang (das Maximum beträgt 255 Zeichen).",
|
||||||
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
|
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
|
||||||
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
|
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
|
"Username cannot be an email address": "Nombre de usuario no puede ser una dirección de correo electrónico",
|
||||||
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
|
"Username cannot contain white spaces": "Nombre de usuario no puede contener espacios en blanco",
|
||||||
"Username cannot start with a digit": "El nombre de usuario no puede empezar con un dígito",
|
"Username cannot start with a digit": "El nombre de usuario no puede empezar con un dígito",
|
||||||
"Username is too long (maximum is 39 characters).": "El nombre de usuario es demasiado largo (el máximo es de 39 caracteres).",
|
"Username is too long (maximum is 255 characters).": "El nombre de usuario es demasiado largo (el máximo es de 255 caracteres).",
|
||||||
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
|
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
|
||||||
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
|
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
"Username cannot be an email address": "نام کاربری نمیتواند یک آدرس ایمیل باشد",
|
||||||
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
"Username cannot contain white spaces": "نام کاربری نمیتواند حاوی فاصله باشد",
|
||||||
"Username cannot start with a digit": "نام کاربری نمیتواند با یک رقم شروع شود",
|
"Username cannot start with a digit": "نام کاربری نمیتواند با یک رقم شروع شود",
|
||||||
"Username is too long (maximum is 39 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
|
"Username is too long (maximum is 255 characters).": "نام کاربری بیش از حد طولانی است (حداکثر ۳۹ کاراکتر).",
|
||||||
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
|
"Username must have at least 2 characters": "نام کاربری باید حداقل ۲ کاراکتر داشته باشد",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کردهاید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "شما رمز عبور یا کد اشتباه را بیش از حد وارد کردهاید، لطفاً %d دقیقه صبر کنید و دوباره تلاش کنید",
|
||||||
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
"Your region is not allow to signup by phone": "منطقه شما اجازه ثبتنام با تلفن را ندارد",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
|
"Username cannot be an email address": "Nom d'utilisateur ne peut pas être une adresse e-mail",
|
||||||
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
|
"Username cannot contain white spaces": "Nom d'utilisateur ne peut pas contenir d'espaces blancs",
|
||||||
"Username cannot start with a digit": "Nom d'utilisateur ne peut pas commencer par un chiffre",
|
"Username cannot start with a digit": "Nom d'utilisateur ne peut pas commencer par un chiffre",
|
||||||
"Username is too long (maximum is 39 characters).": "Nom d'utilisateur est trop long (maximum de 39 caractères).",
|
"Username is too long (maximum is 255 characters).": "Nom d'utilisateur est trop long (maximum de 255 caractères).",
|
||||||
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
|
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
|
||||||
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
|
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
|
"Username cannot be an email address": "Username tidak bisa menjadi alamat email",
|
||||||
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
|
"Username cannot contain white spaces": "Username tidak boleh mengandung spasi",
|
||||||
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
|
"Username cannot start with a digit": "Username tidak dapat dimulai dengan angka",
|
||||||
"Username is too long (maximum is 39 characters).": "Nama pengguna terlalu panjang (maksimum 39 karakter).",
|
"Username is too long (maximum is 255 characters).": "Nama pengguna terlalu panjang (maksimum 255 karakter).",
|
||||||
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
|
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
|
||||||
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
|
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
|
"Username cannot be an email address": "ユーザー名には電子メールアドレスを使用できません",
|
||||||
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
|
"Username cannot contain white spaces": "ユーザ名にはスペースを含めることはできません",
|
||||||
"Username cannot start with a digit": "ユーザー名は数字で始めることはできません",
|
"Username cannot start with a digit": "ユーザー名は数字で始めることはできません",
|
||||||
"Username is too long (maximum is 39 characters).": "ユーザー名が長すぎます(最大39文字)。",
|
"Username is too long (maximum is 255 characters).": "ユーザー名が長すぎます(最大255文字)。",
|
||||||
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
|
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
|
||||||
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
|
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
|
"Username cannot be an email address": "사용자 이름은 이메일 주소가 될 수 없습니다",
|
||||||
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
|
"Username cannot contain white spaces": "사용자 이름에는 공백이 포함될 수 없습니다",
|
||||||
"Username cannot start with a digit": "사용자 이름은 숫자로 시작할 수 없습니다",
|
"Username cannot start with a digit": "사용자 이름은 숫자로 시작할 수 없습니다",
|
||||||
"Username is too long (maximum is 39 characters).": "사용자 이름이 너무 깁니다 (최대 39자).",
|
"Username is too long (maximum is 255 characters).": "사용자 이름이 너무 깁니다 (최대 255자).",
|
||||||
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
|
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
|
||||||
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
|
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "O nome de usuário não pode começar com um dígito",
|
"Username cannot start with a digit": "O nome de usuário não pode começar com um dígito",
|
||||||
"Username is too long (maximum is 39 characters).": "Nome de usuário é muito longo (máximo é 39 caracteres).",
|
"Username is too long (maximum is 255 characters).": "Nome de usuário é muito longo (máximo é 255 caracteres).",
|
||||||
"Username must have at least 2 characters": "Nome de usuário deve ter pelo menos 2 caracteres",
|
"Username must have at least 2 characters": "Nome de usuário deve ter pelo menos 2 caracteres",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
"Username cannot be an email address": "Имя пользователя не может быть адресом электронной почты",
|
||||||
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
"Username cannot contain white spaces": "Имя пользователя не может содержать пробелы",
|
||||||
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
|
"Username cannot start with a digit": "Имя пользователя не может начинаться с цифры",
|
||||||
"Username is too long (maximum is 39 characters).": "Имя пользователя слишком длинное (максимальная длина - 39 символов).",
|
"Username is too long (maximum is 255 characters).": "Имя пользователя слишком длинное (максимальная длина - 255 символов).",
|
||||||
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
|
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
|
||||||
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
|
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
|
"Username cannot be an email address": "Používateľské meno nemôže byť e-mailová adresa",
|
||||||
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
|
"Username cannot contain white spaces": "Používateľské meno nemôže obsahovať medzery",
|
||||||
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
|
"Username cannot start with a digit": "Používateľské meno nemôže začínať číslicou",
|
||||||
"Username is too long (maximum is 39 characters).": "Používateľské meno je príliš dlhé (maximum je 39 znakov).",
|
"Username is too long (maximum is 255 characters).": "Používateľské meno je príliš dlhé (maximum je 255 znakov).",
|
||||||
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
|
"Username must have at least 2 characters": "Používateľské meno musí mať aspoň 2 znaky",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Zadali ste nesprávne heslo alebo kód príliš veľa krát, prosím, počkajte %d minút a skúste to znova",
|
||||||
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
|
"Your region is not allow to signup by phone": "Váš región neumožňuje registráciu cez telefón",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Kullanıcı adı bir e-mail adresi olamaz",
|
"Username cannot be an email address": "Kullanıcı adı bir e-mail adresi olamaz",
|
||||||
"Username cannot contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
|
"Username cannot contain white spaces": "Kullanıcı adı boşluk karakteri içeremez",
|
||||||
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz",
|
"Username cannot start with a digit": "Kullanıcı adı rakamla başlayamaz",
|
||||||
"Username is too long (maximum is 39 characters).": "Kullanıcı adı çok uzun (en fazla 39 karakter olmalı).",
|
"Username is too long (maximum is 255 characters).": "Kullanıcı adı çok uzun (en fazla 255 karakter olmalı).",
|
||||||
"Username must have at least 2 characters": "Kullanıcı adı en az iki karakterden oluşmalı",
|
"Username must have at least 2 characters": "Kullanıcı adı en az iki karakterden oluşmalı",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre denemesi yaptınız. %d dakika kadar bekleyip yeniden giriş yapmayı deneyebilirsiniz.",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Çok fazla hatalı şifre denemesi yaptınız. %d dakika kadar bekleyip yeniden giriş yapmayı deneyebilirsiniz.",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Username cannot be an email address",
|
"Username cannot be an email address": "Username cannot be an email address",
|
||||||
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
"Username cannot contain white spaces": "Username cannot contain white spaces",
|
||||||
"Username cannot start with a digit": "Username cannot start with a digit",
|
"Username cannot start with a digit": "Username cannot start with a digit",
|
||||||
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
|
"Username is too long (maximum is 255 characters).": "Username is too long (maximum is 255 characters).",
|
||||||
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
"Username must have at least 2 characters": "Username must have at least 2 characters",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
|
||||||
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
|
"Username cannot be an email address": "Tên người dùng không thể là địa chỉ email",
|
||||||
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
|
"Username cannot contain white spaces": "Tên người dùng không thể chứa khoảng trắng",
|
||||||
"Username cannot start with a digit": "Tên người dùng không thể bắt đầu bằng chữ số",
|
"Username cannot start with a digit": "Tên người dùng không thể bắt đầu bằng chữ số",
|
||||||
"Username is too long (maximum is 39 characters).": "Tên đăng nhập quá dài (tối đa là 39 ký tự).",
|
"Username is too long (maximum is 255 characters).": "Tên đăng nhập quá dài (tối đa là 255 ký tự).",
|
||||||
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
|
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
|
||||||
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
|
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"Username cannot be an email address": "用户名不可以是邮箱地址",
|
"Username cannot be an email address": "用户名不可以是邮箱地址",
|
||||||
"Username cannot contain white spaces": "用户名禁止包含空格",
|
"Username cannot contain white spaces": "用户名禁止包含空格",
|
||||||
"Username cannot start with a digit": "用户名禁止使用数字开头",
|
"Username cannot start with a digit": "用户名禁止使用数字开头",
|
||||||
"Username is too long (maximum is 39 characters).": "用户名过长(最大允许长度为39个字符)",
|
"Username is too long (maximum is 255 characters).": "用户名过长(最大允许长度为255个字符)",
|
||||||
"Username must have at least 2 characters": "用户名至少要有2个字符",
|
"Username must have at least 2 characters": "用户名至少要有2个字符",
|
||||||
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
|
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
|
||||||
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
|
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
|
||||||
|
@ -136,12 +136,12 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
|||||||
dtUserInfo := &DingTalkUserResponse{}
|
dtUserInfo := &DingTalkUserResponse{}
|
||||||
accessToken := token.AccessToken
|
accessToken := token.AccessToken
|
||||||
|
|
||||||
reqest, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
|
request, err := http.NewRequest("GET", idp.Config.Endpoint.AuthURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
reqest.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
request.Header.Add("x-acs-dingtalk-access-token", accessToken)
|
||||||
resp, err := idp.Client.Do(reqest)
|
resp, err := idp.Client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -299,12 +299,12 @@ func GetWechatOfficialAccountQRCode(clientId string, clientSecret string, provid
|
|||||||
params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
|
params := fmt.Sprintf(`{"expire_seconds": 3600, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "%s"}}}`, providerId)
|
||||||
|
|
||||||
bodyData := bytes.NewReader([]byte(params))
|
bodyData := bytes.NewReader([]byte(params))
|
||||||
requeset, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
request, err := http.NewRequest("POST", qrCodeUrl, bodyData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(requeset)
|
resp, err := client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
@ -434,7 +434,7 @@
|
|||||||
"isTopGroup": true,
|
"isTopGroup": true,
|
||||||
"title": "",
|
"title": "",
|
||||||
"key": "",
|
"key": "",
|
||||||
"children": "",
|
"children": [],
|
||||||
"isEnabled": true
|
"isEnabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -185,12 +185,9 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
|
|||||||
attr := string(f.AttributeDesc())
|
attr := string(f.AttributeDesc())
|
||||||
|
|
||||||
if attr == ldapMemberOfAttr {
|
if attr == ldapMemberOfAttr {
|
||||||
groupId := string(f.AssertionValue())
|
|
||||||
users, err := object.GetGroupUsers(groupId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var names []string
|
var names []string
|
||||||
|
groupId := string(f.AssertionValue())
|
||||||
|
users := object.GetGroupUsersWithoutError(groupId)
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
names = append(names, user.Name)
|
names = append(names, user.Name)
|
||||||
}
|
}
|
||||||
@ -249,7 +246,7 @@ func buildSafeCondition(filter interface{}) builder.Cond {
|
|||||||
condition, err := buildUserFilterCondition(filter)
|
condition, err := buildUserFilterCondition(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("err = %v", err.Error())
|
log.Printf("err = %v", err.Error())
|
||||||
return nil
|
return builder.And(builder.Expr("1 != 1"))
|
||||||
}
|
}
|
||||||
return condition
|
return condition
|
||||||
}
|
}
|
||||||
|
2
main.go
2
main.go
@ -22,6 +22,7 @@ import (
|
|||||||
_ "github.com/beego/beego/session/redis"
|
_ "github.com/beego/beego/session/redis"
|
||||||
"github.com/casdoor/casdoor/authz"
|
"github.com/casdoor/casdoor/authz"
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
|
"github.com/casdoor/casdoor/controllers"
|
||||||
"github.com/casdoor/casdoor/ldap"
|
"github.com/casdoor/casdoor/ldap"
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
"github.com/casdoor/casdoor/proxy"
|
"github.com/casdoor/casdoor/proxy"
|
||||||
@ -45,6 +46,7 @@ func main() {
|
|||||||
object.InitCasvisorConfig()
|
object.InitCasvisorConfig()
|
||||||
|
|
||||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||||
|
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
|
||||||
|
|
||||||
// beego.DelStaticPath("/static")
|
// beego.DelStaticPath("/static")
|
||||||
// beego.SetStaticPath("/static", "web/build/static")
|
// beego.SetStaticPath("/static", "web/build/static")
|
||||||
|
29
notification/cucloud.go
Normal file
29
notification/cucloud.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// 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 notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/casdoor/notify"
|
||||||
|
"github.com/casdoor/notify/service/cucloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCucloudProvider(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType string) (notify.Notifier, error) {
|
||||||
|
cucloud := cucloud.New(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType)
|
||||||
|
|
||||||
|
notifier := notify.New()
|
||||||
|
notifier.UseServices(cucloud)
|
||||||
|
|
||||||
|
return notifier, nil
|
||||||
|
}
|
@ -16,7 +16,7 @@ package notification
|
|||||||
|
|
||||||
import "github.com/casdoor/notify"
|
import "github.com/casdoor/notify"
|
||||||
|
|
||||||
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string) (notify.Notifier, error) {
|
func GetNotificationProvider(typ string, clientId string, clientSecret string, clientId2 string, clientSecret2 string, appId string, receiver string, method string, title string, metaData string, regionId string) (notify.Notifier, error) {
|
||||||
if typ == "Telegram" {
|
if typ == "Telegram" {
|
||||||
return NewTelegramProvider(clientSecret, receiver)
|
return NewTelegramProvider(clientSecret, receiver)
|
||||||
} else if typ == "Custom HTTP" {
|
} else if typ == "Custom HTTP" {
|
||||||
@ -53,6 +53,8 @@ func GetNotificationProvider(typ string, clientId string, clientSecret string, c
|
|||||||
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
|
return NewRocketChatProvider(clientId, clientSecret, appId, receiver)
|
||||||
} else if typ == "Viber" {
|
} else if typ == "Viber" {
|
||||||
return NewViberProvider(clientId, clientSecret, appId, receiver)
|
return NewViberProvider(clientId, clientSecret, appId, receiver)
|
||||||
|
} else if typ == "CUCloud" {
|
||||||
|
return NewCucloudProvider(clientId, clientSecret, appId, title, regionId, clientId2, metaData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -191,12 +191,7 @@ func (adapter *Adapter) InitAdapter() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tableName string
|
tableName := adapter.Table
|
||||||
if driverName == "mssql" {
|
|
||||||
tableName = fmt.Sprintf("[%s]", adapter.Table)
|
|
||||||
} else {
|
|
||||||
tableName = adapter.Table
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
|
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, tableName, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -71,6 +71,7 @@ type Application struct {
|
|||||||
Description string `xorm:"varchar(100)" json:"description"`
|
Description string `xorm:"varchar(100)" json:"description"`
|
||||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||||
|
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||||
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
||||||
EnablePassword bool `json:"enablePassword"`
|
EnablePassword bool `json:"enablePassword"`
|
||||||
EnableSignUp bool `json:"enableSignUp"`
|
EnableSignUp bool `json:"enableSignUp"`
|
||||||
@ -100,6 +101,7 @@ type Application struct {
|
|||||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||||
|
ForcedRedirectOrigin string `xorm:"varchar(100)" json:"forcedRedirectOrigin"`
|
||||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||||
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
||||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||||
@ -120,6 +122,7 @@ type Application struct {
|
|||||||
FormOffset int `json:"formOffset"`
|
FormOffset int `json:"formOffset"`
|
||||||
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
|
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
|
||||||
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
|
||||||
|
FormBackgroundUrlMobile string `xorm:"varchar(200)" json:"formBackgroundUrlMobile"`
|
||||||
|
|
||||||
FailedSigninLimit int `json:"failedSigninLimit"`
|
FailedSigninLimit int `json:"failedSigninLimit"`
|
||||||
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
|
||||||
@ -481,7 +484,10 @@ func GetApplicationByClientId(clientId string) (*Application, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetApplication(id string) (*Application, error) {
|
func GetApplication(id string) (*Application, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return getApplication(owner, name)
|
return getApplication(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,7 +542,7 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
|||||||
|
|
||||||
providerItems := []*ProviderItem{}
|
providerItems := []*ProviderItem{}
|
||||||
for _, providerItem := range application.Providers {
|
for _, providerItem := range application.Providers {
|
||||||
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML") {
|
if providerItem.Provider != nil && (providerItem.Provider.Category == "OAuth" || providerItem.Provider.Category == "Web3" || providerItem.Provider.Category == "Captcha" || providerItem.Provider.Category == "SAML" || providerItem.Provider.Category == "Face ID") {
|
||||||
providerItems = append(providerItems, providerItem)
|
providerItems = append(providerItems, providerItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,12 @@ func getCertByName(name string) (*Cert, error) {
|
|||||||
|
|
||||||
func GetCert(id string) (*Cert, error) {
|
func GetCert(id string) (*Cert, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name := util.GetOwnerAndNameFromId(id)
|
||||||
return getCert(owner, name)
|
cert, err := getCert(owner, name)
|
||||||
|
if cert == nil && owner != "admin" {
|
||||||
|
return getCert("admin", name)
|
||||||
|
} else {
|
||||||
|
return cert, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateCert(id string, cert *Cert) (bool, error) {
|
func UpdateCert(id string, cert *Cert) (bool, error) {
|
||||||
|
@ -241,6 +241,10 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
|||||||
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
|
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if password == "" {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Password cannot be empty"))
|
||||||
|
}
|
||||||
|
|
||||||
passwordType := user.PasswordType
|
passwordType := user.PasswordType
|
||||||
if passwordType == "" {
|
if passwordType == "" {
|
||||||
passwordType = organization.PasswordType
|
passwordType = organization.PasswordType
|
||||||
@ -248,7 +252,7 @@ func CheckPassword(user *User, password string, lang string, options ...bool) er
|
|||||||
credManager := cred.GetCredManager(passwordType)
|
credManager := cred.GetCredManager(passwordType)
|
||||||
if credManager != nil {
|
if credManager != nil {
|
||||||
if organization.MasterPassword != "" {
|
if organization.MasterPassword != "" {
|
||||||
if credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
|
if password == organization.MasterPassword || credManager.IsPasswordCorrect(password, organization.MasterPassword, "", organization.PasswordSalt) {
|
||||||
return resetUserSigninErrorTimes(user)
|
return resetUserSigninErrorTimes(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -513,8 +517,8 @@ func CheckLoginPermission(userId string, application *Application) (bool, error)
|
|||||||
func CheckUsername(username string, lang string) string {
|
func CheckUsername(username string, lang string) string {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return i18n.Translate(lang, "check:Empty username.")
|
return i18n.Translate(lang, "check:Empty username.")
|
||||||
} else if len(username) > 39 {
|
} else if len(username) > 255 {
|
||||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||||
@ -529,8 +533,8 @@ func CheckUsername(username string, lang string) string {
|
|||||||
func CheckUsernameWithEmail(username string, lang string) string {
|
func CheckUsernameWithEmail(username string, lang string) string {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return i18n.Translate(lang, "check:Empty username.")
|
return i18n.Translate(lang, "check:Empty username.")
|
||||||
} else if len(username) > 39 {
|
} else if len(username) > 255 {
|
||||||
return i18n.Translate(lang, "check:Username is too long (maximum is 39 characters).")
|
return i18n.Translate(lang, "check:Username is too long (maximum is 255 characters).")
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
// https://stackoverflow.com/questions/58726546/github-username-convention-using-regex
|
||||||
|
@ -74,7 +74,7 @@ func checkPasswordComplexity(password string, options []string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(options) == 0 {
|
if len(options) == 0 {
|
||||||
options = []string{"AtLeast6"}
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
checkers := map[string]ValidatorFunc{
|
checkers := map[string]ValidatorFunc{
|
||||||
|
@ -16,27 +16,22 @@
|
|||||||
|
|
||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import "github.com/casdoor/casdoor/email"
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/email"
|
// TestSmtpServer Test the SMTP server
|
||||||
"github.com/casdoor/gomail/v2"
|
func TestSmtpServer(provider *Provider) error {
|
||||||
)
|
smtpEmailProvider := email.NewSmtpEmailProvider(provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.Type, provider.DisableSsl)
|
||||||
|
sender, err := smtpEmailProvider.Dialer.Dial()
|
||||||
func getDialer(provider *Provider) *gomail.Dialer {
|
if err != nil {
|
||||||
dialer := &gomail.Dialer{}
|
return err
|
||||||
dialer = gomail.NewDialer(provider.Host, provider.Port, provider.ClientId, provider.ClientSecret)
|
|
||||||
if provider.Type == "SUBMAIL" {
|
|
||||||
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
}
|
||||||
|
defer sender.Close()
|
||||||
|
|
||||||
dialer.SSL = !provider.DisableSsl
|
return nil
|
||||||
|
|
||||||
return dialer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||||
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method)
|
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl)
|
||||||
|
|
||||||
fromAddress := provider.ClientId2
|
fromAddress := provider.ClientId2
|
||||||
if fromAddress == "" {
|
if fromAddress == "" {
|
||||||
@ -50,16 +45,3 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
|
|||||||
|
|
||||||
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DailSmtpServer Dail Smtp server
|
|
||||||
func DailSmtpServer(provider *Provider) error {
|
|
||||||
dialer := getDialer(provider)
|
|
||||||
|
|
||||||
sender, err := dialer.Dial()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sender.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -17,6 +17,8 @@ package object
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardDateItem struct {
|
type DashboardDateItem struct {
|
||||||
@ -40,11 +42,12 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
|
|||||||
time30day := time.Now().AddDate(0, 0, -30)
|
time30day := time.Now().AddDate(0, 0, -30)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var err error
|
var err error
|
||||||
|
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
|
||||||
wg.Add(len(tableNames))
|
wg.Add(len(tableNames))
|
||||||
ch := make(chan error, len(tableNames))
|
ch := make(chan error, len(tableNames))
|
||||||
for _, tableName := range tableNames {
|
for _, tableName := range tableNames {
|
||||||
dashboard[tableName+"Counts"] = make([]int64, 31)
|
dashboard[tableName+"Counts"] = make([]int64, 31)
|
||||||
tableName := tableName
|
tableFullName := tableNamePrefix + tableName
|
||||||
go func(ch chan error) {
|
go func(ch chan error) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
dashboardDateItems := []DashboardDateItem{}
|
dashboardDateItems := []DashboardDateItem{}
|
||||||
@ -58,16 +61,16 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
|
|||||||
dbQueryBefore = dbQueryBefore.And("owner = ?", owner)
|
dbQueryBefore = dbQueryBefore.And("owner = ?", owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableName).Count(); err != nil {
|
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableFullName).Count(); err != nil {
|
||||||
ch <- err
|
ch <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableName).Find(&dashboardDateItems); err != nil {
|
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableFullName).Find(&dashboardDateItems); err != nil {
|
||||||
ch <- err
|
ch <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardMap.Store(tableName, DashboardMapItem{
|
dashboardMap.Store(tableFullName, DashboardMapItem{
|
||||||
dashboardDateItems: dashboardDateItems,
|
dashboardDateItems: dashboardDateItems,
|
||||||
itemCount: countResult,
|
itemCount: countResult,
|
||||||
})
|
})
|
||||||
|
@ -17,7 +17,6 @@ package object
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
@ -36,11 +35,13 @@ type Group struct {
|
|||||||
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
||||||
Type string `xorm:"varchar(100)" json:"type"`
|
Type string `xorm:"varchar(100)" json:"type"`
|
||||||
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
ParentId string `xorm:"varchar(100)" json:"parentId"`
|
||||||
|
ParentName string `xorm:"-" json:"parentName"`
|
||||||
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
||||||
Users []string `xorm:"-" json:"users"`
|
Users []string `xorm:"-" json:"users"`
|
||||||
|
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty"`
|
||||||
|
HaveChildren bool `xorm:"-" json:"haveChildren"`
|
||||||
Children []*Group `json:"children,omitempty"`
|
Children []*Group `json:"children,omitempty"`
|
||||||
|
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
@ -79,6 +80,30 @@ func GetPaginationGroups(owner string, offset, limit int, field, value, sortFiel
|
|||||||
return groups, nil
|
return groups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, group := range groupsHaveChildren {
|
||||||
|
resultMap[group.ParentId] = groupMap[group.ParentId]
|
||||||
|
}
|
||||||
|
return resultMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getGroup(owner string, name string) (*Group, error) {
|
func getGroup(owner string, name string) (*Group, error) {
|
||||||
if owner == "" || name == "" {
|
if owner == "" || name == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -281,7 +306,10 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
|
|||||||
|
|
||||||
func GetGroupUsers(groupId string) ([]*User, error) {
|
func GetGroupUsers(groupId string) ([]*User, error) {
|
||||||
users := []*User{}
|
users := []*User{}
|
||||||
owner, _ := util.GetOwnerAndNameFromId(groupId)
|
owner, _, err := util.GetOwnerAndNameFromIdWithError(groupId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
|
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -293,22 +321,21 @@ func GetGroupUsers(groupId string) ([]*User, error) {
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetGroupUsersWithoutError(groupId string) []*User {
|
||||||
|
users, _ := GetGroupUsers(groupId)
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
func ExtendGroupWithUsers(group *Group) error {
|
func ExtendGroupWithUsers(group *Group) error {
|
||||||
if group == nil {
|
if group == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := GetUsers(group.Owner)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupId := group.GetId()
|
groupId := group.GetId()
|
||||||
userIds := []string{}
|
userIds := []string{}
|
||||||
for _, user := range users {
|
userIds, err := userEnforcer.GetAllUsersByGroup(groupId)
|
||||||
if util.InSlice(user.Groups, groupId) {
|
if err != nil {
|
||||||
userIds = append(userIds, user.GetId())
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.Users = userIds
|
group.Users = userIds
|
||||||
@ -316,29 +343,14 @@ func ExtendGroupWithUsers(group *Group) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExtendGroupsWithUsers(groups []*Group) error {
|
func ExtendGroupsWithUsers(groups []*Group) error {
|
||||||
var wg sync.WaitGroup
|
|
||||||
errChan := make(chan error, len(groups))
|
|
||||||
|
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
wg.Add(1)
|
users, err := userEnforcer.GetAllUsersByGroup(group.GetId())
|
||||||
go func(group *Group) {
|
|
||||||
defer wg.Done()
|
|
||||||
err := ExtendGroupWithUsers(group)
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(errChan)
|
|
||||||
|
|
||||||
for err := range errChan {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
group.Users = users
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +103,7 @@ func initBuiltInOrganization() bool {
|
|||||||
PasswordOptions: []string{"AtLeast6"},
|
PasswordOptions: []string{"AtLeast6"},
|
||||||
CountryCodes: []string{"US", "ES", "FR", "DE", "GB", "CN", "JP", "KR", "VN", "ID", "SG", "IN"},
|
CountryCodes: []string{"US", "ES", "FR", "DE", "GB", "CN", "JP", "KR", "VN", "ID", "SG", "IN"},
|
||||||
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
|
DefaultAvatar: fmt.Sprintf("%s/img/casbin.svg", conf.GetConfigString("staticBaseUrl")),
|
||||||
|
UserTypes: []string{},
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"},
|
Languages: []string{"en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "pt"},
|
||||||
InitScore: 2000,
|
InitScore: 2000,
|
||||||
|
@ -70,12 +70,12 @@ func InitFromFile() {
|
|||||||
for _, provider := range initData.Providers {
|
for _, provider := range initData.Providers {
|
||||||
initDefinedProvider(provider)
|
initDefinedProvider(provider)
|
||||||
}
|
}
|
||||||
for _, user := range initData.Users {
|
|
||||||
initDefinedUser(user)
|
|
||||||
}
|
|
||||||
for _, application := range initData.Applications {
|
for _, application := range initData.Applications {
|
||||||
initDefinedApplication(application)
|
initDefinedApplication(application)
|
||||||
}
|
}
|
||||||
|
for _, user := range initData.Users {
|
||||||
|
initDefinedUser(user)
|
||||||
|
}
|
||||||
for _, cert := range initData.Certs {
|
for _, cert := range initData.Certs {
|
||||||
initDefinedCert(cert)
|
initDefinedCert(cert)
|
||||||
}
|
}
|
||||||
|
@ -27,12 +27,14 @@ type Ldap struct {
|
|||||||
Host string `xorm:"varchar(100)" json:"host"`
|
Host string `xorm:"varchar(100)" json:"host"`
|
||||||
Port int `xorm:"int" json:"port"`
|
Port int `xorm:"int" json:"port"`
|
||||||
EnableSsl bool `xorm:"bool" json:"enableSsl"`
|
EnableSsl bool `xorm:"bool" json:"enableSsl"`
|
||||||
|
AllowSelfSignedCert bool `xorm:"bool" json:"allowSelfSignedCert"`
|
||||||
Username string `xorm:"varchar(100)" json:"username"`
|
Username string `xorm:"varchar(100)" json:"username"`
|
||||||
Password string `xorm:"varchar(100)" json:"password"`
|
Password string `xorm:"varchar(100)" json:"password"`
|
||||||
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
|
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
|
||||||
Filter string `xorm:"varchar(200)" json:"filter"`
|
Filter string `xorm:"varchar(200)" json:"filter"`
|
||||||
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
FilterFields []string `xorm:"varchar(100)" json:"filterFields"`
|
||||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||||
|
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||||
|
|
||||||
AutoSync int `json:"autoSync"`
|
AutoSync int `json:"autoSync"`
|
||||||
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
LastSync string `xorm:"varchar(100)" json:"lastSync"`
|
||||||
@ -149,7 +151,7 @@ func UpdateLdap(ldap *Ldap) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
affected, err := ormer.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
|
||||||
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group").Update(ldap)
|
"port", "enable_ssl", "username", "password", "base_dn", "filter", "filter_fields", "auto_sync", "default_group", "password_type", "allow_self_signed_cert").Update(ldap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -106,6 +106,12 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
existed, failed, err := SyncLdapUsers(ldap.Owner, AutoAdjustLdapUser(users), ldap.Id)
|
existed, failed, err := SyncLdapUsers(ldap.Owner, AutoAdjustLdapUser(users), ldap.Id)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if len(failed) != 0 {
|
if len(failed) != 0 {
|
||||||
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
|
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(existed)-len(failed), len(failed)), failed)
|
||||||
logs.Warning(err.Error())
|
logs.Warning(err.Error())
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -62,8 +65,11 @@ type LdapUser struct {
|
|||||||
|
|
||||||
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
|
||||||
var conn *goldap.Conn
|
var conn *goldap.Conn
|
||||||
|
tlsConfig := tls.Config{
|
||||||
|
InsecureSkipVerify: ldap.AllowSelfSignedCert,
|
||||||
|
}
|
||||||
if ldap.EnableSsl {
|
if ldap.EnableSsl {
|
||||||
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), nil)
|
conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), &tlsConfig)
|
||||||
} else {
|
} else {
|
||||||
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
|
conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
|
||||||
}
|
}
|
||||||
@ -373,7 +379,7 @@ func GetExistUuids(owner string, uuids []string) ([]string, error) {
|
|||||||
return existUuids, nil
|
return existUuids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetLdapPassword(user *User, newPassword string, lang string) error {
|
func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang string) error {
|
||||||
ldaps, err := GetLdaps(user.Owner)
|
ldaps, err := GetLdaps(user.Owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -416,8 +422,32 @@ func ResetLdapPassword(user *User, newPassword string, lang string) error {
|
|||||||
}
|
}
|
||||||
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
|
||||||
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
|
modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
|
||||||
|
} else if oldPassword != "" {
|
||||||
|
modifyPasswordRequestWithOldPassword := goldap.NewPasswordModifyRequest(userDn, oldPassword, newPassword)
|
||||||
|
_, err = conn.Conn.PasswordModify(modifyPasswordRequestWithOldPassword)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
} else {
|
} else {
|
||||||
|
switch ldapServer.PasswordType {
|
||||||
|
case "SSHA":
|
||||||
|
pwdEncoded, err = generateSSHA(newPassword)
|
||||||
|
break
|
||||||
|
case "MD5":
|
||||||
|
md5Byte := md5.Sum([]byte(newPassword))
|
||||||
|
md5Password := base64.StdEncoding.EncodeToString(md5Byte[:])
|
||||||
|
pwdEncoded = "{MD5}" + md5Password
|
||||||
|
break
|
||||||
|
case "Plain":
|
||||||
pwdEncoded = newPassword
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
pwdEncoded = newPassword
|
||||||
|
break
|
||||||
|
}
|
||||||
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
object/ldap_password_type.go
Normal file
36
object/ldap_password_type.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSSHA(password string) (string, error) {
|
||||||
|
salt := make([]byte, 4)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := append([]byte(password), salt...)
|
||||||
|
hash := sha1.Sum(combined)
|
||||||
|
hashWithSalt := append(hash[:], salt...)
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(hashWithSalt)
|
||||||
|
|
||||||
|
return "{SSHA}" + encoded, nil
|
||||||
|
}
|
@ -60,7 +60,8 @@ func (mfa *SmsMfa) Enable(user *User) error {
|
|||||||
columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
|
columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
|
||||||
} else if mfa.MfaType == EmailType {
|
} else if mfa.MfaType == EmailType {
|
||||||
user.MfaEmailEnabled = true
|
user.MfaEmailEnabled = true
|
||||||
columns = append(columns, "mfa_email_enabled", "email")
|
user.EmailVerified = true
|
||||||
|
columns = append(columns, "mfa_email_enabled", "email", "email_verified")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := UpdateUser(user.GetId(), user, columns, false)
|
_, err := UpdateUser(user.GetId(), user, columns, false)
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
|
func getNotificationClient(provider *Provider) (notify.Notifier, error) {
|
||||||
var client notify.Notifier
|
var client notify.Notifier
|
||||||
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata)
|
client, err := notification.GetNotificationProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.ClientId2, provider.ClientSecret2, provider.AppId, provider.Receiver, provider.Method, provider.Title, provider.Metadata, provider.RegionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
|||||||
return origin, origin
|
return origin, origin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDev := conf.GetConfigString("runmode") == "dev"
|
||||||
// "door.casdoor.com"
|
// "door.casdoor.com"
|
||||||
protocol := "https://"
|
protocol := "https://"
|
||||||
if !strings.Contains(host, ".") {
|
if !strings.Contains(host, ".") {
|
||||||
@ -87,7 +88,7 @@ func getOriginFromHostInternal(host string) (string, string) {
|
|||||||
protocol = "http://"
|
protocol = "http://"
|
||||||
}
|
}
|
||||||
|
|
||||||
if host == "localhost:8000" {
|
if host == "localhost:8000" && isDev {
|
||||||
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
|
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
|
||||||
} else {
|
} else {
|
||||||
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
|
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
|
||||||
|
@ -63,14 +63,15 @@ type Organization struct {
|
|||||||
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
||||||
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
||||||
PasswordExpireDays int `json:"passwordExpireDays"`
|
PasswordExpireDays int `json:"passwordExpireDays"`
|
||||||
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
CountryCodes []string `xorm:"mediumtext" json:"countryCodes"`
|
||||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||||
|
UserTypes []string `xorm:"mediumtext" json:"userTypes"`
|
||||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||||
Languages []string `xorm:"varchar(255)" json:"languages"`
|
Languages []string `xorm:"varchar(255)" json:"languages"`
|
||||||
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
ThemeData *ThemeData `xorm:"json" json:"themeData"`
|
||||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
MasterPassword string `xorm:"varchar(200)" json:"masterPassword"`
|
||||||
DefaultPassword string `xorm:"varchar(100)" json:"defaultPassword"`
|
DefaultPassword string `xorm:"varchar(200)" json:"defaultPassword"`
|
||||||
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
|
MasterVerificationCode string `xorm:"varchar(100)" json:"masterVerificationCode"`
|
||||||
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"`
|
||||||
InitScore int `json:"initScore"`
|
InitScore int `json:"initScore"`
|
||||||
@ -79,6 +80,8 @@ type Organization struct {
|
|||||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||||
EnableTour bool `json:"enableTour"`
|
EnableTour bool `json:"enableTour"`
|
||||||
IpRestriction string `json:"ipRestriction"`
|
IpRestriction string `json:"ipRestriction"`
|
||||||
|
NavItems []string `xorm:"varchar(1000)" json:"navItems"`
|
||||||
|
WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"`
|
||||||
|
|
||||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||||
@ -151,7 +154,10 @@ func getOrganization(owner string, name string) (*Organization, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetOrganization(id string) (*Organization, error) {
|
func GetOrganization(id string) (*Organization, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return getOrganization(owner, name)
|
return getOrganization(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,9 +198,10 @@ func GetMaskedOrganizations(organizations []*Organization, errs ...error) ([]*Or
|
|||||||
return organizations, nil
|
return organizations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateOrganization(id string, organization *Organization) (bool, error) {
|
func UpdateOrganization(id string, organization *Organization, isGlobalAdmin bool) (bool, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name := util.GetOwnerAndNameFromId(id)
|
||||||
if org, err := getOrganization(owner, name); err != nil {
|
org, err := getOrganization(owner, name)
|
||||||
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if org == nil {
|
} else if org == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -219,6 +226,11 @@ func UpdateOrganization(id string, organization *Organization) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isGlobalAdmin {
|
||||||
|
organization.NavItems = org.NavItems
|
||||||
|
organization.WidgetItems = org.WidgetItems
|
||||||
|
}
|
||||||
|
|
||||||
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||||
|
|
||||||
if organization.MasterPassword == "***" {
|
if organization.MasterPassword == "***" {
|
||||||
|
@ -157,7 +157,7 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) (*Ormer
|
|||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAdapterFromdb is the constructor for Ormer.
|
// NewAdapterFromDb is the constructor for Ormer.
|
||||||
func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, db *sql.DB) (*Ormer, error) {
|
func NewAdapterFromDb(driverName string, dataSourceName string, dbName string, db *sql.DB) (*Ormer, error) {
|
||||||
a := &Ormer{}
|
a := &Ormer{}
|
||||||
a.driverName = driverName
|
a.driverName = driverName
|
||||||
|
@ -148,7 +148,7 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if permission.ResourceType == "Application" && permission.Model != "" {
|
if permission.ResourceType == "Application" && permission.Model != "" {
|
||||||
model, err := GetModelEx(util.GetId(owner, permission.Model))
|
model, err := GetModelEx(util.GetId(permission.Owner, permission.Model))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if model == nil {
|
} else if model == nil {
|
||||||
|
@ -219,8 +219,11 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
ProductName: product.Name,
|
ProductName: product.Name,
|
||||||
PayerName: payerName,
|
PayerName: payerName,
|
||||||
PayerId: user.Id,
|
PayerId: user.Id,
|
||||||
|
PayerEmail: user.Email,
|
||||||
PaymentName: paymentName,
|
PaymentName: paymentName,
|
||||||
ProductDisplayName: product.DisplayName,
|
ProductDisplayName: product.DisplayName,
|
||||||
|
ProductDescription: product.Description,
|
||||||
|
ProductImage: product.Image,
|
||||||
Price: product.Price,
|
Price: product.Price,
|
||||||
Currency: product.Currency,
|
Currency: product.Currency,
|
||||||
ReturnUrl: returnUrl,
|
ReturnUrl: returnUrl,
|
||||||
|
@ -48,6 +48,7 @@ type Provider struct {
|
|||||||
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
||||||
Scopes string `xorm:"varchar(100)" json:"scopes"`
|
Scopes string `xorm:"varchar(100)" json:"scopes"`
|
||||||
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"`
|
UserMapping map[string]string `xorm:"varchar(500)" json:"userMapping"`
|
||||||
|
HttpHeaders map[string]string `xorm:"varchar(500)" json:"httpHeaders"`
|
||||||
|
|
||||||
Host string `xorm:"varchar(100)" json:"host"`
|
Host string `xorm:"varchar(100)" json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
@ -325,6 +326,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return pp, nil
|
return pp, nil
|
||||||
|
} else if typ == "AirWallex" {
|
||||||
|
pp, err := pp.NewAirwallexPaymentProvider(p.ClientId, p.ClientSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pp, nil
|
||||||
} else if typ == "Balance" {
|
} else if typ == "Balance" {
|
||||||
pp, err := pp.NewBalancePaymentProvider()
|
pp, err := pp.NewBalancePaymentProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -378,6 +385,44 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFaceIdProviderByOwnerName(applicationId, lang string) (*Provider, error) {
|
||||||
|
owner, name := util.GetOwnerAndNameFromId(applicationId)
|
||||||
|
provider := Provider{Owner: owner, Name: name, Category: "Face ID"}
|
||||||
|
existed, err := ormer.Engine.Get(&provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !existed {
|
||||||
|
return nil, fmt.Errorf(i18n.Translate(lang, "provider:the provider: %s does not exist"), applicationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFaceIdProviderByApplication(applicationId, isCurrentProvider, lang string) (*Provider, error) {
|
||||||
|
if isCurrentProvider == "true" {
|
||||||
|
return GetFaceIdProviderByOwnerName(applicationId, lang)
|
||||||
|
}
|
||||||
|
application, err := GetApplication(applicationId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if application == nil || len(application.Providers) == 0 {
|
||||||
|
return nil, fmt.Errorf(i18n.Translate(lang, "provider:Invalid application id"))
|
||||||
|
}
|
||||||
|
for _, provider := range application.Providers {
|
||||||
|
if provider.Provider == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if provider.Provider.Category == "Face ID" {
|
||||||
|
return GetFaceIdProviderByOwnerName(util.GetId(provider.Provider.Owner, provider.Provider.Name), lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func providerChangeTrigger(oldName string, newName string) error {
|
func providerChangeTrigger(oldName string, newName string) error {
|
||||||
session := ormer.Engine.NewSession()
|
session := ormer.Engine.NewSession()
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
@ -30,7 +30,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
saml "github.com/russellhaering/gosaml2"
|
saml "github.com/russellhaering/gosaml2"
|
||||||
dsig "github.com/russellhaering/goxmldsig"
|
dsig "github.com/russellhaering/goxmldsig"
|
||||||
@ -338,6 +338,9 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
|||||||
} else if authnRequest.AssertionConsumerServiceURL == "" {
|
} else if authnRequest.AssertionConsumerServiceURL == "" {
|
||||||
return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>")
|
return "", "", "", fmt.Errorf("err: SAML request don't has attribute 'AssertionConsumerServiceURL' in <samlp:AuthnRequest>")
|
||||||
}
|
}
|
||||||
|
if authnRequest.ProtocolBinding == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" {
|
||||||
|
method = "POST"
|
||||||
|
}
|
||||||
|
|
||||||
_, originBackend := getOriginFromHost(host)
|
_, originBackend := getOriginFromHost(host)
|
||||||
|
|
||||||
|
@ -123,8 +123,7 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
|||||||
|
|
||||||
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
||||||
switch tokenTypeHint {
|
switch tokenTypeHint {
|
||||||
case "access_token":
|
case "access_token", "access-token":
|
||||||
case "access-token":
|
|
||||||
token, err := GetTokenByAccessToken(tokenValue)
|
token, err := GetTokenByAccessToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -132,8 +131,7 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
|||||||
if token != nil {
|
if token != nil {
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
case "refresh_token":
|
case "refresh_token", "refresh-token":
|
||||||
case "refresh-token":
|
|
||||||
token, err := GetTokenByRefreshToken(tokenValue)
|
token, err := GetTokenByRefreshToken(tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -146,13 +144,13 @@ func GetTokenByTokenValue(tokenValue, tokenTypeHint string) (*Token, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUsedByCode(token *Token) bool {
|
func updateUsedByCode(token *Token) (bool, error) {
|
||||||
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
|
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return affected != 0
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetToken(id string) (*Token, error) {
|
func GetToken(id string) (*Token, error) {
|
||||||
|
@ -21,7 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
@ -30,6 +30,8 @@ type Claims struct {
|
|||||||
Nonce string `json:"nonce,omitempty"`
|
Nonce string `json:"nonce,omitempty"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
Scope string `json:"scope,omitempty"`
|
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"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +139,7 @@ type ClaimsShort struct {
|
|||||||
TokenType string `json:"tokenType,omitempty"`
|
TokenType string `json:"tokenType,omitempty"`
|
||||||
Nonce string `json:"nonce,omitempty"`
|
Nonce string `json:"nonce,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
|
Azp string `json:"azp,omitempty"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +158,7 @@ type ClaimsWithoutThirdIdp struct {
|
|||||||
Nonce string `json:"nonce,omitempty"`
|
Nonce string `json:"nonce,omitempty"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
|
Azp string `json:"azp,omitempty"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,6 +273,7 @@ func getShortClaims(claims Claims) ClaimsShort {
|
|||||||
Nonce: claims.Nonce,
|
Nonce: claims.Nonce,
|
||||||
Scope: claims.Scope,
|
Scope: claims.Scope,
|
||||||
RegisteredClaims: claims.RegisteredClaims,
|
RegisteredClaims: claims.RegisteredClaims,
|
||||||
|
Azp: claims.Azp,
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -281,6 +286,7 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
|
|||||||
Tag: claims.Tag,
|
Tag: claims.Tag,
|
||||||
Scope: claims.Scope,
|
Scope: claims.Scope,
|
||||||
RegisteredClaims: claims.RegisteredClaims,
|
RegisteredClaims: claims.RegisteredClaims,
|
||||||
|
Azp: claims.Azp,
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -301,6 +307,7 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
|
|||||||
res["nonce"] = claims.Nonce
|
res["nonce"] = claims.Nonce
|
||||||
res["tag"] = claims.Tag
|
res["tag"] = claims.Tag
|
||||||
res["scope"] = claims.Scope
|
res["scope"] = claims.Scope
|
||||||
|
res["azp"] = claims.Azp
|
||||||
|
|
||||||
for _, field := range tokenField {
|
for _, field := range tokenField {
|
||||||
userField := userValue.FieldByName(field)
|
userField := userValue.FieldByName(field)
|
||||||
@ -357,6 +364,7 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
|||||||
// FIXME: A workaround for custom claim by reusing `tag` in user info
|
// FIXME: A workaround for custom claim by reusing `tag` in user info
|
||||||
Tag: user.Tag,
|
Tag: user.Tag,
|
||||||
Scope: scope,
|
Scope: scope,
|
||||||
|
Azp: application.ClientId,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Issuer: originBackend,
|
Issuer: originBackend,
|
||||||
Subject: user.Id,
|
Subject: user.Id,
|
||||||
|
@ -248,7 +248,10 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
|||||||
|
|
||||||
token.CodeIsUsed = true
|
token.CodeIsUsed = true
|
||||||
|
|
||||||
go updateUsedByCode(token)
|
_, err = updateUsedByCode(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tokenWrapper := &TokenWrapper{
|
tokenWrapper := &TokenWrapper{
|
||||||
AccessToken: token.AccessToken,
|
AccessToken: token.AccessToken,
|
||||||
|
@ -19,7 +19,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClaimsStandard struct {
|
type ClaimsStandard struct {
|
||||||
@ -32,6 +32,7 @@ type ClaimsStandard struct {
|
|||||||
Nonce string `json:"nonce,omitempty"`
|
Nonce string `json:"nonce,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
Address OIDCAddress `json:"address,omitempty"`
|
Address OIDCAddress `json:"address,omitempty"`
|
||||||
|
Azp string `json:"azp,omitempty"`
|
||||||
|
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
@ -52,6 +53,7 @@ func getStandardClaims(claims Claims) ClaimsStandard {
|
|||||||
Nonce: claims.Nonce,
|
Nonce: claims.Nonce,
|
||||||
Scope: claims.Scope,
|
Scope: claims.Scope,
|
||||||
RegisteredClaims: claims.RegisteredClaims,
|
RegisteredClaims: claims.RegisteredClaims,
|
||||||
|
Azp: claims.Azp,
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Phone = ""
|
res.Phone = ""
|
||||||
|
@ -15,13 +15,17 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
|
"github.com/casdoor/casdoor/faceId"
|
||||||
|
"github.com/casdoor/casdoor/proxy"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/xorm-io/builder"
|
"github.com/xorm-io/builder"
|
||||||
@ -48,7 +52,7 @@ func InitUserManager() {
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(255) notnull pk" json:"name"`
|
||||||
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
|
CreatedTime string `xorm:"varchar(100) index" json:"createdTime"`
|
||||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||||
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
|
DeletedTime string `xorm:"varchar(100)" json:"deletedTime"`
|
||||||
@ -244,6 +248,7 @@ type MfaAccount struct {
|
|||||||
type FaceId struct {
|
type FaceId struct {
|
||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||||
FaceIdData []float64 `json:"faceIdData"`
|
FaceIdData []float64 `json:"faceIdData"`
|
||||||
|
ImageUrl string `json:"ImageUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
|
func GetUserFieldStringValue(user *User, fieldName string) (bool, string, error) {
|
||||||
@ -454,6 +459,31 @@ func GetUserByEmail(owner string, email string) (*User, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserByWebauthID(webauthId string) (*User, error) {
|
||||||
|
user := User{}
|
||||||
|
existed := false
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if ormer.driverName == "postgres" {
|
||||||
|
existed, err = ormer.Engine.Where(builder.Like{"\"webauthnCredentials\"", webauthId}).Get(&user)
|
||||||
|
} else if ormer.driverName == "mssql" {
|
||||||
|
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS VARCHAR(MAX)) like ?", "%"+webauthId+"%").Get(&user)
|
||||||
|
} else if ormer.driverName == "sqlite" {
|
||||||
|
existed, err = ormer.Engine.Where("CAST(webauthnCredentials AS text) like ?", "%"+webauthId+"%").Get(&user)
|
||||||
|
} else {
|
||||||
|
existed, err = ormer.Engine.Where("webauthnCredentials like ?", "%"+webauthId+"%").Get(&user)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !existed {
|
||||||
|
return nil, fmt.Errorf("user not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, err
|
||||||
|
}
|
||||||
|
|
||||||
func GetUserByEmailOnly(email string) (*User, error) {
|
func GetUserByEmailOnly(email string) (*User, error) {
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -807,6 +837,10 @@ func AddUser(user *User) (bool, error) {
|
|||||||
return false, fmt.Errorf("the user's owner and name should not be empty")
|
return false, fmt.Errorf("the user's owner and name should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CheckUsername(user.Name, "en") != "" {
|
||||||
|
user.Name = util.GetRandomName()
|
||||||
|
}
|
||||||
|
|
||||||
organization, err := GetOrganizationByUser(user)
|
organization, err := GetOrganizationByUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -815,6 +849,16 @@ func AddUser(user *User) (bool, error) {
|
|||||||
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
|
return false, fmt.Errorf("the organization: %s is not found", user.Owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Owner != "built-in" {
|
||||||
|
applicationCount, err := GetOrganizationApplicationCount(organization.Owner, organization.Name, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if applicationCount == 0 {
|
||||||
|
return false, fmt.Errorf("The organization: %s should have one application at least", organization.Owner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if organization.DefaultPassword != "" && user.Password == "123" {
|
if organization.DefaultPassword != "" && user.Password == "123" {
|
||||||
user.Password = organization.DefaultPassword
|
user.Password = organization.DefaultPassword
|
||||||
}
|
}
|
||||||
@ -846,11 +890,14 @@ func AddUser(user *User) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rankingItem := GetAccountItemByName("Ranking", organization)
|
||||||
|
if rankingItem != nil {
|
||||||
count, err := GetUserCount(user.Owner, "", "", "")
|
count, err := GetUserCount(user.Owner, "", "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
user.Ranking = int(count + 1)
|
user.Ranking = int(count + 1)
|
||||||
|
}
|
||||||
|
|
||||||
if user.Groups != nil && len(user.Groups) > 0 {
|
if user.Groups != nil && len(user.Groups) > 0 {
|
||||||
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||||
@ -962,6 +1009,11 @@ func DeleteUser(user *User) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = userEnforcer.DeleteGroupsForUser(user.GetId())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
organization, err := GetOrganizationByUser(user)
|
organization, err := GetOrganizationByUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -1171,6 +1223,40 @@ func (user *User) IsGlobalAdmin() bool {
|
|||||||
return user.Owner == "built-in"
|
return user.Owner == "built-in"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) CheckUserFace(faceIdImage []string, provider *Provider) (bool, error) {
|
||||||
|
faceIdChecker := faceId.GetFaceIdProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Endpoint)
|
||||||
|
httpClient := proxy.DefaultHttpClient
|
||||||
|
errList := []error{}
|
||||||
|
for _, userFaceId := range user.FaceIds {
|
||||||
|
if userFaceId.ImageUrl != "" {
|
||||||
|
imgResp, err := httpClient.Get(userFaceId.ImageUrl)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imgByte, err := io.ReadAll(imgResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Img := base64.StdEncoding.EncodeToString(imgByte)
|
||||||
|
for _, imgBase64 := range faceIdImage {
|
||||||
|
isSuccess, err := faceIdChecker.Check(imgBase64, base64Img)
|
||||||
|
if err != nil {
|
||||||
|
errList = append(errList, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isSuccess {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errList) > 0 {
|
||||||
|
return false, errList[0]
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GenerateIdForNewUser(application *Application) (string, error) {
|
func GenerateIdForNewUser(application *Application) (string, error) {
|
||||||
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
|
if application == nil || application.GetSignupItemRule("ID") != "Incremental" {
|
||||||
return util.GenerateId(), nil
|
return util.GenerateId(), nil
|
||||||
|
@ -86,9 +86,9 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
|
|||||||
title := provider.Title
|
title := provider.Title
|
||||||
|
|
||||||
code := getRandomCode(6)
|
code := getRandomCode(6)
|
||||||
if organization.MasterVerificationCode != "" {
|
// if organization.MasterVerificationCode != "" {
|
||||||
code = organization.MasterVerificationCode
|
// code = organization.MasterVerificationCode
|
||||||
}
|
// }
|
||||||
|
|
||||||
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
|
||||||
content := strings.Replace(provider.Content, "%s", code, 1)
|
content := strings.Replace(provider.Content, "%s", code, 1)
|
||||||
@ -124,9 +124,9 @@ func SendVerificationCodeToPhone(organization *Organization, user *User, provide
|
|||||||
}
|
}
|
||||||
|
|
||||||
code := getRandomCode(6)
|
code := getRandomCode(6)
|
||||||
if organization.MasterVerificationCode != "" {
|
// if organization.MasterVerificationCode != "" {
|
||||||
code = organization.MasterVerificationCode
|
// code = organization.MasterVerificationCode
|
||||||
}
|
// }
|
||||||
|
|
||||||
err = SendSms(provider, code, dest)
|
err = SendSms(provider, code, dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -38,6 +38,7 @@ type Webhook struct {
|
|||||||
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
ContentType string `xorm:"varchar(100)" json:"contentType"`
|
||||||
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
Headers []*Header `xorm:"mediumtext" json:"headers"`
|
||||||
Events []string `xorm:"varchar(1000)" json:"events"`
|
Events []string `xorm:"varchar(1000)" json:"events"`
|
||||||
|
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||||
IsUserExtended bool `json:"isUserExtended"`
|
IsUserExtended bool `json:"isUserExtended"`
|
||||||
SingleOrgOnly bool `json:"singleOrgOnly"`
|
SingleOrgOnly bool `json:"singleOrgOnly"`
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
|
@ -17,6 +17,7 @@ package object
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
@ -25,7 +26,32 @@ import (
|
|||||||
|
|
||||||
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
|
func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *User) (int, string, error) {
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
userMap := make(map[string]interface{})
|
||||||
|
var body io.Reader
|
||||||
|
|
||||||
|
if webhook.TokenFields != nil && len(webhook.TokenFields) > 0 && extendedUser != nil {
|
||||||
|
userValue := reflect.ValueOf(extendedUser).Elem()
|
||||||
|
|
||||||
|
for _, field := range webhook.TokenFields {
|
||||||
|
userField := userValue.FieldByName(field)
|
||||||
|
if userField.IsValid() {
|
||||||
|
newfield := util.SnakeToCamel(util.CamelToSnakeCase(field))
|
||||||
|
userMap[newfield] = userField.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordEx struct {
|
||||||
|
casvisorsdk.Record
|
||||||
|
ExtendedUser map[string]interface{} `json:"extendedUser"`
|
||||||
|
}
|
||||||
|
|
||||||
|
recordEx := &RecordEx{
|
||||||
|
Record: *record,
|
||||||
|
ExtendedUser: userMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
body = strings.NewReader(util.StructToJson(recordEx))
|
||||||
|
} else {
|
||||||
type RecordEx struct {
|
type RecordEx struct {
|
||||||
casvisorsdk.Record
|
casvisorsdk.Record
|
||||||
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
ExtendedUser *User `xorm:"-" json:"extendedUser"`
|
||||||
@ -35,7 +61,8 @@ func sendWebhook(webhook *Webhook, record *casvisorsdk.Record, extendedUser *Use
|
|||||||
ExtendedUser: extendedUser,
|
ExtendedUser: extendedUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
body := strings.NewReader(util.StructToJson(recordEx))
|
body = strings.NewReader(util.StructToJson(recordEx))
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
|
req, err := http.NewRequest(webhook.Method, webhook.Url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
294
pp/airwallex.go
Normal file
294
pp/airwallex.go
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
package pp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AirwallexPaymentProvider struct {
|
||||||
|
Client *AirwallexClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAirwallexPaymentProvider(clientId string, apiKey string) (*AirwallexPaymentProvider, error) {
|
||||||
|
isProd := conf.GetConfigString("runmode") == "prod"
|
||||||
|
apiEndpoint := "https://api-demo.airwallex.com/api/v1"
|
||||||
|
apiCheckout := "https://checkout-demo.airwallex.com/#/standalone/checkout?"
|
||||||
|
if isProd {
|
||||||
|
apiEndpoint = "https://api.airwallex.com/api/v1"
|
||||||
|
apiCheckout = "https://checkout.airwallex.com/#/standalone/checkout?"
|
||||||
|
}
|
||||||
|
client := &AirwallexClient{
|
||||||
|
ClientId: clientId,
|
||||||
|
APIKey: apiKey,
|
||||||
|
APIEndpoint: apiEndpoint,
|
||||||
|
APICheckout: apiCheckout,
|
||||||
|
client: &http.Client{Timeout: 15 * time.Second},
|
||||||
|
}
|
||||||
|
pp := &AirwallexPaymentProvider{
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
return pp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||||
|
// Create a payment intent
|
||||||
|
intent, err := pp.Client.CreateIntent(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payUrl, err := pp.Client.GetCheckoutUrl(intent, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PayResp{
|
||||||
|
PayUrl: payUrl,
|
||||||
|
OrderId: intent.MerchantOrderId,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||||
|
notifyResult := &NotifyResult{}
|
||||||
|
intent, err := pp.Client.GetIntentByOrderId(orderId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Check intent status
|
||||||
|
switch intent.Status {
|
||||||
|
case "PENDING", "REQUIRES_PAYMENT_METHOD", "REQUIRES_CUSTOMER_ACTION", "REQUIRES_CAPTURE":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCreated
|
||||||
|
return notifyResult, nil
|
||||||
|
case "CANCELLED":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCanceled
|
||||||
|
return notifyResult, nil
|
||||||
|
case "EXPIRED":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateTimeout
|
||||||
|
return notifyResult, nil
|
||||||
|
case "SUCCEEDED":
|
||||||
|
// Skip
|
||||||
|
default:
|
||||||
|
notifyResult.PaymentStatus = PaymentStateError
|
||||||
|
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout status: %v", intent.Status)
|
||||||
|
return notifyResult, nil
|
||||||
|
}
|
||||||
|
// Check attempt status
|
||||||
|
if intent.PaymentStatus != "" {
|
||||||
|
switch intent.PaymentStatus {
|
||||||
|
case "CANCELLED", "EXPIRED", "RECEIVED", "AUTHENTICATION_REDIRECTED", "AUTHORIZED", "CAPTURE_REQUESTED":
|
||||||
|
notifyResult.PaymentStatus = PaymentStateCreated
|
||||||
|
return notifyResult, nil
|
||||||
|
case "PAID", "SETTLED":
|
||||||
|
// Skip
|
||||||
|
default:
|
||||||
|
notifyResult.PaymentStatus = PaymentStateError
|
||||||
|
notifyResult.NotifyMessage = fmt.Sprintf("unexpected airwallex checkout payment status: %v", intent.PaymentStatus)
|
||||||
|
return notifyResult, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The Payment has succeeded.
|
||||||
|
var productDisplayName, productName, providerName string
|
||||||
|
if description, ok := intent.Metadata["description"]; ok {
|
||||||
|
productName, productDisplayName, providerName, _ = parseAttachString(description.(string))
|
||||||
|
}
|
||||||
|
orderId = intent.MerchantOrderId
|
||||||
|
return &NotifyResult{
|
||||||
|
PaymentName: orderId,
|
||||||
|
PaymentStatus: PaymentStatePaid,
|
||||||
|
ProductName: productName,
|
||||||
|
ProductDisplayName: productDisplayName,
|
||||||
|
ProviderName: providerName,
|
||||||
|
Price: priceStringToFloat64(intent.Amount.String()),
|
||||||
|
Currency: intent.Currency,
|
||||||
|
OrderId: orderId,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) GetInvoice(paymentName, personName, personIdCard, personEmail, personPhone, invoiceType, invoiceTitle, invoiceTaxId string) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *AirwallexPaymentProvider) GetResponseError(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return "success"
|
||||||
|
}
|
||||||
|
return "fail"
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Airwallex Client implementation (to be removed upon official SDK release)
|
||||||
|
*/
|
||||||
|
|
||||||
|
type AirwallexClient struct {
|
||||||
|
ClientId string
|
||||||
|
APIKey string
|
||||||
|
APIEndpoint string
|
||||||
|
APICheckout string
|
||||||
|
client *http.Client
|
||||||
|
tokenCache *AirWallexTokenInfo
|
||||||
|
tokenMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirWallexTokenInfo struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt string `json:"expires_at"`
|
||||||
|
parsedExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirWallexIntentResp struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
MerchantOrderId string `json:"merchant_order_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) GetToken() (string, error) {
|
||||||
|
c.tokenMutex.Lock()
|
||||||
|
defer c.tokenMutex.Unlock()
|
||||||
|
if c.tokenCache != nil && time.Now().Before(c.tokenCache.parsedExpiresAt) {
|
||||||
|
return c.tokenCache.Token, nil
|
||||||
|
}
|
||||||
|
req, _ := http.NewRequest("POST", c.APIEndpoint+"/authentication/login", bytes.NewBuffer([]byte("{}")))
|
||||||
|
req.Header.Set("x-client-id", c.ClientId)
|
||||||
|
req.Header.Set("x-api-key", c.APIKey)
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var result AirWallexTokenInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if result.Token == "" {
|
||||||
|
return "", fmt.Errorf("invalid token response")
|
||||||
|
}
|
||||||
|
expiresAt := strings.Replace(result.ExpiresAt, "+0000", "+00:00", 1)
|
||||||
|
result.parsedExpiresAt, _ = time.Parse(time.RFC3339, expiresAt)
|
||||||
|
c.tokenCache = &result
|
||||||
|
return result.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) authRequest(method, url string, body interface{}) (map[string]interface{}, error) {
|
||||||
|
token, err := c.GetToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(body)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) CreateIntent(r *PayReq) (*AirWallexIntentResp, error) {
|
||||||
|
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||||
|
orderId := r.PaymentName
|
||||||
|
intentReq := map[string]interface{}{
|
||||||
|
"currency": r.Currency,
|
||||||
|
"amount": r.Price,
|
||||||
|
"merchant_order_id": orderId,
|
||||||
|
"request_id": orderId,
|
||||||
|
"descriptor": strings.ReplaceAll(string([]rune(description)[:32]), "\x00", ""),
|
||||||
|
"metadata": map[string]interface{}{"description": description},
|
||||||
|
"order": map[string]interface{}{"products": []map[string]interface{}{{"name": r.ProductDisplayName, "quantity": 1, "desc": r.ProductDescription, "image_url": r.ProductImage}}},
|
||||||
|
"customer": map[string]interface{}{"merchant_customer_id": r.PayerId, "email": r.PayerEmail, "first_name": r.PayerName, "last_name": r.PayerName},
|
||||||
|
}
|
||||||
|
intentUrl := fmt.Sprintf("%s/pa/payment_intents/create", c.APIEndpoint)
|
||||||
|
intentRes, err := c.authRequest("POST", intentUrl, intentReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create payment intent: %v", err)
|
||||||
|
}
|
||||||
|
return &AirWallexIntentResp{
|
||||||
|
Id: intentRes["id"].(string),
|
||||||
|
ClientSecret: intentRes["client_secret"].(string),
|
||||||
|
MerchantOrderId: intentRes["merchant_order_id"].(string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirwallexIntent struct {
|
||||||
|
Amount json.Number `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Descriptor string `json:"descriptor"`
|
||||||
|
MerchantOrderId string `json:"merchant_order_id"`
|
||||||
|
LatestPaymentAttempt struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"latest_payment_attempt"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirwallexIntents struct {
|
||||||
|
Items []AirwallexIntent `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirWallexIntentInfo struct {
|
||||||
|
Amount json.Number
|
||||||
|
Currency string
|
||||||
|
Id string
|
||||||
|
Status string
|
||||||
|
Descriptor string
|
||||||
|
MerchantOrderId string
|
||||||
|
PaymentStatus string
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) GetIntentByOrderId(orderId string) (*AirWallexIntentInfo, error) {
|
||||||
|
intentUrl := fmt.Sprintf("%s/pa/payment_intents/?merchant_order_id=%s", c.APIEndpoint, orderId)
|
||||||
|
intentRes, err := c.authRequest("GET", intentUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get payment intent: %v", err)
|
||||||
|
}
|
||||||
|
items := intentRes["items"].([]interface{})
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil, fmt.Errorf("no payment intent found for order id: %s", orderId)
|
||||||
|
}
|
||||||
|
var intent AirwallexIntent
|
||||||
|
if b, err := json.Marshal(items[0]); err == nil {
|
||||||
|
json.Unmarshal(b, &intent)
|
||||||
|
}
|
||||||
|
return &AirWallexIntentInfo{
|
||||||
|
Id: intent.Id,
|
||||||
|
Amount: intent.Amount,
|
||||||
|
Currency: intent.Currency,
|
||||||
|
Status: intent.Status,
|
||||||
|
Descriptor: intent.Descriptor,
|
||||||
|
MerchantOrderId: intent.MerchantOrderId,
|
||||||
|
PaymentStatus: intent.LatestPaymentAttempt.Status,
|
||||||
|
Metadata: intent.Metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AirwallexClient) GetCheckoutUrl(intent *AirWallexIntentResp, r *PayReq) (string, error) {
|
||||||
|
return fmt.Sprintf("%sintent_id=%s&client_secret=%s&mode=payment¤cy=%s&amount=%v&requiredBillingContactFields=%s&successUrl=%s&failUrl=%s&logoUrl=%s",
|
||||||
|
c.APICheckout,
|
||||||
|
intent.Id,
|
||||||
|
intent.ClientSecret,
|
||||||
|
r.Currency,
|
||||||
|
r.Price,
|
||||||
|
url.QueryEscape(`["address"]`),
|
||||||
|
r.ReturnUrl,
|
||||||
|
r.ReturnUrl,
|
||||||
|
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", // replace default logo
|
||||||
|
), nil
|
||||||
|
}
|
@ -33,8 +33,11 @@ type PayReq struct {
|
|||||||
ProductName string
|
ProductName string
|
||||||
PayerName string
|
PayerName string
|
||||||
PayerId string
|
PayerId string
|
||||||
|
PayerEmail string
|
||||||
PaymentName string
|
PaymentName string
|
||||||
ProductDisplayName string
|
ProductDisplayName string
|
||||||
|
ProductDescription string
|
||||||
|
ProductImage string
|
||||||
Price float64
|
Price float64
|
||||||
Currency string
|
Currency string
|
||||||
|
|
||||||
|
@ -175,6 +175,7 @@ func initAPI() {
|
|||||||
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
|
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
|
||||||
|
|
||||||
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
|
beego.Router("/api/run-casbin-command", &controllers.ApiController{}, "GET:RunCasbinCommand")
|
||||||
|
beego.Router("/api/refresh-engines", &controllers.ApiController{}, "POST:RefreshEngines")
|
||||||
|
|
||||||
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
|
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
|
||||||
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
|
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
|
||||||
|
@ -80,6 +80,15 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAllowed, err := object.CheckLoginPermission(userId, application)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAllowed {
|
||||||
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -133,6 +142,14 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
path += urlPath
|
path += urlPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preventing synchronization problems from concurrency
|
||||||
|
ctx.Input.CruSession = nil
|
||||||
|
|
||||||
|
organizationThemeCookie, err := appendThemeCookie(ctx, urlPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(path, "/../") || !util.FileExist(path) {
|
if strings.Contains(path, "/../") || !util.FileExist(path) {
|
||||||
path = webBuildFolder + "/index.html"
|
path = webBuildFolder + "/index.html"
|
||||||
}
|
}
|
||||||
@ -149,13 +166,13 @@ func StaticFilter(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oldStaticBaseUrl == newStaticBaseUrl {
|
if oldStaticBaseUrl == newStaticBaseUrl {
|
||||||
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path)
|
makeGzipResponse(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
} else {
|
} else {
|
||||||
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path)
|
serveFileWithReplace(ctx.ResponseWriter, ctx.Request, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
f, err := os.Open(filepath.Clean(name))
|
f, err := os.Open(filepath.Clean(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -168,7 +185,13 @@ func serveFileWithReplace(w http.ResponseWriter, r *http.Request, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
oldContent := util.ReadStringFromPath(name)
|
oldContent := util.ReadStringFromPath(name)
|
||||||
newContent := strings.ReplaceAll(oldContent, oldStaticBaseUrl, newStaticBaseUrl)
|
newContent := oldContent
|
||||||
|
if organizationThemeCookie != nil {
|
||||||
|
newContent = strings.ReplaceAll(newContent, "https://cdn.casbin.org/img/favicon.png", organizationThemeCookie.Favicon)
|
||||||
|
newContent = strings.ReplaceAll(newContent, "<title>Casdoor</title>", fmt.Sprintf("<title>%s</title>", organizationThemeCookie.DisplayName))
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = strings.ReplaceAll(newContent, oldStaticBaseUrl, newStaticBaseUrl)
|
||||||
|
|
||||||
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
http.ServeContent(w, r, d.Name(), d.ModTime(), strings.NewReader(newContent))
|
||||||
}
|
}
|
||||||
@ -182,14 +205,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
|||||||
return w.Writer.Write(b)
|
return w.Writer.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string) {
|
func makeGzipResponse(w http.ResponseWriter, r *http.Request, path string, organizationThemeCookie *OrganizationThemeCookie) {
|
||||||
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
if !enableGzip || !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
serveFileWithReplace(w, r, path)
|
serveFileWithReplace(w, r, path, organizationThemeCookie)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
gz := gzip.NewWriter(w)
|
gz := gzip.NewWriter(w)
|
||||||
defer gz.Close()
|
defer gz.Close()
|
||||||
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
serveFileWithReplace(gzw, r, path)
|
serveFileWithReplace(gzw, r, path, organizationThemeCookie)
|
||||||
}
|
}
|
||||||
|
131
routers/theme_filter.go
Normal file
131
routers/theme_filter.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 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 routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/beego/beego/context"
|
||||||
|
"github.com/casdoor/casdoor/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrganizationThemeCookie struct {
|
||||||
|
ThemeData *object.ThemeData
|
||||||
|
LogoUrl string
|
||||||
|
FooterHtml string
|
||||||
|
Favicon string
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendThemeCookie(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
|
||||||
|
organizationThemeCookie, err := getOrganizationThemeCookieFromUrlPath(ctx, urlPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if organizationThemeCookie != nil {
|
||||||
|
return organizationThemeCookie, setThemeDataCookie(ctx, organizationThemeCookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrganizationThemeCookieFromUrlPath(ctx *context.Context, urlPath string) (*OrganizationThemeCookie, error) {
|
||||||
|
var application *object.Application
|
||||||
|
var organization *object.Organization
|
||||||
|
var err error
|
||||||
|
if urlPath == "/login" || urlPath == "/signup" {
|
||||||
|
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/built-in"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(urlPath, "/oauth/authorize") {
|
||||||
|
clientId := ctx.Input.Query("client_id")
|
||||||
|
if clientId == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
application, err = object.GetApplicationByClientId(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(urlPath, "/login/saml") {
|
||||||
|
owner, _ := strings.CutPrefix(urlPath, "/login/saml/authorize/")
|
||||||
|
application, err = object.GetApplication(owner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(urlPath, "/login/") {
|
||||||
|
owner, _ := strings.CutPrefix(urlPath, "/login/")
|
||||||
|
if owner == "undefined" || strings.Count(owner, "/") > 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(urlPath, "/signup/") {
|
||||||
|
owner, _ := strings.CutPrefix(urlPath, "/signup/")
|
||||||
|
if owner == "undefined" || strings.Count(owner, "/") > 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
application, err = object.GetDefaultApplication(fmt.Sprintf("admin/%s", owner))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(urlPath, "/cas/") && strings.HasSuffix(urlPath, "/login") {
|
||||||
|
owner, _ := strings.CutPrefix(urlPath, "/cas/")
|
||||||
|
owner, _ = strings.CutSuffix(owner, "/login")
|
||||||
|
application, err = object.GetApplication(owner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if application == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
organization = application.OrganizationObj
|
||||||
|
if organization == nil {
|
||||||
|
organization, err = object.GetOrganization(fmt.Sprintf("admin/%s", application.Organization))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
organizationThemeCookie := &OrganizationThemeCookie{
|
||||||
|
ThemeData: application.ThemeData,
|
||||||
|
LogoUrl: application.Logo,
|
||||||
|
FooterHtml: application.FooterHtml,
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization != nil {
|
||||||
|
organizationThemeCookie.Favicon = organization.Favicon
|
||||||
|
organizationThemeCookie.DisplayName = organization.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizationThemeCookie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setThemeDataCookie(ctx *context.Context, organizationThemeCookie *OrganizationThemeCookie) error {
|
||||||
|
themeDataString, err := json.Marshal(organizationThemeCookie.ThemeData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.SetCookie("organizationTheme", string(themeDataString))
|
||||||
|
ctx.SetCookie("organizationLogo", organizationThemeCookie.LogoUrl)
|
||||||
|
ctx.SetCookie("organizationFootHtml", organizationThemeCookie.FooterHtml)
|
||||||
|
return nil
|
||||||
|
}
|
@ -118,6 +118,6 @@ func IsValidOrigin(origin string) (bool, error) {
|
|||||||
originHostOnly = fmt.Sprintf("%s://%s", urlObj.Scheme, urlObj.Hostname())
|
originHostOnly = fmt.Sprintf("%s://%s", urlObj.Scheme, urlObj.Hostname())
|
||||||
}
|
}
|
||||||
|
|
||||||
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-app" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
|
res := originHostOnly == "http://localhost" || originHostOnly == "https://localhost" || originHostOnly == "http://127.0.0.1" || originHostOnly == "http://casdoor-authenticator" || strings.HasSuffix(originHostOnly, ".chromiumapp.org")
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,16 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/cssinjs": "^1.10.1",
|
"@ant-design/cssinjs": "^1.23.0",
|
||||||
"@ant-design/icons": "^4.7.0",
|
"@ant-design/icons": "^5.6.1",
|
||||||
"@craco/craco": "^6.4.5",
|
"@craco/craco": "^6.4.5",
|
||||||
"@crowdin/cli": "^3.7.10",
|
"@crowdin/cli": "^3.7.10",
|
||||||
"@ctrl/tinycolor": "^3.5.0",
|
"@ctrl/tinycolor": "^3.5.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@metamask/eth-sig-util": "^6.0.0",
|
"@metamask/eth-sig-util": "^6.0.0",
|
||||||
|
"@uiw/codemirror-extensions-langs": "^4.23.8",
|
||||||
|
"@uiw/codemirror-theme-material": "^4.23.8",
|
||||||
|
"@uiw/react-codemirror": "^4.23.8",
|
||||||
"@web3-onboard/coinbase": "^2.2.5",
|
"@web3-onboard/coinbase": "^2.2.5",
|
||||||
"@web3-onboard/core": "^2.20.5",
|
"@web3-onboard/core": "^2.20.5",
|
||||||
"@web3-onboard/frontier": "^2.0.4",
|
"@web3-onboard/frontier": "^2.0.4",
|
||||||
@ -20,10 +23,10 @@
|
|||||||
"@web3-onboard/sequence": "^2.0.8",
|
"@web3-onboard/sequence": "^2.0.8",
|
||||||
"@web3-onboard/taho": "^2.0.5",
|
"@web3-onboard/taho": "^2.0.5",
|
||||||
"@web3-onboard/trust": "^2.0.4",
|
"@web3-onboard/trust": "^2.0.4",
|
||||||
"antd": "5.2.3",
|
"antd": "5.24.1",
|
||||||
"antd-token-previewer": "^1.1.0-22",
|
"antd-token-previewer": "^2.0.8",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"codemirror": "^5.61.1",
|
"codemirror": "^6.0.1",
|
||||||
"copy-to-clipboard": "^3.3.1",
|
"copy-to-clipboard": "^3.3.1",
|
||||||
"core-js": "^3.25.0",
|
"core-js": "^3.25.0",
|
||||||
"craco-less": "^2.0.0",
|
"craco-less": "^2.0.0",
|
||||||
@ -40,7 +43,6 @@
|
|||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-polyfill": "^3.0.0",
|
"react-app-polyfill": "^3.0.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
|
||||||
"react-cropper": "^2.1.7",
|
"react-cropper": "^2.1.7",
|
||||||
"react-device-detect": "^2.2.2",
|
"react-device-detect": "^2.2.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -36,6 +36,7 @@ const {Footer, Content} = Layout;
|
|||||||
|
|
||||||
import {setTwoToneColor} from "@ant-design/icons";
|
import {setTwoToneColor} from "@ant-design/icons";
|
||||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||||
|
import * as Cookie from "cookie";
|
||||||
|
|
||||||
setTwoToneColor("rgb(87,52,211)");
|
setTwoToneColor("rgb(87,52,211)");
|
||||||
|
|
||||||
@ -269,7 +270,9 @@ class App extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooter() {
|
renderFooter(logo, footerHtml) {
|
||||||
|
logo = logo ?? this.state.logo;
|
||||||
|
footerHtml = footerHtml ?? this.state.application?.footerHtml;
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
|
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
|
||||||
@ -280,14 +283,14 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
{
|
{
|
||||||
this.state.application?.footerHtml && this.state.application.footerHtml !== "" ?
|
footerHtml && footerHtml !== "" ?
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div dangerouslySetInnerHTML={{__html: this.state.application.footerHtml}} />
|
<div dangerouslySetInnerHTML={{__html: footerHtml}} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
: (
|
: (
|
||||||
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
Conf.CustomFooter !== null ? Conf.CustomFooter : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
|
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={logo} /></a>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -324,7 +327,7 @@ class App extends Component {
|
|||||||
isAiAssistantOpen: false,
|
isAiAssistantOpen: false,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
visible={this.state.isAiAssistantOpen}
|
open={this.state.isAiAssistantOpen}
|
||||||
>
|
>
|
||||||
<iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
<iframe id="iframeHelper" title={"iframeHelper"} src={`${Conf.AiAssistantUrl}/?isRaw=1`} width="100%" height="100%" scrolling="no" frameBorder="no" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@ -358,13 +361,37 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onLoginSuccess(redirectUrl) {
|
||||||
|
window.google?.accounts?.id?.cancel();
|
||||||
|
if (redirectUrl) {
|
||||||
|
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
||||||
|
}
|
||||||
|
this.getAccount();
|
||||||
|
}
|
||||||
|
|
||||||
renderPage() {
|
renderPage() {
|
||||||
if (this.isDoorPages()) {
|
if (this.isDoorPages()) {
|
||||||
|
let themeData = this.state.themeData;
|
||||||
|
let logo = this.state.logo;
|
||||||
|
let footerHtml = null;
|
||||||
|
if (this.state.organization === undefined) {
|
||||||
|
const curCookie = Cookie.parse(document.cookie);
|
||||||
|
if (curCookie["organizationTheme"] && curCookie["organizationTheme"] !== "null") {
|
||||||
|
themeData = JSON.parse(curCookie["organizationTheme"]);
|
||||||
|
}
|
||||||
|
if (curCookie["organizationLogo"] && curCookie["organizationLogo"] !== "") {
|
||||||
|
logo = curCookie["organizationLogo"];
|
||||||
|
}
|
||||||
|
if (curCookie["organizationFootHtml"] && curCookie["organizationFootHtml"] !== "") {
|
||||||
|
footerHtml = curCookie["organizationFootHtml"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={{
|
<ConfigProvider theme={{
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: this.state.themeData.colorPrimary,
|
colorPrimary: themeData.colorPrimary,
|
||||||
borderRadius: this.state.themeData.borderRadius,
|
borderRadius: themeData.borderRadius,
|
||||||
},
|
},
|
||||||
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
|
||||||
}}>
|
}}>
|
||||||
@ -382,26 +409,20 @@ class App extends Component {
|
|||||||
application: application,
|
application: application,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onLoginSuccess={(redirectUrl) => {
|
onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
|
||||||
window.google?.accounts?.id?.cancel();
|
|
||||||
if (redirectUrl) {
|
|
||||||
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
|
||||||
}
|
|
||||||
this.getAccount();
|
|
||||||
}}
|
|
||||||
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
||||||
updataThemeData={this.setTheme}
|
updataThemeData={this.setTheme}
|
||||||
/> :
|
/> :
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/callback" component={AuthCallback} />
|
<Route exact path="/callback" render={(props) => <AuthCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||||
<Route exact path="/callback/saml" component={SamlCallback} />
|
<Route exact path="/callback/saml" render={(props) => <SamlCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||||
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
|
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
|
||||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
|
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
}
|
}
|
||||||
</Content>
|
</Content>
|
||||||
{
|
{
|
||||||
this.renderFooter()
|
this.renderFooter(logo, footerHtml)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.renderAiAssistant()
|
this.renderAiAssistant()
|
||||||
|
@ -58,6 +58,16 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.org-select {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(50%);
|
||||||
|
margin: 0 10px !important;
|
||||||
|
float: right;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.rightDropDown {
|
.rightDropDown {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user