Compare commits

...

36 Commits

Author SHA1 Message Date
87e2b97813 feat: translate Ukrainian language i18n 2024-04-20 02:14:23 +08:00
d9e44c1f2d fix: add "Is used" to verification list page 2024-04-20 00:18:52 +08:00
dfa4503f24 feat: support "mfa_phone_enabled", "mfa_email_enabled" in update-user API 2024-04-20 00:16:45 +08:00
f7fb32893b fix: close file in LocalFileSystemProvider's Put() (#2882) 2024-04-20 00:11:52 +08:00
66d0758b13 feat: fix DisableVerificationCode bug about empty email and phone 2024-04-19 13:28:13 +08:00
46ad0fe0be Improve Email Send() logic 2024-04-11 19:09:48 +08:00
6b637e3b2e feat: fix SendgridEmailProvider error handling, fix send-email template 2024-04-11 00:18:39 +08:00
3354945119 feat: add SendGrid Email provider (#2865)
* feat: add support for email provider send grid

* feat: rename send grid to sendgrid

* feat: rename send grid to sendgrid

* feat: change logo url of send grid
2024-04-09 22:16:01 +08:00
19c4416f10 feat: degrade the ant-design/cssinjs version to fix the Chrome 87 broken UI issue (#2861) 2024-04-09 09:15:39 +08:00
2077db9091 fix: fix bug in VerificationListPage 2024-04-07 15:39:25 +08:00
800f0ed249 feat: add tzdata package in Dockerfile to fix timezone issue (#2857)
Add tzdata to resolve possible time zone errors
2024-04-07 14:27:45 +08:00
xyt
6161040c67 fix: Dismiss google one tap after logged in by setting disableCancelOnUnmount to false (#2854)
* fix: Google One Tap should be hidden after logged in

* Change the call location for google.accounts.id.cancel()

* fix: hide google one tap after login by set disableCancelOnUnmount to false
2024-04-05 23:39:33 +08:00
xyt
1d785e61c6 feat: Google One Tap should be hidden after logged in (#2853)
* fix: Google One Tap should be hidden after logged in

* Change the call location for google.accounts.id.cancel()
2024-04-05 20:10:13 +08:00
0329d24867 feat: add isUsernameLowered to config 2024-04-02 21:54:16 +08:00
fb6f3623ee feat: add requireProviderPermission() 2024-03-30 23:24:59 +08:00
eb448bd043 fix: fix permission problem in provider (#2848) 2024-03-30 23:18:03 +08:00
xyt
ea88839db9 feat: add back button in forget password page (#2847)
* feat: add back button in forget password page

* fix: can't step back when directly entering forgot password page

* feat: forget password page always return to login page

* feat: if has history then go back to history & change style

* Update ForgetPage.js

* fix: reset button position

* Update ForgetPage.js

* Update ForgetPage.js

---------

Co-authored-by: Eric Luo <hsluoyz@qq.com>
2024-03-30 23:17:47 +08:00
cb95f6977a fix: fix PasswordModal error when changing username 2024-03-30 12:28:55 +08:00
9067df92a7 feat: revert "feat: Support metamask mobile login" (#2845)
This reverts commit bfa2ab63ad.
2024-03-30 00:36:25 +08:00
bfa2ab63ad feat: Support metamask mobile login (#2844) 2024-03-30 00:08:52 +08:00
505054b0eb feat: use minWidth for a better display effect in org select (#2843) 2024-03-29 15:47:27 +08:00
f95ce13b82 fix: support "Email or Phone" in signup table 2024-03-29 09:07:37 +08:00
xyt
5315f16a48 feat: can specify UI theme via /?theme=default and /?theme=dark (#2842)
* feat: set themeType through URL parameter

* Update App.js

---------

Co-authored-by: Eric Luo <hsluoyz@qq.com>
2024-03-29 00:52:18 +08:00
d054f3e001 feat: The /login/oauth/access_token api supports the token and id_token grant types. (#2836)
* In the response of the /api/get-captcha endpoint, add the parameters "owner" and "name" because these two parameters will be used when calling the /api/verify-captcha endpoint.

* The /login/oauth/access_token api supports the token and id_token grant types.
2024-03-28 00:41:54 +08:00
b158b840bd Add "new-user" to webhook event list 2024-03-27 15:23:06 +08:00
b16f1807b3 fix: fix bug in "new-user" record 2024-03-27 15:15:40 +08:00
d0cce1bf7a Order by "id" in GetPaginationRecords() 2024-03-27 15:14:41 +08:00
9892cd20ab Improve erorr message in CheckVerificationCode() 2024-03-27 15:14:20 +08:00
d1f31dd327 feat: fix linter 2024-03-26 23:24:53 +08:00
94743246a1 Improve "%{user.friendlyName}" handling 2024-03-25 21:26:36 +08:00
39ad1bc593 Add signup's object in AfterRecordMessage() 2024-03-25 21:20:33 +08:00
d97f833d2a feat: Add 'owner' and 'name' Parameters to /api/get-captcha Response for /api/verify-captcha Usage (#2834) 2024-03-25 16:34:42 +08:00
948fa911e2 feat: add users to getGroups() and getGroup() APIs 2024-03-22 23:32:30 +08:00
6073a0f63d Rename GroupListPage and GroupEditPage 2024-03-22 23:14:05 +08:00
91268bca70 Improve enableAutoSignin option UI 2024-03-22 22:55:10 +08:00
23dbb0b926 feat: add response to Records page (#2830)
* feat: add response to Records page

* feat: improve AddRecord

* feat: remove log and return err

* feat: improve record in signup and record deny

* fix: filter will generate 403 record correctly
2024-03-22 14:53:38 +08:00
65 changed files with 1883 additions and 1412 deletions

View File

@ -16,6 +16,7 @@ ARG USER=casdoor
RUN sed -i 's/https/http/' /etc/apk/repositories RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add --update sudo RUN apk add --update sudo
RUN apk add tzdata
RUN apk add curl RUN apk add curl
RUN apk add ca-certificates && update-ca-certificates RUN apk add ca-certificates && update-ca-certificates

View File

@ -15,6 +15,7 @@ socks5Proxy = "127.0.0.1:10808"
verificationCodeTimeout = 10 verificationCodeTimeout = 10
initScore = 0 initScore = 0
logPostOnly = true logPostOnly = true
isUsernameLowered = false
origin = origin =
originFrontend = originFrontend =
staticBaseUrl = "https://cdn.casbin.org" staticBaseUrl = "https://cdn.casbin.org"

View File

@ -44,6 +44,8 @@ type Response struct {
} }
type Captcha struct { type Captcha struct {
Owner string `json:"owner"`
Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
Scene string `json:"scene"` Scene string `json:"scene"`
@ -259,22 +261,24 @@ func (c *ApiController) Signup() {
c.SetSessionUsername(user.GetId()) c.SetSessionUsername(user.GetId())
} }
err = object.DisableVerificationCode(authForm.Email) if authForm.Email != "" {
if err != nil { err = object.DisableVerificationCode(authForm.Email)
c.ResponseError(err.Error()) if err != nil {
return c.ResponseError(err.Error())
return
}
} }
err = object.DisableVerificationCode(checkPhone) if checkPhone != "" {
if err != nil { err = object.DisableVerificationCode(checkPhone)
c.ResponseError(err.Error()) if err != nil {
return c.ResponseError(err.Error())
return
}
} }
record := object.NewRecord(c.Ctx) c.Ctx.Input.SetParam("recordUserId", user.GetId())
record.Organization = application.Organization c.Ctx.Input.SetParam("recordSignup", "true")
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
userId := user.GetId() userId := user.GetId()
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId) util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
@ -532,10 +536,12 @@ func (c *ApiController) GetCaptcha() {
return return
} }
c.ResponseOk(Captcha{Type: captchaProvider.Type, CaptchaId: id, CaptchaImage: img}) c.ResponseOk(Captcha{Owner: captchaProvider.Owner, Name: captchaProvider.Name, Type: captchaProvider.Type, CaptchaId: id, CaptchaImage: img})
return return
} else if captchaProvider.Type != "" { } else if captchaProvider.Type != "" {
c.ResponseOk(Captcha{ c.ResponseOk(Captcha{
Owner: captchaProvider.Owner,
Name: captchaProvider.Name,
Type: captchaProvider.Type, Type: captchaProvider.Type,
SubType: captchaProvider.SubType, SubType: captchaProvider.SubType,
ClientId: captchaProvider.ClientId, ClientId: captchaProvider.ClientId,

View File

@ -508,10 +508,7 @@ func (c *ApiController) Login() {
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx) c.Ctx.Input.SetParam("recordUserId", user.GetId())
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} }
} else if authForm.Provider != "" { } else if authForm.Provider != "" {
var application *object.Application var application *object.Application
@ -632,10 +629,7 @@ func (c *ApiController) Login() {
} }
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx) c.Ctx.Input.SetParam("recordUserId", user.GetId())
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else if provider.Category == "OAuth" || provider.Category == "Web3" { } else if provider.Category == "OAuth" || provider.Category == "Web3" {
// Sign up via OAuth // Sign up via OAuth
if application.EnableLinkWithEmail { if application.EnableLinkWithEmail {
@ -768,16 +762,8 @@ func (c *ApiController) Login() {
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx) c.Ctx.Input.SetParam("recordUserId", user.GetId())
record.Organization = application.Organization c.Ctx.Input.SetParam("recordSignup", "true")
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
record2 := object.NewRecord(c.Ctx)
record2.Action = "signup"
record2.Organization = application.Organization
record2.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record2) })
} else if provider.Category == "SAML" { } else if provider.Category == "SAML" {
// TODO: since we get the user info from SAML response, we can try to create the user // TODO: since we get the user info from SAML response, we can try to create the user
resp = &Response{Status: "error", Msg: fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id))} resp = &Response{Status: "error", Msg: fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(application.Organization, userInfo.Id))}
@ -879,10 +865,7 @@ func (c *ApiController) Login() {
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)
c.setMfaUserSession("") c.setMfaUserSession("")
record := object.NewRecord(c.Ctx) c.Ctx.Input.SetParam("recordUserId", user.GetId())
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else { } else {
if c.GetSessionUsername() != "" { if c.GetSessionUsername() != "" {
// user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in // user already signed in to Casdoor, so let the user click the avatar button to do the quick sign-in
@ -901,10 +884,7 @@ func (c *ApiController) Login() {
user := c.getCurrentUser() user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &authForm) resp = c.HandleLoggedIn(application, user, &authForm)
record := object.NewRecord(c.Ctx) c.Ctx.Input.SetParam("recordUserId", user.GetId())
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else { } else {
c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), form = %s"), util.StructToJson(authForm))) c.ResponseError(fmt.Sprintf(c.T("auth:Unknown authentication type (not password or provider), form = %s"), util.StructToJson(authForm)))
return return

View File

@ -68,7 +68,7 @@ func (c *ApiController) GetCerts() {
// GetGlobalCerts // GetGlobalCerts
// @Title GetGlobalCerts // @Title GetGlobalCerts
// @Tag Cert API // @Tag Cert API
// @Description get globle certs // @Description get global certs
// @Success 200 {array} object.Cert The Response object // @Success 200 {array} object.Cert The Response object
// @router /get-global-certs [get] // @router /get-global-certs [get]
func (c *ApiController) GetGlobalCerts() { func (c *ApiController) GetGlobalCerts() {

View File

@ -43,13 +43,20 @@ func (c *ApiController) GetGroups() {
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} else {
if withTree == "true" {
c.ResponseOk(object.ConvertToTreeData(groups, owner))
return
}
c.ResponseOk(groups)
} }
err = object.ExtendGroupsWithUsers(groups)
if err != nil {
c.ResponseError(err.Error())
return
}
if withTree == "true" {
c.ResponseOk(object.ConvertToTreeData(groups, owner))
return
}
c.ResponseOk(groups)
} else { } else {
limit := util.ParseInt(limit) limit := util.ParseInt(limit)
count, err := object.GetGroupCount(owner, field, value) count, err := object.GetGroupCount(owner, field, value)
@ -64,6 +71,12 @@ func (c *ApiController) GetGroups() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} else { } else {
err = object.ExtendGroupsWithUsers(groups)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(groups, paginator.Nums()) c.ResponseOk(groups, paginator.Nums())
} }
} }
@ -84,6 +97,13 @@ func (c *ApiController) GetGroup() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
err = object.ExtendGroupWithUsers(group)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(group) c.ResponseOk(group)
} }

View File

@ -141,6 +141,20 @@ func (c *ApiController) GetProvider() {
c.ResponseOk(object.GetMaskedProvider(provider, isMaskEnabled)) c.ResponseOk(object.GetMaskedProvider(provider, isMaskEnabled))
} }
func (c *ApiController) requireProviderPermission(provider *object.Provider) bool {
isGlobalAdmin, user := c.isGlobalAdmin()
if isGlobalAdmin {
return true
}
if provider.Owner == "admin" || user.Owner != provider.Owner {
c.ResponseError(c.T("auth:Unauthorized operation"))
return false
}
return true
}
// UpdateProvider // UpdateProvider
// @Title UpdateProvider // @Title UpdateProvider
// @Tag Provider API // @Tag Provider API
@ -159,6 +173,11 @@ func (c *ApiController) UpdateProvider() {
return return
} }
ok := c.requireProviderPermission(&provider)
if !ok {
return
}
c.Data["json"] = wrapActionResponse(object.UpdateProvider(id, &provider)) c.Data["json"] = wrapActionResponse(object.UpdateProvider(id, &provider))
c.ServeJSON() c.ServeJSON()
} }
@ -184,11 +203,17 @@ func (c *ApiController) AddProvider() {
return return
} }
if err := checkQuotaForProvider(int(count)); err != nil { err = checkQuotaForProvider(int(count))
if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
ok := c.requireProviderPermission(&provider)
if !ok {
return
}
c.Data["json"] = wrapActionResponse(object.AddProvider(&provider)) c.Data["json"] = wrapActionResponse(object.AddProvider(&provider))
c.ServeJSON() c.ServeJSON()
} }
@ -208,6 +233,11 @@ func (c *ApiController) DeleteProvider() {
return return
} }
ok := c.requireProviderPermission(&provider)
if !ok {
return
}
c.Data["json"] = wrapActionResponse(object.DeleteProvider(&provider)) c.Data["json"] = wrapActionResponse(object.DeleteProvider(&provider))
c.ServeJSON() c.ServeJSON()
} }

