mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-15 22:43:50 +08:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
7f52755e32 | |||
eaa6f50085 | |||
f35a5f9a47 | |||
7481b229a4 | |||
39e485ae82 | |||
764c64e67c | |||
e755a7331d | |||
6d9d595f86 | |||
d52058d2ae | |||
bcfbfc6947 | |||
75699c4a26 | |||
3e8bfb52a8 | |||
bbbd857a45 | |||
498900df76 | |||
7e3c1a6581 | |||
6e28043dba | |||
cb200687dc | |||
23bb0ee450 | |||
117259dfc5 | |||
e71d0476f0 | |||
b5d26767b2 | |||
5c4e22288e | |||
3ac4be64b8 | |||
97db54b6b9 | |||
3a19d4c7c8 | |||
a60be2b2ab | |||
06ef97a080 | |||
167c1b0f1b | |||
7d0eae230e | |||
901867e8bb | |||
b7be1943fa | |||
bbbda1982f | |||
e593f5be5b | |||
0918757e85 | |||
ce0d45a70b | |||
c4096788b2 | |||
523186f895 | |||
ef373ca736 | |||
721a681ff1 | |||
8b1c4b0c75 | |||
540f22f8bd | |||
79f81f1356 | |||
4e145f71b5 | |||
104f975a2f |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@ -127,7 +127,7 @@ jobs:
|
|||||||
release-and-push:
|
release-and-push:
|
||||||
name: Release And Push
|
name: Release And Push
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'casbin/casdoor' && github.event_name == 'push'
|
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
|
||||||
needs: [ frontend, backend, linter, e2e ]
|
needs: [ frontend, backend, linter, e2e ]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -182,14 +182,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
- name: Push to Docker Hub
|
- name: Push to Docker Hub
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
target: STANDARD
|
target: STANDARD
|
||||||
@ -199,7 +199,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Push All In One Version to Docker Hub
|
- name: Push All In One Version to Docker Hub
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
if: github.repository == 'casbin/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
target: ALLINONE
|
target: ALLINONE
|
||||||
|
2
.github/workflows/sync.yml
vendored
2
.github/workflows/sync.yml
vendored
@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'casbin/casdoor' && github.event_name == 'push'
|
if: github.repository == 'casdoor/casdoor' && github.event_name == 'push'
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -87,8 +87,7 @@ https://casdoor.org/docs/category/integrations
|
|||||||
## How to contact?
|
## How to contact?
|
||||||
|
|
||||||
- Discord: https://discord.gg/5rPsrAzK7S
|
- Discord: https://discord.gg/5rPsrAzK7S
|
||||||
- Forum: https://forum.casbin.com
|
- Contact: https://casdoor.org/help
|
||||||
- Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e
|
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
|
@ -51,7 +51,8 @@ p, *, *, GET, /api/get-account, *, *
|
|||||||
p, *, *, GET, /api/userinfo, *, *
|
p, *, *, GET, /api/userinfo, *, *
|
||||||
p, *, *, GET, /api/user, *, *
|
p, *, *, GET, /api/user, *, *
|
||||||
p, *, *, GET, /api/health, *, *
|
p, *, *, GET, /api/health, *, *
|
||||||
p, *, *, POST, /api/webhook, *, *
|
p, *, *, *, /api/webhook, *, *
|
||||||
|
p, *, *, GET, /api/get-qrcode, *, *
|
||||||
p, *, *, GET, /api/get-webhook-event, *, *
|
p, *, *, GET, /api/get-webhook-event, *, *
|
||||||
p, *, *, GET, /api/get-captcha-status, *, *
|
p, *, *, GET, /api/get-captcha-status, *, *
|
||||||
p, *, *, *, /api/login/oauth, *, *
|
p, *, *, *, /api/login/oauth, *, *
|
||||||
@ -80,6 +81,7 @@ p, *, *, *, /.well-known/jwks, *, *
|
|||||||
p, *, *, GET, /api/get-saml-login, *, *
|
p, *, *, GET, /api/get-saml-login, *, *
|
||||||
p, *, *, POST, /api/acs, *, *
|
p, *, *, POST, /api/acs, *, *
|
||||||
p, *, *, GET, /api/saml/metadata, *, *
|
p, *, *, GET, /api/saml/metadata, *, *
|
||||||
|
p, *, *, *, /api/saml/redirect, *, *
|
||||||
p, *, *, *, /cas, *, *
|
p, *, *, *, /cas, *, *
|
||||||
p, *, *, *, /scim, *, *
|
p, *, *, *, /scim, *, *
|
||||||
p, *, *, *, /api/webauthn, *, *
|
p, *, *, *, /api/webauthn, *, *
|
||||||
@ -95,6 +97,7 @@ p, *, *, GET, /api/get-organization-names, *, *
|
|||||||
p, *, *, GET, /api/get-all-objects, *, *
|
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/get-invitation-info, *, *
|
||||||
`
|
`
|
||||||
|
|
||||||
sa := stringadapter.NewAdapter(ruleText)
|
sa := stringadapter.NewAdapter(ruleText)
|
||||||
@ -150,7 +153,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/") {
|
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" {
|
||||||
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
|
||||||
|
14
conf/conf.go
14
conf/conf.go
@ -71,7 +71,10 @@ func GetConfigInt64(key string) (int64, error) {
|
|||||||
|
|
||||||
func GetConfigDataSourceName() string {
|
func GetConfigDataSourceName() string {
|
||||||
dataSourceName := GetConfigString("dataSourceName")
|
dataSourceName := GetConfigString("dataSourceName")
|
||||||
|
return ReplaceDataSourceNameByDocker(dataSourceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplaceDataSourceNameByDocker(dataSourceName string) string {
|
||||||
runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
|
runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
|
||||||
if runningInDocker == "true" {
|
if runningInDocker == "true" {
|
||||||
// https://stackoverflow.com/questions/48546124/what-is-linux-equivalent-of-host-docker-internal
|
// https://stackoverflow.com/questions/48546124/what-is-linux-equivalent-of-host-docker-internal
|
||||||
@ -81,7 +84,6 @@ func GetConfigDataSourceName() string {
|
|||||||
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "host.docker.internal")
|
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "host.docker.internal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSourceName
|
return dataSourceName
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,13 +110,3 @@ func GetConfigBatchSize() int {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfigRealDataSourceName(driverName string) string {
|
|
||||||
var dataSourceName string
|
|
||||||
if driverName != "mysql" {
|
|
||||||
dataSourceName = GetConfigDataSourceName()
|
|
||||||
} else {
|
|
||||||
dataSourceName = GetConfigDataSourceName() + GetConfigString("dbName")
|
|
||||||
}
|
|
||||||
return dataSourceName
|
|
||||||
}
|
|
||||||
|
@ -93,6 +93,10 @@ func (c *ApiController) Signup() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if application == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !application.EnableSignUp {
|
if !application.EnableSignUp {
|
||||||
c.ResponseError(c.T("account:The application does not allow to sign up new account"))
|
c.ResponseError(c.T("account:The application does not allow to sign up new account"))
|
||||||
@ -105,6 +109,11 @@ func (c *ApiController) Signup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if organization == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s does not exist"), authForm.Organization))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
|
msg := object.CheckUserSignup(application, organization, &authForm, c.GetAcceptLanguage())
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
c.ResponseError(msg)
|
c.ResponseError(msg)
|
||||||
@ -227,7 +236,7 @@ func (c *ApiController) Signup() {
|
|||||||
|
|
||||||
if invitation != nil {
|
if invitation != nil {
|
||||||
invitation.UsedCount += 1
|
invitation.UsedCount += 1
|
||||||
_, err := object.UpdateInvitation(invitation.GetId(), invitation)
|
_, err := object.UpdateInvitation(invitation.GetId(), invitation, c.GetAcceptLanguage())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -139,6 +139,10 @@ func (c *ApiController) GetUserApplication() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if application == nil {
|
||||||
|
c.ResponseError(fmt.Sprintf(c.T("general:The organization: %s should have one application at least"), user.Owner))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.ResponseOk(object.GetMaskedApplication(application, userId))
|
c.ResponseOk(object.GetMaskedApplication(application, userId))
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/captcha"
|
"github.com/casdoor/casdoor/captcha"
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
@ -37,11 +36,6 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
wechatScanType string
|
|
||||||
lock sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func codeToResponse(code *object.Code) *Response {
|
func codeToResponse(code *object.Code) *Response {
|
||||||
if code.Code == "" {
|
if code.Code == "" {
|
||||||
return &Response{Status: "error", Msg: code.Message, Data: code.Code}
|
return &Response{Status: "error", Msg: code.Message, Data: code.Code}
|
||||||
@ -896,6 +890,7 @@ func (c *ApiController) GetSamlLogin() {
|
|||||||
authURL, method, err := object.GenerateSamlRequest(providerId, relayState, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
authURL, method, err := object.GenerateSamlRequest(providerId, relayState, c.Ctx.Request.Host, c.GetAcceptLanguage())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.ResponseOk(authURL, method)
|
c.ResponseOk(authURL, method)
|
||||||
}
|
}
|
||||||
@ -906,62 +901,126 @@ func (c *ApiController) HandleSamlLogin() {
|
|||||||
decode, err := base64.StdEncoding.DecodeString(relayState)
|
decode, err := base64.StdEncoding.DecodeString(relayState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
slice := strings.Split(string(decode), "&")
|
slice := strings.Split(string(decode), "&")
|
||||||
relayState = url.QueryEscape(relayState)
|
relayState = url.QueryEscape(relayState)
|
||||||
samlResponse = url.QueryEscape(samlResponse)
|
samlResponse = url.QueryEscape(samlResponse)
|
||||||
targetUrl := fmt.Sprintf("%s?relayState=%s&samlResponse=%s",
|
targetUrl := fmt.Sprintf("%s?relayState=%s&samlResponse=%s",
|
||||||
slice[4], relayState, samlResponse)
|
slice[4], relayState, samlResponse)
|
||||||
c.Redirect(targetUrl, 303)
|
c.Redirect(targetUrl, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleOfficialAccountEvent ...
|
// HandleOfficialAccountEvent ...
|
||||||
// @Tag System API
|
// @Tag System API
|
||||||
// @Title HandleOfficialAccountEvent
|
// @Title HandleOfficialAccountEvent
|
||||||
// @router /webhook [POST]
|
// @router /webhook [POST]
|
||||||
// @Success 200 {object} object.Userinfo The Response object
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
func (c *ApiController) HandleOfficialAccountEvent() {
|
func (c *ApiController) HandleOfficialAccountEvent() {
|
||||||
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body)
|
if c.Ctx.Request.Method == "GET" {
|
||||||
|
s := c.Ctx.Request.FormValue("echostr")
|
||||||
|
echostr, _ := strconv.Atoi(s)
|
||||||
|
c.SetData(echostr)
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respBytes, err := io.ReadAll(c.Ctx.Request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
signature := c.Input().Get("signature")
|
||||||
|
timestamp := c.Input().Get("timestamp")
|
||||||
|
nonce := c.Input().Get("nonce")
|
||||||
var data struct {
|
var data struct {
|
||||||
MsgType string `xml:"MsgType"`
|
MsgType string `xml:"MsgType"`
|
||||||
Event string `xml:"Event"`
|
Event string `xml:"Event"`
|
||||||
EventKey string `xml:"EventKey"`
|
EventKey string `xml:"EventKey"`
|
||||||
|
FromUserName string `xml:"FromUserName"`
|
||||||
|
Ticket string `xml:"Ticket"`
|
||||||
}
|
}
|
||||||
err = xml.Unmarshal(respBytes, &data)
|
err = xml.Unmarshal(respBytes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.ToUpper(data.Event) != "SCAN" && strings.ToUpper(data.Event) != "SUBSCRIBE" {
|
||||||
lock.Lock()
|
|
||||||
defer lock.Unlock()
|
|
||||||
if data.EventKey != "" {
|
|
||||||
wechatScanType = data.Event
|
|
||||||
c.Ctx.WriteString("")
|
c.Ctx.WriteString("")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if data.Ticket == "" {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
providerId := data.EventKey
|
||||||
|
provider, err := object.GetProvider(providerId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data.Ticket == "" {
|
||||||
|
c.ResponseError("empty ticket")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !idp.VerifyWechatSignature(provider.Content, nonce, timestamp, signature) {
|
||||||
|
c.ResponseError("invalid signature")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idp.Lock.Lock()
|
||||||
|
if idp.WechatCacheMap == nil {
|
||||||
|
idp.WechatCacheMap = make(map[string]idp.WechatCacheMapValue)
|
||||||
|
}
|
||||||
|
idp.WechatCacheMap[data.Ticket] = idp.WechatCacheMapValue{
|
||||||
|
IsScanned: true,
|
||||||
|
WechatUnionId: data.FromUserName,
|
||||||
|
}
|
||||||
|
idp.Lock.Unlock()
|
||||||
|
|
||||||
|
c.Ctx.WriteString("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebhookEventType ...
|
// GetWebhookEventType ...
|
||||||
// @Tag System API
|
// @Tag System API
|
||||||
// @Title GetWebhookEventType
|
// @Title GetWebhookEventType
|
||||||
// @router /get-webhook-event [GET]
|
// @router /get-webhook-event [GET]
|
||||||
// @Success 200 {object} object.Userinfo The Response object
|
// @Param ticket query string true "The eventId of QRCode"
|
||||||
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
func (c *ApiController) GetWebhookEventType() {
|
func (c *ApiController) GetWebhookEventType() {
|
||||||
lock.Lock()
|
ticket := c.Input().Get("ticket")
|
||||||
defer lock.Unlock()
|
|
||||||
resp := &Response{
|
idp.Lock.RLock()
|
||||||
Status: "ok",
|
_, ok := idp.WechatCacheMap[ticket]
|
||||||
Msg: "",
|
idp.Lock.RUnlock()
|
||||||
Data: wechatScanType,
|
if !ok {
|
||||||
|
c.ResponseError("ticket not found")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.Data["json"] = resp
|
|
||||||
wechatScanType = ""
|
c.ResponseOk("SCAN", ticket)
|
||||||
c.ServeJSON()
|
}
|
||||||
|
|
||||||
|
// GetQRCode
|
||||||
|
// @Tag System API
|
||||||
|
// @Title GetWechatQRCode
|
||||||
|
// @router /get-qrcode [GET]
|
||||||
|
// @Param id query string true "The id ( owner/name ) of provider"
|
||||||
|
// @Success 200 {object} controllers.Response The Response object
|
||||||
|
func (c *ApiController) GetQRCode() {
|
||||||
|
providerId := c.Input().Get("id")
|
||||||
|
provider, err := object.GetProvider(providerId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code, ticket, err := idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2, providerId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ResponseOk(code, ticket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCaptchaStatus
|
// GetCaptchaStatus
|
||||||
|
@ -85,7 +85,7 @@ func (c *ApiController) Enforce() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if permission == nil {
|
if permission == nil {
|
||||||
c.ResponseError(fmt.Sprintf("permission: %s doesn't exist", permissionId))
|
c.ResponseError(fmt.Sprintf(c.T("permission:The permission: \"%s\" doesn't exist"), permissionId))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +121,10 @@ func (c *ApiController) Enforce() {
|
|||||||
}
|
}
|
||||||
} else if owner != "" {
|
} else if owner != "" {
|
||||||
permissions, err = object.GetPermissions(owner)
|
permissions, err = object.GetPermissions(owner)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
c.ResponseError(c.T("general:Missing parameter"))
|
c.ResponseError(c.T("general:Missing parameter"))
|
||||||
return
|
return
|
||||||
@ -205,7 +209,7 @@ func (c *ApiController) BatchEnforce() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if permission == nil {
|
if permission == nil {
|
||||||
c.ResponseError(fmt.Sprintf("permission: %s doesn't exist", permissionId))
|
c.ResponseError(fmt.Sprintf(c.T("permission:The permission: \"%s\" doesn't exist"), permissionId))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +239,10 @@ func (c *ApiController) BatchEnforce() {
|
|||||||
}
|
}
|
||||||
} else if owner != "" {
|
} else if owner != "" {
|
||||||
permissions, err = object.GetPermissions(owner)
|
permissions, err = object.GetPermissions(owner)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
c.ResponseError(c.T("general:Missing parameter"))
|
c.ResponseError(c.T("general:Missing parameter"))
|
||||||
return
|
return
|
||||||
|
@ -84,6 +84,32 @@ func (c *ApiController) GetInvitation() {
|
|||||||
c.ResponseOk(invitation)
|
c.ResponseOk(invitation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInvitationCodeInfo
|
||||||
|
// @Title GetInvitationCodeInfo
|
||||||
|
// @Tag Invitation API
|
||||||
|
// @Description get invitation code information
|
||||||
|
// @Param code query string true "Invitation code"
|
||||||
|
// @Success 200 {object} object.Invitation The Response object
|
||||||
|
// @router /get-invitation-info [get]
|
||||||
|
func (c *ApiController) GetInvitationCodeInfo() {
|
||||||
|
code := c.Input().Get("code")
|
||||||
|
applicationId := c.Input().Get("applicationId")
|
||||||
|
|
||||||
|
application, err := object.GetApplication(applicationId)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invitation, msg := object.GetInvitationByCode(code, application.Organization, c.GetAcceptLanguage())
|
||||||
|
if msg != "" {
|
||||||
|
c.ResponseError(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ResponseOk(object.GetMaskedInvitation(invitation))
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateInvitation
|
// UpdateInvitation
|
||||||
// @Title UpdateInvitation
|
// @Title UpdateInvitation
|
||||||
// @Tag Invitation API
|
// @Tag Invitation API
|
||||||
@ -102,7 +128,7 @@ func (c *ApiController) UpdateInvitation() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdateInvitation(id, &invitation))
|
c.Data["json"] = wrapActionResponse(object.UpdateInvitation(id, &invitation, c.GetAcceptLanguage()))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +147,7 @@ func (c *ApiController) AddInvitation() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Data["json"] = wrapActionResponse(object.AddInvitation(&invitation))
|
c.Data["json"] = wrapActionResponse(object.AddInvitation(&invitation, c.GetAcceptLanguage()))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,14 @@ import (
|
|||||||
|
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MfaRecoveryCodesSession = "mfa_recovery_codes"
|
||||||
|
MfaCountryCodeSession = "mfa_country_code"
|
||||||
|
MfaDestSession = "mfa_dest"
|
||||||
|
MfaTotpSecretSession = "mfa_totp_secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MfaSetupInitiate
|
// MfaSetupInitiate
|
||||||
@ -57,12 +65,20 @@ func (c *ApiController) MfaSetupInitiate() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mfaProps, err := MfaUtil.Initiate(c.Ctx, user.GetId())
|
mfaProps, err := MfaUtil.Initiate(user.GetId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recoveryCode := uuid.NewString()
|
||||||
|
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
|
||||||
|
if mfaType == object.TotpType {
|
||||||
|
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaProps.RecoveryCodes = []string{recoveryCode}
|
||||||
|
|
||||||
resp := mfaProps
|
resp := mfaProps
|
||||||
c.ResponseOk(resp)
|
c.ResponseOk(resp)
|
||||||
}
|
}
|
||||||
@ -83,13 +99,46 @@ func (c *ApiController) MfaSetupVerify() {
|
|||||||
c.ResponseError("missing auth type or passcode")
|
c.ResponseError("missing auth type or passcode")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mfaUtil := object.GetMfaUtil(mfaType, nil)
|
|
||||||
|
config := &object.MfaProps{
|
||||||
|
MfaType: mfaType,
|
||||||
|
}
|
||||||
|
if mfaType == object.TotpType {
|
||||||
|
secret := c.GetSession(MfaTotpSecretSession)
|
||||||
|
if secret == nil {
|
||||||
|
c.ResponseError("totp secret is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.Secret = secret.(string)
|
||||||
|
} else if mfaType == object.SmsType {
|
||||||
|
dest := c.GetSession(MfaDestSession)
|
||||||
|
if dest == nil {
|
||||||
|
c.ResponseError("destination is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.Secret = dest.(string)
|
||||||
|
countryCode := c.GetSession(MfaCountryCodeSession)
|
||||||
|
if countryCode == nil {
|
||||||
|
c.ResponseError("country code is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.CountryCode = countryCode.(string)
|
||||||
|
} else if mfaType == object.EmailType {
|
||||||
|
dest := c.GetSession(MfaDestSession)
|
||||||
|
if dest == nil {
|
||||||
|
c.ResponseError("destination is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.Secret = dest.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||||
if mfaUtil == nil {
|
if mfaUtil == nil {
|
||||||
c.ResponseError("Invalid multi-factor authentication type")
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mfaUtil.SetupVerify(c.Ctx, passcode)
|
err := mfaUtil.SetupVerify(passcode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
} else {
|
} else {
|
||||||
@ -122,18 +171,69 @@ func (c *ApiController) MfaSetupEnable() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mfaUtil := object.GetMfaUtil(mfaType, nil)
|
config := &object.MfaProps{
|
||||||
|
MfaType: mfaType,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mfaType == object.TotpType {
|
||||||
|
secret := c.GetSession(MfaTotpSecretSession)
|
||||||
|
if secret == nil {
|
||||||
|
c.ResponseError("totp secret is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.Secret = secret.(string)
|
||||||
|
} else if mfaType == object.EmailType {
|
||||||
|
if user.Email == "" {
|
||||||
|
dest := c.GetSession(MfaDestSession)
|
||||||
|
if dest == nil {
|
||||||
|
c.ResponseError("destination is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Email = dest.(string)
|
||||||
|
}
|
||||||
|
} else if mfaType == object.SmsType {
|
||||||
|
if user.Phone == "" {
|
||||||
|
dest := c.GetSession(MfaDestSession)
|
||||||
|
if dest == nil {
|
||||||
|
c.ResponseError("destination is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Phone = dest.(string)
|
||||||
|
countryCode := c.GetSession(MfaCountryCodeSession)
|
||||||
|
if countryCode == nil {
|
||||||
|
c.ResponseError("country code is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.CountryCode = countryCode.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
|
||||||
|
if recoveryCodes == nil {
|
||||||
|
c.ResponseError("recovery codes is missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.RecoveryCodes = []string{recoveryCodes.(string)}
|
||||||
|
|
||||||
|
mfaUtil := object.GetMfaUtil(mfaType, config)
|
||||||
if mfaUtil == nil {
|
if mfaUtil == nil {
|
||||||
c.ResponseError("Invalid multi-factor authentication type")
|
c.ResponseError("Invalid multi-factor authentication type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mfaUtil.Enable(c.Ctx, user)
|
err = mfaUtil.Enable(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.DelSession(MfaRecoveryCodesSession)
|
||||||
|
if mfaType == object.TotpType {
|
||||||
|
c.DelSession(MfaTotpSecretSession)
|
||||||
|
} else {
|
||||||
|
c.DelSession(MfaCountryCodeSession)
|
||||||
|
c.DelSession(MfaDestSession)
|
||||||
|
}
|
||||||
|
|
||||||
c.ResponseOk(http.StatusText(http.StatusOK))
|
c.ResponseOk(http.StatusText(http.StatusOK))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/object"
|
"github.com/casdoor/casdoor/object"
|
||||||
)
|
)
|
||||||
@ -34,7 +35,13 @@ func (c *ApiController) GetSamlMeta() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata, err := object.GetSamlMeta(application, host)
|
enablePostBinding, err := c.GetBool("enablePostBinding", false)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := object.GetSamlMeta(application, host, enablePostBinding)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
@ -43,3 +50,17 @@ func (c *ApiController) GetSamlMeta() {
|
|||||||
c.Data["xml"] = metadata
|
c.Data["xml"] = metadata
|
||||||
c.ServeXML()
|
c.ServeXML()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ApiController) HandleSamlRedirect() {
|
||||||
|
host := c.Ctx.Request.Host
|
||||||
|
|
||||||
|
owner := c.Ctx.Input.Param(":owner")
|
||||||
|
application := c.Ctx.Input.Param(":application")
|
||||||
|
|
||||||
|
relayState := c.Input().Get("RelayState")
|
||||||
|
samlRequest := c.Input().Get("SAMLRequest")
|
||||||
|
|
||||||
|
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host)
|
||||||
|
|
||||||
|
c.Redirect(targetURL, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
@ -47,6 +47,11 @@ func (c *ApiController) GetSystemInfo() {
|
|||||||
// @router /get-version-info [get]
|
// @router /get-version-info [get]
|
||||||
func (c *ApiController) GetVersionInfo() {
|
func (c *ApiController) GetVersionInfo() {
|
||||||
versionInfo, err := util.GetVersionInfo()
|
versionInfo, err := util.GetVersionInfo()
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if versionInfo.Version != "" {
|
if versionInfo.Version != "" {
|
||||||
c.ResponseOk(versionInfo)
|
c.ResponseOk(versionInfo)
|
||||||
return
|
return
|
||||||
|
@ -271,6 +271,14 @@ func (c *ApiController) RefreshToken() {
|
|||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ApiController) ResponseTokenError(errorMsg string) {
|
||||||
|
c.Data["json"] = &object.TokenError{
|
||||||
|
Error: errorMsg,
|
||||||
|
}
|
||||||
|
c.SetTokenErrorHttpStatus()
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
// IntrospectToken
|
// IntrospectToken
|
||||||
// @Title IntrospectToken
|
// @Title IntrospectToken
|
||||||
// @Tag Login API
|
// @Tag Login API
|
||||||
@ -293,40 +301,33 @@ func (c *ApiController) IntrospectToken() {
|
|||||||
clientId = c.Input().Get("client_id")
|
clientId = c.Input().Get("client_id")
|
||||||
clientSecret = c.Input().Get("client_secret")
|
clientSecret = c.Input().Get("client_secret")
|
||||||
if clientId == "" || clientSecret == "" {
|
if clientId == "" || clientSecret == "" {
|
||||||
c.ResponseError(c.T("token:Empty clientId or clientSecret"))
|
c.ResponseTokenError(object.InvalidRequest)
|
||||||
c.Data["json"] = &object.TokenError{
|
|
||||||
Error: object.InvalidRequest,
|
|
||||||
}
|
|
||||||
c.SetTokenErrorHttpStatus()
|
|
||||||
c.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
application, err := object.GetApplicationByClientId(clientId)
|
application, err := object.GetApplicationByClientId(clientId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseTokenError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if application == nil || application.ClientSecret != clientSecret {
|
if application == nil || application.ClientSecret != clientSecret {
|
||||||
c.ResponseError(c.T("token:Invalid application or wrong clientSecret"))
|
c.ResponseTokenError(c.T("token:Invalid application or wrong clientSecret"))
|
||||||
c.Data["json"] = &object.TokenError{
|
|
||||||
Error: object.InvalidClient,
|
|
||||||
}
|
|
||||||
c.SetTokenErrorHttpStatus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token, err := object.GetTokenByTokenAndApplication(tokenValue, application.Name)
|
|
||||||
if err != nil {
|
|
||||||
c.ResponseError(err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token, err := object.GetTokenByTokenValue(tokenValue)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseTokenError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
if token == nil {
|
if token == nil {
|
||||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||||
if err != nil || jwtToken.Valid() != nil {
|
if err != nil || jwtToken.Valid() != nil {
|
||||||
// and token revoked case. but we not implement
|
// and token revoked case. but we not implement
|
||||||
|
@ -156,6 +156,10 @@ func (c *ApiController) GetUser() {
|
|||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if userFromUserId == nil {
|
||||||
|
c.ResponseOk(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
|
id = util.GetId(userFromUserId.Owner, userFromUserId.Name)
|
||||||
}
|
}
|
||||||
@ -200,7 +204,7 @@ func (c *ApiController) GetUser() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if organization == nil {
|
if organization == nil {
|
||||||
c.ResponseError(fmt.Sprintf("the organization: %s is not found", owner))
|
c.ResponseError(fmt.Sprintf(c.T("auth:The organization: %s does not exist"), owner))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,16 +161,16 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
vform.Dest = mfaProps.Secret
|
vform.Dest = mfaProps.Secret
|
||||||
}
|
}
|
||||||
} else if vform.Method == MfaSetupVerification {
|
} else if vform.Method == MfaSetupVerification {
|
||||||
c.SetSession(object.MfaDestSession, vform.Dest)
|
c.SetSession(MfaDestSession, vform.Dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := application.GetEmailProvider()
|
provider, err = application.GetEmailProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
c.ResponseError(fmt.Sprintf("please add an Email provider to the \"Providers\" list for the application: %s", application.Name))
|
c.ResponseError(fmt.Sprintf(c.T("verification:please add an Email provider to the \"Providers\" list for the application: %s"), application.Name))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,8 +198,8 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if vform.Method == MfaSetupVerification {
|
if vform.Method == MfaSetupVerification {
|
||||||
c.SetSession(object.MfaCountryCodeSession, vform.CountryCode)
|
c.SetSession(MfaCountryCodeSession, vform.CountryCode)
|
||||||
c.SetSession(object.MfaDestSession, vform.Dest)
|
c.SetSession(MfaDestSession, vform.Dest)
|
||||||
}
|
}
|
||||||
} else if vform.Method == MfaAuthVerification {
|
} else if vform.Method == MfaAuthVerification {
|
||||||
mfaProps := user.GetPreferredMfaProps(false)
|
mfaProps := user.GetPreferredMfaProps(false)
|
||||||
@ -210,13 +210,13 @@ func (c *ApiController) SendVerificationCode() {
|
|||||||
vform.CountryCode = mfaProps.CountryCode
|
vform.CountryCode = mfaProps.CountryCode
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := application.GetSmsProvider()
|
provider, err = application.GetSmsProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
c.ResponseError(fmt.Sprintf("please add a SMS provider to the \"Providers\" list for the application: %s", application.Name))
|
c.ResponseError(fmt.Sprintf(c.T("verification:please add a SMS provider to the \"Providers\" list for the application: %s"), application.Name))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ func GetCredManager(passwordType string) CredManager {
|
|||||||
return NewPlainCredManager()
|
return NewPlainCredManager()
|
||||||
} else if passwordType == "salt" {
|
} else if passwordType == "salt" {
|
||||||
return NewSha256SaltCredManager()
|
return NewSha256SaltCredManager()
|
||||||
|
} else if passwordType == "sha512-salt" {
|
||||||
|
return NewSha512SaltCredManager()
|
||||||
} else if passwordType == "md5-salt" {
|
} else if passwordType == "md5-salt" {
|
||||||
return NewMd5UserSaltCredManager()
|
return NewMd5UserSaltCredManager()
|
||||||
} else if passwordType == "bcrypt" {
|
} else if passwordType == "bcrypt" {
|
||||||
|
50
cred/sha512-salt.go
Normal file
50
cred/sha512-salt.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 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 cred
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sha512SaltCredManager struct{}
|
||||||
|
|
||||||
|
func getSha512(data []byte) []byte {
|
||||||
|
hash := sha512.Sum512(data)
|
||||||
|
return hash[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSha512HexDigest(s string) string {
|
||||||
|
b := getSha512([]byte(s))
|
||||||
|
res := hex.EncodeToString(b)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSha512SaltCredManager() *Sha512SaltCredManager {
|
||||||
|
cm := &Sha512SaltCredManager{}
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *Sha512SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||||
|
res := getSha512HexDigest(password)
|
||||||
|
if organizationSalt != "" {
|
||||||
|
res = getSha512HexDigest(res + organizationSalt)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *Sha512SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||||
|
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
|
||||||
|
}
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein",
|
"Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
|
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
|
||||||
"DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname",
|
"DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname",
|
||||||
"Email already exists": "E-Mail existiert bereits",
|
"Email already exists": "E-Mail existiert bereits",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Afiliación no puede estar en blanco",
|
"Affiliation cannot be blank": "Afiliación no puede estar en blanco",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
|
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
|
||||||
"DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido",
|
"DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido",
|
||||||
"Email already exists": "El correo electrónico ya existe",
|
"Email already exists": "El correo electrónico ya existe",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation ne peut pas être vide",
|
"Affiliation cannot be blank": "Affiliation ne peut pas être vide",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
|
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
|
||||||
"DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide",
|
"DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide",
|
||||||
"Email already exists": "E-mail déjà existant",
|
"Email already exists": "E-mail déjà existant",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Keterkaitan tidak boleh kosong",
|
"Affiliation cannot be blank": "Keterkaitan tidak boleh kosong",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong",
|
"DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong",
|
||||||
"DisplayName is not valid real name": "DisplayName bukanlah nama asli yang valid",
|
"DisplayName is not valid real name": "DisplayName bukanlah nama asli yang valid",
|
||||||
"Email already exists": "Email sudah ada",
|
"Email already exists": "Email sudah ada",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "所属は空白にできません",
|
"Affiliation cannot be blank": "所属は空白にできません",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "表示名は空白にできません",
|
"DisplayName cannot be blank": "表示名は空白にできません",
|
||||||
"DisplayName is not valid real name": "表示名は有効な実名ではありません",
|
"DisplayName is not valid real name": "表示名は有効な実名ではありません",
|
||||||
"Email already exists": "メールは既に存在します",
|
"Email already exists": "メールは既に存在します",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "소속은 비워 둘 수 없습니다",
|
"Affiliation cannot be blank": "소속은 비워 둘 수 없습니다",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다",
|
"DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다",
|
||||||
"DisplayName is not valid real name": "DisplayName는 유효한 실제 이름이 아닙니다",
|
"DisplayName is not valid real name": "DisplayName는 유효한 실제 이름이 아닙니다",
|
||||||
"Email already exists": "이메일이 이미 존재합니다",
|
"Email already exists": "이메일이 이미 존재합니다",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Принадлежность не может быть пустым значением",
|
"Affiliation cannot be blank": "Принадлежность не может быть пустым значением",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "Имя отображения не может быть пустым",
|
"DisplayName cannot be blank": "Имя отображения не может быть пустым",
|
||||||
"DisplayName is not valid real name": "DisplayName не является действительным именем",
|
"DisplayName is not valid real name": "DisplayName не является действительным именем",
|
||||||
"Email already exists": "Электронная почта уже существует",
|
"Email already exists": "Электронная почта уже существует",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
"Affiliation cannot be blank": "Affiliation cannot be blank",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
"DisplayName cannot be blank": "DisplayName cannot be blank",
|
||||||
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
"DisplayName is not valid real name": "DisplayName is not valid real name",
|
||||||
"Email already exists": "Email already exists",
|
"Email already exists": "Email already exists",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "Tình trạng liên kết không thể để trống",
|
"Affiliation cannot be blank": "Tình trạng liên kết không thể để trống",
|
||||||
|
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
|
||||||
"DisplayName cannot be blank": "Tên hiển thị không thể để trống",
|
"DisplayName cannot be blank": "Tên hiển thị không thể để trống",
|
||||||
"DisplayName is not valid real name": "DisplayName không phải là tên thật hợp lệ",
|
"DisplayName is not valid real name": "DisplayName không phải là tên thật hợp lệ",
|
||||||
"Email already exists": "Email đã tồn tại",
|
"Email already exists": "Email đã tồn tại",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"Affiliation cannot be blank": "工作单位不可为空",
|
"Affiliation cannot be blank": "工作单位不可为空",
|
||||||
|
"Default code does not match the code's matching rules": "邀请码默认值和邀请码规则不匹配",
|
||||||
"DisplayName cannot be blank": "显示名称不可为空",
|
"DisplayName cannot be blank": "显示名称不可为空",
|
||||||
"DisplayName is not valid real name": "显示名称必须是真实姓名",
|
"DisplayName is not valid real name": "显示名称必须是真实姓名",
|
||||||
"Email already exists": "该邮箱已存在",
|
"Email already exists": "该邮箱已存在",
|
||||||
@ -37,9 +38,9 @@
|
|||||||
"Email is invalid": "无效邮箱",
|
"Email is invalid": "无效邮箱",
|
||||||
"Empty username.": "用户名不可为空",
|
"Empty username.": "用户名不可为空",
|
||||||
"FirstName cannot be blank": "名不可以为空",
|
"FirstName cannot be blank": "名不可以为空",
|
||||||
"Invitation code cannot be blank": "Invitation code cannot be blank",
|
"Invitation code cannot be blank": "邀请码不能为空",
|
||||||
"Invitation code exhausted": "邀请码使用次数已耗尽",
|
"Invitation code exhausted": "邀请码使用次数已耗尽",
|
||||||
"Invitation code is invalid": "Invitation code is invalid",
|
"Invitation code is invalid": "邀请码无效",
|
||||||
"Invitation code suspended": "邀请码已被禁止使用",
|
"Invitation code suspended": "邀请码已被禁止使用",
|
||||||
"LDAP user name or password incorrect": "LDAP密码错误",
|
"LDAP user name or password incorrect": "LDAP密码错误",
|
||||||
"LastName cannot be blank": "姓不可以为空",
|
"LastName cannot be blank": "姓不可以为空",
|
||||||
|
@ -121,6 +121,9 @@ func (idp *AdfsIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var respKeys struct {
|
var respKeys struct {
|
||||||
Keys []interface{} `json:"keys"`
|
Keys []interface{} `json:"keys"`
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,7 @@ import (
|
|||||||
"github.com/markbates/goth/providers/twitterv2"
|
"github.com/markbates/goth/providers/twitterv2"
|
||||||
"github.com/markbates/goth/providers/typetalk"
|
"github.com/markbates/goth/providers/typetalk"
|
||||||
"github.com/markbates/goth/providers/uber"
|
"github.com/markbates/goth/providers/uber"
|
||||||
|
"github.com/markbates/goth/providers/vk"
|
||||||
"github.com/markbates/goth/providers/wepay"
|
"github.com/markbates/goth/providers/wepay"
|
||||||
"github.com/markbates/goth/providers/xero"
|
"github.com/markbates/goth/providers/xero"
|
||||||
"github.com/markbates/goth/providers/yahoo"
|
"github.com/markbates/goth/providers/yahoo"
|
||||||
@ -371,6 +372,11 @@ func NewGothIdProvider(providerType string, clientId string, clientSecret string
|
|||||||
Provider: uber.New(clientId, clientSecret, redirectUrl),
|
Provider: uber.New(clientId, clientSecret, redirectUrl),
|
||||||
Session: &uber.Session{},
|
Session: &uber.Session{},
|
||||||
}
|
}
|
||||||
|
case "VK":
|
||||||
|
idp = GothIdProvider{
|
||||||
|
Provider: vk.New(clientId, clientSecret, redirectUrl),
|
||||||
|
Session: &vk.Session{},
|
||||||
|
}
|
||||||
case "Wepay":
|
case "Wepay":
|
||||||
idp = GothIdProvider{
|
idp = GothIdProvider{
|
||||||
Provider: wepay.New(clientId, clientSecret, redirectUrl),
|
Provider: wepay.New(clientId, clientSecret, redirectUrl),
|
||||||
|
104
idp/wechat.go
104
idp/wechat.go
@ -16,25 +16,38 @@ package idp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
WechatCacheMap map[string]WechatCacheMapValue
|
||||||
|
Lock sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
type WeChatIdProvider struct {
|
type WeChatIdProvider struct {
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
Config *oauth2.Config
|
Config *oauth2.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WechatCacheMapValue struct {
|
||||||
|
IsScanned bool
|
||||||
|
WechatUnionId string
|
||||||
|
}
|
||||||
|
|
||||||
func NewWeChatIdProvider(clientId string, clientSecret string, redirectUrl string) *WeChatIdProvider {
|
func NewWeChatIdProvider(clientId string, clientSecret string, redirectUrl string) *WeChatIdProvider {
|
||||||
idp := &WeChatIdProvider{}
|
idp := &WeChatIdProvider{}
|
||||||
|
|
||||||
@ -77,6 +90,15 @@ type WechatAccessToken struct {
|
|||||||
// GetToken use code get access_token (*operation of getting code ought to be done in front)
|
// GetToken use code get access_token (*operation of getting code ought to be done in front)
|
||||||
// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
|
// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
|
||||||
func (idp *WeChatIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
func (idp *WeChatIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||||
|
if strings.HasPrefix(code, "wechat_oa:") {
|
||||||
|
token := oauth2.Token{
|
||||||
|
AccessToken: code,
|
||||||
|
TokenType: "WeChatAccessToken",
|
||||||
|
Expiry: time.Time{},
|
||||||
|
}
|
||||||
|
return &token, nil
|
||||||
|
}
|
||||||
|
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("grant_type", "authorization_code")
|
params.Add("grant_type", "authorization_code")
|
||||||
params.Add("appid", idp.Config.ClientID)
|
params.Add("appid", idp.Config.ClientID)
|
||||||
@ -157,6 +179,29 @@ type WechatUserInfo struct {
|
|||||||
func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
func (idp *WeChatIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||||
var wechatUserInfo WechatUserInfo
|
var wechatUserInfo WechatUserInfo
|
||||||
accessToken := token.AccessToken
|
accessToken := token.AccessToken
|
||||||
|
|
||||||
|
if strings.HasPrefix(accessToken, "wechat_oa:") {
|
||||||
|
Lock.RLock()
|
||||||
|
mapValue, ok := WechatCacheMap[accessToken[10:]]
|
||||||
|
Lock.RUnlock()
|
||||||
|
|
||||||
|
if !ok || mapValue.WechatUnionId == "" {
|
||||||
|
return nil, fmt.Errorf("error ticket")
|
||||||
|
}
|
||||||
|
|
||||||
|
Lock.Lock()
|
||||||
|
delete(WechatCacheMap, accessToken[10:])
|
||||||
|
Lock.Unlock()
|
||||||
|
|
||||||
|
userInfo := UserInfo{
|
||||||
|
Id: mapValue.WechatUnionId,
|
||||||
|
Username: "wx_user_" + mapValue.WechatUnionId,
|
||||||
|
DisplayName: "wx_user_" + mapValue.WechatUnionId,
|
||||||
|
AvatarUrl: "",
|
||||||
|
}
|
||||||
|
return &userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
openid := token.Extra("Openid")
|
openid := token.Extra("Openid")
|
||||||
|
|
||||||
userInfoUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s", accessToken, openid)
|
userInfoUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s", accessToken, openid)
|
||||||
@ -204,60 +249,70 @@ func BuildWechatOpenIdKey(appId string) string {
|
|||||||
return fmt.Sprintf("wechat_openid_%s", appId)
|
return fmt.Sprintf("wechat_openid_%s", appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, error) {
|
func GetWechatOfficialAccountAccessToken(clientId string, clientSecret string) (string, string, error) {
|
||||||
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
|
accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", clientId, clientSecret)
|
||||||
request, err := http.NewRequest("GET", accessTokenUrl, nil)
|
request, err := http.NewRequest("GET", accessTokenUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := new(http.Client)
|
client := new(http.Client)
|
||||||
resp, err := client.Do(request)
|
resp, err := client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
respBytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
ExpireIn int `json:"expires_in"`
|
ExpireIn int `json:"expires_in"`
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
|
ErrCode int `json:"errcode"`
|
||||||
|
Errmsg string `json:"errmsg"`
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(respBytes, &data)
|
err = json.Unmarshal(respBytes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.AccessToken, nil
|
return data.AccessToken, data.Errmsg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWechatOfficialAccountQRCode(clientId string, clientSecret string) (string, error) {
|
func GetWechatOfficialAccountQRCode(clientId string, clientSecret string, providerId string) (string, string, error) {
|
||||||
accessToken, err := GetWechatOfficialAccountAccessToken(clientId, clientSecret)
|
accessToken, errMsg, err := GetWechatOfficialAccountAccessToken(clientId, clientSecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errMsg != "" {
|
||||||
|
return "", "", fmt.Errorf("Fail to fetch WeChat QRcode: %s", errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
client := new(http.Client)
|
client := new(http.Client)
|
||||||
|
|
||||||
weChatEndpoint := "https://api.weixin.qq.com/cgi-bin/qrcode/create"
|
weChatEndpoint := "https://api.weixin.qq.com/cgi-bin/qrcode/create"
|
||||||
qrCodeUrl := fmt.Sprintf("%s?access_token=%s", weChatEndpoint, accessToken)
|
qrCodeUrl := fmt.Sprintf("%s?access_token=%s", weChatEndpoint, accessToken)
|
||||||
params := `{"action_name": "QR_LIMIT_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}`
|
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)
|
requeset, 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(requeset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
respBytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
var data struct {
|
var data struct {
|
||||||
Ticket string `json:"ticket"`
|
Ticket string `json:"ticket"`
|
||||||
@ -266,11 +321,26 @@ func GetWechatOfficialAccountQRCode(clientId string, clientSecret string) (strin
|
|||||||
}
|
}
|
||||||
err = json.Unmarshal(respBytes, &data)
|
err = json.Unmarshal(respBytes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var png []byte
|
var png []byte
|
||||||
png, err = qrcode.Encode(data.URL, qrcode.Medium, 256)
|
png, err = qrcode.Encode(data.URL, qrcode.Medium, 256)
|
||||||
base64Image := base64.StdEncoding.EncodeToString(png)
|
base64Image := base64.StdEncoding.EncodeToString(png)
|
||||||
return base64Image, nil
|
return base64Image, data.Ticket, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyWechatSignature(token string, nonce string, timestamp string, signature string) bool {
|
||||||
|
// verify the signature
|
||||||
|
tmpArr := sort.StringSlice{token, timestamp, nonce}
|
||||||
|
sort.Sort(tmpArr)
|
||||||
|
|
||||||
|
tmpStr := ""
|
||||||
|
for _, str := range tmpArr {
|
||||||
|
tmpStr = tmpStr + str
|
||||||
|
}
|
||||||
|
|
||||||
|
b := sha1.Sum([]byte(tmpStr))
|
||||||
|
res := hex.EncodeToString(b[:])
|
||||||
|
return res == signature
|
||||||
}
|
}
|
||||||
|
@ -120,6 +120,7 @@
|
|||||||
"rule": "None"
|
"rule": "None"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grantTypes": ["authorization_code", "password", "client_credentials", "token", "id_token", "refresh_token"],
|
||||||
"redirectUris": [""],
|
"redirectUris": [""],
|
||||||
"expireInHours": 168,
|
"expireInHours": 168,
|
||||||
"failedSigninLimit": 5,
|
"failedSigninLimit": 5,
|
||||||
@ -146,7 +147,8 @@
|
|||||||
"isForbidden": false,
|
"isForbidden": false,
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"signupApplication": "",
|
"signupApplication": "",
|
||||||
"createdIp": ""
|
"createdIp": "",
|
||||||
|
"groups": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"providers": [
|
"providers": [
|
||||||
@ -349,5 +351,74 @@
|
|||||||
"owner": "",
|
"owner": "",
|
||||||
"url": ""
|
"url": ""
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"owner": "",
|
||||||
|
"name":"",
|
||||||
|
"displayName": "",
|
||||||
|
"manager": "",
|
||||||
|
"contactEmail": "",
|
||||||
|
"type": "",
|
||||||
|
"parent_id": "",
|
||||||
|
"isTopGroup": true,
|
||||||
|
"title": "",
|
||||||
|
"key": "",
|
||||||
|
"children": "",
|
||||||
|
"isEnabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"adapters": [
|
||||||
|
{
|
||||||
|
"owner": "",
|
||||||
|
"name": "",
|
||||||
|
"table": "",
|
||||||
|
"useSameDb": true,
|
||||||
|
"type": "",
|
||||||
|
"databaseType": "",
|
||||||
|
"database": "",
|
||||||
|
"host": "",
|
||||||
|
"port": 0,
|
||||||
|
"user": "",
|
||||||
|
"password": "",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enforcers": [
|
||||||
|
{
|
||||||
|
"owner": "",
|
||||||
|
"name": "",
|
||||||
|
"displayName": "",
|
||||||
|
"description": "",
|
||||||
|
"model": "",
|
||||||
|
"adapter": "",
|
||||||
|
"enforcer": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plans": [
|
||||||
|
{
|
||||||
|
"owner": "",
|
||||||
|
"name": "",
|
||||||
|
"displayName": "",
|
||||||
|
"description": "",
|
||||||
|
"price": 0,
|
||||||
|
"currency": "",
|
||||||
|
"period": "",
|
||||||
|
"product": "",
|
||||||
|
"paymentProviders": [],
|
||||||
|
"isEnabled": true,
|
||||||
|
"role", ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pricings": [
|
||||||
|
{
|
||||||
|
"owner": "",
|
||||||
|
"name": "",
|
||||||
|
"displayName": "",
|
||||||
|
"description": "",
|
||||||
|
"plans": [],
|
||||||
|
"isEnabled": true,
|
||||||
|
"trialDuration": 0,
|
||||||
|
"application": "",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
2
main.go
2
main.go
@ -36,12 +36,12 @@ func main() {
|
|||||||
object.CreateTables()
|
object.CreateTables()
|
||||||
|
|
||||||
object.InitDb()
|
object.InitDb()
|
||||||
object.InitFromFile()
|
|
||||||
object.InitDefaultStorageProvider()
|
object.InitDefaultStorageProvider()
|
||||||
object.InitLdapAutoSynchronizer()
|
object.InitLdapAutoSynchronizer()
|
||||||
proxy.InitHttpClient()
|
proxy.InitHttpClient()
|
||||||
authz.InitApi()
|
authz.InitApi()
|
||||||
object.InitUserManager()
|
object.InitUserManager()
|
||||||
|
object.InitFromFile()
|
||||||
object.InitCasvisorConfig()
|
object.InitCasvisorConfig()
|
||||||
|
|
||||||
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
|
||||||
|
@ -37,7 +37,7 @@ type Adapter struct {
|
|||||||
Host string `xorm:"varchar(100)" json:"host"`
|
Host string `xorm:"varchar(100)" json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
User string `xorm:"varchar(100)" json:"user"`
|
User string `xorm:"varchar(100)" json:"user"`
|
||||||
Password string `xorm:"varchar(100)" json:"password"`
|
Password string `xorm:"varchar(150)" json:"password"`
|
||||||
Database string `xorm:"varchar(100)" json:"database"`
|
Database string `xorm:"varchar(100)" json:"database"`
|
||||||
|
|
||||||
*xormadapter.Adapter `xorm:"-" json:"-"`
|
*xormadapter.Adapter `xorm:"-" json:"-"`
|
||||||
@ -178,6 +178,7 @@ func (adapter *Adapter) InitAdapter() error {
|
|||||||
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
|
dataSourceName = strings.ReplaceAll(dataSourceName, "dbi.", "db.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataSourceName = conf.ReplaceDataSourceNameByDocker(dataSourceName)
|
||||||
engine, err := xorm.NewEngine(driverName, dataSourceName)
|
engine, err := xorm.NewEngine(driverName, dataSourceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/idp"
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/xorm-io/core"
|
"github.com/xorm-io/core"
|
||||||
)
|
)
|
||||||
@ -41,6 +40,15 @@ type SignupItem struct {
|
|||||||
Rule string `json:"rule"`
|
Rule string `json:"rule"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SigninItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Visible bool `json:"visible"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Placeholder string `json:"placeholder"`
|
||||||
|
Rule string `json:"rule"`
|
||||||
|
IsCustom bool `json:"isCustom"`
|
||||||
|
}
|
||||||
|
|
||||||
type SamlItem struct {
|
type SamlItem struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
NameFormat string `json:"nameFormat"`
|
NameFormat string `json:"nameFormat"`
|
||||||
@ -52,31 +60,33 @@ type Application struct {
|
|||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||||
|
|
||||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||||
Logo string `xorm:"varchar(200)" json:"logo"`
|
Logo string `xorm:"varchar(200)" json:"logo"`
|
||||||
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
|
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
|
||||||
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"`
|
||||||
EnablePassword bool `json:"enablePassword"`
|
EnablePassword bool `json:"enablePassword"`
|
||||||
EnableSignUp bool `json:"enableSignUp"`
|
EnableSignUp bool `json:"enableSignUp"`
|
||||||
EnableSigninSession bool `json:"enableSigninSession"`
|
EnableSigninSession bool `json:"enableSigninSession"`
|
||||||
EnableAutoSignin bool `json:"enableAutoSignin"`
|
EnableAutoSignin bool `json:"enableAutoSignin"`
|
||||||
EnableCodeSignin bool `json:"enableCodeSignin"`
|
EnableCodeSignin bool `json:"enableCodeSignin"`
|
||||||
EnableSamlCompress bool `json:"enableSamlCompress"`
|
EnableSamlCompress bool `json:"enableSamlCompress"`
|
||||||
EnableSamlC14n10 bool `json:"enableSamlC14n10"`
|
EnableSamlC14n10 bool `json:"enableSamlC14n10"`
|
||||||
EnableWebAuthn bool `json:"enableWebAuthn"`
|
EnableSamlPostBinding bool `json:"enableSamlPostBinding"`
|
||||||
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
|
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||||
OrgChoiceMode string `json:"orgChoiceMode"`
|
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
|
||||||
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
|
OrgChoiceMode string `json:"orgChoiceMode"`
|
||||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
|
||||||
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
|
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||||
SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"`
|
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
|
||||||
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"`
|
||||||
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
|
SigninItems []*SigninItem `xorm:"mediumtext" json:"signinItems"`
|
||||||
CertPublicKey string `xorm:"-" json:"certPublicKey"`
|
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
||||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
|
||||||
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
|
CertPublicKey string `xorm:"-" json:"certPublicKey"`
|
||||||
|
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||||
|
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
|
||||||
|
|
||||||
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"`
|
||||||
@ -163,15 +173,6 @@ func getProviderMap(owner string) (m map[string]*Provider, err error) {
|
|||||||
|
|
||||||
m = map[string]*Provider{}
|
m = map[string]*Provider{}
|
||||||
for _, provider := range providers {
|
for _, provider := range providers {
|
||||||
// Get QRCode only once
|
|
||||||
if provider.Type == "WeChat" && provider.DisableSsl && provider.Content == "" {
|
|
||||||
provider.Content, err = idp.GetWechatOfficialAccountQRCode(provider.ClientId2, provider.ClientSecret2)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
UpdateProvider(provider.Owner+"/"+provider.Name, provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
m[provider.Name] = GetMaskedProvider(provider, true)
|
m[provider.Name] = GetMaskedProvider(provider, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,6 +200,100 @@ func extendApplicationWithOrg(application *Application) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extendApplicationWithSigninItems(application *Application) (err error) {
|
||||||
|
if len(application.SigninItems) == 0 {
|
||||||
|
signinItem := &SigninItem{
|
||||||
|
Name: "Back button",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .back-button {\n top: 65px;\n left: 15px;\n position: absolute;\n }\n</style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Languages",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-languages {\n top: 55px;\n right: 5px;\n position: absolute;\n }\n</style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Logo",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-logo-box {\n }\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Signin methods",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .signin-methods {\n }\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Username",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-username {\n }\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Password",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-password {\n }\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Agreement",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-agreement {\n }\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Forgot password?",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-forget-password {\n display: inline-flex;\n justify-content: space-between;\n width: 320px;\n margin-bottom: 25px;\n }\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Login button",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-button-box {\n margin-bottom: 5px;\n }\n .login-button {\n width: 100%;\n }\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Signup link",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .login-signup-link {\n margin-bottom: 24px;\n display: flex;\n justify-content: end;\n}\n<style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
signinItem = &SigninItem{
|
||||||
|
Name: "Providers",
|
||||||
|
Visible: true,
|
||||||
|
Label: "\n<style>\n .provider-img {\n width: 30px;\n margin: 5px;\n }\n .provider-big-img {\n margin-bottom: 10px;\n }\n</style>\n",
|
||||||
|
Placeholder: "",
|
||||||
|
Rule: "None",
|
||||||
|
}
|
||||||
|
application.SigninItems = append(application.SigninItems, signinItem)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func extendApplicationWithSigninMethods(application *Application) (err error) {
|
func extendApplicationWithSigninMethods(application *Application) (err error) {
|
||||||
if len(application.SigninMethods) == 0 {
|
if len(application.SigninMethods) == 0 {
|
||||||
if application.EnablePassword {
|
if application.EnablePassword {
|
||||||
@ -249,6 +344,10 @@ func getApplication(owner string, name string) (*Application, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = extendApplicationWithSigninItems(&application)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &application, nil
|
return &application, nil
|
||||||
} else {
|
} else {
|
||||||
@ -279,6 +378,11 @@ func GetApplicationByOrganizationName(organization string) (*Application, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = extendApplicationWithSigninItems(&application)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &application, nil
|
return &application, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -331,6 +435,11 @@ func GetApplicationByClientId(clientId string) (*Application, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = extendApplicationWithSigninItems(&application)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &application, nil
|
return &application, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
BIN
object/cert.go~
BIN
object/cert.go~
Binary file not shown.
@ -184,6 +184,15 @@ func CheckInvitationCode(application *Application, organization *Organization, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckInvitationDefaultCode(code string, defaultCode string, lang string) error {
|
||||||
|
if matched, err := util.IsInvitationCodeMatch(code, defaultCode); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !matched {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Default code does not match the code's matching rules"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkSigninErrorTimes(user *User, lang string) error {
|
func checkSigninErrorTimes(user *User, lang string) error {
|
||||||
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
|
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -35,6 +35,11 @@ type InitData struct {
|
|||||||
Syncers []*Syncer `json:"syncers"`
|
Syncers []*Syncer `json:"syncers"`
|
||||||
Tokens []*Token `json:"tokens"`
|
Tokens []*Token `json:"tokens"`
|
||||||
Webhooks []*Webhook `json:"webhooks"`
|
Webhooks []*Webhook `json:"webhooks"`
|
||||||
|
Groups []*Group `json:"groups"`
|
||||||
|
Adapters []*Adapter `json:"adapters"`
|
||||||
|
Enforcers []*Enforcer `json:"enforcers"`
|
||||||
|
Plans []*Plan `json:"plans"`
|
||||||
|
Pricings []*Pricing `json:"pricings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitFromFile() {
|
func InitFromFile() {
|
||||||
@ -94,6 +99,21 @@ func InitFromFile() {
|
|||||||
for _, webhook := range initData.Webhooks {
|
for _, webhook := range initData.Webhooks {
|
||||||
initDefinedWebhook(webhook)
|
initDefinedWebhook(webhook)
|
||||||
}
|
}
|
||||||
|
for _, group := range initData.Groups {
|
||||||
|
initDefinedGroup(group)
|
||||||
|
}
|
||||||
|
for _, adapter := range initData.Adapters {
|
||||||
|
initDefinedAdapter(adapter)
|
||||||
|
}
|
||||||
|
for _, enforcer := range initData.Enforcers {
|
||||||
|
initDefinedEnforcer(enforcer)
|
||||||
|
}
|
||||||
|
for _, plan := range initData.Plans {
|
||||||
|
initDefinedPlan(plan)
|
||||||
|
}
|
||||||
|
for _, pricing := range initData.Pricings {
|
||||||
|
initDefinedPricing(pricing)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +140,11 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
|
|||||||
Syncers: []*Syncer{},
|
Syncers: []*Syncer{},
|
||||||
Tokens: []*Token{},
|
Tokens: []*Token{},
|
||||||
Webhooks: []*Webhook{},
|
Webhooks: []*Webhook{},
|
||||||
|
Groups: []*Group{},
|
||||||
|
Adapters: []*Adapter{},
|
||||||
|
Enforcers: []*Enforcer{},
|
||||||
|
Plans: []*Plan{},
|
||||||
|
Pricings: []*Pricing{},
|
||||||
}
|
}
|
||||||
err := util.JsonToStruct(s, data)
|
err := util.JsonToStruct(s, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -190,7 +215,16 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
|
|||||||
webhook.Headers = []*Header{}
|
webhook.Headers = []*Header{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, plan := range data.Plans {
|
||||||
|
if plan.PaymentProviders == nil {
|
||||||
|
plan.PaymentProviders = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pricing := range data.Pricings {
|
||||||
|
if pricing.Plans == nil {
|
||||||
|
pricing.Plans = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,3 +468,78 @@ func initDefinedWebhook(webhook *Webhook) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initDefinedGroup(group *Group) {
|
||||||
|
existed, err := getGroup(group.Owner, group.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if existed != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
group.CreatedTime = util.GetCurrentTime()
|
||||||
|
_, err = AddGroup(group)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDefinedAdapter(adapter *Adapter) {
|
||||||
|
existed, err := getAdapter(adapter.Owner, adapter.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if existed != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adapter.CreatedTime = util.GetCurrentTime()
|
||||||
|
_, err = AddAdapter(adapter)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDefinedEnforcer(enforcer *Enforcer) {
|
||||||
|
existed, err := getEnforcer(enforcer.Owner, enforcer.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if existed != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enforcer.CreatedTime = util.GetCurrentTime()
|
||||||
|
_, err = AddEnforcer(enforcer)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDefinedPlan(plan *Plan) {
|
||||||
|
existed, err := getPlan(plan.Owner, plan.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if existed != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plan.CreatedTime = util.GetCurrentTime()
|
||||||
|
_, err = AddPlan(plan)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDefinedPricing(pricing *Pricing) {
|
||||||
|
existed, err := getPlan(pricing.Owner, pricing.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if existed != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pricing.CreatedTime = util.GetCurrentTime()
|
||||||
|
_, err = AddPricing(pricing)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -96,6 +96,31 @@ func writeInitDataToFile(filePath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groups, err := GetGroups("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
adapters, err := GetAdapters("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
enforcers, err := GetEnforcers("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plans, err := GetPlans("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pricings, err := GetPricings("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
data := &InitData{
|
data := &InitData{
|
||||||
Organizations: organizations,
|
Organizations: organizations,
|
||||||
Applications: applications,
|
Applications: applications,
|
||||||
@ -112,6 +137,11 @@ func writeInitDataToFile(filePath string) error {
|
|||||||
Syncers: syncers,
|
Syncers: syncers,
|
||||||
Tokens: tokens,
|
Tokens: tokens,
|
||||||
Webhooks: webhooks,
|
Webhooks: webhooks,
|
||||||
|
Groups: groups,
|
||||||
|
Adapters: adapters,
|
||||||
|
Enforcers: enforcers,
|
||||||
|
Plans: plans,
|
||||||
|
Pricings: pricings,
|
||||||
}
|
}
|
||||||
|
|
||||||
text := util.StructToJsonFormatted(data)
|
text := util.StructToJsonFormatted(data)
|
||||||
|
@ -40,6 +40,7 @@ type Invitation struct {
|
|||||||
Phone string `xorm:"varchar(100)" json:"phone"`
|
Phone string `xorm:"varchar(100)" json:"phone"`
|
||||||
|
|
||||||
SignupGroup string `xorm:"varchar(100)" json:"signupGroup"`
|
SignupGroup string `xorm:"varchar(100)" json:"signupGroup"`
|
||||||
|
DefaultCode string `xorm:"varchar(100)" json:"defaultCode"`
|
||||||
|
|
||||||
State string `xorm:"varchar(100)" json:"state"`
|
State string `xorm:"varchar(100)" json:"state"`
|
||||||
}
|
}
|
||||||
@ -93,7 +94,45 @@ func GetInvitation(id string) (*Invitation, error) {
|
|||||||
return getInvitation(owner, name)
|
return getInvitation(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
|
func GetInvitationByCode(code string, organizationName string, lang string) (*Invitation, string) {
|
||||||
|
invitations, err := GetInvitations(organizationName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err.Error()
|
||||||
|
}
|
||||||
|
errMsg := ""
|
||||||
|
for _, invitation := range invitations {
|
||||||
|
if isValid, msg := invitation.SimpleCheckInvitationCode(code, lang); isValid {
|
||||||
|
return invitation, msg
|
||||||
|
} else if msg != "" && errMsg == "" {
|
||||||
|
errMsg = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errMsg != "" {
|
||||||
|
return nil, errMsg
|
||||||
|
} else {
|
||||||
|
return nil, i18n.Translate(lang, "check:Invitation code is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMaskedInvitation(invitation *Invitation) *Invitation {
|
||||||
|
if invitation == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
invitation.CreatedTime = ""
|
||||||
|
invitation.UpdatedTime = ""
|
||||||
|
invitation.Code = "***"
|
||||||
|
invitation.DefaultCode = "***"
|
||||||
|
invitation.IsRegexp = false
|
||||||
|
invitation.Quota = -1
|
||||||
|
invitation.UsedCount = -1
|
||||||
|
invitation.SignupGroup = ""
|
||||||
|
|
||||||
|
return invitation
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateInvitation(id string, invitation *Invitation, lang string) (bool, error) {
|
||||||
owner, name := util.GetOwnerAndNameFromId(id)
|
owner, name := util.GetOwnerAndNameFromId(id)
|
||||||
if p, err := getInvitation(owner, name); err != nil {
|
if p, err := getInvitation(owner, name); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -107,6 +146,11 @@ func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
|
|||||||
invitation.IsRegexp = isRegexp
|
invitation.IsRegexp = isRegexp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := CheckInvitationDefaultCode(invitation.Code, invitation.DefaultCode, lang)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(invitation)
|
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(invitation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -115,13 +159,18 @@ func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
|
|||||||
return affected != 0, nil
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddInvitation(invitation *Invitation) (bool, error) {
|
func AddInvitation(invitation *Invitation, lang string) (bool, error) {
|
||||||
if isRegexp, err := util.IsRegexp(invitation.Code); err != nil {
|
if isRegexp, err := util.IsRegexp(invitation.Code); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else {
|
} else {
|
||||||
invitation.IsRegexp = isRegexp
|
invitation.IsRegexp = isRegexp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := CheckInvitationDefaultCode(invitation.Code, invitation.DefaultCode, lang)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.Insert(invitation)
|
affected, err := ormer.Engine.Insert(invitation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -147,7 +196,7 @@ func VerifyInvitation(id string) (payment *Payment, attachInfo map[string]interf
|
|||||||
return nil, nil, fmt.Errorf("the invitation: %s does not exist", id)
|
return nil, nil, fmt.Errorf("the invitation: %s does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (invitation *Invitation) IsInvitationCodeValid(application *Application, invitationCode string, username string, email string, phone string, lang string) (bool, string) {
|
func (invitation *Invitation) SimpleCheckInvitationCode(invitationCode string, lang string) (bool, string) {
|
||||||
if matched, err := util.IsInvitationCodeMatch(invitation.Code, invitationCode); err != nil {
|
if matched, err := util.IsInvitationCodeMatch(invitation.Code, invitationCode); err != nil {
|
||||||
return false, err.Error()
|
return false, err.Error()
|
||||||
} else if !matched {
|
} else if !matched {
|
||||||
@ -160,15 +209,6 @@ func (invitation *Invitation) IsInvitationCodeValid(application *Application, in
|
|||||||
if invitation.UsedCount >= invitation.Quota {
|
if invitation.UsedCount >= invitation.Quota {
|
||||||
return false, i18n.Translate(lang, "check:Invitation code exhausted")
|
return false, i18n.Translate(lang, "check:Invitation code exhausted")
|
||||||
}
|
}
|
||||||
if application.IsSignupItemRequired("Username") && invitation.Username != "" && invitation.Username != username {
|
|
||||||
return false, i18n.Translate(lang, "check:Please register using the username corresponding to the invitation code")
|
|
||||||
}
|
|
||||||
if application.IsSignupItemRequired("Email") && invitation.Email != "" && invitation.Email != email {
|
|
||||||
return false, i18n.Translate(lang, "check:Please register using the email corresponding to the invitation code")
|
|
||||||
}
|
|
||||||
if application.IsSignupItemRequired("Phone") && invitation.Phone != "" && invitation.Phone != phone {
|
|
||||||
return false, i18n.Translate(lang, "check:Please register using the phone corresponding to the invitation code")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine whether the invitation code is in the form of a regular expression other than pure numbers and letters
|
// Determine whether the invitation code is in the form of a regular expression other than pure numbers and letters
|
||||||
if invitation.IsRegexp {
|
if invitation.IsRegexp {
|
||||||
@ -179,3 +219,19 @@ func (invitation *Invitation) IsInvitationCodeValid(application *Application, in
|
|||||||
}
|
}
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (invitation *Invitation) IsInvitationCodeValid(application *Application, invitationCode string, username string, email string, phone string, lang string) (bool, string) {
|
||||||
|
if isValid, msg := invitation.SimpleCheckInvitationCode(invitationCode, lang); !isValid {
|
||||||
|
return false, msg
|
||||||
|
}
|
||||||
|
if application.IsSignupItemRequired("Username") && invitation.Username != "" && invitation.Username != username {
|
||||||
|
return false, i18n.Translate(lang, "check:Please register using the username corresponding to the invitation code")
|
||||||
|
}
|
||||||
|
if application.IsSignupItemRequired("Email") && invitation.Email != "" && invitation.Email != email {
|
||||||
|
return false, i18n.Translate(lang, "check:Please register using the email corresponding to the invitation code")
|
||||||
|
}
|
||||||
|
if application.IsSignupItemRequired("Phone") && invitation.Phone != "" && invitation.Phone != phone {
|
||||||
|
return false, i18n.Translate(lang, "check:Please register using the phone corresponding to the invitation code")
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
@ -18,12 +18,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
|
|
||||||
"github.com/beego/beego/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const MfaRecoveryCodesSession = "mfa_recovery_codes"
|
|
||||||
|
|
||||||
type MfaProps struct {
|
type MfaProps struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
IsPreferred bool `json:"isPreferred"`
|
IsPreferred bool `json:"isPreferred"`
|
||||||
@ -35,9 +31,9 @@ type MfaProps struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MfaInterface interface {
|
type MfaInterface interface {
|
||||||
Initiate(ctx *context.Context, userId string) (*MfaProps, error)
|
Initiate(userId string) (*MfaProps, error)
|
||||||
SetupVerify(ctx *context.Context, passcode string) error
|
SetupVerify(passcode string) error
|
||||||
Enable(ctx *context.Context, user *User) error
|
Enable(user *User) error
|
||||||
Verify(passcode string) error
|
Verify(passcode string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,90 +16,46 @@ package object
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/beego/beego/context"
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MfaCountryCodeSession = "mfa_country_code"
|
|
||||||
MfaDestSession = "mfa_dest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmsMfa struct {
|
type SmsMfa struct {
|
||||||
Config *MfaProps
|
*MfaProps
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *SmsMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
|
func (mfa *SmsMfa) Initiate(userId string) (*MfaProps, error) {
|
||||||
recoveryCode := uuid.NewString()
|
|
||||||
|
|
||||||
err := ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mfaProps := MfaProps{
|
mfaProps := MfaProps{
|
||||||
MfaType: mfa.Config.MfaType,
|
MfaType: mfa.MfaType,
|
||||||
RecoveryCodes: []string{recoveryCode},
|
|
||||||
}
|
}
|
||||||
return &mfaProps, nil
|
return &mfaProps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
|
func (mfa *SmsMfa) SetupVerify(passCode string) error {
|
||||||
destSession := ctx.Input.CruSession.Get(MfaDestSession)
|
if !util.IsEmailValid(mfa.Secret) {
|
||||||
if destSession == nil {
|
mfa.Secret, _ = util.GetE164Number(mfa.Secret, mfa.CountryCode)
|
||||||
return errors.New("dest session is missing")
|
|
||||||
}
|
|
||||||
dest := destSession.(string)
|
|
||||||
|
|
||||||
if !util.IsEmailValid(dest) {
|
|
||||||
countryCodeSession := ctx.Input.CruSession.Get(MfaCountryCodeSession)
|
|
||||||
if countryCodeSession == nil {
|
|
||||||
return errors.New("country code is missing")
|
|
||||||
}
|
|
||||||
countryCode := countryCodeSession.(string)
|
|
||||||
|
|
||||||
dest, _ = util.GetE164Number(dest, countryCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result := CheckVerificationCode(dest, passCode, "en"); result.Code != VerificationSuccess {
|
if result := CheckVerificationCode(mfa.Secret, passCode, "en"); result.Code != VerificationSuccess {
|
||||||
return errors.New(result.Msg)
|
return errors.New(result.Msg)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
|
func (mfa *SmsMfa) Enable(user *User) error {
|
||||||
recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
|
|
||||||
if len(recoveryCodes) == 0 {
|
|
||||||
return fmt.Errorf("recovery codes is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
columns := []string{"recovery_codes", "preferred_mfa_type"}
|
columns := []string{"recovery_codes", "preferred_mfa_type"}
|
||||||
|
|
||||||
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...)
|
user.RecoveryCodes = append(user.RecoveryCodes, mfa.RecoveryCodes...)
|
||||||
if user.PreferredMfaType == "" {
|
if user.PreferredMfaType == "" {
|
||||||
user.PreferredMfaType = mfa.Config.MfaType
|
user.PreferredMfaType = mfa.MfaType
|
||||||
}
|
}
|
||||||
|
|
||||||
if mfa.Config.MfaType == SmsType {
|
if mfa.MfaType == SmsType {
|
||||||
user.MfaPhoneEnabled = true
|
user.MfaPhoneEnabled = true
|
||||||
columns = append(columns, "mfa_phone_enabled")
|
columns = append(columns, "mfa_phone_enabled", "phone", "country_code")
|
||||||
|
} else if mfa.MfaType == EmailType {
|
||||||
if user.Phone == "" {
|
|
||||||
user.Phone = ctx.Input.CruSession.Get(MfaDestSession).(string)
|
|
||||||
user.CountryCode = ctx.Input.CruSession.Get(MfaCountryCodeSession).(string)
|
|
||||||
columns = append(columns, "phone", "country_code")
|
|
||||||
}
|
|
||||||
} else if mfa.Config.MfaType == EmailType {
|
|
||||||
user.MfaEmailEnabled = true
|
user.MfaEmailEnabled = true
|
||||||
columns = append(columns, "mfa_email_enabled")
|
columns = append(columns, "mfa_email_enabled", "email")
|
||||||
|
|
||||||
if user.Email == "" {
|
|
||||||
user.Email = ctx.Input.CruSession.Get(MfaDestSession).(string)
|
|
||||||
columns = append(columns, "email")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := UpdateUser(user.GetId(), user, columns, false)
|
_, err := UpdateUser(user.GetId(), user, columns, false)
|
||||||
@ -107,18 +63,14 @@ func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
|
|
||||||
ctx.Input.CruSession.Delete(MfaDestSession)
|
|
||||||
ctx.Input.CruSession.Delete(MfaCountryCodeSession)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *SmsMfa) Verify(passCode string) error {
|
func (mfa *SmsMfa) Verify(passCode string) error {
|
||||||
if !util.IsEmailValid(mfa.Config.Secret) {
|
if !util.IsEmailValid(mfa.Secret) {
|
||||||
mfa.Config.Secret, _ = util.GetE164Number(mfa.Config.Secret, mfa.Config.CountryCode)
|
mfa.Secret, _ = util.GetE164Number(mfa.Secret, mfa.CountryCode)
|
||||||
}
|
}
|
||||||
if result := CheckVerificationCode(mfa.Config.Secret, passCode, "en"); result.Code != VerificationSuccess {
|
if result := CheckVerificationCode(mfa.Secret, passCode, "en"); result.Code != VerificationSuccess {
|
||||||
return errors.New(result.Msg)
|
return errors.New(result.Msg)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -131,7 +83,7 @@ func NewSmsMfaUtil(config *MfaProps) *SmsMfa {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &SmsMfa{
|
return &SmsMfa{
|
||||||
Config: config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +94,6 @@ func NewEmailMfaUtil(config *MfaProps) *SmsMfa {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &SmsMfa{
|
return &SmsMfa{
|
||||||
Config: config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,28 +16,24 @@ package object
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beego/beego/context"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/pquerna/otp"
|
"github.com/pquerna/otp"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MfaTotpSecretSession = "mfa_totp_secret"
|
|
||||||
MfaTotpPeriodInSeconds = 30
|
MfaTotpPeriodInSeconds = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
type TotpMfa struct {
|
type TotpMfa struct {
|
||||||
Config *MfaProps
|
*MfaProps
|
||||||
period uint
|
period uint
|
||||||
secretSize uint
|
secretSize uint
|
||||||
digits otp.Digits
|
digits otp.Digits
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
|
func (mfa *TotpMfa) Initiate(userId string) (*MfaProps, error) {
|
||||||
//issuer := beego.AppConfig.String("appname")
|
//issuer := beego.AppConfig.String("appname")
|
||||||
//if issuer == "" {
|
//if issuer == "" {
|
||||||
// issuer = "casdoor"
|
// issuer = "casdoor"
|
||||||
@ -55,33 +51,16 @@ func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ctx.Input.CruSession.Set(MfaTotpSecretSession, key.Secret())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
recoveryCode := uuid.NewString()
|
|
||||||
err = ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mfaProps := MfaProps{
|
mfaProps := MfaProps{
|
||||||
MfaType: mfa.Config.MfaType,
|
MfaType: mfa.MfaType,
|
||||||
RecoveryCodes: []string{recoveryCode},
|
Secret: key.Secret(),
|
||||||
Secret: key.Secret(),
|
URL: key.URL(),
|
||||||
URL: key.URL(),
|
|
||||||
}
|
}
|
||||||
return &mfaProps, nil
|
return &mfaProps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
|
func (mfa *TotpMfa) SetupVerify(passcode string) error {
|
||||||
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession)
|
result, err := totp.ValidateCustom(passcode, mfa.Secret, time.Now().UTC(), totp.ValidateOpts{
|
||||||
if secret == nil {
|
|
||||||
return errors.New("totp secret is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := totp.ValidateCustom(passcode, secret.(string), time.Now().UTC(), totp.ValidateOpts{
|
|
||||||
Period: MfaTotpPeriodInSeconds,
|
Period: MfaTotpPeriodInSeconds,
|
||||||
Skew: 1,
|
Skew: 1,
|
||||||
Digits: otp.DigitsSix,
|
Digits: otp.DigitsSix,
|
||||||
@ -98,22 +77,13 @@ func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
|
func (mfa *TotpMfa) Enable(user *User) error {
|
||||||
recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
|
|
||||||
if len(recoveryCodes) == 0 {
|
|
||||||
return fmt.Errorf("recovery codes is missing")
|
|
||||||
}
|
|
||||||
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string)
|
|
||||||
if secret == "" {
|
|
||||||
return fmt.Errorf("totp secret is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
columns := []string{"recovery_codes", "preferred_mfa_type", "totp_secret"}
|
columns := []string{"recovery_codes", "preferred_mfa_type", "totp_secret"}
|
||||||
|
|
||||||
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...)
|
user.RecoveryCodes = append(user.RecoveryCodes, mfa.RecoveryCodes...)
|
||||||
user.TotpSecret = secret
|
user.TotpSecret = mfa.Secret
|
||||||
if user.PreferredMfaType == "" {
|
if user.PreferredMfaType == "" {
|
||||||
user.PreferredMfaType = mfa.Config.MfaType
|
user.PreferredMfaType = mfa.MfaType
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := updateUser(user.GetId(), user, columns)
|
_, err := updateUser(user.GetId(), user, columns)
|
||||||
@ -121,14 +91,11 @@ func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Input.CruSession.Delete(MfaRecoveryCodesSession)
|
|
||||||
ctx.Input.CruSession.Delete(MfaTotpSecretSession)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mfa *TotpMfa) Verify(passcode string) error {
|
func (mfa *TotpMfa) Verify(passcode string) error {
|
||||||
result, err := totp.ValidateCustom(passcode, mfa.Config.Secret, time.Now().UTC(), totp.ValidateOpts{
|
result, err := totp.ValidateCustom(passcode, mfa.Secret, time.Now().UTC(), totp.ValidateOpts{
|
||||||
Period: MfaTotpPeriodInSeconds,
|
Period: MfaTotpPeriodInSeconds,
|
||||||
Skew: 1,
|
Skew: 1,
|
||||||
Digits: otp.DigitsSix,
|
Digits: otp.DigitsSix,
|
||||||
@ -153,7 +120,7 @@ func NewTotpMfaUtil(config *MfaProps) *TotpMfa {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &TotpMfa{
|
return &TotpMfa{
|
||||||
Config: config,
|
MfaProps: config,
|
||||||
period: MfaTotpPeriodInSeconds,
|
period: MfaTotpPeriodInSeconds,
|
||||||
secretSize: 20,
|
secretSize: 20,
|
||||||
digits: otp.DigitsSix,
|
digits: otp.DigitsSix,
|
||||||
|
@ -342,6 +342,11 @@ func GetDefaultApplication(id string) (*Application, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = extendApplicationWithSigninItems(defaultApplication)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return defaultApplication, nil
|
return defaultApplication, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,8 +312,6 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
|||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
|
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) GetId() string {
|
func (p *Provider) GetId() string {
|
||||||
|
@ -116,7 +116,7 @@ func getFilteredWebhooks(webhooks []*Webhook, action string) []*Webhook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SendWebhooks(record *casvisorsdk.Record) error {
|
func SendWebhooks(record *casvisorsdk.Record) error {
|
||||||
webhooks, err := getWebhooksByOrganization(record.Organization)
|
webhooks, err := getWebhooksByOrganization("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,7 @@ type Attribute struct {
|
|||||||
Values []string `xml:"AttributeValue"`
|
Values []string `xml:"AttributeValue"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, error) {
|
func GetSamlMeta(application *Application, host string, enablePostBinding bool) (*IdpEntityDescriptor, error) {
|
||||||
cert, err := getCertByApplication(application)
|
cert, err := getCertByApplication(application)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -217,6 +217,13 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
|
|||||||
|
|
||||||
originFrontend, originBackend := getOriginFromHost(host)
|
originFrontend, originBackend := getOriginFromHost(host)
|
||||||
|
|
||||||
|
idpLocation := ""
|
||||||
|
if enablePostBinding {
|
||||||
|
idpLocation = fmt.Sprintf("%s/api/saml/redirect/%s/%s", originBackend, application.Owner, application.Name)
|
||||||
|
} else {
|
||||||
|
idpLocation = fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name)
|
||||||
|
}
|
||||||
|
|
||||||
d := IdpEntityDescriptor{
|
d := IdpEntityDescriptor{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Local: "md:EntityDescriptor",
|
Local: "md:EntityDescriptor",
|
||||||
@ -248,7 +255,7 @@ func GetSamlMeta(application *Application, host string) (*IdpEntityDescriptor, e
|
|||||||
},
|
},
|
||||||
SingleSignOnService: SingleSignOnService{
|
SingleSignOnService: SingleSignOnService{
|
||||||
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
||||||
Location: fmt.Sprintf("%s/login/saml/authorize/%s/%s", originFrontend, application.Owner, application.Name),
|
Location: idpLocation,
|
||||||
},
|
},
|
||||||
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
|
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
|
||||||
},
|
},
|
||||||
@ -442,3 +449,8 @@ func NewSamlResponse11(user *User, requestID string, host string) *etree.Element
|
|||||||
|
|
||||||
return samlResponse
|
return samlResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string) string {
|
||||||
|
originF, _ := getOriginFromHost(host)
|
||||||
|
return fmt.Sprintf("%s/login/saml/authorize/%s/%s?relayState=%s&samlRequest=%s", originF, owner, application, relayState, samlRequest)
|
||||||
|
}
|
||||||
|
@ -27,7 +27,7 @@ func getSmsClient(provider *Provider) (sender.SmsClient, error) {
|
|||||||
if provider.Type == sender.HuaweiCloud || provider.Type == sender.AzureACS {
|
if provider.Type == sender.HuaweiCloud || provider.Type == sender.AzureACS {
|
||||||
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
|
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
|
||||||
} else if provider.Type == "Custom HTTP SMS" {
|
} else if provider.Type == "Custom HTTP SMS" {
|
||||||
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title)
|
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title, provider.TemplateCode)
|
||||||
} else {
|
} else {
|
||||||
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
|
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
|
||||||
}
|
}
|
||||||
|
@ -27,20 +27,26 @@ type HttpSmsClient struct {
|
|||||||
endpoint string
|
endpoint string
|
||||||
method string
|
method string
|
||||||
paramName string
|
paramName string
|
||||||
|
template string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHttpSmsClient(endpoint string, method string, paramName string) (*HttpSmsClient, error) {
|
func newHttpSmsClient(endpoint, method, paramName, template string) (*HttpSmsClient, error) {
|
||||||
|
if template == "" {
|
||||||
|
template = "%s"
|
||||||
|
}
|
||||||
client := &HttpSmsClient{
|
client := &HttpSmsClient{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
method: method,
|
method: method,
|
||||||
paramName: paramName,
|
paramName: paramName,
|
||||||
|
template: template,
|
||||||
}
|
}
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HttpSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
func (c *HttpSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
|
||||||
phoneNumber := targetPhoneNumber[0]
|
phoneNumber := targetPhoneNumber[0]
|
||||||
content := param["code"]
|
code := param["code"]
|
||||||
|
content := fmt.Sprintf(c.template, code)
|
||||||
|
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
var err error
|
var err error
|
||||||
|
@ -43,7 +43,7 @@ type Syncer struct {
|
|||||||
Host string `xorm:"varchar(100)" json:"host"`
|
Host string `xorm:"varchar(100)" json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
User string `xorm:"varchar(100)" json:"user"`
|
User string `xorm:"varchar(100)" json:"user"`
|
||||||
Password string `xorm:"varchar(100)" json:"password"`
|
Password string `xorm:"varchar(150)" json:"password"`
|
||||||
Database string `xorm:"varchar(100)" json:"database"`
|
Database string `xorm:"varchar(100)" json:"database"`
|
||||||
Table string `xorm:"varchar(100)" json:"table"`
|
Table string `xorm:"varchar(100)" json:"table"`
|
||||||
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
|
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
|
||||||
|
@ -93,6 +93,8 @@ func (syncer *Syncer) setUserByKeyValue(user *User, key string, value string) {
|
|||||||
user.CreatedTime = value
|
user.CreatedTime = value
|
||||||
case "UpdatedTime":
|
case "UpdatedTime":
|
||||||
user.UpdatedTime = value
|
user.UpdatedTime = value
|
||||||
|
case "DeletedTime":
|
||||||
|
user.DeletedTime = value
|
||||||
case "Id":
|
case "Id":
|
||||||
user.Id = value
|
user.Id = value
|
||||||
case "Type":
|
case "Type":
|
||||||
@ -266,6 +268,7 @@ func (syncer *Syncer) getMapFromOriginalUser(user *OriginalUser) map[string]stri
|
|||||||
m["Name"] = user.Name
|
m["Name"] = user.Name
|
||||||
m["CreatedTime"] = user.CreatedTime
|
m["CreatedTime"] = user.CreatedTime
|
||||||
m["UpdatedTime"] = user.UpdatedTime
|
m["UpdatedTime"] = user.UpdatedTime
|
||||||
|
m["DeletedTime"] = user.DeletedTime
|
||||||
m["Id"] = user.Id
|
m["Id"] = user.Id
|
||||||
m["Type"] = user.Type
|
m["Type"] = user.Type
|
||||||
m["Password"] = user.Password
|
m["Password"] = user.Password
|
||||||
|
@ -186,6 +186,26 @@ func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
|
|||||||
return &token, nil
|
return &token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetTokenByTokenValue(tokenValue string) (*Token, error) {
|
||||||
|
token, err := GetTokenByAccessToken(tokenValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if token != nil {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err = GetTokenByRefreshToken(tokenValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if token != nil {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func updateUsedByCode(token *Token) bool {
|
func updateUsedByCode(token *Token) bool {
|
||||||
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 {
|
||||||
@ -283,20 +303,6 @@ func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, e
|
|||||||
return affected != 0, application, token, nil
|
return affected != 0, application, token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTokenByTokenAndApplication(token string, application string) (*Token, error) {
|
|
||||||
tokenResult := Token{}
|
|
||||||
existed, err := ormer.Engine.Where("(refresh_token = ? or access_token = ? ) and application = ?", token, token, application).Get(&tokenResult)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !existed {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &tokenResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string, lang string) (string, *Application, error) {
|
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string, lang string) (string, *Application, error) {
|
||||||
if responseType != "code" && responseType != "token" && responseType != "id_token" {
|
if responseType != "code" && responseType != "token" && responseType != "id_token" {
|
||||||
return fmt.Sprintf(i18n.Translate(lang, "token:Grant_type: %s is not supported in this application"), responseType), nil, nil
|
return fmt.Sprintf(i18n.Translate(lang, "token:Grant_type: %s is not supported in this application"), responseType), nil, nil
|
||||||
|
@ -40,7 +40,7 @@ type UserShort struct {
|
|||||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||||
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
||||||
Email string `xorm:"varchar(100) index" json:"email"`
|
Email string `xorm:"varchar(100) index" json:"email"`
|
||||||
Phone string `xorm:"varchar(20) index" json:"phone"`
|
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserWithoutThirdIdp struct {
|
type UserWithoutThirdIdp struct {
|
||||||
@ -48,10 +48,11 @@ type UserWithoutThirdIdp struct {
|
|||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(100) 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"`
|
||||||
|
|
||||||
Id string `xorm:"varchar(100) index" json:"id"`
|
Id string `xorm:"varchar(100) index" json:"id"`
|
||||||
Type string `xorm:"varchar(100)" json:"type"`
|
Type string `xorm:"varchar(100)" json:"type"`
|
||||||
Password string `xorm:"varchar(100)" json:"password"`
|
Password string `xorm:"varchar(150)" json:"password"`
|
||||||
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
||||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||||
@ -62,7 +63,7 @@ type UserWithoutThirdIdp struct {
|
|||||||
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
||||||
Email string `xorm:"varchar(100) index" json:"email"`
|
Email string `xorm:"varchar(100) index" json:"email"`
|
||||||
EmailVerified bool `json:"emailVerified"`
|
EmailVerified bool `json:"emailVerified"`
|
||||||
Phone string `xorm:"varchar(20) index" json:"phone"`
|
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||||
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
|
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
|
||||||
Region string `xorm:"varchar(100)" json:"region"`
|
Region string `xorm:"varchar(100)" json:"region"`
|
||||||
Location string `xorm:"varchar(100)" json:"location"`
|
Location string `xorm:"varchar(100)" json:"location"`
|
||||||
@ -167,6 +168,7 @@ func getUserWithoutThirdIdp(user *User) *UserWithoutThirdIdp {
|
|||||||
Name: user.Name,
|
Name: user.Name,
|
||||||
CreatedTime: user.CreatedTime,
|
CreatedTime: user.CreatedTime,
|
||||||
UpdatedTime: user.UpdatedTime,
|
UpdatedTime: user.UpdatedTime,
|
||||||
|
DeletedTime: user.DeletedTime,
|
||||||
|
|
||||||
Id: user.Id,
|
Id: user.Id,
|
||||||
Type: user.Type,
|
Type: user.Type,
|
||||||
|
@ -49,11 +49,12 @@ type User struct {
|
|||||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
Name string `xorm:"varchar(100) 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"`
|
||||||
|
|
||||||
Id string `xorm:"varchar(100) index" json:"id"`
|
Id string `xorm:"varchar(100) index" json:"id"`
|
||||||
ExternalId string `xorm:"varchar(100) index" json:"externalId"`
|
ExternalId string `xorm:"varchar(100) index" json:"externalId"`
|
||||||
Type string `xorm:"varchar(100)" json:"type"`
|
Type string `xorm:"varchar(100)" json:"type"`
|
||||||
Password string `xorm:"varchar(100)" json:"password"`
|
Password string `xorm:"varchar(150)" json:"password"`
|
||||||
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
||||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||||
@ -64,7 +65,7 @@ type User struct {
|
|||||||
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
||||||
Email string `xorm:"varchar(100) index" json:"email"`
|
Email string `xorm:"varchar(100) index" json:"email"`
|
||||||
EmailVerified bool `json:"emailVerified"`
|
EmailVerified bool `json:"emailVerified"`
|
||||||
Phone string `xorm:"varchar(20) index" json:"phone"`
|
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||||
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
|
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
|
||||||
Region string `xorm:"varchar(100)" json:"region"`
|
Region string `xorm:"varchar(100)" json:"region"`
|
||||||
Location string `xorm:"varchar(100)" json:"location"`
|
Location string `xorm:"varchar(100)" json:"location"`
|
||||||
@ -658,6 +659,10 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
|||||||
columns = append(columns, "updated_time")
|
columns = append(columns, "updated_time")
|
||||||
user.UpdatedTime = util.GetCurrentTime()
|
user.UpdatedTime = util.GetCurrentTime()
|
||||||
|
|
||||||
|
if len(user.DeletedTime) > 0 {
|
||||||
|
columns = append(columns, "deleted_time")
|
||||||
|
}
|
||||||
|
|
||||||
if util.ContainsString(columns, "groups") {
|
if util.ContainsString(columns, "groups") {
|
||||||
_, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
_, err := userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -788,6 +793,13 @@ func AddUser(user *User) (bool, error) {
|
|||||||
}
|
}
|
||||||
user.Ranking = int(count + 1)
|
user.Ranking = int(count + 1)
|
||||||
|
|
||||||
|
if user.Groups != nil && len(user.Groups) > 0 {
|
||||||
|
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.Insert(user)
|
affected, err := ormer.Engine.Insert(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -817,6 +829,13 @@ func AddUsers(users []*User) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Groups != nil && len(user.Groups) > 0 {
|
||||||
|
_, err = userEnforcer.UpdateGroupsForUser(user.GetId(), user.Groups)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
affected, err := ormer.Engine.Insert(users)
|
affected, err := ormer.Engine.Insert(users)
|
||||||
|
@ -41,11 +41,7 @@ func downloadImage(client *http.Client, url string) (*bytes.Buffer, string, erro
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, resp.Status)
|
fmt.Printf("downloadImage() error for url [%s]: %s\n", url, resp.Status)
|
||||||
if resp.StatusCode == 404 {
|
return nil, "", nil
|
||||||
return nil, "", nil
|
|
||||||
} else {
|
|
||||||
return nil, "", fmt.Errorf("failed to download gravatar image: %s", resp.Status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the content type and determine the file extension
|
// Get the content type and determine the file extension
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errors2 "errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/casbin/casbin/v2"
|
|
||||||
"github.com/casbin/casbin/v2/errors"
|
"github.com/casbin/casbin/v2/errors"
|
||||||
|
|
||||||
|
"github.com/casbin/casbin/v2"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -87,7 +89,7 @@ func (e *UserGroupEnforcer) GetAllUsersByGroup(group string) ([]string, error) {
|
|||||||
|
|
||||||
users, err := e.enforcer.GetUsersForRole(GetGroupWithPrefix(group))
|
users, err := e.enforcer.GetUsersForRole(GetGroupWithPrefix(group))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errors.ErrNameNotFound {
|
if errors2.Is(err, errors.ErrNameNotFound) {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -134,6 +134,7 @@ func UploadUsers(owner string, path string) (bool, error) {
|
|||||||
LastSigninIp: parseLineItem(&line, 38),
|
LastSigninIp: parseLineItem(&line, 38),
|
||||||
Ldap: "",
|
Ldap: "",
|
||||||
Properties: map[string]string{},
|
Properties: map[string]string{},
|
||||||
|
DeletedTime: parseLineItem(&line, 39),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := oldUserMap[user.GetId()]; !ok {
|
if _, ok := oldUserMap[user.GetId()]; !ok {
|
||||||
|
@ -73,10 +73,12 @@ func getObject(ctx *context.Context) (string, string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// query == "?id=built-in/admin"
|
if !(strings.HasPrefix(ctx.Request.URL.Path, "/api/get-") && strings.HasSuffix(ctx.Request.URL.Path, "s")) {
|
||||||
id := ctx.Input.Query("id")
|
// query == "?id=built-in/admin"
|
||||||
if id != "" {
|
id := ctx.Input.Query("id")
|
||||||
return util.GetOwnerAndNameFromIdNoCheck(id)
|
if id != "" {
|
||||||
|
return util.GetOwnerAndNameFromIdNoCheck(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
owner := ctx.Input.Query("owner")
|
owner := ctx.Input.Query("owner")
|
||||||
@ -164,6 +166,10 @@ func getUrlPath(urlPath string) string {
|
|||||||
return "/api/webauthn"
|
return "/api/webauthn"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(urlPath, "/api/saml/redirect") {
|
||||||
|
return "/api/saml/redirect"
|
||||||
|
}
|
||||||
|
|
||||||
return urlPath
|
return urlPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,9 @@ func initAPI() {
|
|||||||
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
|
beego.Router("/api/get-saml-login", &controllers.ApiController{}, "GET:GetSamlLogin")
|
||||||
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
|
beego.Router("/api/acs", &controllers.ApiController{}, "POST:HandleSamlLogin")
|
||||||
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
|
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
|
||||||
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
|
beego.Router("/api/saml/redirect/:owner/:application", &controllers.ApiController{}, "*:HandleSamlRedirect")
|
||||||
|
beego.Router("/api/webhook", &controllers.ApiController{}, "*:HandleOfficialAccountEvent")
|
||||||
|
beego.Router("/api/get-qrcode", &controllers.ApiController{}, "GET:GetQRCode")
|
||||||
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
|
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
|
||||||
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
|
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
|
||||||
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
|
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
|
||||||
@ -93,6 +95,7 @@ func initAPI() {
|
|||||||
|
|
||||||
beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
|
beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
|
||||||
beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
|
beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
|
||||||
|
beego.Router("/api/get-invitation-info", &controllers.ApiController{}, "GET:GetInvitationCodeInfo")
|
||||||
beego.Router("/api/update-invitation", &controllers.ApiController{}, "POST:UpdateInvitation")
|
beego.Router("/api/update-invitation", &controllers.ApiController{}, "POST:UpdateInvitation")
|
||||||
beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation")
|
beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation")
|
||||||
beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation")
|
beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation")
|
||||||
|
@ -5592,6 +5592,9 @@
|
|||||||
"enableSamlCompress": {
|
"enableSamlCompress": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"enableSamlPostBinding": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"enableSignUp": {
|
"enableSignUp": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -7446,6 +7449,9 @@
|
|||||||
"displayName": {
|
"displayName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"deletedTime": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"douyin": {
|
"douyin": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -4900,6 +4900,8 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
deezer:
|
deezer:
|
||||||
type: string
|
type: string
|
||||||
|
deletedTime:
|
||||||
|
type: string
|
||||||
digitalocean:
|
digitalocean:
|
||||||
type: string
|
type: string
|
||||||
dingtalk:
|
dingtalk:
|
||||||
|
32
util/json.go
32
util/json.go
@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
func StructToJson(v interface{}) string {
|
func StructToJson(v interface{}) string {
|
||||||
data, err := json.Marshal(v)
|
data, err := json.Marshal(v)
|
||||||
@ -37,3 +40,30 @@ func StructToJsonFormatted(v interface{}) string {
|
|||||||
func JsonToStruct(data string, v interface{}) error {
|
func JsonToStruct(data string, v interface{}) error {
|
||||||
return json.Unmarshal([]byte(data), v)
|
return json.Unmarshal([]byte(data), v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryJsonToAnonymousStruct(j string) (interface{}, error) {
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(j), &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a slice of StructFields
|
||||||
|
fields := make([]reflect.StructField, 0, len(data))
|
||||||
|
for k, v := range data {
|
||||||
|
fields = append(fields, reflect.StructField{
|
||||||
|
Name: k,
|
||||||
|
Type: reflect.TypeOf(v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the struct type
|
||||||
|
t := reflect.StructOf(fields)
|
||||||
|
|
||||||
|
// Unmarshal again, this time to the new struct type
|
||||||
|
val := reflect.New(t)
|
||||||
|
i := val.Interface()
|
||||||
|
if err := json.Unmarshal([]byte(j), &i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
@ -16,7 +16,6 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -40,7 +39,7 @@ func GetPath(path string) string {
|
|||||||
func ListFiles(path string) []string {
|
func ListFiles(path string) []string {
|
||||||
res := []string{}
|
res := []string{}
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(path)
|
files, err := os.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,10 @@
|
|||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import "io/ioutil"
|
import "os"
|
||||||
|
|
||||||
func GetUploadXlsxPath(fileId string) string {
|
func GetUploadXlsxPath(fileId string) string {
|
||||||
file, err := ioutil.TempFile("", fileId)
|
file, err := os.CreateTemp("", fileId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -324,9 +324,16 @@ func GetUsernameFromEmail(email string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StringToInterfaceArray(array []string) []interface{} {
|
func StringToInterfaceArray(array []string) []interface{} {
|
||||||
var interfaceArray []interface{}
|
var (
|
||||||
for _, v := range array {
|
interfaceArray []interface{}
|
||||||
interfaceArray = append(interfaceArray, v)
|
elem interface{}
|
||||||
|
)
|
||||||
|
for _, elem = range array {
|
||||||
|
jStruct, err := TryJsonToAnonymousStruct(elem.(string))
|
||||||
|
if err == nil {
|
||||||
|
elem = jStruct
|
||||||
|
}
|
||||||
|
interfaceArray = append(interfaceArray, elem)
|
||||||
}
|
}
|
||||||
return interfaceArray
|
return interfaceArray
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,9 @@ func GetVersionInfo() (*VersionInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cIter, err := r.Log(&git.LogOptions{From: ref.Hash()})
|
cIter, err := r.Log(&git.LogOptions{From: ref.Hash()})
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
commitOffset := 0
|
commitOffset := 0
|
||||||
version := ""
|
version := ""
|
||||||
|
@ -70,6 +70,9 @@ func TestGetVersion(t *testing.T) {
|
|||||||
|
|
||||||
testHash := plumbing.NewHash("f8bc87eb4e5ba3256424cf14aafe0549f812f1cf")
|
testHash := plumbing.NewHash("f8bc87eb4e5ba3256424cf14aafe0549f812f1cf")
|
||||||
cIter, err := r.Log(&git.LogOptions{From: testHash})
|
cIter, err := r.Log(&git.LogOptions{From: testHash})
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
|
||||||
aheadCnt := 0
|
aheadCnt := 0
|
||||||
releaseVersion := ""
|
releaseVersion := ""
|
||||||
|
@ -34,7 +34,7 @@ func init() {
|
|||||||
rePhone, _ = regexp.Compile(`(\d{3})\d*(\d{4})`)
|
rePhone, _ = regexp.Compile(`(\d{3})\d*(\d{4})`)
|
||||||
ReWhiteSpace, _ = regexp.Compile(`\s`)
|
ReWhiteSpace, _ = regexp.Compile(`\s`)
|
||||||
ReFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
|
ReFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
|
||||||
ReUserName, _ = regexp.Compile("^[a-zA-Z0-9]+((?:-[a-zA-Z0-9]+)|(?:_[a-zA-Z0-9]+))*$")
|
ReUserName, _ = regexp.Compile("^[a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsEmailValid(email string) bool {
|
func IsEmailValid(email string) bool {
|
||||||
|
@ -735,7 +735,9 @@ class App extends Component {
|
|||||||
account={this.state.account}
|
account={this.state.account}
|
||||||
theme={this.state.themeData}
|
theme={this.state.themeData}
|
||||||
onLoginSuccess={(redirectUrl) => {
|
onLoginSuccess={(redirectUrl) => {
|
||||||
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
if (redirectUrl) {
|
||||||
|
localStorage.setItem("mfaRedirectUrl", redirectUrl);
|
||||||
|
}
|
||||||
this.getAccount();
|
this.getAccount();
|
||||||
}}
|
}}
|
||||||
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
onUpdateAccount={(account) => this.onUpdateAccount(account)}
|
||||||
|
@ -36,6 +36,7 @@ import ThemeEditor from "./common/theme/ThemeEditor";
|
|||||||
|
|
||||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
import {Controlled as CodeMirror} from "react-codemirror2";
|
||||||
import "codemirror/lib/codemirror.css";
|
import "codemirror/lib/codemirror.css";
|
||||||
|
import SigninTable from "./table/SigninTable";
|
||||||
|
|
||||||
require("codemirror/theme/material-darker.css");
|
require("codemirror/theme/material-darker.css");
|
||||||
require("codemirror/mode/htmlmixed/htmlmixed");
|
require("codemirror/mode/htmlmixed/htmlmixed");
|
||||||
@ -116,7 +117,6 @@ class ApplicationEditPage extends React.Component {
|
|||||||
this.getApplication();
|
this.getApplication();
|
||||||
this.getOrganizations();
|
this.getOrganizations();
|
||||||
this.getProviders();
|
this.getProviders();
|
||||||
this.getSamlMetadata();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplication() {
|
getApplication() {
|
||||||
@ -146,6 +146,8 @@ class ApplicationEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.getCerts(application.organization);
|
this.getCerts(application.organization);
|
||||||
|
|
||||||
|
this.getSamlMetadata(application.enableSamlPostBinding);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,8 +188,8 @@ class ApplicationEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSamlMetadata() {
|
getSamlMetadata(checked) {
|
||||||
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName)
|
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName, checked)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
samlMetadata: data,
|
samlMetadata: data,
|
||||||
@ -663,6 +665,17 @@ class ApplicationEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("application:Enable SAML POST binding"), i18next.t("application:Enable SAML POST binding - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={1} >
|
||||||
|
<Switch checked={this.state.application.enableSamlPostBinding} onChange={checked => {
|
||||||
|
this.updateApplicationField("enableSamlPostBinding", checked);
|
||||||
|
this.getSamlMetadata(checked);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:SAML attributes"), i18next.t("general:SAML attributes - Tooltip"))} :
|
||||||
@ -688,7 +701,7 @@ class ApplicationEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
|
||||||
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}`);
|
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}&post=${this.state.application.enableSamlPostBinding}`);
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -852,6 +865,24 @@ class ApplicationEditPage extends React.Component {
|
|||||||
}
|
}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
{
|
||||||
|
<React.Fragment>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("application:Signin items"), i18next.t("application:Signin items - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<SigninTable
|
||||||
|
title={i18next.t("application:Signin items")}
|
||||||
|
table={this.state.application.signinItems}
|
||||||
|
onUpdateTable={(value) => {
|
||||||
|
this.updateApplicationField("signinItems", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
{
|
{
|
||||||
!this.state.application.enableSignUp ? null : (
|
!this.state.application.enableSignUp ? null : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -61,6 +61,7 @@ class ApplicationListPage extends BaseListPage {
|
|||||||
{name: "Phone", visible: true, required: true, rule: "None"},
|
{name: "Phone", visible: true, required: true, rule: "None"},
|
||||||
{name: "Agreement", visible: true, required: true, rule: "None"},
|
{name: "Agreement", visible: true, required: true, rule: "None"},
|
||||||
],
|
],
|
||||||
|
grantTypes: ["authorization_code", "password", "client_credentials", "token", "id_token", "refresh_token"],
|
||||||
cert: "cert-built-in",
|
cert: "cert-built-in",
|
||||||
redirectUris: ["http://localhost:9000/callback"],
|
redirectUris: ["http://localhost:9000/callback"],
|
||||||
tokenFormat: "JWT",
|
tokenFormat: "JWT",
|
||||||
|
@ -19,6 +19,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
|
|||||||
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
|
||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
@ -99,6 +100,18 @@ class InvitationEditPage extends React.Component {
|
|||||||
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}
|
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}
|
||||||
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||||
|
<Button style={{marginLeft: "20px"}} onClick={() => {
|
||||||
|
let defaultApplication;
|
||||||
|
if (this.state.invitation.owner === "built-in") {
|
||||||
|
defaultApplication = "app-built-in";
|
||||||
|
} else {
|
||||||
|
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
||||||
|
}
|
||||||
|
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
||||||
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
|
}}>
|
||||||
|
{i18next.t("application:Copy signup page URL")}
|
||||||
|
</Button>
|
||||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||||
</div>
|
</div>
|
||||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||||
@ -140,10 +153,24 @@ class InvitationEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input value={this.state.invitation.code} onChange={e => {
|
<Input value={this.state.invitation.code} onChange={e => {
|
||||||
|
const regex = /[^a-zA-Z0-9]/;
|
||||||
|
if (!regex.test(e.target.value)) {
|
||||||
|
this.updateInvitationField("defaultCode", e.target.value);
|
||||||
|
}
|
||||||
this.updateInvitationField("code", e.target.value);
|
this.updateInvitationField("code", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("invitation:Default code"), i18next.t("invitation:Default code - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input value={this.state.invitation.defaultCode} onChange={e => {
|
||||||
|
this.updateInvitationField("defaultCode", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} :
|
{Setting.getLabel(i18next.t("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} :
|
||||||
@ -274,6 +301,18 @@ class InvitationEditPage extends React.Component {
|
|||||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||||
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||||
|
<Button style={{marginLeft: "20px"}} size="large" onClick={() => {
|
||||||
|
let defaultApplication;
|
||||||
|
if (this.state.invitation.owner === "built-in") {
|
||||||
|
defaultApplication = "app-built-in";
|
||||||
|
} else {
|
||||||
|
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
|
||||||
|
}
|
||||||
|
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
|
||||||
|
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
||||||
|
}}>
|
||||||
|
{i18next.t("application:Copy signup page URL")}
|
||||||
|
</Button>
|
||||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,19 +22,20 @@ import * as InvitationBackend from "./backend/InvitationBackend";
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import BaseListPage from "./BaseListPage";
|
import BaseListPage from "./BaseListPage";
|
||||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
|
|
||||||
class InvitationListPage extends BaseListPage {
|
class InvitationListPage extends BaseListPage {
|
||||||
newInvitation() {
|
newInvitation() {
|
||||||
const randomName = Setting.getRandomName();
|
const randomName = Setting.getRandomName();
|
||||||
const owner = Setting.getRequestOrganization(this.props.account);
|
const owner = Setting.getRequestOrganization(this.props.account);
|
||||||
|
const code = Math.random().toString(36).slice(-10);
|
||||||
return {
|
return {
|
||||||
owner: owner,
|
owner: owner,
|
||||||
name: `invitation_${randomName}`,
|
name: `invitation_${randomName}`,
|
||||||
createdTime: moment().format(),
|
createdTime: moment().format(),
|
||||||
updatedTime: moment().format(),
|
updatedTime: moment().format(),
|
||||||
displayName: `New Invitation - ${randomName}`,
|
displayName: `New Invitation - ${randomName}`,
|
||||||
code: Math.random().toString(36).slice(-10),
|
code: code,
|
||||||
|
defaultCode: code,
|
||||||
quota: 1,
|
quota: 1,
|
||||||
usedCount: 0,
|
usedCount: 0,
|
||||||
application: "All",
|
application: "All",
|
||||||
@ -225,17 +226,11 @@ class InvitationListPage extends BaseListPage {
|
|||||||
title: i18next.t("general:Action"),
|
title: i18next.t("general:Action"),
|
||||||
dataIndex: "",
|
dataIndex: "",
|
||||||
key: "op",
|
key: "op",
|
||||||
width: "350px",
|
width: "180px",
|
||||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => {
|
|
||||||
copy(`${window.location.origin}/login/${record.owner}?invitation_code=${record.code}`);
|
|
||||||
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
|
|
||||||
}}>
|
|
||||||
{i18next.t("application:Copy signup page URL")}
|
|
||||||
</Button>
|
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/invitations/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/invitations/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||||
<PopconfirmModal
|
<PopconfirmModal
|
||||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||||
|
@ -184,7 +184,7 @@ class OrganizationEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
|
<Select virtual={false} style={{width: "100%"}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField("passwordType", value);})}
|
||||||
options={["plain", "salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))}
|
options={["plain", "salt", "sha512-salt", "md5-salt", "bcrypt", "pbkdf2-salt", "argon2id"].map(item => Setting.getOption(item, item))}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Card, Checkbox, Col, Input, InputNumber, Row, Select, Switch} from "antd";
|
import {Button, Card, Checkbox, Col, Input, InputNumber, Radio, Row, Select, Switch} from "antd";
|
||||||
import {LinkOutlined} from "@ant-design/icons";
|
import {LinkOutlined} from "@ant-design/icons";
|
||||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||||
@ -118,7 +118,23 @@ class ProviderEditPage extends React.Component {
|
|||||||
provider["cert"] = "";
|
provider["cert"] = "";
|
||||||
this.getCerts(value);
|
this.getCerts(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
provider[key] = value;
|
provider[key] = value;
|
||||||
|
|
||||||
|
if (provider["type"] === "WeChat") {
|
||||||
|
if (!provider["clientId"]) {
|
||||||
|
provider["signName"] = "media";
|
||||||
|
provider["disableSsl"] = true;
|
||||||
|
}
|
||||||
|
if (!provider["clientId2"]) {
|
||||||
|
provider["signName"] = "open";
|
||||||
|
provider["disableSsl"] = false;
|
||||||
|
}
|
||||||
|
if (!provider["disableSsl"]) {
|
||||||
|
provider["signName"] = "open";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
provider: provider,
|
provider: provider,
|
||||||
});
|
});
|
||||||
@ -756,16 +772,44 @@ class ProviderEditPage extends React.Component {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.state.provider.type !== "WeChat" ? null : (
|
this.state.provider.type !== "WeChat" ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<React.Fragment>
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Row style={{marginTop: "20px"}} >
|
||||||
{Setting.getLabel(i18next.t("provider:Enable QR code"), i18next.t("provider:Enable QR code - Tooltip"))} :
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
</Col>
|
{Setting.getLabel(i18next.t("provider:Use WeChat Media Platform in PC"), i18next.t("provider:Use WeChat Media Platform in PC - Tooltip"))} :
|
||||||
<Col span={1} >
|
</Col>
|
||||||
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
|
<Col span={1} >
|
||||||
this.updateProviderField("disableSsl", checked);
|
<Switch disabled={!this.state.provider.clientId} checked={this.state.provider.disableSsl} onChange={checked => {
|
||||||
}} />
|
this.updateProviderField("disableSsl", checked);
|
||||||
</Col>
|
}} />
|
||||||
</Row>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("token:Access token"), i18next.t("token:Access token - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input value={this.state.provider.content} disabled={!this.state.provider.disableSsl || !this.state.provider.clientId2} onChange={e => {
|
||||||
|
this.updateProviderField("content", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("provider:Follow-up action"), i18next.t("provider:Follow-up action - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Radio.Group value={this.state.provider.signName}
|
||||||
|
disabled={!this.state.provider.disableSsl || !this.state.provider.clientId || !this.state.provider.clientId2}
|
||||||
|
buttonStyle="solid"
|
||||||
|
onChange={e => {
|
||||||
|
this.updateProviderField("signName", e.target.value);
|
||||||
|
}}>
|
||||||
|
<Radio.Button value="open">{i18next.t("provider:Use WeChat Open Platform to login")}</Radio.Button>
|
||||||
|
<Radio.Button value="media">{i18next.t("provider:Use WeChat Media Platform to login")}</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@ -832,7 +876,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{["Custom HTTP SMS", "MinIO", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : (
|
{["Custom HTTP SMS", "Google Cloud Storage", "Qiniu Cloud Kodo", "Synology"].includes(this.state.provider.type) ? null : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={2}>
|
<Col style={{marginTop: "5px"}} span={2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||||
@ -1041,7 +1085,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{["Custom HTTP SMS", "Infobip SMS"].includes(this.state.provider.type) ?
|
{["Infobip SMS"].includes(this.state.provider.type) ?
|
||||||
null :
|
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}>
|
||||||
|
@ -1164,7 +1164,7 @@ export function getLoginLink(application) {
|
|||||||
let url;
|
let url;
|
||||||
if (application === null) {
|
if (application === null) {
|
||||||
url = null;
|
url = null;
|
||||||
} else if (!isPasswordEnabled(application) && window.location.pathname.includes("/auto-signup/oauth/authorize")) {
|
} else if (window.location.pathname.includes("/auto-signup/oauth/authorize")) {
|
||||||
url = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
|
url = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
|
||||||
} else if (authConfig.appName === application.name) {
|
} else if (authConfig.appName === application.name) {
|
||||||
url = "/login";
|
url = "/login";
|
||||||
@ -1176,11 +1176,6 @@ export function getLoginLink(application) {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderLoginLink(application, text) {
|
|
||||||
const url = getLoginLink(application);
|
|
||||||
return renderLink(url, text, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function redirectToLoginPage(application, history) {
|
export function redirectToLoginPage(application, history) {
|
||||||
const loginLink = getLoginLink(application);
|
const loginLink = getLoginLink(application);
|
||||||
if (loginLink.startsWith("http://") || loginLink.startsWith("https://")) {
|
if (loginLink.startsWith("http://") || loginLink.startsWith("https://")) {
|
||||||
@ -1205,7 +1200,7 @@ function renderLink(url, text, onClick) {
|
|||||||
);
|
);
|
||||||
} else if (url.startsWith("http")) {
|
} else if (url.startsWith("http")) {
|
||||||
return (
|
return (
|
||||||
<a target="_blank" rel="noopener noreferrer" style={{float: "right"}} href={url} onClick={() => {
|
<a style={{float: "right"}} href={url} onClick={() => {
|
||||||
if (onClick !== null) {
|
if (onClick !== null) {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
@ -1220,7 +1215,7 @@ export function renderSignupLink(application, text) {
|
|||||||
let url;
|
let url;
|
||||||
if (application === null) {
|
if (application === null) {
|
||||||
url = null;
|
url = null;
|
||||||
} else if (!isPasswordEnabled(application) && window.location.pathname.includes("/login/oauth/authorize")) {
|
} else if (window.location.pathname.includes("/login/oauth/authorize")) {
|
||||||
url = window.location.href.replace("/login/oauth/authorize", "/auto-signup/oauth/authorize");
|
url = window.location.href.replace("/login/oauth/authorize", "/auto-signup/oauth/authorize");
|
||||||
} else if (authConfig.appName === application.name) {
|
} else if (authConfig.appName === application.name) {
|
||||||
url = "/signup";
|
url = "/signup";
|
||||||
@ -1253,7 +1248,11 @@ export function renderForgetLink(application, text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderLink(url, text, null);
|
const storeSigninUrl = () => {
|
||||||
|
sessionStorage.setItem("signinUrl", window.location.href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderLink(url, text, storeSigninUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderHelmet(application) {
|
export function renderHelmet(application) {
|
||||||
@ -1448,7 +1447,7 @@ export function getFriendlyUserName(account) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getUserCommonFields() {
|
export function getUserCommonFields() {
|
||||||
return ["Owner", "Name", "CreatedTime", "UpdatedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
|
return ["Owner", "Name", "CreatedTime", "UpdatedTime", "DeletedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
|
||||||
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
|
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
|
||||||
"Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp",
|
"Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp",
|
||||||
"PreferredMfaType", "TotpSecret", "SignupApplication"];
|
"PreferredMfaType", "TotpSecret", "SignupApplication"];
|
||||||
|
@ -27,6 +27,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
|
|||||||
import PasswordModal from "./common/modal/PasswordModal";
|
import PasswordModal from "./common/modal/PasswordModal";
|
||||||
import ResetModal from "./common/modal/ResetModal";
|
import ResetModal from "./common/modal/ResetModal";
|
||||||
import AffiliationSelect from "./common/select/AffiliationSelect";
|
import AffiliationSelect from "./common/select/AffiliationSelect";
|
||||||
|
import moment from "moment";
|
||||||
import OAuthWidget from "./common/OAuthWidget";
|
import OAuthWidget from "./common/OAuthWidget";
|
||||||
import SamlWidget from "./common/SamlWidget";
|
import SamlWidget from "./common/SamlWidget";
|
||||||
import RegionSelect from "./common/select/RegionSelect";
|
import RegionSelect from "./common/select/RegionSelect";
|
||||||
@ -122,6 +123,17 @@ class UserEditPage extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
applications: res.data || [],
|
applications: res.data || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const applications = res.data;
|
||||||
|
if (this.state.user) {
|
||||||
|
if (this.state.user.signupApplication === "" || applications.filter(application => application.name === this.state.user.signupApplication).length === 0) {
|
||||||
|
if (applications.length > 0) {
|
||||||
|
this.updateUserField("signupApplication", applications[0].name);
|
||||||
|
} else {
|
||||||
|
this.updateUserField("signupApplication", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -858,6 +870,7 @@ class UserEditPage extends React.Component {
|
|||||||
<Col span={(Setting.isMobile()) ? 22 : 2} >
|
<Col span={(Setting.isMobile()) ? 22 : 2} >
|
||||||
<Switch checked={this.state.user.isDeleted} onChange={checked => {
|
<Switch checked={this.state.user.isDeleted} onChange={checked => {
|
||||||
this.updateUserField("isDeleted", checked);
|
this.updateUserField("isDeleted", checked);
|
||||||
|
this.updateUserField("deletedTime", checked ? moment().format() : "");
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -890,11 +903,9 @@ class UserEditPage extends React.Component {
|
|||||||
</Space>
|
</Space>
|
||||||
{item.enabled ? (
|
{item.enabled ? (
|
||||||
<Space>
|
<Space>
|
||||||
{item.enabled ?
|
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
{i18next.t("general:Enabled")}
|
||||||
{i18next.t("general:Enabled")}
|
</Tag>
|
||||||
</Tag> : null
|
|
||||||
}
|
|
||||||
{item.isPreferred ?
|
{item.isPreferred ?
|
||||||
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
|
<Tag icon={<CheckCircleOutlined />} color="blue" style={{marginRight: 20}} >
|
||||||
{i18next.t("mfa:preferred")}
|
{i18next.t("mfa:preferred")}
|
||||||
@ -916,18 +927,23 @@ class UserEditPage extends React.Component {
|
|||||||
{i18next.t("mfa:Set preferred")}
|
{i18next.t("mfa:Set preferred")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
{this.isSelf() ? <Button type={"default"} onClick={() => {
|
||||||
|
this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
|
||||||
|
}}>
|
||||||
|
{i18next.t("general:Edit")}
|
||||||
|
</Button> : null}
|
||||||
</Space>
|
</Space>
|
||||||
) :
|
) :
|
||||||
<Space>
|
<Space>
|
||||||
{item.mfaType !== TotpMfaType && Setting.isAdminUser(this.props.account) && window.location.href.indexOf("/users") !== -1 ?
|
{item.mfaType !== TotpMfaType && Setting.isLocalAdminUser(this.props.account) && !this.isSelf() ?
|
||||||
<EnableMfaModal user={this.state.user} mfaType={item.mfaType} onSuccess={() => {
|
<EnableMfaModal user={this.state.user} mfaType={item.mfaType} onSuccess={() => {
|
||||||
this.getUser();
|
this.getUser();
|
||||||
}} /> : null}
|
}} /> : null}
|
||||||
<Button type={"default"} onClick={() => {
|
{this.isSelf() ? <Button type={"default"} onClick={() => {
|
||||||
this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
|
this.props.history.push(`/mfa/setup?mfaType=${item.mfaType}`);
|
||||||
}}>
|
}}>
|
||||||
{i18next.t("mfa:Setup")}
|
{i18next.t("mfa:Setup")}
|
||||||
</Button>
|
</Button> : null}
|
||||||
</Space>}
|
</Space>}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
|
@ -62,6 +62,7 @@ const userTemplate = {
|
|||||||
"name": "admin",
|
"name": "admin",
|
||||||
"createdTime": "2020-07-16T21:46:52+08:00",
|
"createdTime": "2020-07-16T21:46:52+08:00",
|
||||||
"updatedTime": "",
|
"updatedTime": "",
|
||||||
|
"deletedTime": "",
|
||||||
"id": "9eb20f79-3bb5-4e74-99ac-39e3b9a171e8",
|
"id": "9eb20f79-3bb5-4e74-99ac-39e3b9a171e8",
|
||||||
"type": "normal-user",
|
"type": "normal-user",
|
||||||
"password": "***",
|
"password": "***",
|
||||||
|
@ -135,8 +135,18 @@ export function loginWithSaml(values, param) {
|
|||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWechatMessageEvent() {
|
export function getWechatMessageEvent(ticket) {
|
||||||
return fetch(`${Setting.ServerUrl}/api/get-webhook-event`, {
|
return fetch(`${Setting.ServerUrl}/api/get-webhook-event?ticket=${ticket}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Accept-Language": Setting.getAcceptLanguage(),
|
||||||
|
},
|
||||||
|
}).then(res => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWechatQRCode(providerId) {
|
||||||
|
return fetch(`${Setting.ServerUrl}/api/get-qrcode?id=${providerId}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -47,6 +47,7 @@ class ForgetPage extends React.Component {
|
|||||||
|
|
||||||
this.form = React.createRef();
|
this.form = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.getApplicationObj() === undefined) {
|
if (this.getApplicationObj() === undefined) {
|
||||||
if (this.state.applicationName !== undefined) {
|
if (this.state.applicationName !== undefined) {
|
||||||
@ -153,7 +154,12 @@ class ForgetPage extends React.Component {
|
|||||||
values.userOwner = this.getApplicationObj()?.organizationObj.name;
|
values.userOwner = this.getApplicationObj()?.organizationObj.name;
|
||||||
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword, this.state.code).then(res => {
|
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword, this.state.code).then(res => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
|
const linkInStorage = sessionStorage.getItem("signinUrl");
|
||||||
|
if (linkInStorage !== null && linkInStorage !== "") {
|
||||||
|
Setting.goToLink(linkInStorage);
|
||||||
|
} else {
|
||||||
|
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", res.msg);
|
Setting.showMessage("error", res.msg);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd";
|
import {Button, Checkbox, Col, Form, Input, Result, Spin, Tabs} from "antd";
|
||||||
import {ArrowLeftOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
|
import {ArrowLeftOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
|
||||||
import {withRouter} from "react-router-dom";
|
import {withRouter} from "react-router-dom";
|
||||||
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
||||||
@ -32,11 +32,11 @@ import i18next from "i18next";
|
|||||||
import CustomGithubCorner from "../common/CustomGithubCorner";
|
import CustomGithubCorner from "../common/CustomGithubCorner";
|
||||||
import {SendCodeInput} from "../common/SendCodeInput";
|
import {SendCodeInput} from "../common/SendCodeInput";
|
||||||
import LanguageSelect from "../common/select/LanguageSelect";
|
import LanguageSelect from "../common/select/LanguageSelect";
|
||||||
import {CaptchaModal} from "../common/modal/CaptchaModal";
|
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
||||||
import {CaptchaRule} from "../common/modal/CaptchaModal";
|
|
||||||
import RedirectForm from "../common/RedirectForm";
|
import RedirectForm from "../common/RedirectForm";
|
||||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||||
|
|
||||||
class LoginPage extends React.Component {
|
class LoginPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -494,6 +494,200 @@ class LoginPage extends React.Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderFormItem(application, signinItem) {
|
||||||
|
if (!signinItem.visible && signinItem.name !== "ForgetPassword") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signinItem.name === "Logo") {
|
||||||
|
return (
|
||||||
|
<div className="login-logo-box">
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
{
|
||||||
|
Setting.renderHelmet(application)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Setting.renderLogo(application)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Back button") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
{
|
||||||
|
this.renderBackButton()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Languages") {
|
||||||
|
return (
|
||||||
|
<div className="login-languages">
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
<LanguageSelect languages={application.organizationObj.languages} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Signin methods") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
{this.renderMethodChoiceBox()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
;
|
||||||
|
} else if (signinItem.name === "Username") {
|
||||||
|
return (
|
||||||
|
<div className="login-username">
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: () => {
|
||||||
|
switch (this.state.loginMethod) {
|
||||||
|
case "verificationCodeEmail":
|
||||||
|
return i18next.t("login:Please input your Email!");
|
||||||
|
case "verificationCodePhone":
|
||||||
|
return i18next.t("login:Please input your Phone!");
|
||||||
|
case "ldap":
|
||||||
|
return i18next.t("login:Please input your LDAP username!");
|
||||||
|
default:
|
||||||
|
return i18next.t("login:Please input your Email or Phone!");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (_, value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.loginMethod === "verificationCode") {
|
||||||
|
if (!Setting.isValidEmail(value) && !Setting.isValidPhone(value)) {
|
||||||
|
this.setState({validEmailOrPhone: false});
|
||||||
|
return Promise.reject(i18next.t("login:The input is not valid Email or phone number!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Setting.isValidEmail(value)) {
|
||||||
|
this.setState({validEmail: true});
|
||||||
|
} else {
|
||||||
|
this.setState({validEmail: false});
|
||||||
|
}
|
||||||
|
} else if (this.state.loginMethod === "verificationCodeEmail") {
|
||||||
|
if (!Setting.isValidEmail(value)) {
|
||||||
|
this.setState({validEmail: false});
|
||||||
|
this.setState({validEmailOrPhone: false});
|
||||||
|
return Promise.reject(i18next.t("login:The input is not valid Email!"));
|
||||||
|
} else {
|
||||||
|
this.setState({validEmail: true});
|
||||||
|
}
|
||||||
|
} else if (this.state.loginMethod === "verificationCodePhone") {
|
||||||
|
if (!Setting.isValidPhone(value)) {
|
||||||
|
this.setState({validEmailOrPhone: false});
|
||||||
|
return Promise.reject(i18next.t("login:The input is not valid phone number!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({validEmailOrPhone: true});
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="input"
|
||||||
|
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||||
|
placeholder={this.getPlaceholder()}
|
||||||
|
onChange={e => {
|
||||||
|
this.setState({
|
||||||
|
username: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Password") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
{this.renderPasswordOrCodeInput()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Forgot password?") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
<div className="login-forget-password">
|
||||||
|
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
|
||||||
|
<Checkbox style={{float: "left"}}>
|
||||||
|
{i18next.t("login:Auto sign in")}
|
||||||
|
</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
{
|
||||||
|
signinItem.visible ? Setting.renderForgetLink(application, i18next.t("login:Forgot password?")) : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Agreement") {
|
||||||
|
return AgreementModal.isAgreementRequired(application) ? AgreementModal.renderAgreementFormItem(application, true, {}, this) : null;
|
||||||
|
} else if (signinItem.name === "Login button") {
|
||||||
|
return (
|
||||||
|
<Form.Item className="login-button-box">
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
className="login-button"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
this.state.loginMethod === "webAuthn" ? i18next.t("login:Sign in with WebAuthn") :
|
||||||
|
i18next.t("login:Sign In")
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
{
|
||||||
|
this.renderCaptchaModal(application)
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Providers") {
|
||||||
|
const showForm = Setting.isPasswordEnabled(application) || Setting.isCodeSigninEnabled(application) || Setting.isWebAuthnEnabled(application) || Setting.isLdapEnabled(application);
|
||||||
|
if (signinItem.rule === "None" || signinItem.rule === "") {
|
||||||
|
signinItem.rule = showForm ? "small" : "big";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
<Form.Item>
|
||||||
|
{
|
||||||
|
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
|
||||||
|
return ProviderButton.renderProviderLogo(providerItem.provider, application, null, null, signinItem.rule, this.props.location);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.renderOtherFormProvider(application)
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (signinItem.name.startsWith("Text ") || signinItem?.isCustom) {
|
||||||
|
return (
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
);
|
||||||
|
} else if (signinItem.name === "Signup link") {
|
||||||
|
return (
|
||||||
|
<div style={{width: "100%"}} className="login-signup-link">
|
||||||
|
<div dangerouslySetInnerHTML={{__html: signinItem.label}} />
|
||||||
|
{this.renderFooter(application)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderForm(application) {
|
renderForm(application) {
|
||||||
if (this.state.msg !== null) {
|
if (this.state.msg !== null) {
|
||||||
return Util.renderMessage(this.state.msg);
|
return Util.renderMessage(this.state.msg);
|
||||||
@ -569,116 +763,10 @@ class LoginPage extends React.Component {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{this.renderMethodChoiceBox()}
|
|
||||||
<Row style={{minHeight: 130, alignItems: "center"}}>
|
|
||||||
<Col span={24}>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: () => {
|
|
||||||
switch (this.state.loginMethod) {
|
|
||||||
case "verificationCodeEmail": return i18next.t("login:Please input your Email!");
|
|
||||||
case "verificationCodePhone": return i18next.t("login:Please input your Phone!");
|
|
||||||
case "ldap": return i18next.t("login:Please input your LDAP username!");
|
|
||||||
default: return i18next.t("login:Please input your Email or Phone!");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
validator: (_, value) => {
|
|
||||||
if (value === "") {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.loginMethod === "verificationCode") {
|
{
|
||||||
if (!Setting.isValidEmail(value) && !Setting.isValidPhone(value)) {
|
application.signinItems?.map(signinItem => this.renderFormItem(application, signinItem))
|
||||||
this.setState({validEmailOrPhone: false});
|
}
|
||||||
return Promise.reject(i18next.t("login:The input is not valid Email or phone number!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Setting.isValidEmail(value)) {
|
|
||||||
this.setState({validEmail: true});
|
|
||||||
} else {
|
|
||||||
this.setState({validEmail: false});
|
|
||||||
}
|
|
||||||
} else if (this.state.loginMethod === "verificationCodeEmail") {
|
|
||||||
if (!Setting.isValidEmail(value)) {
|
|
||||||
this.setState({validEmail: false});
|
|
||||||
this.setState({validEmailOrPhone: false});
|
|
||||||
return Promise.reject(i18next.t("login:The input is not valid Email!"));
|
|
||||||
} else {
|
|
||||||
this.setState({validEmail: true});
|
|
||||||
}
|
|
||||||
} else if (this.state.loginMethod === "verificationCodePhone") {
|
|
||||||
if (!Setting.isValidPhone(value)) {
|
|
||||||
this.setState({validEmailOrPhone: false});
|
|
||||||
return Promise.reject(i18next.t("login:The input is not valid phone number!"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({validEmailOrPhone: true});
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
id="input"
|
|
||||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
|
||||||
placeholder={this.getPlaceholder()}
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({
|
|
||||||
username: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
{
|
|
||||||
this.renderPasswordOrCodeInput()
|
|
||||||
}
|
|
||||||
</Row>
|
|
||||||
<div style={{display: "inline-flex", justifyContent: "space-between", width: "320px", marginBottom: AgreementModal.isAgreementRequired(application) ? "5px" : "25px"}}>
|
|
||||||
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
|
|
||||||
<Checkbox style={{float: "left"}}>
|
|
||||||
{i18next.t("login:Auto sign in")}
|
|
||||||
</Checkbox>
|
|
||||||
</Form.Item>
|
|
||||||
{
|
|
||||||
Setting.renderForgetLink(application, i18next.t("login:Forgot password?"))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{AgreementModal.isAgreementRequired(application) ? AgreementModal.renderAgreementFormItem(application, true, {}, this) : null}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
style={{width: "100%", marginBottom: "5px"}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
this.state.loginMethod === "webAuthn" ? i18next.t("login:Sign in with WebAuthn") :
|
|
||||||
i18next.t("login:Sign In")
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
{
|
|
||||||
this.renderCaptchaModal(application)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.renderFooter(application)
|
|
||||||
}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
{
|
|
||||||
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
|
|
||||||
return ProviderButton.renderProviderLogo(providerItem.provider, application, 30, 5, "small", this.props.location);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.renderOtherFormProvider(application)
|
|
||||||
}
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -695,19 +783,8 @@ class LoginPage extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
{
|
{
|
||||||
application.providers?.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
|
application?.signinItems.map(signinItem => signinItem.name === "Providers" || signinItem.name === "Signup link" ? this.renderFormItem(application, signinItem) : null)
|
||||||
return ProviderButton.renderProviderLogo(providerItem.provider, application, 40, 10, "big", this.props.location);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
{
|
|
||||||
this.renderOtherFormProvider(application)
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
{
|
|
||||||
this.renderFooter(application)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -760,7 +837,7 @@ class LoginPage extends React.Component {
|
|||||||
|
|
||||||
renderFooter(application) {
|
renderFooter(application) {
|
||||||
return (
|
return (
|
||||||
<span style={{float: "right"}}>
|
<div>
|
||||||
{
|
{
|
||||||
!application.enableSignUp ? null : (
|
!application.enableSignUp ? null : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@ -771,7 +848,7 @@ class LoginPage extends React.Component {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -889,33 +966,37 @@ class LoginPage extends React.Component {
|
|||||||
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
|
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
|
||||||
return (
|
return (
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Form.Item
|
<div className="login-password">
|
||||||
name="password"
|
<Form.Item
|
||||||
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
name="password"
|
||||||
>
|
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
||||||
<Input.Password
|
>
|
||||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
<Input.Password
|
||||||
type="password"
|
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||||
placeholder={i18next.t("general:Password")}
|
type="password"
|
||||||
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
|
placeholder={i18next.t("general:Password")}
|
||||||
/>
|
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
|
||||||
</Form.Item>
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
} else if (this.state.loginMethod?.includes("verificationCode")) {
|
} else if (this.state.loginMethod?.includes("verificationCode")) {
|
||||||
return (
|
return (
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Form.Item
|
<div className="login-password">
|
||||||
name="code"
|
<Form.Item
|
||||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
name="code"
|
||||||
>
|
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||||
<SendCodeInput
|
>
|
||||||
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
<SendCodeInput
|
||||||
method={"login"}
|
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
||||||
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
method={"login"}
|
||||||
application={application}
|
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||||
/>
|
application={application}
|
||||||
</Form.Item>
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -952,7 +1033,7 @@ class LoginPage extends React.Component {
|
|||||||
if (items.length > 1) {
|
if (items.length > 1) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tabs items={items} size={"small"} defaultActiveKey={this.getDefaultLoginMethod(application)} onChange={(key) => {
|
<Tabs className="signin-methods" items={items} size={"small"} defaultActiveKey={this.getDefaultLoginMethod(application)} onChange={(key) => {
|
||||||
this.setState({loginMethod: key});
|
this.setState({loginMethod: key});
|
||||||
}} centered>
|
}} centered>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@ -1047,10 +1128,10 @@ class LoginPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderBackButton() {
|
renderBackButton() {
|
||||||
if (this.state.orgChoiceMode === "None") {
|
if (this.state.orgChoiceMode === "None" || this.props.preview === "auto") {
|
||||||
return (
|
return (
|
||||||
<Button type="text" size="large" icon={<ArrowLeftOutlined />}
|
<Button type="text" size="large" icon={<ArrowLeftOutlined />}
|
||||||
style={{top: "65px", left: "15px", position: "absolute"}}
|
className="back-button"
|
||||||
onClick={() => history.back()}>
|
onClick={() => history.back()}>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -1099,16 +1180,6 @@ class LoginPage extends React.Component {
|
|||||||
<div className="login-form">
|
<div className="login-form">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{
|
|
||||||
Setting.renderHelmet(application)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
Setting.renderLogo(application)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.renderBackButton()
|
|
||||||
}
|
|
||||||
<LanguageSelect languages={application.organizationObj.languages} style={{top: "55px", right: "5px", position: "absolute"}} />
|
|
||||||
{
|
{
|
||||||
this.renderLoginPanel(application)
|
this.renderLoginPanel(application)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Col, Result, Row, Steps} from "antd";
|
import {Button, Col, Result, Row, Spin, Steps} from "antd";
|
||||||
import {withRouter} from "react-router-dom";
|
import {withRouter} from "react-router-dom";
|
||||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
@ -42,13 +42,20 @@ class MfaSetupPage extends React.Component {
|
|||||||
mfaProps: null,
|
mfaProps: null,
|
||||||
mfaType: params.get("mfaType") ?? SmsMfaType,
|
mfaType: params.get("mfaType") ?? SmsMfaType,
|
||||||
isPromptPage: props.isPromptPage || location.state?.from !== undefined,
|
isPromptPage: props.isPromptPage || location.state?.from !== undefined,
|
||||||
|
loading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.getApplication();
|
this.getApplication();
|
||||||
if (this.state.current === 1) {
|
if (this.state.current === 1) {
|
||||||
this.initMfaProps();
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.initMfaProps();
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +92,7 @@ class MfaSetupPage extends React.Component {
|
|||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
this.setState({
|
this.setState({
|
||||||
mfaProps: res.data,
|
mfaProps: res.data,
|
||||||
|
loading: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
|
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
|
||||||
@ -98,7 +106,7 @@ class MfaSetupPage extends React.Component {
|
|||||||
|
|
||||||
renderMfaTypeSwitch() {
|
renderMfaTypeSwitch() {
|
||||||
const renderSmsLink = () => {
|
const renderSmsLink = () => {
|
||||||
if (this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) {
|
if (this.state.mfaType === SmsMfaType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (<Button type={"link"} onClick={() => {
|
return (<Button type={"link"} onClick={() => {
|
||||||
@ -112,7 +120,7 @@ class MfaSetupPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderEmailLink = () => {
|
const renderEmailLink = () => {
|
||||||
if (this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) {
|
if (this.state.mfaType === EmailMfaType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (<Button type={"link"} onClick={() => {
|
return (<Button type={"link"} onClick={() => {
|
||||||
@ -126,7 +134,7 @@ class MfaSetupPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderTotpLink = () => {
|
const renderTotpLink = () => {
|
||||||
if (this.state.mfaType === TotpMfaType || this.props.account.totpSecret !== "") {
|
if (this.state.mfaType === TotpMfaType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (<Button type={"link"} onClick={() => {
|
return (<Button type={"link"} onClick={() => {
|
||||||
@ -191,7 +199,9 @@ class MfaSetupPage extends React.Component {
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
|
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
|
||||||
this.props.onfinish();
|
this.props.onfinish();
|
||||||
if (localStorage.getItem("mfaRedirectUrl") !== null) {
|
|
||||||
|
const mfaRedirectUrl = localStorage.getItem("mfaRedirectUrl");
|
||||||
|
if (mfaRedirectUrl !== undefined && mfaRedirectUrl !== null) {
|
||||||
Setting.goToLink(localStorage.getItem("mfaRedirectUrl"));
|
Setting.goToLink(localStorage.getItem("mfaRedirectUrl"));
|
||||||
localStorage.removeItem("mfaRedirectUrl");
|
localStorage.removeItem("mfaRedirectUrl");
|
||||||
} else {
|
} else {
|
||||||
@ -229,15 +239,17 @@ class MfaSetupPage extends React.Component {
|
|||||||
<p style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</p>
|
<p style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</p>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Steps current={this.state.current}
|
<Spin spinning={this.state.loading}>
|
||||||
items={[
|
<Steps current={this.state.current}
|
||||||
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
|
items={[
|
||||||
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
|
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
|
||||||
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
|
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
|
||||||
]}
|
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
|
||||||
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "50px",
|
]}
|
||||||
}} >
|
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "50px",
|
||||||
</Steps>
|
}} >
|
||||||
|
</Steps>
|
||||||
|
</Spin>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
|
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
|
||||||
<div style={{marginTop: "10px", textAlign: "center"}}>
|
<div style={{marginTop: "10px", textAlign: "center"}}>
|
||||||
|
@ -377,7 +377,7 @@ export function getProviderLogoWidget(provider) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthUrl(application, provider, method) {
|
export function getAuthUrl(application, provider, method, code) {
|
||||||
if (application === null || provider === null) {
|
if (application === null || provider === null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -418,6 +418,9 @@ export function getAuthUrl(application, provider, method) {
|
|||||||
if (navigator.userAgent.includes("MicroMessenger")) {
|
if (navigator.userAgent.includes("MicroMessenger")) {
|
||||||
return `${authInfo[provider.type].mpEndpoint}?appid=${provider.clientId2}&redirect_uri=${redirectUri}&state=${state}&scope=${authInfo[provider.type].mpScope}&response_type=code#wechat_redirect`;
|
return `${authInfo[provider.type].mpEndpoint}?appid=${provider.clientId2}&redirect_uri=${redirectUri}&state=${state}&scope=${authInfo[provider.type].mpScope}&response_type=code#wechat_redirect`;
|
||||||
} else {
|
} else {
|
||||||
|
if (provider.clientId2 && provider?.disableSsl && provider?.signName === "media") {
|
||||||
|
return `${window.location.origin}/callback?state=${state}&code=${"wechat_oa:" + code}`;
|
||||||
|
}
|
||||||
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}#wechat_redirect`;
|
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}#wechat_redirect`;
|
||||||
}
|
}
|
||||||
} else if (provider.type === "WeCom") {
|
} else if (provider.type === "WeCom") {
|
||||||
|
@ -43,6 +43,7 @@ import OktaLoginButton from "./OktaLoginButton";
|
|||||||
import DouyinLoginButton from "./DouyinLoginButton";
|
import DouyinLoginButton from "./DouyinLoginButton";
|
||||||
import LoginButton from "./LoginButton";
|
import LoginButton from "./LoginButton";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
|
import * as Setting from "../Setting";
|
||||||
import {getEvent} from "./Util";
|
import {getEvent} from "./Util";
|
||||||
import {Modal} from "antd";
|
import {Modal} from "antd";
|
||||||
|
|
||||||
@ -132,43 +133,52 @@ export function goToWeb3Url(application, provider, method) {
|
|||||||
export function renderProviderLogo(provider, application, width, margin, size, location) {
|
export function renderProviderLogo(provider, application, width, margin, size, location) {
|
||||||
if (size === "small") {
|
if (size === "small") {
|
||||||
if (provider.category === "OAuth") {
|
if (provider.category === "OAuth") {
|
||||||
if (provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.content !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger")) {
|
if (provider.type === "WeChat" && provider.clientId2 !== "" && provider.clientSecret2 !== "" && provider.disableSsl === true && !navigator.userAgent.includes("MicroMessenger")) {
|
||||||
const info = async() => {
|
const info = async() => {
|
||||||
const t1 = setInterval(await getEvent, 1000, application, provider);
|
AuthBackend.getWechatQRCode(`${provider.owner}/${provider.name}`).then(
|
||||||
{Modal.info({
|
async res => {
|
||||||
title: i18next.t("provider:Please use WeChat and scan the QR code to sign in"),
|
if (res.status !== "ok") {
|
||||||
content: (
|
Setting.showMessage("error", res?.msg);
|
||||||
<div>
|
return;
|
||||||
<img width={256} height={256} src = {"data:image/png;base64," + provider.content} alt="Wechat QR code" style={{margin: margin}} />
|
}
|
||||||
</div>
|
|
||||||
),
|
const t1 = setInterval(await getEvent, 1000, application, provider, res.data2);
|
||||||
onOk() {
|
{Modal.info({
|
||||||
window.clearInterval(t1);
|
title: i18next.t("provider:Please use WeChat to scan the QR code and follow the official account for sign in"),
|
||||||
},
|
content: (
|
||||||
});}
|
<div style={{marginRight: "34px"}}>
|
||||||
|
<img src = {"data:image/png;base64," + res.data} alt="Wechat QR code" style={{width: "100%"}} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
window.clearInterval(t1);
|
||||||
|
},
|
||||||
|
});}
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<a key={provider.displayName} >
|
<a key={provider.displayName} >
|
||||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} onClick={info} />
|
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={{margin: margin}} onClick={info} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")}>
|
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")}>
|
||||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={{margin: margin}} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (provider.category === "SAML") {
|
} else if (provider.category === "SAML") {
|
||||||
return (
|
return (
|
||||||
<a key={provider.displayName} onClick={() => goToSamlUrl(provider, location)}>
|
<a key={provider.displayName} onClick={() => goToSamlUrl(provider, location)}>
|
||||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={{margin: margin}} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (provider.category === "Web3") {
|
} else if (provider.category === "Web3") {
|
||||||
return (
|
return (
|
||||||
<a key={provider.displayName} onClick={() => goToWeb3Url(application, provider, "signup")}>
|
<a key={provider.displayName} onClick={() => goToWeb3Url(application, provider, "signup")}>
|
||||||
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={{margin: margin}} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -183,7 +193,7 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
|||||||
return (
|
return (
|
||||||
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")} style={customAStyle}>
|
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")} style={customAStyle}>
|
||||||
<button style={customButtonStyle}>
|
<button style={customButtonStyle}>
|
||||||
<img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} style={customImgStyle} />
|
<img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={customImgStyle} />
|
||||||
<span style={customSpanStyle}>{text}</span>
|
<span style={customSpanStyle}>{text}</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
@ -192,7 +202,7 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
|||||||
return (
|
return (
|
||||||
<a key={provider.displayName} onClick={() => goToSamlUrl(provider, location)} style={customAStyle}>
|
<a key={provider.displayName} onClick={() => goToSamlUrl(provider, location)} style={customAStyle}>
|
||||||
<button style={customButtonStyle}>
|
<button style={customButtonStyle}>
|
||||||
<img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} style={customImgStyle} />
|
<img width={26} src={getProviderLogoURL(provider)} alt={provider.displayName} className="provider-img" style={customImgStyle} />
|
||||||
<span style={customSpanStyle}>{text}</span>
|
<span style={customSpanStyle}>{text}</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
@ -202,7 +212,7 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
|||||||
// big button, for disable password signin
|
// big button, for disable password signin
|
||||||
if (provider.category === "SAML") {
|
if (provider.category === "SAML") {
|
||||||
return (
|
return (
|
||||||
<div key={provider.displayName} style={{marginBottom: "10px"}}>
|
<div key={provider.displayName} className="provider-big-img">
|
||||||
<a onClick={() => goToSamlUrl(provider, location)}>
|
<a onClick={() => goToSamlUrl(provider, location)}>
|
||||||
{
|
{
|
||||||
getSigninButton(provider)
|
getSigninButton(provider)
|
||||||
@ -212,7 +222,7 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
|||||||
);
|
);
|
||||||
} else if (provider.category === "Web3") {
|
} else if (provider.category === "Web3") {
|
||||||
return (
|
return (
|
||||||
<div key={provider.displayName} style={{marginBottom: "10px"}}>
|
<div key={provider.displayName} className="provider-big-img">
|
||||||
<a onClick={() => goToWeb3Url(application, provider, "signup")}>
|
<a onClick={() => goToWeb3Url(application, provider, "signup")}>
|
||||||
{
|
{
|
||||||
getSigninButton(provider)
|
getSigninButton(provider)
|
||||||
@ -222,7 +232,7 @@ export function renderProviderLogo(provider, application, width, margin, size, l
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div key={provider.displayName} style={{marginBottom: "10px"}}>
|
<div key={provider.displayName} className="provider-big-img">
|
||||||
<a href={Provider.getAuthUrl(application, provider, "signup")}>
|
<a href={Provider.getAuthUrl(application, provider, "signup")}>
|
||||||
{
|
{
|
||||||
getSigninButton(provider)
|
getSigninButton(provider)
|
||||||
|
@ -29,6 +29,7 @@ import LanguageSelect from "../common/select/LanguageSelect";
|
|||||||
import {withRouter} from "react-router-dom";
|
import {withRouter} from "react-router-dom";
|
||||||
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
|
import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
|
||||||
import * as PasswordChecker from "../common/PasswordChecker";
|
import * as PasswordChecker from "../common/PasswordChecker";
|
||||||
|
import * as InvitationBackend from "../backend/InvitationBackend";
|
||||||
|
|
||||||
const formItemLayout = {
|
const formItemLayout = {
|
||||||
labelCol: {
|
labelCol: {
|
||||||
@ -93,6 +94,15 @@ class SignupPage extends React.Component {
|
|||||||
if (this.getApplicationObj() === undefined) {
|
if (this.getApplicationObj() === undefined) {
|
||||||
if (this.state.applicationName !== null) {
|
if (this.state.applicationName !== null) {
|
||||||
this.getApplication(this.state.applicationName);
|
this.getApplication(this.state.applicationName);
|
||||||
|
|
||||||
|
const sp = new URLSearchParams(window.location.search);
|
||||||
|
if (sp.has("invitationCode")) {
|
||||||
|
const invitationCode = sp.get("invitationCode");
|
||||||
|
this.setState({invitationCode: invitationCode});
|
||||||
|
if (invitationCode !== "") {
|
||||||
|
this.getInvitationCodeInfo(invitationCode, "admin/" + this.state.applicationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (oAuthParams !== null) {
|
} else if (oAuthParams !== null) {
|
||||||
this.getApplicationLogin(oAuthParams);
|
this.getApplicationLogin(oAuthParams);
|
||||||
} else {
|
} else {
|
||||||
@ -133,6 +143,17 @@ class SignupPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInvitationCodeInfo(invitationCode, application) {
|
||||||
|
InvitationBackend.getInvitationCodeInfo(invitationCode, application)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === "error") {
|
||||||
|
Setting.showMessage("error", res.msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({invitation: res.data});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getResultPath(application, signupParams) {
|
getResultPath(application, signupParams) {
|
||||||
if (signupParams?.plan && signupParams?.pricing) {
|
if (signupParams?.plan && signupParams?.pricing) {
|
||||||
// the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page
|
// the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page
|
||||||
@ -235,7 +256,7 @@ class SignupPage extends React.Component {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder={signupItem.placeholder} />
|
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.username !== ""} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
} else if (signupItem.name === "Display name") {
|
} else if (signupItem.name === "Display name") {
|
||||||
@ -363,7 +384,7 @@ class SignupPage extends React.Component {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder={signupItem.placeholder} onChange={e => this.setState({email: e.target.value})} />
|
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{
|
{
|
||||||
signupItem.rule !== "No verification" &&
|
signupItem.rule !== "No verification" &&
|
||||||
@ -434,6 +455,7 @@ class SignupPage extends React.Component {
|
|||||||
<Input
|
<Input
|
||||||
placeholder={signupItem.placeholder}
|
placeholder={signupItem.placeholder}
|
||||||
style={{width: "65%"}}
|
style={{width: "65%"}}
|
||||||
|
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
|
||||||
onChange={e => this.setState({phone: e.target.value})}
|
onChange={e => this.setState({phone: e.target.value})}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -524,7 +546,7 @@ class SignupPage extends React.Component {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder={signupItem.placeholder} />
|
<Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation !== ""} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
} else if (signupItem.name === "Agreement") {
|
} else if (signupItem.name === "Agreement") {
|
||||||
@ -554,6 +576,20 @@ class SignupPage extends React.Component {
|
|||||||
</Result>
|
</Result>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (this.state.invitation !== undefined) {
|
||||||
|
if (this.state.invitation.username !== "") {
|
||||||
|
this.form.current?.setFieldValue("username", this.state.invitation.username);
|
||||||
|
}
|
||||||
|
if (this.state.invitation.email !== "") {
|
||||||
|
this.form.current?.setFieldValue("email", this.state.invitation.email);
|
||||||
|
}
|
||||||
|
if (this.state.invitation.phone !== "") {
|
||||||
|
this.form.current?.setFieldValue("phone", this.state.invitation.phone);
|
||||||
|
}
|
||||||
|
if (this.state.invitationCode !== "") {
|
||||||
|
this.form.current?.setFieldValue("invitationCode", this.state.invitationCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
|
@ -188,11 +188,12 @@ export function getQueryParamsFromState(state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEvent(application, provider) {
|
export function getEvent(application, provider, ticket) {
|
||||||
getWechatMessageEvent()
|
getWechatMessageEvent(ticket)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.data === "SCAN" || res.data === "subscribe") {
|
if (res.data === "SCAN" || res.data === "subscribe") {
|
||||||
Setting.goToLink(Provider.getAuthUrl(application, provider, "signup"));
|
const code = res?.data2;
|
||||||
|
Setting.goToLink(Provider.getAuthUrl(application, provider, "signup", code));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -89,8 +89,8 @@ export function deleteApplication(application) {
|
|||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSamlMetadata(owner, name) {
|
export function getSamlMetadata(owner, name, enablePostBinding) {
|
||||||
return fetch(`${Setting.ServerUrl}/api/saml/metadata?application=${owner}/${encodeURIComponent(name)}`, {
|
return fetch(`${Setting.ServerUrl}/api/saml/metadata?application=${owner}/${encodeURIComponent(name)}&enablePostBinding=${enablePostBinding}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user