View File

@ -113,23 +113,25 @@ func (c *ApiController) SendEmail() {
content := emailForm.Content content := emailForm.Content
if content == "" { if content == "" {
code := "123456" content = provider.Content
}
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes." code := "123456"
content = strings.Replace(provider.Content, "%s", code, 1) // "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
if !strings.HasPrefix(userId, "app/") { content = strings.Replace(content, "%s", code, 1)
var user *object.User userString := "Hi"
user, err = object.GetUser(userId) if !strings.HasPrefix(userId, "app/") {
if err != nil { var user *object.User
c.ResponseError(err.Error()) user, err = object.GetUser(userId)
return if err != nil {
} c.ResponseError(err.Error())
return
if user != nil { }
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1) if user != nil {
} userString = user.GetFriendlyName()
} }
} }
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
for _, receiver := range emailForm.Receivers { for _, receiver := range emailForm.Receivers {
err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender) err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender)

View File

@ -164,6 +164,7 @@ func (c *ApiController) GetOAuthToken() {
code := c.Input().Get("code") code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier") verifier := c.Input().Get("code_verifier")
scope := c.Input().Get("scope") scope := c.Input().Get("scope")
nonce := c.Input().Get("nonce")
username := c.Input().Get("username") username := c.Input().Get("username")
password := c.Input().Get("password") password := c.Input().Get("password")
tag := c.Input().Get("tag") tag := c.Input().Get("tag")
@ -197,6 +198,9 @@ func (c *ApiController) GetOAuthToken() {
if scope == "" { if scope == "" {
scope = tokenRequest.Scope scope = tokenRequest.Scope
} }
if nonce == "" {
nonce = tokenRequest.Nonce
}
if username == "" { if username == "" {
username = tokenRequest.Username username = tokenRequest.Username
} }
@ -216,7 +220,7 @@ func (c *ApiController) GetOAuthToken() {
} }
host := c.Ctx.Request.Host host := c.Ctx.Request.Host
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage()) token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -21,6 +21,7 @@ type TokenRequest struct {
Code string `json:"code"` Code string `json:"code"`
Verifier string `json:"code_verifier"` Verifier string `json:"code_verifier"`
Scope string `json:"scope"` Scope string `json:"scope"`
Nonce string `json:"nonce"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Tag string `json:"tag"` Tag string `json:"tag"`

View File

@ -111,46 +111,44 @@ func newEmail(fromAddress string, toAddress string, subject string, content stri
Subject: subject, Subject: subject,
HTML: content, HTML: content,
}, },
Importance: importanceNormal, Importance: importanceNormal,
Attachments: []Attachment{},
} }
} }
func (a *AzureACSEmailProvider) sendEmail(e *Email) error { func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
postBody, err := json.Marshal(e) email := newEmail(fromAddress, toAddress, subject, content)
if err != nil {
return fmt.Errorf("email JSON marshall failed: %s", err)
}
bodyBuffer := bytes.NewBuffer(postBody) postBody, err := json.Marshal(email)
if err != nil {
return err
}
endpoint := strings.TrimSuffix(a.Endpoint, "/") endpoint := strings.TrimSuffix(a.Endpoint, "/")
url := fmt.Sprintf("%s/emails:send?api-version=2023-03-31", endpoint) url := fmt.Sprintf("%s/emails:send?api-version=2023-03-31", endpoint)
bodyBuffer := bytes.NewBuffer(postBody)
req, err := http.NewRequest("POST", url, bodyBuffer) req, err := http.NewRequest("POST", url, bodyBuffer)
if err != nil { if err != nil {
return fmt.Errorf("error creating AzureACS API request: %s", err) return err
} }
// Sign the request using the AzureACS access key and HMAC-SHA256
err = signRequestHMAC(a.AccessKey, req) err = signRequestHMAC(a.AccessKey, req)
if err != nil { if err != nil {
return fmt.Errorf("error signing AzureACS API request: %s", err) return err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
// Some important header
req.Header.Set("repeatability-request-id", uuid.New().String()) req.Header.Set("repeatability-request-id", uuid.New().String())
req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat)) req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat))
// Send request
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("error sending AzureACS API request: %s", err) return err
} }
defer resp.Body.Close() defer resp.Body.Close()
// Response error Handling
if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized { if resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized {
commError := ErrorResponse{} commError := ErrorResponse{}
@ -159,11 +157,11 @@ func (a *AzureACSEmailProvider) sendEmail(e *Email) error {
return err return err
} }
return fmt.Errorf("error sending email: %s", commError.Error.Message) return fmt.Errorf("status code: %d, error message: %s", resp.StatusCode, commError.Error.Message)
} }
if resp.StatusCode != http.StatusAccepted { if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("error sending email: status: %d", resp.StatusCode) return fmt.Errorf("status code: %d", resp.StatusCode)
} }
return nil return nil
@ -221,9 +219,3 @@ func GetHmac(content string, key []byte) string {
return base64.StdEncoding.EncodeToString(hmac.Sum(nil)) return base64.StdEncoding.EncodeToString(hmac.Sum(nil))
} }
func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
e := newEmail(fromAddress, toAddress, subject, content)
return a.sendEmail(e)
}

View File

@ -23,6 +23,8 @@ func GetEmailProvider(typ string, clientId string, clientSecret string, host str
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)
} else if typ == "SendGrid" {
return NewSendgridEmailProvider(clientSecret)
} else { } else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl) return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
} }

68
email/sendgrid.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2024 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 email
import (
"encoding/json"
"fmt"
"strings"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)
type SendgridEmailProvider struct {
ApiKey string
}
type SendgridResponseBody struct {
Errors []struct {
Message string `json:"message"`
Field interface{} `json:"field"`
Help interface{} `json:"help"`
} `json:"errors"`
}
func NewSendgridEmailProvider(apiKey string) *SendgridEmailProvider {
return &SendgridEmailProvider{ApiKey: apiKey}
}
func (s *SendgridEmailProvider) Send(fromAddress string, fromName, toAddress string, subject string, content string) error {
from := mail.NewEmail(fromName, fromAddress)
to := mail.NewEmail("", toAddress)
message := mail.NewSingleEmail(from, subject, to, "", content)
client := sendgrid.NewSendClient(s.ApiKey)
response, err := client.Send(message)
if err != nil {
return err
}
if response.StatusCode >= 300 {
var responseBody SendgridResponseBody
err = json.Unmarshal([]byte(response.Body), &responseBody)
if err != nil {
return err
}
messages := []string{}
for _, sendgridError := range responseBody.Errors {
messages = append(messages, sendgridError.Message)
}
return fmt.Errorf("SendGrid status code: %d, error message: %s", response.StatusCode, strings.Join(messages, " | "))
}
return nil
}

1
go.mod
View File

@ -45,6 +45,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
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 // indirect
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect 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

3
go.sum
View File

@ -1907,8 +1907,11 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM= github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM=
github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA= github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sendgrid/sendgrid-go v3.13.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=

View File

@ -59,6 +59,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage) beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
beego.InsertFilter("*", beego.AfterExec, routers.AfterRecordMessage, false)
beego.BConfig.WebConfig.Session.SessionOn = true beego.BConfig.WebConfig.Session.SessionOn = true
beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id" beego.BConfig.WebConfig.Session.SessionName = "casdoor_session_id"

View File

@ -17,6 +17,7 @@ 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"
@ -30,13 +31,13 @@ type Group struct {
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"` UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Manager string `xorm:"varchar(100)" json:"manager"` Manager string `xorm:"varchar(100)" json:"manager"`
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"`
IsTopGroup bool `xorm:"bool" json:"isTopGroup"` IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
Users []*User `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"`
@ -288,6 +289,55 @@ func GetGroupUsers(groupId string) ([]*User, error) {
return users, nil return users, nil
} }
func ExtendGroupWithUsers(group *Group) error {
if group == nil {
return nil
}
users, err := GetUsers(group.Owner)
if err != nil {
return err
}
groupId := group.GetId()
userIds := []string{}
for _, user := range users {
if util.InSlice(user.Groups, groupId) {
userIds = append(userIds, user.GetId())
}
}
group.Users = userIds
return nil
}
func ExtendGroupsWithUsers(groups []*Group) error {
var wg sync.WaitGroup
errChan := make(chan error, len(groups))
for _, group := range groups {
wg.Add(1)
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 {
return err
}
}
return nil
}
func GroupChangeTrigger(oldName, newName string) error { func GroupChangeTrigger(oldName, newName string) error {
session := ormer.Engine.NewSession() session := ormer.Engine.NewSession()
defer session.Close() defer session.Close()

View File

@ -15,6 +15,7 @@
package object package object
import ( import (
"encoding/json"
"fmt" "fmt"
"strings" "strings"
@ -34,7 +35,12 @@ type Record struct {
casvisorsdk.Record casvisorsdk.Record
} }
func NewRecord(ctx *context.Context) *casvisorsdk.Record { type Response struct {
Status string `json:"status"`
Msg string `json:"msg"`
}
func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
ip := strings.Replace(util.GetIPFromRequest(ctx.Request), ": ", "", -1) ip := strings.Replace(util.GetIPFromRequest(ctx.Request), ": ", "", -1)
action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1) action := strings.Replace(ctx.Request.URL.Path, "/api/", "", -1)
requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"}) requestUri := util.FilterQuery(ctx.Request.RequestURI, []string{"accessToken"})
@ -47,6 +53,17 @@ func NewRecord(ctx *context.Context) *casvisorsdk.Record {
object = string(ctx.Input.RequestBody) object = string(ctx.Input.RequestBody)
} }
respBytes, err := json.Marshal(ctx.Input.Data()["json"])
if err != nil {
return nil, err
}
var resp Response
err = json.Unmarshal(respBytes, &resp)
if err != nil {
return nil, err
}
language := ctx.Request.Header.Get("Accept-Language") language := ctx.Request.Header.Get("Accept-Language")
if len(language) > 2 { if len(language) > 2 {
language = language[0:2] language = language[0:2]
@ -63,10 +80,10 @@ func NewRecord(ctx *context.Context) *casvisorsdk.Record {
Action: action, Action: action,
Language: languageCode, Language: languageCode,
Object: object, Object: object,
Response: "", Response: fmt.Sprintf("{status:\"%s\", msg:\"%s\"}", resp.Status, resp.Msg),
IsTriggered: false, IsTriggered: false,
} }
return &record return &record, nil
} }
func AddRecord(record *casvisorsdk.Record) bool { func AddRecord(record *casvisorsdk.Record) bool {
@ -123,6 +140,12 @@ func GetRecords() ([]*casvisorsdk.Record, error) {
func GetPaginationRecords(offset, limit int, field, value, sortField, sortOrder string, filterRecord *casvisorsdk.Record) ([]*casvisorsdk.Record, error) { func GetPaginationRecords(offset, limit int, field, value, sortField, sortOrder string, filterRecord *casvisorsdk.Record) ([]*casvisorsdk.Record, error) {
records := []*casvisorsdk.Record{} records := []*casvisorsdk.Record{}
if sortField == "" || sortOrder == "" {
sortField = "id"
sortOrder = "descend"
}
session := GetSession("", offset, limit, field, value, sortField, sortOrder) session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&records, filterRecord) err := session.Find(&records, filterRecord)
if err != nil { if err != nil {
@ -142,6 +165,25 @@ func GetRecordsByField(record *casvisorsdk.Record) ([]*casvisorsdk.Record, error
return records, nil return records, nil
} }
func CopyRecord(record *casvisorsdk.Record) *casvisorsdk.Record {
res := &casvisorsdk.Record{
Owner: record.Owner,
Name: record.Name,
CreatedTime: record.CreatedTime,
Organization: record.Organization,
ClientIp: record.ClientIp,
User: record.User,
Method: record.Method,
RequestUri: record.RequestUri,
Action: record.Action,
Language: record.Language,
Object: record.Object,
Response: record.Response,
IsTriggered: record.IsTriggered,
}
return res
}
func getFilteredWebhooks(webhooks []*Webhook, organization string, action string) []*Webhook { func getFilteredWebhooks(webhooks []*Webhook, organization string, action string) []*Webhook {
res := []*Webhook{} res := []*Webhook{}
for _, webhook := range webhooks { for _, webhook := range webhooks {

View File

@ -189,7 +189,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}, nil }, nil
} }
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string) (interface{}, error) { func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string) (interface{}, error) {
application, err := GetApplicationByClientId(clientId) application, err := GetApplicationByClientId(clientId)
if err != nil { if err != nil {
return nil, err return nil, err
@ -220,6 +220,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
token, tokenError, err = GetPasswordToken(application, username, password, scope, host) token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
case "client_credentials": // Client Credentials Grant case "client_credentials": // Client Credentials Grant
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host) token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
case "token", "id_token": // Implicit Grant
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
case "refresh_token": case "refresh_token":
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host) refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
if err != nil { if err != nil {
@ -582,6 +584,33 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
return token, nil, nil return token, nil, nil
} }
// GetImplicitToken
// Implicit flow
func GetImplicitToken(application *Application, username string, scope string, nonce string, host string) (*Token, *TokenError, error) {
user, err := GetUserByFields(application.Organization, username)
if err != nil {
return nil, nil, err
}
if user == nil {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "the user does not exist",
}, nil
}
if user.IsForbidden {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
}, nil
}
token, err := GetTokenByUser(application, user, scope, nonce, host)
if err != nil {
return nil, nil, err
}
return token, nil, nil
}
// GetTokenByUser // GetTokenByUser
// Implicit flow // Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) { func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {

View File

@ -675,7 +675,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"owner", "display_name", "avatar", "first_name", "last_name", "owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon", "baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox", "auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
@ -833,6 +833,11 @@ func AddUser(user *User) (bool, error) {
} }
} }
isUsernameLowered := conf.GetConfigBool("isUsernameLowered")
if isUsernameLowered {
user.Name = strings.ToLower(user.Name)
}
affected, err := ormer.Engine.Insert(user) affected, err := ormer.Engine.Insert(user)
if err != nil { if err != nil {
return false, err return false, err
@ -846,6 +851,8 @@ func AddUsers(users []*User) (bool, error) {
return false, fmt.Errorf("no users are provided") return false, fmt.Errorf("no users are provided")
} }
isUsernameLowered := conf.GetConfigBool("isUsernameLowered")
// organization := GetOrganizationByUser(users[0]) // organization := GetOrganizationByUser(users[0])
for _, user := range users { for _, user := range users {
// this function is only used for syncer or batch upload, so no need to encrypt the password // this function is only used for syncer or batch upload, so no need to encrypt the password
@ -869,6 +876,10 @@ func AddUsers(users []*User) (bool, error) {
return false, err return false, err
} }
} }
if isUsernameLowered {
user.Name = strings.ToLower(user.Name)
}
} }
affected, err := ormer.Engine.Insert(users) affected, err := ormer.Engine.Insert(users)

View File

@ -92,9 +92,12 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
// "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)
userString := "Hi"
if user != nil { if user != nil {
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1) userString = user.GetFriendlyName()
} }
content = strings.Replace(content, "%{user.friendlyName}", userString, 1)
err := IsAllowSend(user, remoteAddr, provider.Category) err := IsAllowSend(user, remoteAddr, provider.Category)
if err != nil { if err != nil {
@ -187,14 +190,17 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet, or has already been used!")}, nil return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet, or has already been used!")}, nil
} }
timeout, err := conf.GetConfigInt64("verificationCodeTimeout") timeoutInMinutes, err := conf.GetConfigInt64("verificationCodeTimeout")
if err != nil { if err != nil {
return nil, err return nil, err
} }
now := time.Now().Unix() now := time.Now().Unix()
if now-record.Time > timeout*60 { if now-record.Time > timeoutInMinutes*60*10 {
return &VerifyResult{timeoutError, fmt.Sprintf(i18n.Translate(lang, "verification:You should verify your code in %d min!"), timeout)}, nil return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet!")}, nil
}
if now-record.Time > timeoutInMinutes*60 {
return &VerifyResult{timeoutError, fmt.Sprintf(i18n.Translate(lang, "verification:You should verify your code in %d min!"), timeoutInMinutes)}, nil
} }
if record.Code != code { if record.Code != code {

View File

@ -20,6 +20,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/casdoor/casdoor/object"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/authz" "github.com/casdoor/casdoor/authz"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -211,5 +213,17 @@ func ApiFilter(ctx *context.Context) {
if !isAllowed { if !isAllowed {
denyRequest(ctx) denyRequest(ctx)
record, err := object.NewRecord(ctx)
if err != nil {
return
}
record.Organization = subOwner
record.User = subName // auth:Unauthorized operation
record.Response = fmt.Sprintf("{status:\"error\", msg:\"%s\"}", T(ctx, "auth:Unauthorized operation"))
util.SafeGoroutine(func() {
object.AddRecord(record)
})
} }
} }

View File

@ -15,9 +15,12 @@
package routers package routers
import ( import (
"fmt"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/casvisor/casvisor-go-sdk/casvisorsdk"
) )
func getUser(ctx *context.Context) (username string) { func getUser(ctx *context.Context) (username string) {
@ -60,12 +63,49 @@ func RecordMessage(ctx *context.Context) {
return return
} }
record := object.NewRecord(ctx)
userId := getUser(ctx) userId := getUser(ctx)
ctx.Input.SetParam("recordUserId", userId)
}
func AfterRecordMessage(ctx *context.Context) {
record, err := object.NewRecord(ctx)
if err != nil {
fmt.Printf("AfterRecordMessage() error: %s\n", err.Error())
return
}
userId := ctx.Input.Params()["recordUserId"]
if userId != "" { if userId != "" {
record.Organization, record.User = util.GetOwnerAndNameFromId(userId) record.Organization, record.User = util.GetOwnerAndNameFromId(userId)
} }
util.SafeGoroutine(func() { object.AddRecord(record) }) var record2 *casvisorsdk.Record
recordSignup := ctx.Input.Params()["recordSignup"]
if recordSignup == "true" {
record2 = object.CopyRecord(record)
record2.Action = "new-user"
var user *object.User
user, err = object.GetUser(userId)
if err != nil {
fmt.Printf("AfterRecordMessage() error: %s\n", err.Error())
return
}
if user == nil {
err = fmt.Errorf("the user: %s is not found", userId)
fmt.Printf("AfterRecordMessage() error: %s\n", err.Error())
return
}
record2.Object = util.StructToJson(user)
}
util.SafeGoroutine(func() {
object.AddRecord(record)
if record2 != nil {
object.AddRecord(record2)
}
})
} }

View File

@ -70,6 +70,7 @@ func (sp LocalFileSystemProvider) Put(path string, reader io.Reader) (*oss.Objec
dst, err := os.Create(filepath.Clean(fullPath)) dst, err := os.Create(filepath.Clean(fullPath))
if err == nil { if err == nil {
defer dst.Close()
if seeker, ok := reader.(io.ReadSeeker); ok { if seeker, ok := reader.(io.ReadSeeker); ok {
seeker.Seek(0, 0) seeker.Seek(0, 0)
} }

View File

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "1.16.1", "@ant-design/cssinjs": "^1.10.1",
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@craco/craco": "^6.4.5", "@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10", "@crowdin/cli": "^3.7.10",

View File

@ -41,6 +41,7 @@ setTwoToneColor("rgb(87,52,211)");
class App extends Component { class App extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.setThemeAlgorithm();
let storageThemeAlgorithm = []; let storageThemeAlgorithm = [];
try { try {
storageThemeAlgorithm = localStorage.getItem("themeAlgorithm") ? JSON.parse(localStorage.getItem("themeAlgorithm")) : ["default"]; storageThemeAlgorithm = localStorage.getItem("themeAlgorithm") ? JSON.parse(localStorage.getItem("themeAlgorithm")) : ["default"];
@ -157,6 +158,15 @@ class App extends Component {
return Setting.getLogo(themes); return Setting.getLogo(themes);
} }
setThemeAlgorithm() {
const currentUrl = window.location.href;
const url = new URL(currentUrl);
const themeType = url.searchParams.get("theme");
if (themeType === "dark" || themeType === "default") {
localStorage.setItem("themeAlgorithm", JSON.stringify([themeType]));
}
}
setLanguage(account) { setLanguage(account) {
const language = account?.language; const language = account?.language;
if (language !== null && language !== "" && language !== i18next.language) { if (language !== null && language !== "" && language !== i18next.language) {
@ -364,6 +374,7 @@ class App extends Component {
}); });
}} }}
onLoginSuccess={(redirectUrl) => { onLoginSuccess={(redirectUrl) => {
window.google?.accounts?.id?.cancel();
if (redirectUrl) { if (redirectUrl) {
localStorage.setItem("mfaRedirectUrl", redirectUrl); localStorage.setItem("mfaRedirectUrl", redirectUrl);
} }

View File

@ -456,6 +456,10 @@ class ApplicationEditPage extends React.Component {
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.application.enableSigninSession} onChange={checked => { <Switch checked={this.state.application.enableSigninSession} onChange={checked => {
if (!checked) {
this.updateApplicationField("enableAutoSignin", false);
}
this.updateApplicationField("enableSigninSession", checked); this.updateApplicationField("enableSigninSession", checked);
}} /> }} />
</Col> </Col>
@ -466,6 +470,11 @@ class ApplicationEditPage extends React.Component {
</Col> </Col>
<Col span={1} > <Col span={1} >
<Switch checked={this.state.application.enableAutoSignin} onChange={checked => { <Switch checked={this.state.application.enableAutoSignin} onChange={checked => {
if (!this.state.application.enableSigninSession && checked) {
Setting.showMessage("error", i18next.t("application:Please enable \"Signin session\" first before enabling \"Auto signin\""));
return;
}
this.updateApplicationField("enableAutoSignin", checked); this.updateApplicationField("enableAutoSignin", checked);
}} /> }} />
</Col> </Col>

View File

@ -177,6 +177,16 @@ class GroupEditPage extends React.Component {
)} /> )} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Users"), i18next.t("general:Users - Tooltip"))} :
</Col>
<Col style={{marginTop: "5px"}} span={22} >
{
Setting.getTags(this.state.group.users, "users")
}
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :

View File

@ -195,6 +195,17 @@ class GroupListPage extends BaseListPage {
</Link>; </Link>;
}, },
}, },
{
title: i18next.t("general:Users"),
dataIndex: "users",
key: "users",
// width: "200px",
sorter: true,
...this.getColumnSearchProps("users"),
render: (text, record, index) => {
return Setting.getTags(text, "users");
},
},
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: "", dataIndex: "",

View File

@ -34,8 +34,8 @@ import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage"; import OrganizationEditPage from "./OrganizationEditPage";
import UserListPage from "./UserListPage"; import UserListPage from "./UserListPage";
import GroupTreePage from "./GroupTreePage"; import GroupTreePage from "./GroupTreePage";
import GroupListPage from "./GroupList"; import GroupListPage from "./GroupListPage";
import GroupEditPage from "./GroupEdit"; import GroupEditPage from "./GroupEditPage";
import UserEditPage from "./UserEditPage"; import UserEditPage from "./UserEditPage";
import InvitationListPage from "./InvitationListPage"; import InvitationListPage from "./InvitationListPage";
import InvitationEditPage from "./InvitationEditPage"; import InvitationEditPage from "./InvitationEditPage";

View File

@ -244,7 +244,7 @@ class ProviderEditPage extends React.Component {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
} }
case "Email": case "Email":
if (provider.type === "Azure ACS") { if (provider.type === "Azure ACS" || provider.type === "SendGrid") {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip")); return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
} else { } else {
return Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip")); return Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"));
@ -729,7 +729,7 @@ class ProviderEditPage extends React.Component {
<React.Fragment> <React.Fragment>
{ {
(this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") || (this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") ||
(this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Line" || this.state.provider.type === "Telegram" || this.state.provider.type === "Bark" || this.state.provider.type === "Discord" || this.state.provider.type === "Slack" || this.state.provider.type === "Pushbullet" || this.state.provider.type === "Pushover" || this.state.provider.type === "Lark" || this.state.provider.type === "Microsoft Teams")) ? null : ( (this.state.provider.category === "Notification" && (this.state.provider.type === "Line" || this.state.provider.type === "Telegram" || this.state.provider.type === "Bark" || this.state.provider.type === "Discord" || this.state.provider.type === "Slack" || this.state.provider.type === "Pushbullet" || this.state.provider.type === "Pushover" || this.state.provider.type === "Lark" || this.state.provider.type === "Microsoft Teams")) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@ -770,7 +770,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
{ {
(this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") ? null : ( (this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && (this.state.provider.type === "Azure ACS" || this.state.provider.type === "SendGrid")) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecret2Label(this.state.provider)} : {this.getClientSecret2Label(this.state.provider)} :
@ -985,17 +985,19 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) : this.state.provider.category === "Email" ? ( ) : this.state.provider.category === "Email" ? (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > {["SendGrid"].includes(this.state.provider.type) ? null : (
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Row style={{marginTop: "20px"}} >
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} : <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
</Col> {Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
<Col span={22} > </Col>
<Input prefix={<LinkOutlined />} value={this.state.provider.host} onChange={e => { <Col span={22} >
this.updateProviderField("host", e.target.value); <Input prefix={<LinkOutlined />} value={this.state.provider.host} onChange={e => {
}} /> this.updateProviderField("host", e.target.value);
</Col> }} />
</Row> </Col>
{["Azure ACS"].includes(this.state.provider.type) ? null : ( </Row>
)}
{["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
@ -1007,7 +1009,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
)} )}
{["Azure ACS"].includes(this.state.provider.type) ? null : ( {["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
@ -1073,7 +1075,7 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("receiver", e.target.value); this.updateProviderField("receiver", e.target.value);
}} /> }} />
</Col> </Col>
{["Azure ACS"].includes(this.state.provider.type) ? null : ( {["Azure ACS", "SendGrid"].includes(this.state.provider.type) ? null : (
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} > <Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test SMTP Connection")} {i18next.t("provider:Test SMTP Connection")}
</Button> </Button>

View File

@ -151,6 +151,14 @@ class RecordListPage extends BaseListPage {
sorter: true, sorter: true,
...this.getColumnSearchProps("language"), ...this.getColumnSearchProps("language"),
}, },
{
title: i18next.t("record:Response"),
dataIndex: "response",
key: "response",
width: "90px",
sorter: true,
...this.getColumnSearchProps("response"),
},
{ {
title: i18next.t("record:Object"), title: i18next.t("record:Object"),
dataIndex: "object", dataIndex: "object",
@ -179,7 +187,7 @@ class RecordListPage extends BaseListPage {
sorter: true, sorter: true,
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
if (!["signup", "login", "logout", "update-user"].includes(record.action)) { if (!["signup", "login", "logout", "update-user", "new-user"].includes(record.action)) {
return null; return null;
} }

View File

@ -181,6 +181,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_azure.png`, logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://learn.microsoft.com/zh-cn/azure/communication-services", url: "https://learn.microsoft.com/zh-cn/azure/communication-services",
}, },
"SendGrid": {
logo: `${StaticBaseUrl}/img/email_sendgrid.png`,
url: "https://sendgrid.com/",
},
"Custom HTTP Email": { "Custom HTTP Email": {
logo: `${StaticBaseUrl}/img/social_default.png`, logo: `${StaticBaseUrl}/img/social_default.png`,
url: "https://casdoor.org/docs/provider/email/overview", url: "https://casdoor.org/docs/provider/email/overview",
@ -1015,6 +1019,7 @@ export function getProviderTypeOptions(category) {
{id: "SUBMAIL", name: "SUBMAIL"}, {id: "SUBMAIL", name: "SUBMAIL"},
{id: "Mailtrap", name: "Mailtrap"}, {id: "Mailtrap", name: "Mailtrap"},
{id: "Azure ACS", name: "Azure ACS"}, {id: "Azure ACS", name: "Azure ACS"},
{id: "SendGrid", name: "SendGrid"},
{id: "Custom HTTP Email", name: "Custom HTTP Email"}, {id: "Custom HTTP Email", name: "Custom HTTP Email"},
] ]
); );

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Card, Col, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd"; import {Button, Card, Col, Form, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag, Tooltip} from "antd";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import {TotpMfaType} from "./auth/MfaSetupPage"; import {TotpMfaType} from "./auth/MfaSetupPage";
import * as GroupBackend from "./backend/GroupBackend"; import * as GroupBackend from "./backend/GroupBackend";
@ -407,7 +407,17 @@ class UserEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} : {Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<PasswordModal user={this.state.user} userName={this.state.userName} organization={this.getUserOrganization()} account={this.props.account} disabled={disabled} /> {
(this.state.user.name === this.state.userName) ? (
<PasswordModal user={this.state.user} userName={this.state.userName} organization={this.getUserOrganization()} account={this.props.account} disabled={disabled} />
) : (
<Tooltip placement={"topLeft"} title={i18next.t("user:You have changed the username, please save your change first before modifying the password")}>
<span>
<PasswordModal user={this.state.user} userName={this.state.userName} organization={this.getUserOrganization()} account={this.props.account} disabled={true} />
</span>
</Tooltip>
)
}
</Col> </Col>
</Row> </Row>
); );

View File

@ -19,7 +19,7 @@ import * as VerificationBackend from "./backend/VerificationBackend";
import i18next from "i18next"; import i18next from "i18next";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import React from "react"; import React from "react";
import {Table} from "antd"; import {Switch, Table} from "antd";
class VerificationListPage extends BaseListPage { class VerificationListPage extends BaseListPage {
newVerification() { newVerification() {
@ -35,26 +35,48 @@ class VerificationListPage extends BaseListPage {
renderTable(verifications) { renderTable(verifications) {
const columns = [ const columns = [
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Organization"),
dataIndex: "name", dataIndex: "owner",
key: "name", key: "owner",
width: "150px", width: "120px",
fixed: "left",
sorter: true, sorter: true,
...this.getColumnSearchProps("name"), ...this.getColumnSearchProps("owner"),
render: (text, record, index) => { render: (text, record, index) => {
if (text === "admin") {
return `(${i18next.t("general:empty")})`;
}
return ( return (
<Link to={`/syncers/${text}`}> <Link to={`/organizations/${text}`}>
{text} {text}
</Link> </Link>
); );
}, },
}, },
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "260px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{ {
title: i18next.t("provider:Type"), title: i18next.t("provider:Type"),
dataIndex: "type", dataIndex: "type",
key: "type", key: "type",
width: "120px", width: "90px",
sorter: true, sorter: true,
...this.getColumnSearchProps("type"), ...this.getColumnSearchProps("type"),
}, },
@ -67,7 +89,7 @@ class VerificationListPage extends BaseListPage {
...this.getColumnSearchProps("user"), ...this.getColumnSearchProps("user"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/users/${record.owner}/${text}`}> <Link to={`/users/${text}`}>
{text} {text}
</Link> </Link>
); );
@ -88,6 +110,26 @@ class VerificationListPage extends BaseListPage {
); );
}, },
}, },
{
title: i18next.t("general:Client IP"),
dataIndex: "remoteAddr",
key: "remoteAddr",
width: "100px",
sorter: true,
...this.getColumnSearchProps("remoteAddr"),
render: (text, record, index) => {
let clientIp = text;
if (clientIp.endsWith(": ")) {
clientIp = clientIp.slice(0, -2);
}
return (
<a target="_blank" rel="noreferrer" href={`https://db-ip.com/${clientIp}`}>
{clientIp}
</a>
);
},
},
{ {
title: i18next.t("verification:Receiver"), title: i18next.t("verification:Receiver"),
dataIndex: "receiver", dataIndex: "receiver",
@ -100,28 +142,20 @@ class VerificationListPage extends BaseListPage {
title: i18next.t("login:Verification code"), title: i18next.t("login:Verification code"),
dataIndex: "code", dataIndex: "code",
key: "code", key: "code",
width: "120px", width: "150px",
sorter: true, sorter: true,
...this.getColumnSearchProps("code"), ...this.getColumnSearchProps("code"),
}, },
{ {
title: i18next.t("general:Timestamp"), title: i18next.t("verification:Is used"),
dataIndex: "time", dataIndex: "isUsed",
key: "time", key: "isUsed",
width: "160px", width: "90px",
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {
return Setting.getFormattedDate(text * 1000); return (
}, <Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text} />
}, );
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}, },
}, },
]; ];
@ -156,7 +190,7 @@ class VerificationListPage extends BaseListPage {
value = params.type; value = params.type;
} }
this.setState({loading: true}); this.setState({loading: true});
VerificationBackend.getVerifications("admin", Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) VerificationBackend.getVerifications("", Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => { .then((res) => {
this.setState({ this.setState({
loading: false, loading: false,

View File

@ -275,7 +275,7 @@ class WebhookEditPage extends React.Component {
}} > }} >
{ {
( (
["signup", "login", "logout"].concat(this.getApiPaths()).map((option, index) => { ["signup", "login", "logout", "new-user"].concat(this.getApiPaths()).map((option, index) => {
return ( return (
<Option key={option} value={option}>{option}</Option> <Option key={option} value={option}>{option}</Option>
); );

View File

@ -21,7 +21,7 @@ import * as Setting from "../Setting";
import i18next from "i18next"; import i18next from "i18next";
import {SendCodeInput} from "../common/SendCodeInput"; import {SendCodeInput} from "../common/SendCodeInput";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import {CheckCircleOutlined, KeyOutlined, LockOutlined, SolutionOutlined, UserOutlined} from "@ant-design/icons"; import {ArrowLeftOutlined, CheckCircleOutlined, KeyOutlined, LockOutlined, SolutionOutlined, UserOutlined} from "@ant-design/icons";
import CustomGithubCorner from "../common/CustomGithubCorner"; import CustomGithubCorner from "../common/CustomGithubCorner";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import * as PasswordChecker from "../common/PasswordChecker"; import * as PasswordChecker from "../common/PasswordChecker";
@ -443,6 +443,18 @@ class ForgetPage extends React.Component {
); );
} }
stepBack() {
if (this.state.current > 0) {
this.setState({
current: this.state.current - 1,
});
} else if (this.props.history.length > 1) {
this.props.history.goBack();
} else {
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
}
}
render() { render() {
const application = this.getApplicationObj(); const application = this.getApplicationObj();
if (application === undefined) { if (application === undefined) {
@ -456,6 +468,9 @@ class ForgetPage extends React.Component {
<React.Fragment> <React.Fragment>
<CustomGithubCorner /> <CustomGithubCorner />
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}> <div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
<Button type="text" style={{position: "relative", left: Setting.isMobile() ? "10px" : "-90px", top: 0}} size={"large"} onClick={() => {this.stepBack();}}>
<ArrowLeftOutlined style={{fontSize: "24px"}} />
</Button>
<Row> <Row>
<Col span={24} style={{justifyContent: "center"}}> <Col span={24} style={{justifyContent: "center"}}>
<Row> <Row>

View File

@ -52,7 +52,7 @@ export function GoogleOneTapLoginVirtualButton(prop) {
redirectUri = `${redirectUri}?state=${state}&code=${encodeURIComponent(code)}`; redirectUri = `${redirectUri}?state=${state}&code=${encodeURIComponent(code)}`;
Setting.goToLink(redirectUri); Setting.goToLink(redirectUri);
}, },
disableCancelOnUnmount: true, disableCancelOnUnmount: false,
}); });
} }

View File

@ -1173,7 +1173,7 @@ class LoginPage extends React.Component {
}; };
return ( return (
<div style={{height: 300}}> <div style={{height: 300, minWidth: 320}}>
{renderChoiceBox()} {renderChoiceBox()}
</div> </div>
); );

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Form, Input, Result} from "antd"; import {Button, Form, Input, Radio, Result, Row} from "antd";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import * as ProviderButton from "./ProviderButton"; import * as ProviderButton from "./ProviderButton";
@ -71,6 +71,7 @@ class SignupPage extends React.Component {
applicationName: (props.applicationName ?? props.match?.params?.applicationName) ?? null, applicationName: (props.applicationName ?? props.match?.params?.applicationName) ?? null,
email: "", email: "",
phone: "", phone: "",
emailOrPhoneMode: "",
countryCode: "", countryCode: "",
emailCode: "", emailCode: "",
phoneCode: "", phoneCode: "",
@ -360,130 +361,176 @@ class SignupPage extends React.Component {
<RegionSelect onChange={(value) => {this.setState({region: value});}} /> <RegionSelect onChange={(value) => {this.setState({region: value});}} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Email") { } else if (signupItem.name === "Email" || signupItem.name === "Phone" || signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
return ( const renderEmailItem = () => {
<React.Fragment> return (
<Form.Item <React.Fragment>
name="email"
label={signupItem.label ? signupItem.label : i18next.t("general:Email")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your Email!"),
},
{
validator: (_, value) => {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.email)) {
this.setState({validEmail: false});
return Promise.reject(i18next.t("signup:The input is not valid Email!"));
}
this.setState({validEmail: true});
return Promise.resolve();
},
},
]}
>
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
</Form.Item>
{
signupItem.rule !== "No verification" &&
<Form.Item <Form.Item
name="emailCode" name="email"
label={signupItem.label ? signupItem.label : i18next.t("code:Email code")} label={signupItem.label ? signupItem.label : i18next.t("general:Email")}
rules={[{
required: required,
message: i18next.t("code:Please input your verification code!"),
}]}
>
<SendCodeInput
disabled={!this.state.validEmail}
method={"signup"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
}
</React.Fragment>
);
} else if (signupItem.name === "Phone") {
return (
<React.Fragment>
<Form.Item label={signupItem.label ? signupItem.label : i18next.t("general:Phone")} required={required}>
<Input.Group compact>
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
style={{width: "35%"}}
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
/>
</Form.Item>
<Form.Item
name="phone"
dependencies={["countryCode"]}
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
({getFieldValue}) => ({
validator: (_, value) => {
if (!required && !value) {
return Promise.resolve();
}
if (value && !Setting.isValidPhone(value, getFieldValue("countryCode"))) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
}),
]}
>
<Input
placeholder={signupItem.placeholder}
style={{width: "65%"}}
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
onChange={e => this.setState({phone: e.target.value})}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{
signupItem.rule !== "No verification" &&
<Form.Item
name="phoneCode"
label={signupItem.label ? signupItem.label : i18next.t("code:Phone code")}
rules={[ rules={[
{ {
required: required, required: required,
message: i18next.t("code:Please input your phone verification code!"), message: i18next.t("signup:Please input your Email!"),
},
{
validator: (_, value) => {
if (this.state.email !== "" && !Setting.isValidEmail(this.state.email)) {
this.setState({validEmail: false});
return Promise.reject(i18next.t("signup:The input is not valid Email!"));
}
this.setState({validEmail: true});
return Promise.resolve();
},
}, },
]} ]}
> >
<SendCodeInput <Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
disabled={!this.state.validPhone}
method={"signup"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application}
countryCode={this.form.current?.getFieldValue("countryCode")}
/>
</Form.Item> </Form.Item>
} {
</React.Fragment> signupItem.rule !== "No verification" &&
); <Form.Item
name="emailCode"
label={signupItem.label ? signupItem.label : i18next.t("code:Email code")}
rules={[{
required: required,
message: i18next.t("code:Please input your verification code!"),
}]}
>
<SendCodeInput
disabled={!this.state.validEmail}
method={"signup"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application}
/>
</Form.Item>
}
</React.Fragment>
);
};
const renderPhoneItem = () => {
return (
<React.Fragment>
<Form.Item label={signupItem.label ? signupItem.label : i18next.t("general:Phone")} required={required}>
<Input.Group compact>
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
style={{width: "35%"}}
countryCodes={this.getApplicationObj().organizationObj.countryCodes}
/>
</Form.Item>
<Form.Item
name="phone"
dependencies={["countryCode"]}
noStyle
rules={[
{
required: required,
message: i18next.t("signup:Please input your phone number!"),
},
({getFieldValue}) => ({
validator: (_, value) => {
if (!required && !value) {
return Promise.resolve();
}
if (value && !Setting.isValidPhone(value, getFieldValue("countryCode"))) {
this.setState({validPhone: false});
return Promise.reject(i18next.t("signup:The input is not valid Phone!"));
}
this.setState({validPhone: true});
return Promise.resolve();
},
}),
]}
>
<Input
placeholder={signupItem.placeholder}
style={{width: "65%"}}
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
onChange={e => this.setState({phone: e.target.value})}
/>
</Form.Item>
</Input.Group>
</Form.Item>
{
signupItem.rule !== "No verification" &&
<Form.Item
name="phoneCode"
label={signupItem.label ? signupItem.label : i18next.t("code:Phone code")}
rules={[
{
required: required,
message: i18next.t("code:Please input your phone verification code!"),
},
]}
>
<SendCodeInput
disabled={!this.state.validPhone}
method={"signup"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application}
countryCode={this.form.current?.getFieldValue("countryCode")}
/>
</Form.Item>
}
</React.Fragment>
);
};
if (signupItem.name === "Email") {
return renderEmailItem();
} else if (signupItem.name === "Phone") {
return renderPhoneItem();
} else if (signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
let emailOrPhoneMode = this.state.emailOrPhoneMode;
if (emailOrPhoneMode === "") {
emailOrPhoneMode = signupItem.name === "Email or Phone" ? "Email" : "Phone";
}
return (
<React.Fragment>
<Row style={{marginTop: "30px", marginBottom: "20px"}} >
<Radio.Group style={{width: "400px"}} buttonStyle="solid" onChange={e => {
this.setState({
emailOrPhoneMode: e.target.value,
});
}} value={emailOrPhoneMode}>
{
signupItem.name === "Email or Phone" ? (
<React.Fragment>
<Radio.Button value={"Email"}>{i18next.t("general:Email")}</Radio.Button>
<Radio.Button value={"Phone"}>{i18next.t("general:Phone")}</Radio.Button>
</React.Fragment>
) : (
<React.Fragment>
<Radio.Button value={"Phone"}>{i18next.t("general:Phone")}</Radio.Button>
<Radio.Button value={"Email"}>{i18next.t("general:Email")}</Radio.Button>
</React.Fragment>
)
}
</Radio.Group>
</Row>
{
emailOrPhoneMode === "Email" ? renderEmailItem() : renderPhoneItem()
}
</React.Fragment>
);
} else {
return null;
}
} else if (signupItem.name === "Password") { } else if (signupItem.name === "Password") {
return ( return (
<Form.Item <Form.Item

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Kopiere den Link", "Copy Link": "Kopiere den Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copiar enlace", "Copy Link": "Copiar enlace",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copier le lien", "Copy Link": "Copier le lien",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Salin Tautan", "Copy Link": "Salin Tautan",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "コピー リンク", "Copy Link": "コピー リンク",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "링크 복사하기", "Copy Link": "링크 복사하기",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copiar Link", "Copy Link": "Copiar Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Копировать ссылку", "Copy Link": "Копировать ссылку",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Copy Link", "Copy Link": "Copy Link",

File diff suppressed because it is too large Load Diff

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "Is triggered", "Is triggered": "Is triggered",
"Object": "Object" "Object": "Object",
"Response": "Response"
}, },
"resource": { "resource": {
"Copy Link": "Sao chép liên kết", "Copy Link": "Sao chép liên kết",

View File

@ -892,7 +892,8 @@
}, },
"record": { "record": {
"Is triggered": "是否触发", "Is triggered": "是否触发",
"Object": "实体" "Object": "实体",
"Response": "响应"
}, },
"resource": { "resource": {
"Copy Link": "复制链接", "Copy Link": "复制链接",

View File

@ -81,10 +81,12 @@ class SignupTable extends React.Component {
{name: "Affiliation", displayName: i18next.t("user:Affiliation")}, {name: "Affiliation", displayName: i18next.t("user:Affiliation")},
{name: "Country/Region", displayName: i18next.t("user:Country/Region")}, {name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{name: "ID card", displayName: i18next.t("user:ID card")}, {name: "ID card", displayName: i18next.t("user:ID card")},
{name: "Email", displayName: i18next.t("general:Email")},
{name: "Password", displayName: i18next.t("general:Password")}, {name: "Password", displayName: i18next.t("general:Password")},
{name: "Confirm password", displayName: i18next.t("signup:Confirm")}, {name: "Confirm password", displayName: i18next.t("signup:Confirm")},
{name: "Email", displayName: i18next.t("general:Email")},
{name: "Phone", displayName: i18next.t("general:Phone")}, {name: "Phone", displayName: i18next.t("general:Phone")},
{name: "Email or Phone", displayName: i18next.t("general:Email or Phone")},
{name: "Phone or Email", displayName: i18next.t("general:Phone or Email")},
{name: "Invitation code", displayName: i18next.t("application:Invitation code")}, {name: "Invitation code", displayName: i18next.t("application:Invitation code")},
{name: "Agreement", displayName: i18next.t("signup:Agreement")}, {name: "Agreement", displayName: i18next.t("signup:Agreement")},
{name: "Text 1", displayName: i18next.t("signup:Text 1")}, {name: "Text 1", displayName: i18next.t("signup:Text 1")},

View File

@ -183,19 +183,6 @@
dependencies: dependencies:
"@ctrl/tinycolor" "^3.4.0" "@ctrl/tinycolor" "^3.4.0"
"@ant-design/cssinjs@1.16.1":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-1.16.1.tgz#0032044db5678dd25ac12def1abb1d52e6a4d583"
integrity sha512-KKVB5Or6BDC1Bo3Y4KMlOkyQU0P+6GTodubrQ9YfrtXG1TgO4wpaEfg9I4ZA49R7M+Ij2KKNwb+5abvmXy6K8w==
dependencies:
"@babel/runtime" "^7.11.1"
"@emotion/hash" "^0.8.0"
"@emotion/unitless" "^0.7.5"
classnames "^2.3.1"
csstype "^3.0.10"
rc-util "^5.35.0"
stylis "^4.0.13"
"@ant-design/cssinjs@^1", "@ant-design/cssinjs@^1.10.1", "@ant-design/cssinjs@^1.5.6": "@ant-design/cssinjs@^1", "@ant-design/cssinjs@^1.10.1", "@ant-design/cssinjs@^1.5.6":
version "1.10.1" version "1.10.1"
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-1.10.1.tgz#c9173f38e3d61f0883ca3c17d7cf1e30784e0dd7" resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-1.10.1.tgz#c9173f38e3d61f0883ca3c17d7cf1e30784e0dd7"
@ -12408,14 +12395,6 @@ rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.15.0, rc-util@^5.16.0, rc-util@^5.16.
"@babel/runtime" "^7.18.3" "@babel/runtime" "^7.18.3"
react-is "^16.12.0" react-is "^16.12.0"
rc-util@^5.35.0:
version "5.35.0"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.35.0.tgz#bed1986248b7be525cc0894109e609ac60207f29"
integrity sha512-MTXlixb3EoSTEchsOc7XWsVyoUQqoCsh2Z1a2IptwNgqleMF6ZgQeY52UzUbNj5CcVBg9YljOWjuOV07jSSm4Q==
dependencies:
"@babel/runtime" "^7.18.3"
react-is "^16.12.0"
rc-virtual-list@^3.4.13, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2: rc-virtual-list@^3.4.13, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
version "3.5.2" version "3.5.2"
resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.5.2.tgz#5e1028869bae900eacbae6788d4eca7210736006" resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.5.2.tgz#5e1028869bae900eacbae6788d4eca7210736006"