forked from casdoor/casdoor
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23c86e9018 | ||
|
|
f088827a50 | ||
|
|
663815fefe | ||
|
|
0d003d347e | ||
|
|
7d495ca5f2 | ||
|
|
f89495b35c | ||
|
|
4a3aefc5f5 | ||
|
|
15646b23ff | ||
|
|
4b663a437f | ||
|
|
9fb90fbb95 | ||
|
|
65eeaef8a7 | ||
|
|
ecf8e2eb32 | ||
|
|
e49e678d16 | ||
|
|
623ee23285 | ||
|
|
0901a1d5a0 | ||
|
|
58ff2fe69c | ||
|
|
737f44a059 | ||
|
|
32cef8e828 | ||
|
|
9e854abc77 | ||
|
|
9b3343d3db | ||
|
|
5b71725c94 | ||
|
|
59b6854ccc | ||
|
|
0daf67c52c | ||
|
|
4b612269ea | ||
|
|
f438d39720 | ||
|
|
f8df200dbf | ||
|
|
cb1b3b767e | ||
|
|
3bec49f16c |
16
Dockerfile
16
Dockerfile
@@ -1,12 +1,24 @@
|
||||
FROM --platform=$BUILDPLATFORM node:18.19.0 AS FRONT
|
||||
WORKDIR /web
|
||||
COPY ./web .
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000 && NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
# Copy only dependency files first for better caching
|
||||
COPY ./web/package.json ./web/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000
|
||||
|
||||
# Copy source files and build
|
||||
COPY ./web .
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" yarn run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23.12 AS BACK
|
||||
WORKDIR /go/src/casdoor
|
||||
|
||||
# Copy only go.mod and go.sum first for dependency caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
RUN ./build.sh
|
||||
RUN go test -v -run TestGetVersionInfo ./util/system_test.go ./util/system.go > version_info.txt
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ p, *, *, POST, /api/upload-users, *, *
|
||||
p, *, *, GET, /api/get-resources, *, *
|
||||
p, *, *, GET, /api/get-records, *, *
|
||||
p, *, *, GET, /api/get-product, *, *
|
||||
p, *, *, GET, /api/get-order, *, *
|
||||
p, *, *, GET, /api/get-orders, *, *
|
||||
p, *, *, GET, /api/get-user-orders, *, *
|
||||
p, *, *, GET, /api/get-payment, *, *
|
||||
p, *, *, POST, /api/update-payment, *, *
|
||||
p, *, *, POST, /api/invoice-payment, *, *
|
||||
@@ -132,7 +135,15 @@ p, *, *, GET, /api/faceid-signin-begin, *, *
|
||||
}
|
||||
}
|
||||
|
||||
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
|
||||
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string, extraInfo map[string]interface{}) bool {
|
||||
if urlPath == "/api/mcp" {
|
||||
if detailPath, ok := extraInfo["detailPathUrl"].(string); ok {
|
||||
if detailPath == "initialize" || detailPath == "notifications/initialized" || detailPath == "ping" || detailPath == "tools/list" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conf.IsDemoMode() {
|
||||
if !isAllowedInDemoMode(subOwner, subName, method, urlPath, objOwner, objName) {
|
||||
return false
|
||||
|
||||
@@ -467,15 +467,17 @@ func (c *ApiController) SsoLogout() {
|
||||
var tokens []*object.Token
|
||||
var sessionIds []string
|
||||
|
||||
// Get tokens for notification (needed for both session-level and full logout)
|
||||
// This enables subsystems to identify and invalidate corresponding access tokens
|
||||
// Note: Tokens must be retrieved BEFORE expiration to include their hashes in the notification
|
||||
tokens, err = object.GetTokensByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if logoutAllSessions {
|
||||
// Logout from all sessions: expire all tokens and delete all sessions
|
||||
// Get tokens before expiring them (for session-level logout notification)
|
||||
tokens, err = object.GetTokensByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = object.ExpireTokenByUser(owner, username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -105,6 +105,13 @@ func (c *ApiController) getCurrentUser() *object.User {
|
||||
|
||||
// GetSessionUsername ...
|
||||
func (c *ApiController) GetSessionUsername() string {
|
||||
// prefer username stored in Beego context by ApiFilter
|
||||
if ctxUser := c.Ctx.Input.GetData("currentUserId"); ctxUser != nil {
|
||||
if username, ok := ctxUser.(string); ok {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
// check if user session expired
|
||||
sessionData := c.GetSessionData()
|
||||
|
||||
|
||||
@@ -39,7 +39,26 @@ func (c *ApiController) GetOrders() {
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
orders, err := object.GetOrders(owner)
|
||||
var orders []*object.Order
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
orders, err = object.GetUserOrders(owner, value)
|
||||
} else {
|
||||
orders, err = object.GetOrders(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
orders, err = object.GetUserOrders(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,6 +67,16 @@ func (c *ApiController) GetOrders() {
|
||||
c.ResponseOk(orders)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetOrderCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -39,7 +39,26 @@ func (c *ApiController) GetPayments() {
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
payments, err := object.GetPayments(owner)
|
||||
var payments []*object.Payment
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
payments, err = object.GetUserPayments(owner, value)
|
||||
} else {
|
||||
payments, err = object.GetPayments(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
payments, err = object.GetUserPayments(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,6 +67,16 @@ func (c *ApiController) GetPayments() {
|
||||
c.ResponseOk(payments)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetPaymentCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -17,6 +17,7 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
)
|
||||
@@ -62,6 +63,7 @@ func (c *ApiController) HandleSamlRedirect() {
|
||||
username := c.Ctx.Input.Query("username")
|
||||
loginHint := c.Ctx.Input.Query("login_hint")
|
||||
|
||||
relayState = url.QueryEscape(relayState)
|
||||
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host, username, loginHint)
|
||||
|
||||
c.Redirect(targetURL, http.StatusSeeOther)
|
||||
|
||||
@@ -39,7 +39,26 @@ func (c *ApiController) GetSubscriptions() {
|
||||
sortOrder := c.Ctx.Input.Query("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
subscriptions, err := object.GetSubscriptions(owner)
|
||||
var subscriptions []*object.Subscription
|
||||
var err error
|
||||
|
||||
if c.IsAdmin() {
|
||||
// If field is "user", filter by that user even for admins
|
||||
if field == "user" && value != "" {
|
||||
subscriptions, err = object.GetSubscriptionsByUser(owner, value)
|
||||
} else {
|
||||
subscriptions, err = object.GetSubscriptions(owner)
|
||||
}
|
||||
} else {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
subscriptions, err = object.GetSubscriptionsByUser(owner, userName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@@ -48,6 +67,16 @@ func (c *ApiController) GetSubscriptions() {
|
||||
c.ResponseOk(subscriptions)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
if !c.IsAdmin() {
|
||||
user := c.GetSessionUsername()
|
||||
_, userName, userErr := util.GetOwnerAndNameFromIdWithError(user)
|
||||
if userErr != nil {
|
||||
c.ResponseError(userErr.Error())
|
||||
return
|
||||
}
|
||||
field = "user"
|
||||
value = userName
|
||||
}
|
||||
count, err := object.GetSubscriptionCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
|
||||
@@ -778,6 +778,78 @@ func (c *ApiController) RemoveUserFromGroup() {
|
||||
c.ResponseOk(affected)
|
||||
}
|
||||
|
||||
// ImpersonateUser
|
||||
// @Title ImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description set impersonation user for current admin session
|
||||
// @Param username formData string true "The username to impersonate (owner/name)"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /impersonation-user [post]
|
||||
func (c *ApiController) ImpersonateUser() {
|
||||
org, ok := c.RequireAdmin()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
username := c.Ctx.Request.Form.Get("username")
|
||||
if username == "" {
|
||||
c.ResponseError(c.T("general:Missing parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
owner, _, err := util.GetOwnerAndNameFromIdWithError(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !(owner == org || org == "") {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
targetUser, err := object.GetUser(username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if targetUser == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), username))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.SetSession("impersonateUser", username)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", username, 0, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// ExitImpersonateUser
|
||||
// @Title ExitImpersonateUser
|
||||
// @Tag User API
|
||||
// @Description clear impersonation info for current session
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /exit-impersonation-user [post]
|
||||
func (c *ApiController) ExitImpersonateUser() {
|
||||
_, ok := c.Ctx.Input.GetData("impersonating").(bool)
|
||||
if !ok {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
err := c.SetSession("impersonateUser", "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Ctx.SetCookie("impersonateUser", "", -1, "/")
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
||||
// VerifyIdentification
|
||||
// @Title VerifyIdentification
|
||||
// @Tag User API
|
||||
|
||||
17
go.mod
17
go.mod
@@ -14,6 +14,8 @@ require (
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0
|
||||
github.com/alibabacloud-go/tea v1.3.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
|
||||
github.com/aliyun/credentials-go v1.3.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/beego/beego/v2 v2.3.8
|
||||
github.com/beevik/etree v1.1.0
|
||||
@@ -57,7 +59,7 @@ require (
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/shirou/gopsutil/v4 v4.25.9
|
||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stripe/stripe-go/v74 v74.29.0
|
||||
@@ -110,8 +112,6 @@ require (
|
||||
github.com/alibabacloud-go/tea-utils v1.3.6 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.545 // indirect
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||
github.com/aliyun/credentials-go v1.3.10 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.45.5 // indirect
|
||||
@@ -139,6 +139,7 @@ require (
|
||||
github.com/di-wu/parser v0.2.2 // indirect
|
||||
github.com/di-wu/xsd-datetime v1.0.0 // indirect
|
||||
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||
@@ -187,6 +188,7 @@ require (
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
@@ -204,6 +206,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||
@@ -230,15 +233,15 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.117 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mau.fi/util v0.8.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect
|
||||
@@ -256,7 +259,7 @@ require (
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
|
||||
27
go.sum
27
go.sum
@@ -938,6 +938,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
|
||||
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
@@ -1380,6 +1382,8 @@ github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZyc
|
||||
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
|
||||
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 h1:wIONC+HMNRqmWBjuMxhatuSzHaljStc4gjDeKycxy0A=
|
||||
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
@@ -1511,6 +1515,8 @@ github.com/polarsource/polar-go v0.12.0 h1:um+6ftOPUMg2TQq9Kv/6fKGBOAl7dOc2YiDdx
|
||||
github.com/polarsource/polar-go v0.12.0/go.mod h1:FB11Q4m2n3wIk6l/POOkz0MVOUx1o0Yt4Y97MnQfe0c=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -1588,8 +1594,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
|
||||
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
@@ -1675,10 +1681,10 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
|
||||
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
|
||||
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
|
||||
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/twilio/twilio-go v1.13.0 h1:8uKXSWAgCvO9Kn12iboy3x/Pw7oxPBufs94fTWQGhLk=
|
||||
github.com/twilio/twilio-go v1.13.0/go.mod h1:tdnfQ5TjbewoAu4lf9bMsGvfuJ/QU9gYuv9yx3TSIXU=
|
||||
@@ -1714,8 +1720,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
@@ -2085,6 +2091,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -2155,8 +2162,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
||||
@@ -183,7 +183,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
corpMobile, corpEmail, jobNumber, err := idp.getUserCorpEmail(userId, corpAccessToken)
|
||||
corpMobile, corpEmail, unionId, err := idp.getUserCorpEmail(userId, corpAccessToken)
|
||||
if err == nil {
|
||||
if corpMobile != "" {
|
||||
userInfo.Phone = corpMobile
|
||||
@@ -193,8 +193,8 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
userInfo.Email = corpEmail
|
||||
}
|
||||
|
||||
if jobNumber != "" {
|
||||
userInfo.Username = jobNumber
|
||||
if unionId != "" {
|
||||
userInfo.Username = unionId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,9 +284,9 @@ func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken strin
|
||||
var data struct {
|
||||
ErrMessage string `json:"errmsg"`
|
||||
Result struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
JobNumber string `json:"job_number"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
UnionId string `json:"unionid"`
|
||||
} `json:"result"`
|
||||
}
|
||||
err = json.Unmarshal(respBytes, &data)
|
||||
@@ -296,5 +296,5 @@ func (idp *DingTalkIdProvider) getUserCorpEmail(userId string, accessToken strin
|
||||
if data.ErrMessage != "ok" {
|
||||
return "", "", "", fmt.Errorf(data.ErrMessage)
|
||||
}
|
||||
return data.Result.Mobile, data.Result.Email, data.Result.JobNumber, nil
|
||||
return data.Result.Mobile, data.Result.Email, data.Result.UnionId, nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -65,7 +66,7 @@ func (idp *TelegramIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
}
|
||||
|
||||
// Create a token with the user ID as access token
|
||||
userId, ok := authData["id"].(float64)
|
||||
userId, ok := telegramAsInt64(authData["id"])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid user id in auth data")
|
||||
}
|
||||
@@ -77,7 +78,7 @@ func (idp *TelegramIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: fmt.Sprintf("telegram_%d", int64(userId)),
|
||||
AccessToken: fmt.Sprintf("telegram_%d", userId),
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
|
||||
@@ -97,6 +98,7 @@ func (idp *TelegramIdProvider) verifyTelegramAuth(authData map[string]interface{
|
||||
if !ok {
|
||||
return fmt.Errorf("hash not found in auth data")
|
||||
}
|
||||
hash = strings.TrimSpace(hash)
|
||||
|
||||
// Prepare data check string
|
||||
var dataCheckArr []string
|
||||
@@ -104,13 +106,14 @@ func (idp *TelegramIdProvider) verifyTelegramAuth(authData map[string]interface{
|
||||
if key == "hash" {
|
||||
continue
|
||||
}
|
||||
dataCheckArr = append(dataCheckArr, fmt.Sprintf("%s=%v", key, value))
|
||||
dataCheckArr = append(dataCheckArr, fmt.Sprintf("%s=%s", key, telegramAsString(value)))
|
||||
}
|
||||
sort.Strings(dataCheckArr)
|
||||
dataCheckString := strings.Join(dataCheckArr, "\n")
|
||||
|
||||
// Calculate secret key
|
||||
secretKey := sha256.Sum256([]byte(idp.ClientSecret))
|
||||
clientSecret := strings.TrimSpace(idp.ClientSecret)
|
||||
secretKey := sha256.Sum256([]byte(clientSecret))
|
||||
|
||||
// Calculate hash
|
||||
h := hmac.New(sha256.New, secretKey[:])
|
||||
@@ -139,7 +142,7 @@ func (idp *TelegramIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
}
|
||||
|
||||
// Extract user information from auth data
|
||||
userId, ok := authData["id"].(float64)
|
||||
userId, ok := telegramAsInt64(authData["id"])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid user id in auth data")
|
||||
}
|
||||
@@ -155,11 +158,11 @@ func (idp *TelegramIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
displayName = username
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = strconv.FormatInt(int64(userId), 10)
|
||||
displayName = strconv.FormatInt(userId, 10)
|
||||
}
|
||||
|
||||
userInfo := UserInfo{
|
||||
Id: strconv.FormatInt(int64(userId), 10),
|
||||
Id: strconv.FormatInt(userId, 10),
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
AvatarUrl: photoUrl,
|
||||
@@ -167,3 +170,38 @@ func (idp *TelegramIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
func telegramAsInt64(v interface{}) (int64, bool) {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
if t != math.Trunc(t) {
|
||||
return 0, false
|
||||
}
|
||||
if t > float64(math.MaxInt64) || t < float64(math.MinInt64) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(t), true
|
||||
case string:
|
||||
i, err := strconv.ParseInt(t, 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func telegramAsString(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t
|
||||
case float64:
|
||||
if t == math.Trunc(t) && t <= float64(math.MaxInt64) && t >= float64(math.MinInt64) {
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
}
|
||||
return strconv.FormatFloat(t, 'g', -1, 64)
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
// handleGetApplicationsTool handles the get_applications MCP tool
|
||||
func (c *McpController) handleGetApplicationsTool(id string, args GetApplicationsArgs) {
|
||||
func (c *McpController) handleGetApplicationsTool(id interface{}, args GetApplicationsArgs) {
|
||||
userId := c.GetSessionUsername()
|
||||
|
||||
applications, err := object.GetApplications(args.Owner)
|
||||
@@ -37,7 +37,7 @@ func (c *McpController) handleGetApplicationsTool(id string, args GetApplication
|
||||
}
|
||||
|
||||
// handleGetApplicationTool handles the get_application MCP tool
|
||||
func (c *McpController) handleGetApplicationTool(id string, args GetApplicationArgs) {
|
||||
func (c *McpController) handleGetApplicationTool(id interface{}, args GetApplicationArgs) {
|
||||
userId := c.GetSessionUsername()
|
||||
|
||||
application, err := object.GetApplication(args.Id)
|
||||
@@ -51,7 +51,7 @@ func (c *McpController) handleGetApplicationTool(id string, args GetApplicationA
|
||||
}
|
||||
|
||||
// handleAddApplicationTool handles the add_application MCP tool
|
||||
func (c *McpController) handleAddApplicationTool(id string, args AddApplicationArgs) {
|
||||
func (c *McpController) handleAddApplicationTool(id interface{}, args AddApplicationArgs) {
|
||||
count, err := object.GetApplicationCount("", "", "")
|
||||
if err != nil {
|
||||
c.SendToolErrorResult(id, err.Error())
|
||||
@@ -78,7 +78,7 @@ func (c *McpController) handleAddApplicationTool(id string, args AddApplicationA
|
||||
}
|
||||
|
||||
// handleUpdateApplicationTool handles the update_application MCP tool
|
||||
func (c *McpController) handleUpdateApplicationTool(id string, args UpdateApplicationArgs) {
|
||||
func (c *McpController) handleUpdateApplicationTool(id interface{}, args UpdateApplicationArgs) {
|
||||
if err := object.CheckIpWhitelist(args.Application.IpWhitelist, c.GetAcceptLanguage()); err != nil {
|
||||
c.SendToolErrorResult(id, err.Error())
|
||||
return
|
||||
@@ -94,7 +94,7 @@ func (c *McpController) handleUpdateApplicationTool(id string, args UpdateApplic
|
||||
}
|
||||
|
||||
// handleDeleteApplicationTool handles the delete_application MCP tool
|
||||
func (c *McpController) handleDeleteApplicationTool(id string, args DeleteApplicationArgs) {
|
||||
func (c *McpController) handleDeleteApplicationTool(id interface{}, args DeleteApplicationArgs) {
|
||||
affected, err := object.DeleteApplication(&args.Application)
|
||||
if err != nil {
|
||||
c.SendToolErrorResult(id, err.Error())
|
||||
|
||||
13
mcp/auth.go
13
mcp/auth.go
@@ -26,11 +26,17 @@ type SessionData struct {
|
||||
ExpireTime int64
|
||||
}
|
||||
|
||||
// GetSessionUsername returns the username from session
|
||||
// GetSessionUsername returns the username from session or ctx
|
||||
func (c *McpController) GetSessionUsername() string {
|
||||
// check if user session expired
|
||||
sessionData := c.GetSessionData()
|
||||
// prefer username stored in Beego context by ApiFilter
|
||||
if ctxUser := c.Ctx.Input.GetData("currentUserId"); ctxUser != nil {
|
||||
if username, ok := ctxUser.(string); ok {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to previous session-based logic with expiry check
|
||||
sessionData := c.GetSessionData()
|
||||
if sessionData != nil &&
|
||||
sessionData.ExpireTime != 0 &&
|
||||
sessionData.ExpireTime < time.Now().Unix() {
|
||||
@@ -77,6 +83,7 @@ func (c *McpController) IsGlobalAdmin() bool {
|
||||
|
||||
func (c *McpController) isGlobalAdmin() (bool, *object.User) {
|
||||
username := c.GetSessionUsername()
|
||||
|
||||
if object.IsAppUser(username) {
|
||||
// e.g., "app/app-casnode"
|
||||
return true, nil
|
||||
|
||||
105
mcp/base.go
105
mcp/base.go
@@ -17,6 +17,7 @@ package mcp
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@@ -102,11 +103,11 @@ type DeleteApplicationArgs struct {
|
||||
}
|
||||
|
||||
type McpCallToolResult struct {
|
||||
Content []McpContent `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
Content []TextContent `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
type McpContent struct {
|
||||
type TextContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
@@ -116,55 +117,61 @@ type McpController struct {
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// SendMcpResponse sends a successful MCP response
|
||||
func (c *McpController) SendMcpResponse(id interface{}, result interface{}) {
|
||||
func (c *McpController) Prepare() {
|
||||
c.EnableRender = false
|
||||
}
|
||||
|
||||
func (c *McpController) McpResponseOk(id interface{}, result interface{}) {
|
||||
resp := BuildMcpResponse(id, result, nil)
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *McpController) McpResponseError(id interface{}, code int, message string, data interface{}) {
|
||||
resp := BuildMcpResponse(id, nil, &McpError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
c.Ctx.Output.Header("Content-Type", "application/json")
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetMcpResponse returns a McpResponse object
|
||||
func BuildMcpResponse(id interface{}, result interface{}, err *McpError) McpResponse {
|
||||
resp := McpResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: result,
|
||||
Error: err,
|
||||
}
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// SendMcpError sends an MCP error response
|
||||
func (c *McpController) SendMcpError(id interface{}, code int, message string, data interface{}) {
|
||||
resp := McpResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &McpError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
return resp
|
||||
}
|
||||
|
||||
// sendInvalidParamsError sends an invalid params error
|
||||
func (c *McpController) sendInvalidParamsError(id interface{}, details string) {
|
||||
c.SendMcpError(id, -32602, "Invalid params", details)
|
||||
c.McpResponseError(id, -32602, "Invalid params", details)
|
||||
}
|
||||
|
||||
// SendToolResult sends a successful tool execution result
|
||||
func (c *McpController) SendToolResult(id interface{}, text string) {
|
||||
result := McpCallToolResult{
|
||||
Content: []McpContent{
|
||||
Content: []TextContent{
|
||||
{
|
||||
Type: "text",
|
||||
Text: text,
|
||||
},
|
||||
},
|
||||
IsError: false,
|
||||
}
|
||||
c.SendMcpResponse(id, result)
|
||||
c.McpResponseOk(id, result)
|
||||
}
|
||||
|
||||
// SendToolErrorResult sends a tool execution error result
|
||||
func (c *McpController) SendToolErrorResult(id interface{}, errorMsg string) {
|
||||
result := McpCallToolResult{
|
||||
Content: []McpContent{
|
||||
Content: []TextContent{
|
||||
{
|
||||
Type: "text",
|
||||
Text: errorMsg,
|
||||
@@ -172,7 +179,7 @@ func (c *McpController) SendToolErrorResult(id interface{}, errorMsg string) {
|
||||
},
|
||||
IsError: true,
|
||||
}
|
||||
c.SendMcpResponse(id, result)
|
||||
c.McpResponseOk(id, result)
|
||||
}
|
||||
|
||||
// FormatOperationResult formats the result of CRUD operations in a clear, descriptive way
|
||||
@@ -202,7 +209,7 @@ func (c *McpController) HandleMcp() {
|
||||
var req McpRequest
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
c.SendMcpError(nil, -32700, "Parse error", err.Error())
|
||||
c.McpResponseError(nil, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -210,12 +217,16 @@ func (c *McpController) HandleMcp() {
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
c.handleInitialize(req)
|
||||
case "notifications/initialized":
|
||||
c.handleNotificationsInitialized(req)
|
||||
case "ping":
|
||||
c.handlePing(req)
|
||||
case "tools/list":
|
||||
c.handleToolsList(req)
|
||||
case "tools/call":
|
||||
c.handleToolsCall(req)
|
||||
default:
|
||||
c.SendMcpError(req.ID, -32601, "Method not found", fmt.Sprintf("Method '%s' not found", req.Method))
|
||||
c.McpResponseError(req.ID, -32601, "Method not found", fmt.Sprintf("Method '%s' not found", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +243,9 @@ func (c *McpController) handleInitialize(req McpRequest) {
|
||||
result := McpInitializeResult{
|
||||
ProtocolVersion: "2024-11-05",
|
||||
Capabilities: McpServerCapabilities{
|
||||
Tools: map[string]interface{}{},
|
||||
Tools: map[string]interface{}{
|
||||
"listChanged": true,
|
||||
},
|
||||
},
|
||||
ServerInfo: McpImplementation{
|
||||
Name: "Casdoor MCP Server",
|
||||
@@ -240,7 +253,18 @@ func (c *McpController) handleInitialize(req McpRequest) {
|
||||
},
|
||||
}
|
||||
|
||||
c.SendMcpResponse(req.ID, result)
|
||||
c.McpResponseOk(req.ID, result)
|
||||
}
|
||||
|
||||
func (c *McpController) handleNotificationsInitialized(req McpRequest) {
|
||||
c.Ctx.Output.SetStatus(http.StatusAccepted)
|
||||
c.Ctx.Output.Body([]byte{})
|
||||
}
|
||||
|
||||
func (c *McpController) handlePing(req McpRequest) {
|
||||
// ping method is used to check if the server is alive and responsive
|
||||
// Return an empty object as result to indicate server is active
|
||||
c.McpResponseOk(req.ID, map[string]interface{}{})
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsList(req McpRequest) {
|
||||
@@ -325,7 +349,7 @@ func (c *McpController) handleToolsList(req McpRequest) {
|
||||
Tools: tools,
|
||||
}
|
||||
|
||||
c.SendMcpResponse(req.ID, result)
|
||||
c.McpResponseOk(req.ID, result)
|
||||
}
|
||||
|
||||
func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
@@ -336,9 +360,6 @@ func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert ID to string for tool handlers
|
||||
idStr := fmt.Sprintf("%v", req.ID)
|
||||
|
||||
// Route to the appropriate tool handler
|
||||
switch params.Name {
|
||||
case "get_applications":
|
||||
@@ -347,36 +368,36 @@ func (c *McpController) handleToolsCall(req McpRequest) {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleGetApplicationsTool(idStr, args)
|
||||
c.handleGetApplicationsTool(req.ID, args)
|
||||
case "get_application":
|
||||
var args GetApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleGetApplicationTool(idStr, args)
|
||||
c.handleGetApplicationTool(req.ID, args)
|
||||
case "add_application":
|
||||
var args AddApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleAddApplicationTool(idStr, args)
|
||||
c.handleAddApplicationTool(req.ID, args)
|
||||
case "update_application":
|
||||
var args UpdateApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleUpdateApplicationTool(idStr, args)
|
||||
c.handleUpdateApplicationTool(req.ID, args)
|
||||
case "delete_application":
|
||||
var args DeleteApplicationArgs
|
||||
if err := json.Unmarshal(params.Arguments, &args); err != nil {
|
||||
c.sendInvalidParamsError(req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
c.handleDeleteApplicationTool(idStr, args)
|
||||
c.handleDeleteApplicationTool(req.ID, args)
|
||||
default:
|
||||
c.SendMcpError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
|
||||
c.McpResponseError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,45 +71,46 @@ type Application struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Logo string `xorm:"varchar(200)" json:"logo"`
|
||||
Title string `xorm:"varchar(100)" json:"title"`
|
||||
Favicon string `xorm:"varchar(200)" json:"favicon"`
|
||||
Order int `json:"order"`
|
||||
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
||||
EnablePassword bool `json:"enablePassword"`
|
||||
EnableSignUp bool `json:"enableSignUp"`
|
||||
DisableSignin bool `json:"disableSignin"`
|
||||
EnableSigninSession bool `json:"enableSigninSession"`
|
||||
EnableAutoSignin bool `json:"enableAutoSignin"`
|
||||
EnableCodeSignin bool `json:"enableCodeSignin"`
|
||||
EnableExclusiveSignin bool `json:"enableExclusiveSignin"`
|
||||
EnableSamlCompress bool `json:"enableSamlCompress"`
|
||||
EnableSamlC14n10 bool `json:"enableSamlC14n10"`
|
||||
EnableSamlPostBinding bool `json:"enableSamlPostBinding"`
|
||||
DisableSamlAttributes bool `json:"disableSamlAttributes"`
|
||||
UseEmailAsSamlNameId bool `json:"useEmailAsSamlNameId"`
|
||||
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
|
||||
OrgChoiceMode string `json:"orgChoiceMode"`
|
||||
SamlReplyUrl string `xorm:"varchar(500)" json:"samlReplyUrl"`
|
||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
|
||||
SignupItems []*SignupItem `xorm:"varchar(3000)" json:"signupItems"`
|
||||
SigninItems []*SigninItem `xorm:"mediumtext" json:"signinItems"`
|
||||
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
||||
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
|
||||
CertPublicKey string `xorm:"-" json:"certPublicKey"`
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
|
||||
SamlHashAlgorithm string `xorm:"varchar(20)" json:"samlHashAlgorithm"`
|
||||
IsShared bool `json:"isShared"`
|
||||
IpRestriction string `json:"ipRestriction"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Logo string `xorm:"varchar(200)" json:"logo"`
|
||||
Title string `xorm:"varchar(100)" json:"title"`
|
||||
Favicon string `xorm:"varchar(200)" json:"favicon"`
|
||||
Order int `json:"order"`
|
||||
HomepageUrl string `xorm:"varchar(100)" json:"homepageUrl"`
|
||||
Description string `xorm:"varchar(100)" json:"description"`
|
||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
DefaultGroup string `xorm:"varchar(100)" json:"defaultGroup"`
|
||||
HeaderHtml string `xorm:"mediumtext" json:"headerHtml"`
|
||||
EnablePassword bool `json:"enablePassword"`
|
||||
EnableSignUp bool `json:"enableSignUp"`
|
||||
DisableSignin bool `json:"disableSignin"`
|
||||
EnableSigninSession bool `json:"enableSigninSession"`
|
||||
EnableAutoSignin bool `json:"enableAutoSignin"`
|
||||
EnableCodeSignin bool `json:"enableCodeSignin"`
|
||||
EnableExclusiveSignin bool `json:"enableExclusiveSignin"`
|
||||
EnableSamlCompress bool `json:"enableSamlCompress"`
|
||||
EnableSamlC14n10 bool `json:"enableSamlC14n10"`
|
||||
EnableSamlPostBinding bool `json:"enableSamlPostBinding"`
|
||||
DisableSamlAttributes bool `json:"disableSamlAttributes"`
|
||||
EnableSamlAssertionSignature bool `json:"enableSamlAssertionSignature"`
|
||||
UseEmailAsSamlNameId bool `json:"useEmailAsSamlNameId"`
|
||||
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||
EnableLinkWithEmail bool `json:"enableLinkWithEmail"`
|
||||
OrgChoiceMode string `json:"orgChoiceMode"`
|
||||
SamlReplyUrl string `xorm:"varchar(500)" json:"samlReplyUrl"`
|
||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
|
||||
SignupItems []*SignupItem `xorm:"varchar(3000)" json:"signupItems"`
|
||||
SigninItems []*SigninItem `xorm:"mediumtext" json:"signinItems"`
|
||||
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
||||
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
|
||||
CertPublicKey string `xorm:"-" json:"certPublicKey"`
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
SamlAttributes []*SamlItem `xorm:"varchar(1000)" json:"samlAttributes"`
|
||||
SamlHashAlgorithm string `xorm:"varchar(20)" json:"samlHashAlgorithm"`
|
||||
IsShared bool `json:"isShared"`
|
||||
IpRestriction string `json:"ipRestriction"`
|
||||
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
|
||||
@@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
@@ -28,8 +29,8 @@ type Order struct {
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
|
||||
// Product Info
|
||||
ProductName string `xorm:"varchar(100)" json:"productName"`
|
||||
Products []string `xorm:"varchar(1000)" json:"products"` // Future support for multiple products per order. Using varchar(1000) for simple JSON array storage; can be refactored to separate table if needed
|
||||
Products []string `xorm:"varchar(1000)" json:"products"` // Support for multiple products per order. Using varchar(1000) for simple JSON array storage; can be refactored to separate table if needed
|
||||
ProductInfos []ProductInfo `xorm:"varchar(2000)" json:"productInfos"`
|
||||
|
||||
// Subscription Info (for subscription orders)
|
||||
PricingName string `xorm:"varchar(100)" json:"pricingName"`
|
||||
@@ -52,6 +53,15 @@ type Order struct {
|
||||
EndTime string `xorm:"varchar(100)" json:"endTime"`
|
||||
}
|
||||
|
||||
type ProductInfo struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Image string `json:"image"`
|
||||
Detail string `json:"detail"`
|
||||
Price float64 `json:"price"`
|
||||
IsRecharge bool `json:"isRecharge"`
|
||||
}
|
||||
|
||||
func GetOrderCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Order{Owner: owner})
|
||||
@@ -119,12 +129,46 @@ func UpdateOrder(id string, order *Order) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if o, err := getOrder(owner, name); err != nil {
|
||||
|
||||
var o *Order
|
||||
if o, err = getOrder(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if o == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !slices.Equal(o.Products, order.Products) {
|
||||
existingInfos := make(map[string]ProductInfo, len(o.ProductInfos))
|
||||
for _, info := range o.ProductInfos {
|
||||
existingInfos[info.Name] = info
|
||||
}
|
||||
|
||||
productInfos := make([]ProductInfo, 0, len(order.Products))
|
||||
products, err := getOrderProducts(owner, order.Products)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
price := 0.0
|
||||
for _, product := range products {
|
||||
productInfo := ProductInfo{
|
||||
Name: product.Name,
|
||||
DisplayName: product.DisplayName,
|
||||
Image: product.Image,
|
||||
Detail: product.Detail,
|
||||
Price: product.Price,
|
||||
IsRecharge: product.IsRecharge,
|
||||
}
|
||||
if existingInfo, ok := existingInfos[product.Name]; ok {
|
||||
// Keep historical product info; do not overwrite with current product.
|
||||
productInfo = existingInfo
|
||||
}
|
||||
price += productInfo.Price
|
||||
productInfos = append(productInfos, productInfo)
|
||||
}
|
||||
order.ProductInfos = productInfos
|
||||
order.Price = price
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(order)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -16,6 +16,7 @@ package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/idp"
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
@@ -35,16 +36,6 @@ func PlaceOrder(productId string, user *User, pricingName string, planName strin
|
||||
return nil, fmt.Errorf("the product: %s is out of stock", product.Name)
|
||||
}
|
||||
|
||||
userBalanceCurrency := user.BalanceCurrency
|
||||
if userBalanceCurrency == "" {
|
||||
org, err := getOrganization("admin", user.Owner)
|
||||
if err == nil && org != nil && org.BalanceCurrency != "" {
|
||||
userBalanceCurrency = org.BalanceCurrency
|
||||
} else {
|
||||
userBalanceCurrency = "USD"
|
||||
}
|
||||
}
|
||||
|
||||
productCurrency := product.Currency
|
||||
if productCurrency == "" {
|
||||
productCurrency = "USD"
|
||||
@@ -59,26 +50,37 @@ func PlaceOrder(productId string, user *User, pricingName string, planName strin
|
||||
} else {
|
||||
productPrice = product.Price
|
||||
}
|
||||
price := ConvertCurrency(productPrice, productCurrency, userBalanceCurrency)
|
||||
|
||||
orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
|
||||
productNames := []string{product.Name}
|
||||
productInfos := []ProductInfo{
|
||||
{
|
||||
Name: product.Name,
|
||||
DisplayName: product.DisplayName,
|
||||
Image: product.Image,
|
||||
Detail: product.Detail,
|
||||
Price: productPrice,
|
||||
IsRecharge: product.IsRecharge,
|
||||
},
|
||||
}
|
||||
|
||||
order := &Order{
|
||||
Owner: product.Owner,
|
||||
Name: orderName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: fmt.Sprintf("Order for %s", product.DisplayName),
|
||||
ProductName: product.Name,
|
||||
Products: []string{product.Name},
|
||||
PricingName: pricingName,
|
||||
PlanName: planName,
|
||||
User: user.Name,
|
||||
Payment: "", // Payment will be set when user pays
|
||||
Price: price,
|
||||
Currency: userBalanceCurrency,
|
||||
State: "Created",
|
||||
Message: "",
|
||||
StartTime: util.GetCurrentTime(),
|
||||
EndTime: "",
|
||||
Owner: product.Owner,
|
||||
Name: orderName,
|
||||
DisplayName: orderName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Products: productNames,
|
||||
ProductInfos: productInfos,
|
||||
PricingName: pricingName,
|
||||
PlanName: planName,
|
||||
User: user.Name,
|
||||
Payment: "", // Payment will be set when user pays
|
||||
Price: productPrice,
|
||||
Currency: productCurrency,
|
||||
State: "Created",
|
||||
Message: "",
|
||||
StartTime: util.GetCurrentTime(),
|
||||
EndTime: "",
|
||||
}
|
||||
|
||||
affected, err := AddOrder(order)
|
||||
@@ -96,18 +98,18 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
if order.State != "Created" {
|
||||
return nil, nil, fmt.Errorf("cannot pay for order: %s, current state is %s", order.GetId(), order.State)
|
||||
}
|
||||
|
||||
productId := util.GetId(order.Owner, order.ProductName)
|
||||
product, err := GetProduct(productId)
|
||||
productNames := order.Products
|
||||
products, err := getOrderProducts(order.Owner, productNames)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, nil, fmt.Errorf("the product: %s does not exist", productId)
|
||||
if len(products) == 0 {
|
||||
return nil, nil, fmt.Errorf("order has no products")
|
||||
}
|
||||
|
||||
if !product.IsRecharge && product.Quantity <= 0 {
|
||||
return nil, nil, fmt.Errorf("the product: %s is out of stock", product.Name)
|
||||
for _, product := range products {
|
||||
if !product.IsRecharge && product.Quantity <= 0 {
|
||||
return nil, nil, fmt.Errorf("the product: %s is out of stock", product.Name)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := GetUser(util.GetId(order.Owner, order.User))
|
||||
@@ -118,7 +120,9 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
return nil, nil, fmt.Errorf("the user: %s does not exist", order.User)
|
||||
}
|
||||
|
||||
provider, err := product.getProvider(providerName)
|
||||
// For multi-product orders, the payment provider is determined by the first product
|
||||
baseProduct := products[0]
|
||||
provider, err := baseProduct.getProvider(providerName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -128,7 +132,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
owner := product.Owner
|
||||
owner := baseProduct.Owner
|
||||
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
||||
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
|
||||
|
||||
@@ -163,20 +167,30 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, order.PricingName, sub.Name)
|
||||
}
|
||||
|
||||
if product.SuccessUrl != "" {
|
||||
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", product.SuccessUrl, owner, paymentName)
|
||||
if baseProduct.SuccessUrl != "" {
|
||||
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", baseProduct.SuccessUrl, owner, paymentName)
|
||||
}
|
||||
|
||||
displayNames := make([]string, len(products))
|
||||
descriptions := make([]string, len(products))
|
||||
for i, p := range products {
|
||||
displayNames[i] = p.DisplayName
|
||||
descriptions[i] = p.Description
|
||||
}
|
||||
reqProductName := strings.Join(productNames, ", ")
|
||||
reqProductDisplayName := strings.Join(displayNames, ", ")
|
||||
reqProductDescription := strings.Join(descriptions, ", ")
|
||||
|
||||
payReq := &pp.PayReq{
|
||||
ProviderName: providerName,
|
||||
ProductName: product.Name,
|
||||
ProductName: reqProductName,
|
||||
PayerName: payerName,
|
||||
PayerId: user.Id,
|
||||
PayerEmail: user.Email,
|
||||
PaymentName: paymentName,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
ProductDescription: product.Description,
|
||||
ProductImage: product.Image,
|
||||
ProductDisplayName: reqProductDisplayName,
|
||||
ProductDescription: reqProductDescription,
|
||||
ProductImage: baseProduct.Image,
|
||||
Price: order.Price,
|
||||
Currency: order.Currency,
|
||||
ReturnUrl: returnUrl,
|
||||
@@ -199,7 +213,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
}
|
||||
|
||||
payment = &Payment{
|
||||
Owner: product.Owner,
|
||||
Owner: baseProduct.Owner,
|
||||
Name: paymentName,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: paymentName,
|
||||
@@ -207,12 +221,11 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
Provider: provider.Name,
|
||||
Type: provider.Type,
|
||||
|
||||
ProductName: product.Name,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
Detail: product.Detail,
|
||||
Currency: order.Currency,
|
||||
Price: order.Price,
|
||||
IsRecharge: product.IsRecharge,
|
||||
Products: productNames,
|
||||
ProductsDisplayName: reqProductDisplayName,
|
||||
Detail: reqProductDescription,
|
||||
Currency: order.Currency,
|
||||
Price: order.Price,
|
||||
|
||||
User: user.Name,
|
||||
Order: order.Name,
|
||||
@@ -260,12 +273,21 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(transaction))
|
||||
}
|
||||
|
||||
if product.IsRecharge {
|
||||
hasRecharge := false
|
||||
rechargeAmount := 0.0
|
||||
for _, productInfo := range order.ProductInfos {
|
||||
if productInfo.IsRecharge {
|
||||
hasRecharge = true
|
||||
rechargeAmount += productInfo.Price
|
||||
}
|
||||
}
|
||||
|
||||
if hasRecharge {
|
||||
rechargeTransaction := &Transaction{
|
||||
Owner: payment.Owner,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Application: user.SignupApplication,
|
||||
Amount: payment.Price,
|
||||
Amount: rechargeAmount,
|
||||
Currency: order.Currency,
|
||||
Payment: payment.Name,
|
||||
Category: TransactionCategoryRecharge,
|
||||
@@ -302,7 +324,7 @@ func PayOrder(providerName, host, paymentEnv string, order *Order, lang string)
|
||||
|
||||
// Update product stock after order state is persisted (for instant payment methods)
|
||||
if provider.Type == "Dummy" || provider.Type == "Balance" {
|
||||
err = UpdateProductStock(product)
|
||||
err = UpdateProductStock(products)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
@@ -32,12 +31,11 @@ type Payment struct {
|
||||
Provider string `xorm:"varchar(100)" json:"provider"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
// Product Info
|
||||
ProductName string `xorm:"varchar(100)" json:"productName"`
|
||||
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
|
||||
Detail string `xorm:"varchar(255)" json:"detail"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Price float64 `json:"price"`
|
||||
IsRecharge bool `xorm:"bool" json:"isRecharge"`
|
||||
Products []string `xorm:"varchar(1000)" json:"products"`
|
||||
ProductsDisplayName string `xorm:"varchar(1000)" json:"productsDisplayName"`
|
||||
Detail string `xorm:"varchar(255)" json:"detail"`
|
||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||
Price float64 `json:"price"`
|
||||
|
||||
// Payer Info
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
@@ -178,14 +176,11 @@ func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
product, err := getProduct(owner, payment.ProductName)
|
||||
// Check if the order products exist
|
||||
_, err = getOrderProducts(owner, payment.Products)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if product == nil {
|
||||
err = fmt.Errorf("the product: %s does not exist", payment.ProductName)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
notifyResult, err := pProvider.Notify(body, payment.OutOrderId)
|
||||
if err != nil {
|
||||
@@ -195,13 +190,13 @@ func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp
|
||||
return payment, notifyResult, nil
|
||||
}
|
||||
// Only check paid payment
|
||||
if notifyResult.ProductDisplayName != "" && notifyResult.ProductDisplayName != product.DisplayName {
|
||||
err = fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", notifyResult.ProductDisplayName, product.DisplayName)
|
||||
if notifyResult.ProductDisplayName != "" && notifyResult.ProductDisplayName != payment.ProductsDisplayName {
|
||||
err = fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", notifyResult.ProductDisplayName, payment.ProductsDisplayName)
|
||||
return payment, nil, err
|
||||
}
|
||||
|
||||
if notifyResult.Price != product.Price && !product.IsRecharge {
|
||||
err = fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", notifyResult.Price, product.Price)
|
||||
if notifyResult.Price != payment.Price {
|
||||
err = fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", notifyResult.Price, payment.Price)
|
||||
return payment, nil, err
|
||||
}
|
||||
|
||||
@@ -270,12 +265,17 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
|
||||
return nil, fmt.Errorf("the provider: %s does not exist", payment.Provider)
|
||||
}
|
||||
|
||||
product, err := getProduct(payment.Owner, payment.ProductName)
|
||||
products, err := getOrderProducts(payment.Owner, order.Products)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if product == nil {
|
||||
return nil, fmt.Errorf("the product: %s does not exist", payment.ProductName)
|
||||
if len(products) == 0 {
|
||||
return nil, fmt.Errorf("order has no products")
|
||||
}
|
||||
for _, product := range products {
|
||||
if !product.IsRecharge && product.Quantity <= 0 {
|
||||
return nil, fmt.Errorf("the product: %s is out of stock", product.Name)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := getUser(payment.Owner, payment.User)
|
||||
@@ -311,7 +311,16 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
|
||||
return nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(transaction))
|
||||
}
|
||||
|
||||
if product.IsRecharge {
|
||||
hasRecharge := false
|
||||
rechargeAmount := 0.0
|
||||
for _, productInfo := range order.ProductInfos {
|
||||
if productInfo.IsRecharge {
|
||||
hasRecharge = true
|
||||
rechargeAmount += productInfo.Price
|
||||
}
|
||||
}
|
||||
|
||||
if hasRecharge {
|
||||
rechargeTransaction := &Transaction{
|
||||
Owner: payment.Owner,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
@@ -337,7 +346,7 @@ func NotifyPayment(body []byte, owner string, paymentName string, lang string) (
|
||||
}
|
||||
}
|
||||
|
||||
err = UpdateProductStock(product)
|
||||
err = UpdateProductStock(products)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -98,31 +98,33 @@ func GetProduct(id string) (*Product, error) {
|
||||
return getProduct(owner, name)
|
||||
}
|
||||
|
||||
func UpdateProductStock(product *Product) error {
|
||||
func UpdateProductStock(products []Product) error {
|
||||
var (
|
||||
affected int64
|
||||
err error
|
||||
)
|
||||
if product.IsRecharge {
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Incr("sold", 1).
|
||||
Update(&Product{})
|
||||
} else {
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Where("quantity > 0").
|
||||
Decr("quantity", 1).
|
||||
Incr("sold", 1).
|
||||
Update(&Product{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
for _, product := range products {
|
||||
if product.IsRecharge {
|
||||
return fmt.Errorf("failed to update stock for product: %s", product.Name)
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Incr("sold", 1).
|
||||
Update(&Product{})
|
||||
} else {
|
||||
affected, err = ormer.Engine.ID(core.PK{product.Owner, product.Name}).
|
||||
Where("quantity > 0").
|
||||
Decr("quantity", 1).
|
||||
Incr("sold", 1).
|
||||
Update(&Product{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
if product.IsRecharge {
|
||||
return fmt.Errorf("failed to update stock for product: %s", product.Name)
|
||||
}
|
||||
return fmt.Errorf("insufficient stock for product: %s", product.Name)
|
||||
}
|
||||
return fmt.Errorf("insufficient stock for product: %s", product.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -137,6 +139,12 @@ func UpdateProduct(id string, product *Product) (bool, error) {
|
||||
} else if p == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = checkProduct(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -146,6 +154,11 @@ func UpdateProduct(id string, product *Product) (bool, error) {
|
||||
}
|
||||
|
||||
func AddProduct(product *Product) (bool, error) {
|
||||
err := checkProduct(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.Insert(product)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -154,6 +167,23 @@ func AddProduct(product *Product) (bool, error) {
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func checkProduct(product *Product) error {
|
||||
if product == nil {
|
||||
return fmt.Errorf("the product not exist")
|
||||
}
|
||||
|
||||
for _, providerName := range product.Providers {
|
||||
provider, err := getProvider(product.Owner, providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if provider != nil && provider.Type == "Alipay" && product.Currency != "CNY" {
|
||||
return fmt.Errorf("alipay provider only supports CNY, got: %s", product.Currency)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteProduct(product *Product) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{product.Owner, product.Name}).Delete(&Product{})
|
||||
if err != nil {
|
||||
@@ -167,13 +197,23 @@ func (product *Product) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", product.Owner, product.Name)
|
||||
}
|
||||
|
||||
func (product *Product) isValidProvider(provider *Provider) bool {
|
||||
func (product *Product) isValidProvider(provider *Provider) error {
|
||||
if provider.Type == "Alipay" && product.Currency != "CNY" {
|
||||
return fmt.Errorf("alipay provider only supports CNY, got: %s", product.Currency)
|
||||
}
|
||||
|
||||
providerMatched := false
|
||||
for _, providerName := range product.Providers {
|
||||
if providerName == provider.Name {
|
||||
return true
|
||||
providerMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
if !providerMatched {
|
||||
return fmt.Errorf("the payment provider: %s is not valid for the product: %s", provider.Name, product.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||
@@ -186,8 +226,8 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
|
||||
return nil, fmt.Errorf("the payment provider: %s does not exist", providerName)
|
||||
}
|
||||
|
||||
if !product.isValidProvider(provider) {
|
||||
return nil, fmt.Errorf("the payment provider: %s is not valid for the product: %s", providerName, product.Name)
|
||||
if err := product.isValidProvider(provider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
@@ -249,3 +289,33 @@ func UpdateProductForPlan(plan *Plan, product *Product) {
|
||||
product.Currency = plan.Currency
|
||||
product.Providers = plan.PaymentProviders
|
||||
}
|
||||
|
||||
func getOrderProducts(owner string, productNames []string) ([]Product, error) {
|
||||
if len(productNames) == 0 {
|
||||
return []Product{}, nil
|
||||
}
|
||||
|
||||
var products []Product
|
||||
err := ormer.Engine.
|
||||
Where("owner = ?", owner).
|
||||
In("name", productNames).
|
||||
Find(&products)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
productMap := make(map[string]Product, len(products))
|
||||
for _, product := range products {
|
||||
productMap[product.Name] = product
|
||||
}
|
||||
|
||||
orderedProducts := make([]Product, 0, len(productNames))
|
||||
for _, productName := range productNames {
|
||||
product, ok := productMap[productName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the product: %s does not exist", productName)
|
||||
}
|
||||
orderedProducts = append(orderedProducts, product)
|
||||
}
|
||||
return orderedProducts, nil
|
||||
}
|
||||
|
||||
@@ -312,8 +312,8 @@ func SendWebhooks(record *casvisorsdk.Record) error {
|
||||
errs := []error{}
|
||||
webhooks = getFilteredWebhooks(webhooks, record.Organization, record.Action)
|
||||
|
||||
record2 := *record
|
||||
for _, webhook := range webhooks {
|
||||
record2 := *record
|
||||
|
||||
if len(webhook.ObjectFields) != 0 && webhook.ObjectFields[0] != "All" {
|
||||
record2.Object = filterRecordObject(record.Object, webhook.ObjectFields)
|
||||
|
||||
@@ -394,14 +394,17 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
|
||||
// }
|
||||
|
||||
// Sign the assertion (SAML 2.0 best practice)
|
||||
assertion := samlResponse.FindElement("./Assertion")
|
||||
if assertion != nil {
|
||||
assertionSig, err := ctx.ConstructSignature(assertion, true)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("err: Failed to sign SAML assertion, %s", err.Error())
|
||||
// Only sign if EnableSamlAssertionSignature is true
|
||||
if application.EnableSamlAssertionSignature {
|
||||
assertion := samlResponse.FindElement("./Assertion")
|
||||
if assertion != nil {
|
||||
assertionSig, err := ctx.ConstructSignature(assertion, true)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("err: Failed to sign SAML assertion, %s", err.Error())
|
||||
}
|
||||
// Insert signature as the second child of assertion (after Issuer)
|
||||
assertion.InsertChildAt(1, assertionSig)
|
||||
}
|
||||
// Insert signature as the second child of assertion (after Issuer)
|
||||
assertion.InsertChildAt(1, assertionSig)
|
||||
}
|
||||
|
||||
// Sign the response
|
||||
|
||||
@@ -18,11 +18,58 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
// convertGUIDToString converts a binary GUID byte array to a standard UUID string format
|
||||
// Active Directory GUIDs are 16 bytes in a specific byte order
|
||||
func convertGUIDToString(guidBytes []byte) string {
|
||||
if len(guidBytes) != 16 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Active Directory GUID format is:
|
||||
// Data1 (4 bytes, little-endian) - Data2 (2 bytes, little-endian) - Data3 (2 bytes, little-endian) - Data4 (2 bytes, big-endian) - Data5 (6 bytes, big-endian)
|
||||
// Convert to standard UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||
guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0], // Data1 (reverse byte order)
|
||||
guidBytes[5], guidBytes[4], // Data2 (reverse byte order)
|
||||
guidBytes[7], guidBytes[6], // Data3 (reverse byte order)
|
||||
guidBytes[8], guidBytes[9], // Data4 (big-endian)
|
||||
guidBytes[10], guidBytes[11], guidBytes[12], guidBytes[13], guidBytes[14], guidBytes[15]) // Data5 (big-endian)
|
||||
}
|
||||
|
||||
// sanitizeUTF8 ensures the string contains only valid UTF-8 characters
|
||||
// Invalid UTF-8 sequences are replaced with the Unicode replacement character
|
||||
func sanitizeUTF8(s string) string {
|
||||
if utf8.ValidString(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
// Build a new string with only valid UTF-8
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(s))
|
||||
|
||||
for _, r := range s {
|
||||
if r == utf8.RuneError {
|
||||
// Skip invalid runes
|
||||
continue
|
||||
}
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// getAttributeValueSafe safely retrieves an LDAP attribute value and ensures it's valid UTF-8
|
||||
func getAttributeValueSafe(entry *goldap.Entry, attributeName string) string {
|
||||
value := entry.GetAttributeValue(attributeName)
|
||||
return sanitizeUTF8(value)
|
||||
}
|
||||
|
||||
// ActiveDirectorySyncerProvider implements SyncerProvider for Active Directory LDAP-based syncers
|
||||
type ActiveDirectorySyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
@@ -197,26 +244,32 @@ func (p *ActiveDirectorySyncerProvider) adEntryToOriginalUser(entry *goldap.Entr
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Get basic attributes
|
||||
sAMAccountName := entry.GetAttributeValue("sAMAccountName")
|
||||
userPrincipalName := entry.GetAttributeValue("userPrincipalName")
|
||||
displayName := entry.GetAttributeValue("displayName")
|
||||
givenName := entry.GetAttributeValue("givenName")
|
||||
sn := entry.GetAttributeValue("sn")
|
||||
mail := entry.GetAttributeValue("mail")
|
||||
telephoneNumber := entry.GetAttributeValue("telephoneNumber")
|
||||
mobile := entry.GetAttributeValue("mobile")
|
||||
title := entry.GetAttributeValue("title")
|
||||
department := entry.GetAttributeValue("department")
|
||||
company := entry.GetAttributeValue("company")
|
||||
streetAddress := entry.GetAttributeValue("streetAddress")
|
||||
city := entry.GetAttributeValue("l")
|
||||
state := entry.GetAttributeValue("st")
|
||||
postalCode := entry.GetAttributeValue("postalCode")
|
||||
country := entry.GetAttributeValue("co")
|
||||
objectGUID := entry.GetAttributeValue("objectGUID")
|
||||
whenCreated := entry.GetAttributeValue("whenCreated")
|
||||
userAccountControlStr := entry.GetAttributeValue("userAccountControl")
|
||||
// Get basic attributes with UTF-8 sanitization
|
||||
sAMAccountName := getAttributeValueSafe(entry, "sAMAccountName")
|
||||
userPrincipalName := getAttributeValueSafe(entry, "userPrincipalName")
|
||||
displayName := getAttributeValueSafe(entry, "displayName")
|
||||
givenName := getAttributeValueSafe(entry, "givenName")
|
||||
sn := getAttributeValueSafe(entry, "sn")
|
||||
mail := getAttributeValueSafe(entry, "mail")
|
||||
telephoneNumber := getAttributeValueSafe(entry, "telephoneNumber")
|
||||
mobile := getAttributeValueSafe(entry, "mobile")
|
||||
title := getAttributeValueSafe(entry, "title")
|
||||
department := getAttributeValueSafe(entry, "department")
|
||||
company := getAttributeValueSafe(entry, "company")
|
||||
streetAddress := getAttributeValueSafe(entry, "streetAddress")
|
||||
city := getAttributeValueSafe(entry, "l")
|
||||
state := getAttributeValueSafe(entry, "st")
|
||||
postalCode := getAttributeValueSafe(entry, "postalCode")
|
||||
country := getAttributeValueSafe(entry, "co")
|
||||
whenCreated := getAttributeValueSafe(entry, "whenCreated")
|
||||
userAccountControlStr := getAttributeValueSafe(entry, "userAccountControl")
|
||||
|
||||
// Handle objectGUID specially - it's a binary attribute
|
||||
var objectGUID string
|
||||
objectGUIDBytes := entry.GetRawAttributeValue("objectGUID")
|
||||
if len(objectGUIDBytes) == 16 {
|
||||
objectGUID = convertGUIDToString(objectGUIDBytes)
|
||||
}
|
||||
|
||||
// Set user fields
|
||||
// Use sAMAccountName as the primary username
|
||||
|
||||
@@ -323,8 +323,7 @@ func (p *DingtalkSyncerProvider) getDingtalkUsers() ([]*OriginalUser, error) {
|
||||
for _, deptId := range deptIds {
|
||||
users, err := p.getDingtalkUsersFromDept(accessToken, deptId)
|
||||
if err != nil {
|
||||
// Continue even if one department fails
|
||||
continue
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
|
||||
@@ -43,22 +43,24 @@ type UserShort struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
}
|
||||
|
||||
type UserStandard struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"preferred_username,omitempty"`
|
||||
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"name,omitempty"`
|
||||
Avatar string `xorm:"varchar(500)" json:"picture,omitempty"`
|
||||
Email string `xorm:"varchar(100) index" json:"email,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone,omitempty"`
|
||||
Id string `xorm:"varchar(100) index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"name,omitempty"`
|
||||
Avatar string `xorm:"varchar(500)" json:"picture,omitempty"`
|
||||
Email string `xorm:"varchar(100) index" json:"email,omitempty"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone,omitempty"`
|
||||
}
|
||||
|
||||
type UserWithoutThirdIdp struct {
|
||||
@@ -80,7 +82,7 @@ type UserWithoutThirdIdp struct {
|
||||
AvatarType string `xorm:"varchar(100)" json:"avatarType"`
|
||||
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
CountryCode string `xorm:"varchar(6)" json:"countryCode"`
|
||||
Region string `xorm:"varchar(100)" json:"region"`
|
||||
@@ -190,11 +192,12 @@ func getShortUser(user *User) *UserShort {
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -204,11 +207,12 @@ func getStandardUser(user *User) *UserStandard {
|
||||
Owner: user.Owner,
|
||||
Name: user.Name,
|
||||
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Id: user.Id,
|
||||
DisplayName: user.DisplayName,
|
||||
Avatar: user.Avatar,
|
||||
Email: user.Email,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -319,7 +319,15 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
if err != nil || token == nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "refresh token is invalid, expired or revoked",
|
||||
ErrorDescription: "refresh token is invalid or revoked",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check if the token has been invalidated (e.g., by SSO logout)
|
||||
if token.ExpiresIn <= 0 {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: "refresh token is expired",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -89,16 +89,25 @@ func getStandardClaims(claims Claims) ClaimsStandard {
|
||||
|
||||
func ParseStandardJwtToken(token string, cert *Cert) (*ClaimsStandard, error) {
|
||||
t, err := jwt.ParseWithClaims(token, &ClaimsStandard{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
var (
|
||||
certificate interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
if cert.Certificate == "" {
|
||||
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
|
||||
}
|
||||
|
||||
// RSA certificate
|
||||
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
|
||||
// RSA certificate
|
||||
certificate, err = jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
} else if _, ok := token.Method.(*jwt.SigningMethodECDSA); ok {
|
||||
// ES certificate
|
||||
certificate, err = jwt.ParseECPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
} else {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -212,11 +212,8 @@ func willLog(subOwner string, subName string, method string, urlPath string, obj
|
||||
return true
|
||||
}
|
||||
|
||||
func getUrlPath(urlPath string, ctx *context.Context) string {
|
||||
// Special handling for MCP requests
|
||||
if urlPath == "/api/mcp" {
|
||||
return getMcpUrlPath(ctx)
|
||||
}
|
||||
func getUrlPath(ctx *context.Context) string {
|
||||
urlPath := ctx.Request.URL.Path
|
||||
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
|
||||
return "/cas"
|
||||
@@ -241,10 +238,64 @@ func getUrlPath(urlPath string, ctx *context.Context) string {
|
||||
return urlPath
|
||||
}
|
||||
|
||||
func getExtraInfo(ctx *context.Context, urlPath string) map[string]interface{} {
|
||||
var extra map[string]interface{}
|
||||
if urlPath == "/api/mcp" {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(ctx.Input.RequestBody, &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
method, ok := m["method"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"detailPathUrl": method,
|
||||
}
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
func getImpersonateUser(ctx *context.Context, subOwner, subName, username string) (string, string, string) {
|
||||
impersonateUser, ok := ctx.Input.Session("impersonateUser").(string)
|
||||
impersonateUserCookie := ctx.GetCookie("impersonateUser")
|
||||
if ok && impersonateUser != "" && impersonateUserCookie != "" {
|
||||
user, err := object.GetUser(util.GetId(subOwner, subName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
impUserOwner, impUserName, err := util.GetOwnerAndNameFromIdWithError(impersonateUser)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if user.IsAdmin && impUserOwner == user.Owner {
|
||||
ctx.Input.SetData("impersonating", true)
|
||||
return impUserOwner, impUserName, impersonateUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subOwner, subName, username
|
||||
}
|
||||
|
||||
func ApiFilter(ctx *context.Context) {
|
||||
subOwner, subName := getSubject(ctx)
|
||||
// stash current user info into request context for controllers
|
||||
username := ""
|
||||
if !(subOwner == "anonymous" && subName == "anonymous") {
|
||||
username = fmt.Sprintf("%s/%s", subOwner, subName)
|
||||
subOwner, subName, username = getImpersonateUser(ctx, subOwner, subName, username)
|
||||
}
|
||||
ctx.Input.SetData("currentUserId", username)
|
||||
|
||||
method := ctx.Request.Method
|
||||
urlPath := getUrlPath(ctx.Request.URL.Path, ctx)
|
||||
urlPath := getUrlPath(ctx)
|
||||
extraInfo := getExtraInfo(ctx, urlPath)
|
||||
|
||||
objOwner, objName := "", ""
|
||||
if urlPath != "/api/get-app-login" && urlPath != "/api/get-resource" {
|
||||
@@ -260,7 +311,7 @@ func ApiFilter(ctx *context.Context) {
|
||||
urlPath = "/api/notify-payment"
|
||||
}
|
||||
|
||||
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName)
|
||||
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName, extraInfo)
|
||||
|
||||
result := "deny"
|
||||
if isAllowed {
|
||||
@@ -270,12 +321,20 @@ func ApiFilter(ctx *context.Context) {
|
||||
if willLog(subOwner, subName, method, urlPath, objOwner, objName) {
|
||||
logLine := fmt.Sprintf("subOwner = %s, subName = %s, method = %s, urlPath = %s, obj.Owner = %s, obj.Name = %s, result = %s",
|
||||
subOwner, subName, method, urlPath, objOwner, objName, result)
|
||||
extra := formatExtraInfo(extraInfo)
|
||||
if extra != "" {
|
||||
logLine += fmt.Sprintf(", extraInfo = %s", extra)
|
||||
}
|
||||
fmt.Println(logLine)
|
||||
util.LogInfo(ctx, logLine)
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
denyRequest(ctx)
|
||||
if urlPath == "/api/mcp" {
|
||||
denyMcpRequest(ctx)
|
||||
} else {
|
||||
denyRequest(ctx)
|
||||
}
|
||||
record, err := object.NewRecord(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -290,3 +349,14 @@ func ApiFilter(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatExtraInfo(extra map[string]interface{}) string {
|
||||
if extra == nil {
|
||||
return ""
|
||||
}
|
||||
b, err := json.Marshal(extra)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -28,6 +30,14 @@ func AutoSigninFilter(ctx *context.Context) {
|
||||
if strings.HasPrefix(urlPath, "/api/login/oauth/access_token") {
|
||||
return
|
||||
}
|
||||
if urlPath == "/api/mcp" {
|
||||
var req mcp.McpRequest
|
||||
if err := json.Unmarshal(ctx.Input.RequestBody, &req); err == nil {
|
||||
if req.Method == "initialize" || req.Method == "notifications/initialized" || req.Method == "ping" || req.Method == "tools/list" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
//if getSessionUser(ctx) != "" {
|
||||
// return
|
||||
//}
|
||||
|
||||
@@ -16,14 +16,17 @@ package routers
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/i18n"
|
||||
"github.com/casdoor/casdoor/mcp"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@@ -37,6 +40,11 @@ type Response struct {
|
||||
|
||||
func responseError(ctx *context.Context, error string, data ...interface{}) {
|
||||
// ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
|
||||
urlPath := ctx.Request.URL.Path
|
||||
if urlPath == "/api/mcp" {
|
||||
denyMcpRequest(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp := Response{Status: "error", Msg: error}
|
||||
switch len(data) {
|
||||
@@ -66,6 +74,30 @@ func denyRequest(ctx *context.Context) {
|
||||
responseError(ctx, T(ctx, "auth:Unauthorized operation"))
|
||||
}
|
||||
|
||||
func denyMcpRequest(ctx *context.Context) {
|
||||
req := mcp.McpRequest{}
|
||||
err := json.Unmarshal(ctx.Input.RequestBody, &req)
|
||||
if err != nil {
|
||||
ctx.Output.SetStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.ID == nil {
|
||||
ctx.Output.SetStatus(http.StatusAccepted)
|
||||
ctx.Output.Body([]byte{})
|
||||
return
|
||||
}
|
||||
|
||||
resp := mcp.BuildMcpResponse(req.ID, nil, &mcp.McpError{
|
||||
Code: -32001,
|
||||
Message: "Unauthorized",
|
||||
Data: T(ctx, "auth:Unauthorized operation"),
|
||||
})
|
||||
|
||||
ctx.Output.SetStatus(http.StatusUnauthorized)
|
||||
_ = ctx.Output.JSON(resp, true, false)
|
||||
}
|
||||
|
||||
func getUsernameByClientIdSecret(ctx *context.Context) (string, error) {
|
||||
clientId, clientSecret, ok := ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
|
||||
@@ -127,58 +127,3 @@ func extractOwnerNameFromAppStub(app applicationStub) (string, string, error) {
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func getMcpUrlPath(ctx *context.Context) string {
|
||||
body := ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
return "/api/mcp"
|
||||
}
|
||||
|
||||
type mcpRequest struct {
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type mcpCallToolParams struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var mcpReq mcpRequest
|
||||
err := json.Unmarshal(body, &mcpReq)
|
||||
if err != nil {
|
||||
return "/api/mcp"
|
||||
}
|
||||
|
||||
// Map initialize and tools/list to public endpoints
|
||||
// These operations don't require special permissions beyond authentication
|
||||
// We use /api/get-application as it's a read-only operation that authenticated users can access
|
||||
if mcpReq.Method == "initialize" || mcpReq.Method == "tools/list" {
|
||||
return "/api/get-application"
|
||||
}
|
||||
|
||||
if mcpReq.Method != "tools/call" {
|
||||
return "/api/mcp"
|
||||
}
|
||||
|
||||
var params mcpCallToolParams
|
||||
err = json.Unmarshal(mcpReq.Params, ¶ms)
|
||||
if err != nil {
|
||||
return "/api/mcp"
|
||||
}
|
||||
|
||||
// Map MCP tool names to corresponding API paths
|
||||
switch params.Name {
|
||||
case "get_applications":
|
||||
return "/api/get-applications"
|
||||
case "get_application":
|
||||
return "/api/get-application"
|
||||
case "add_application":
|
||||
return "/api/add-application"
|
||||
case "update_application":
|
||||
return "/api/update-application"
|
||||
case "delete_application":
|
||||
return "/api/delete-application"
|
||||
default:
|
||||
return "/api/mcp"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ func InitAPI() {
|
||||
web.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
|
||||
web.Router("/api/remove-user-from-group", &controllers.ApiController{}, "POST:RemoveUserFromGroup")
|
||||
web.Router("/api/verify-identification", &controllers.ApiController{}, "POST:VerifyIdentification")
|
||||
web.Router("/api/impersonate-user", &controllers.ApiController{}, "POST:ImpersonateUser")
|
||||
web.Router("/api/exit-impersonate-user", &controllers.ApiController{}, "POST:ExitImpersonateUser")
|
||||
|
||||
web.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
|
||||
web.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
|
||||
|
||||
@@ -15,11 +15,55 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/casdoor/oss"
|
||||
"os"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/aliyun/credentials-go/credentials"
|
||||
casdoorOss "github.com/casdoor/oss"
|
||||
"github.com/casdoor/oss/aliyun"
|
||||
)
|
||||
|
||||
func NewAliyunOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||
func NewAliyunOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) casdoorOss.StorageInterface {
|
||||
// Check if RRSA is available (empty credentials + environment variables set)
|
||||
if (clientId == "" || clientId == "rrsa") &&
|
||||
(clientSecret == "" || clientSecret == "rrsa") &&
|
||||
os.Getenv("ALIBABA_CLOUD_ROLE_ARN") != "" {
|
||||
// Use RRSA to get temporary credentials
|
||||
config := &credentials.Config{}
|
||||
config.SetType("oidc_role_arn")
|
||||
config.SetRoleArn(os.Getenv("ALIBABA_CLOUD_ROLE_ARN"))
|
||||
config.SetOIDCProviderArn(os.Getenv("ALIBABA_CLOUD_OIDC_PROVIDER_ARN"))
|
||||
config.SetOIDCTokenFilePath(os.Getenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE"))
|
||||
config.SetRoleSessionName("casdoor-oss")
|
||||
|
||||
// Set STS endpoint if provided
|
||||
if stsEndpoint := os.Getenv("ALIBABA_CLOUD_STS_ENDPOINT"); stsEndpoint != "" {
|
||||
config.SetSTSEndpoint(stsEndpoint)
|
||||
}
|
||||
|
||||
credential, err := credentials.NewCredential(config)
|
||||
if err == nil {
|
||||
accessKeyId, errId := credential.GetAccessKeyId()
|
||||
accessKeySecret, errSecret := credential.GetAccessKeySecret()
|
||||
securityToken, errToken := credential.GetSecurityToken()
|
||||
|
||||
if errId == nil && errSecret == nil && errToken == nil &&
|
||||
accessKeyId != nil && accessKeySecret != nil && securityToken != nil {
|
||||
// Successfully obtained RRSA credentials
|
||||
sp := aliyun.New(&aliyun.Config{
|
||||
AccessID: *accessKeyId,
|
||||
AccessKey: *accessKeySecret,
|
||||
Bucket: bucket,
|
||||
Endpoint: endpoint,
|
||||
ClientOptions: []oss.ClientOption{oss.SecurityToken(*securityToken)},
|
||||
})
|
||||
return sp
|
||||
}
|
||||
}
|
||||
// If RRSA fails, fall through to static credentials (which will fail if empty)
|
||||
}
|
||||
|
||||
// Use static credentials (existing behavior)
|
||||
sp := aliyun.New(&aliyun.Config{
|
||||
AccessID: clientId,
|
||||
AccessKey: clientSecret,
|
||||
|
||||
@@ -28,9 +28,9 @@ import (
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
|
||||
@@ -29,6 +29,7 @@ import EntryPage from "./EntryPage";
|
||||
import * as AuthBackend from "./auth/AuthBackend";
|
||||
import AuthCallback from "./auth/AuthCallback";
|
||||
import SamlCallback from "./auth/SamlCallback";
|
||||
import TelegramLogin from "./auth/TelegramLogin";
|
||||
import i18next from "i18next";
|
||||
import {withTranslation} from "react-i18next";
|
||||
const ManagementPage = lazy(() => import("./ManagementPage"));
|
||||
@@ -521,7 +522,9 @@ class App extends Component {
|
||||
}
|
||||
|
||||
isDoorPages() {
|
||||
return this.isEntryPages() || window.location.pathname.startsWith("/callback");
|
||||
return this.isEntryPages() ||
|
||||
window.location.pathname.startsWith("/callback") ||
|
||||
window.location.pathname.startsWith("/telegram-login");
|
||||
}
|
||||
|
||||
isEntryPages() {
|
||||
@@ -605,6 +608,7 @@ class App extends Component {
|
||||
<Switch>
|
||||
<Route exact path="/callback" render={(props) => <AuthCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||
<Route exact path="/callback/saml" render={(props) => <SamlCallback {...props} {...this.props} application={this.state.application} onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}} />} />
|
||||
<Route exact path="/telegram-login" render={(props) => <TelegramLogin {...props} {...this.props} />} />
|
||||
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
|
||||
</Switch>
|
||||
|
||||
@@ -926,6 +926,16 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Enable SAML assertion signature"), i18next.t("application:Enable SAML assertion signature - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.application.enableSamlAssertionSignature} onChange={checked => {
|
||||
this.updateApplicationField("enableSamlAssertionSignature", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
!this.state.application.disableSamlAttributes ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
||||
@@ -101,6 +101,8 @@ import TransactionEditPage from "./TransactionEditPage";
|
||||
import VerificationListPage from "./VerificationListPage";
|
||||
import TicketListPage from "./TicketListPage";
|
||||
import TicketEditPage from "./TicketEditPage";
|
||||
import * as Cookie from "cookie";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
|
||||
function ManagementPage(props) {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
@@ -155,8 +157,14 @@ function ManagementPage(props) {
|
||||
"/account"
|
||||
));
|
||||
}
|
||||
items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Logout")}</>,
|
||||
"/logout"));
|
||||
const curCookie = Cookie.parse(document.cookie);
|
||||
if (curCookie["impersonateUser"]) {
|
||||
items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Exit impersonation")}</>,
|
||||
"/exit-impersonation"));
|
||||
} else {
|
||||
items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Logout")}</>,
|
||||
"/logout"));
|
||||
}
|
||||
|
||||
const onClick = (e) => {
|
||||
if (e.key === "/account") {
|
||||
@@ -165,6 +173,16 @@ function ManagementPage(props) {
|
||||
props.history.push("/subscription");
|
||||
} else if (e.key === "/logout") {
|
||||
logout();
|
||||
} else if (e.key === "/exit-impersonation") {
|
||||
UserBackend.exitImpersonateUser().then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("account:Exit impersonation"));
|
||||
Setting.goToLinkSoft({props}, "/");
|
||||
window.location.reload();
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -408,8 +426,13 @@ function ManagementPage(props) {
|
||||
|
||||
function renderLoginIfNotLoggedIn(component) {
|
||||
if (props.account === null) {
|
||||
const lastLoginOrg = localStorage.getItem("lastLoginOrg");
|
||||
sessionStorage.setItem("from", window.location.pathname);
|
||||
return <Redirect to="/login" />;
|
||||
if (lastLoginOrg) {
|
||||
return <Redirect to={`/login/${lastLoginOrg}`} />;
|
||||
} else {
|
||||
return <Redirect to="/login" />;
|
||||
}
|
||||
} else if (props.account === undefined) {
|
||||
return null;
|
||||
} else if (props.account.needUpdatePassword) {
|
||||
|
||||
@@ -158,16 +158,25 @@ class OrderEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("order:Product")}:
|
||||
{i18next.t("order:Products")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.order.productName} disabled={isViewMode} onChange={(value) => {
|
||||
this.updateOrderField("productName", value);
|
||||
}}>
|
||||
{
|
||||
this.state.products?.map((product, index) => <Option key={index} value={product.name}>{product.displayName}</Option>)
|
||||
}
|
||||
</Select>
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{width: "100%"}}
|
||||
value={this.state.order?.products || []}
|
||||
disabled={isViewMode}
|
||||
allowClear
|
||||
options={(this.state.products || [])
|
||||
.map((p) => ({
|
||||
label: Setting.getLanguageText(p?.displayName) || p?.name,
|
||||
value: p?.name,
|
||||
}))
|
||||
.filter((o) => o.value)}
|
||||
onChange={(value) => {
|
||||
this.updateOrderField("products", value);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
||||
@@ -14,13 +14,14 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import {Button, List, Table, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as OrderBackend from "./backend/OrderBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import {EditOutlined} from "@ant-design/icons";
|
||||
|
||||
class OrderListPage extends BaseListPage {
|
||||
newOrder() {
|
||||
@@ -31,7 +32,7 @@ class OrderListPage extends BaseListPage {
|
||||
name: `order_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Order - ${randomName}`,
|
||||
productName: "",
|
||||
products: [],
|
||||
user: "",
|
||||
payment: "",
|
||||
state: "Created",
|
||||
@@ -146,20 +147,41 @@ class OrderListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:Product"),
|
||||
dataIndex: "productName",
|
||||
key: "productName",
|
||||
width: "170px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("productName"),
|
||||
title: i18next.t("order:Products"),
|
||||
dataIndex: "products",
|
||||
key: "products",
|
||||
...this.getColumnSearchProps("products"),
|
||||
render: (text, record, index) => {
|
||||
if (text === "") {
|
||||
return "(empty)";
|
||||
const products = record?.products || [];
|
||||
if (products.length === 0) {
|
||||
return `(${i18next.t("general:empty")})`;
|
||||
}
|
||||
return (
|
||||
<Link to={`/products/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
<div>
|
||||
<List
|
||||
size="small"
|
||||
locale={{emptyText: " "}}
|
||||
dataSource={products}
|
||||
style={{
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
renderItem={(productName, i) => {
|
||||
return (
|
||||
<List.Item>
|
||||
<div style={{display: "inline"}}>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
|
||||
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productName}`)} />
|
||||
</Tooltip>
|
||||
<Link to={`/products/${record.owner}/${productName}`}>
|
||||
{productName}
|
||||
</Link>
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -208,7 +230,7 @@ class OrderListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("state"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:Start time"),
|
||||
title: i18next.t("general:Start time"),
|
||||
dataIndex: "startTime",
|
||||
key: "startTime",
|
||||
width: "160px",
|
||||
@@ -218,7 +240,7 @@ class OrderListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("order:End time"),
|
||||
title: i18next.t("general:End time"),
|
||||
dataIndex: "endTime",
|
||||
key: "endTime",
|
||||
width: "160px",
|
||||
@@ -234,7 +256,7 @@ class OrderListPage extends BaseListPage {
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "240px",
|
||||
width: "300px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
const isAdmin = Setting.isLocalAdminUser(this.props.account);
|
||||
@@ -289,7 +311,7 @@ class OrderListPage extends BaseListPage {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
OrderBackend.getOrders(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
OrderBackend.getOrders(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
||||
@@ -27,7 +27,8 @@ class OrderPayPage extends React.Component {
|
||||
owner: props?.match?.params?.organizationName ?? props?.match?.params?.owner ?? null,
|
||||
orderName: props?.match?.params?.orderName ?? null,
|
||||
order: null,
|
||||
product: null,
|
||||
firstProduct: null,
|
||||
productInfos: [],
|
||||
paymentEnv: "",
|
||||
isProcessingPayment: false,
|
||||
isViewMode: params.get("view") === "true",
|
||||
@@ -59,6 +60,7 @@ class OrderPayPage extends React.Component {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
order: res.data,
|
||||
productInfos: res.data?.productInfos,
|
||||
}, () => {
|
||||
this.getProduct();
|
||||
});
|
||||
@@ -68,13 +70,19 @@ class OrderPayPage extends React.Component {
|
||||
}
|
||||
|
||||
async getProduct() {
|
||||
if (!this.state.order || !this.state.order.productName) {
|
||||
if (!this.state.order) {
|
||||
return;
|
||||
}
|
||||
const res = await ProductBackend.getProduct(this.state.order.owner, this.state.order.productName);
|
||||
|
||||
const firstProductName = this.state.order?.products?.[0] ?? this.state.order?.productInfos?.[0]?.name;
|
||||
if (!firstProductName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await ProductBackend.getProduct(this.state.order.owner, firstProductName);
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
product: res.data,
|
||||
firstProduct: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
@@ -86,7 +94,7 @@ class OrderPayPage extends React.Component {
|
||||
}
|
||||
|
||||
getProductPrice(product) {
|
||||
return `${Setting.getCurrencySymbol(product?.currency)}${product?.price} (${Setting.getCurrencyText(product)})`;
|
||||
return `${Setting.getCurrencySymbol(this.state.order?.currency)}${product.price} (${Setting.getCurrencyText(this.state.order)})`;
|
||||
}
|
||||
|
||||
// Call Wechat Pay via jsapi
|
||||
@@ -129,8 +137,8 @@ class OrderPayPage extends React.Component {
|
||||
}
|
||||
|
||||
payOrder(provider) {
|
||||
const {product, order} = this.state;
|
||||
if (!product || !order) {
|
||||
const {firstProduct, order} = this.state;
|
||||
if (!firstProduct || !order) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,20 +206,43 @@ class OrderPayPage extends React.Component {
|
||||
}
|
||||
|
||||
renderPaymentMethods() {
|
||||
const {product} = this.state;
|
||||
if (!product || !product.providerObjs || product.providerObjs.length === 0) {
|
||||
const {firstProduct} = this.state;
|
||||
if (!firstProduct || !firstProduct.providerObjs || firstProduct.providerObjs.length === 0) {
|
||||
return <div>{i18next.t("product:There is no payment channel for this product.")}</div>;
|
||||
}
|
||||
|
||||
return product.providerObjs.map(provider => {
|
||||
return firstProduct.providerObjs.map(provider => {
|
||||
return this.renderProviderButton(provider);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {order, product} = this.state;
|
||||
renderProduct(product) {
|
||||
return (
|
||||
<React.Fragment key={product.name}>
|
||||
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
||||
<span style={{fontSize: 20}}>
|
||||
{Setting.getLanguageText(product?.displayName)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
|
||||
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{marginBottom: "20px"}} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
|
||||
<span style={{fontSize: 18, fontWeight: "bold"}}>
|
||||
{this.getProductPrice(product)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Detail")} span={3}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
|
||||
</Descriptions.Item>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (!order || !product) {
|
||||
render() {
|
||||
const {order, productInfos} = this.state;
|
||||
|
||||
if (!order || !productInfos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -247,22 +278,7 @@ class OrderPayPage extends React.Component {
|
||||
|
||||
<div style={{marginBottom: "20px"}}>
|
||||
<Descriptions title={<span style={Setting.isMobile() ? {fontSize: 18} : {fontSize: 24}}>{i18next.t("product:Product Information")}</span>} bordered column={3}>
|
||||
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
|
||||
<span style={{fontSize: 20}}>
|
||||
{Setting.getLanguageText(product?.displayName)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
|
||||
<img src={product?.image} alt={Setting.getLanguageText(product?.displayName)} height={90} style={{marginBottom: "20px"}} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Price")} span={3}>
|
||||
<span style={{fontSize: 18, fontWeight: "bold"}}>
|
||||
{this.getProductPrice(product)}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("product:Detail")} span={3}>
|
||||
<span style={{fontSize: 16}}>{Setting.getLanguageText(product?.detail)}</span>
|
||||
</Descriptions.Item>
|
||||
{productInfos.map(product => this.renderProduct(product))}
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {InfoCircleTwoTone} from "@ant-design/icons";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@@ -29,6 +30,7 @@ class PaymentEditPage extends React.Component {
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
paymentName: props.match.params.paymentName,
|
||||
payment: null,
|
||||
products: [],
|
||||
isModalVisible: false,
|
||||
isInvoiceLoading: false,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
@@ -37,6 +39,7 @@ class PaymentEditPage extends React.Component {
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getPayment();
|
||||
this.getProducts();
|
||||
}
|
||||
|
||||
getPayment() {
|
||||
@@ -55,6 +58,19 @@ class PaymentEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
ProductBackend.getProducts(this.state.organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
products: res.data,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get products: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goToViewOrder() {
|
||||
const payment = this.state.payment;
|
||||
if (payment && payment.order) {
|
||||
@@ -227,12 +243,25 @@ class PaymentEditPage extends React.Component {
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Product"), i18next.t("payment:Product - Tooltip"))} :
|
||||
{Setting.getLabel(i18next.t("payment:Products"), i18next.t("payment:Products - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.payment.productName} onChange={e => {
|
||||
// this.updatePaymentField('productName', e.target.value);
|
||||
}} />
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{width: "100%"}}
|
||||
value={this.state.payment?.products || []}
|
||||
disabled={isViewMode}
|
||||
allowClear
|
||||
options={(this.state.products || [])
|
||||
.map((p) => ({
|
||||
label: Setting.getLanguageText(p?.displayName) || p?.name,
|
||||
value: p?.name,
|
||||
}))
|
||||
.filter((o) => o.value)}
|
||||
onChange={(value) => {
|
||||
this.updatePaymentField("products", value);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import {Button, List, Table, Tooltip} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
@@ -22,6 +22,7 @@ import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import * as Provider from "./auth/Provider";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import {EditOutlined} from "@ant-design/icons";
|
||||
|
||||
class PaymentListPage extends BaseListPage {
|
||||
newPayment() {
|
||||
@@ -35,9 +36,9 @@ class PaymentListPage extends BaseListPage {
|
||||
provider: "provider_pay_paypal",
|
||||
type: "PayPal",
|
||||
user: "admin",
|
||||
productName: "computer-1",
|
||||
productDisplayName: "A notebook computer",
|
||||
detail: "This is a computer with excellent CPU, memory and disk",
|
||||
products: [],
|
||||
productsDisplayName: "",
|
||||
detail: "This is a payment",
|
||||
tag: "Promotion-1",
|
||||
currency: "USD",
|
||||
price: 300.00,
|
||||
@@ -174,17 +175,41 @@ class PaymentListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("payment:Product"),
|
||||
dataIndex: "productDisplayName",
|
||||
key: "productDisplayName",
|
||||
// width: '160px',
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("productDisplayName"),
|
||||
title: i18next.t("order:Products"),
|
||||
dataIndex: "products",
|
||||
key: "products",
|
||||
...this.getColumnSearchProps("products"),
|
||||
render: (text, record, index) => {
|
||||
const products = record?.products || [];
|
||||
if (products.length === 0) {
|
||||
return `(${i18next.t("general:empty")})`;
|
||||
}
|
||||
return (
|
||||
<Link to={`/products/${record.owner}/${record.productName}`}>
|
||||
{text}
|
||||
</Link>
|
||||
<div>
|
||||
<List
|
||||
size="small"
|
||||
locale={{emptyText: " "}}
|
||||
dataSource={products}
|
||||
style={{
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
renderItem={(productName, i) => {
|
||||
return (
|
||||
<List.Item>
|
||||
<div style={{display: "inline"}}>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Edit")}>
|
||||
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/products/${record.owner}/${productName}`)} />
|
||||
</Tooltip>
|
||||
<Link to={`/products/${record.owner}/${productName}`}>
|
||||
{productName}
|
||||
</Link>
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -202,7 +202,7 @@ class PaymentResultPage extends React.Component {
|
||||
}
|
||||
<Result
|
||||
status="success"
|
||||
title={`${i18next.t("payment:You have successfully completed the payment")}: ${payment.productDisplayName}`}
|
||||
title={`${i18next.t("payment:You have successfully completed the payment")}: ${payment.productsDisplayName}`}
|
||||
subTitle={i18next.t("payment:You can view your order details or return to the order list")}
|
||||
extra={[
|
||||
<Button type="primary" key="viewOrder" onClick={() => {
|
||||
@@ -227,7 +227,7 @@ class PaymentResultPage extends React.Component {
|
||||
}
|
||||
<Result
|
||||
status="info"
|
||||
title={`${i18next.t("payment:The payment is still under processing")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}, ${i18next.t("payment:please wait for a few seconds...")}`}
|
||||
title={`${i18next.t("payment:The payment is still under processing")}: ${payment.productsDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}, ${i18next.t("payment:please wait for a few seconds...")}`}
|
||||
subTitle={i18next.t("payment:You can view your order details or return to the order list")}
|
||||
extra={[
|
||||
<Spin key="returnUrl" size="large" tip={i18next.t("payment:Processing...")} />,
|
||||
@@ -243,7 +243,7 @@ class PaymentResultPage extends React.Component {
|
||||
}
|
||||
<Result
|
||||
status="warning"
|
||||
title={`${i18next.t("payment:The payment has been canceled")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
|
||||
title={`${i18next.t("payment:The payment has been canceled")}: ${payment.productsDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
|
||||
subTitle={i18next.t("payment:You can view your order details or return to the order list")}
|
||||
extra={[
|
||||
<Button type="primary" key="viewOrder" onClick={() => {
|
||||
@@ -268,7 +268,7 @@ class PaymentResultPage extends React.Component {
|
||||
}
|
||||
<Result
|
||||
status="warning"
|
||||
title={`${i18next.t("payment:The payment has time out")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
|
||||
title={`${i18next.t("payment:The payment has timed out")}: ${payment.productsDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
|
||||
subTitle={i18next.t("payment:You can view your order details or return to the order list")}
|
||||
extra={[
|
||||
<Button type="primary" key="viewOrder" onClick={() => {
|
||||
@@ -293,7 +293,7 @@ class PaymentResultPage extends React.Component {
|
||||
}
|
||||
<Result
|
||||
status="error"
|
||||
title={`${i18next.t("payment:The payment has failed")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
|
||||
title={`${i18next.t("payment:The payment has failed")}: ${payment.productsDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
|
||||
subTitle={`${i18next.t("payment:Failed reason")}: ${payment.message}`}
|
||||
extra={[
|
||||
<Button type="primary" key="viewOrder" onClick={() => {
|
||||
|
||||
@@ -138,7 +138,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
...this.getColumnSearchProps("period"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:Start time"),
|
||||
title: i18next.t("general:Start time"),
|
||||
dataIndex: "startTime",
|
||||
key: "startTime",
|
||||
width: "140px",
|
||||
@@ -148,7 +148,7 @@ class SubscriptionListPage extends BaseListPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("subscription:End time"),
|
||||
title: i18next.t("general:End time"),
|
||||
dataIndex: "endTime",
|
||||
key: "endTime",
|
||||
width: "140px",
|
||||
|
||||
@@ -188,6 +188,18 @@ class UserListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
impersonateUser(user) {
|
||||
UserBackend.impersonateUser(user).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Success"));
|
||||
Setting.goToLinkSoft(this, "/");
|
||||
window.location.reload();
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderUpload() {
|
||||
const uploadThis = this;
|
||||
const props = {
|
||||
@@ -533,6 +545,10 @@ class UserListPage extends BaseListPage {
|
||||
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name) || (record.owner === "built-in" && record.name === "admin");
|
||||
return (
|
||||
<Space>
|
||||
<Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
|
||||
this.impersonateUser(`${record.owner}/${record.name}`);
|
||||
}}>{i18next.t("general:Impersonation")}
|
||||
</Button>
|
||||
<Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
|
||||
sessionStorage.setItem("userListUrl", window.location.pathname);
|
||||
this.props.history.push(`/users/${record.owner}/${record.name}`);
|
||||
|
||||
@@ -114,6 +114,36 @@ class AuthCallback extends React.Component {
|
||||
const samlRequest = innerParams.get("SAMLRequest");
|
||||
const casService = innerParams.get("service");
|
||||
|
||||
// Telegram sends auth data as individual URL parameters
|
||||
// Collect them and convert to JSON for backend processing
|
||||
const telegramId = params.get("id");
|
||||
if (telegramId !== null && (code === null || code === "")) {
|
||||
const telegramAuthData = {
|
||||
id: parseInt(telegramId, 10),
|
||||
};
|
||||
|
||||
// Required fields
|
||||
const hash = params.get("hash");
|
||||
const authDate = params.get("auth_date");
|
||||
if (hash) {
|
||||
telegramAuthData.hash = hash;
|
||||
}
|
||||
if (authDate) {
|
||||
telegramAuthData.auth_date = authDate;
|
||||
}
|
||||
|
||||
// Optional fields - only include if present
|
||||
const optionalFields = ["first_name", "last_name", "username", "photo_url"];
|
||||
optionalFields.forEach(field => {
|
||||
const value = params.get(field);
|
||||
if (value !== null && value !== "") {
|
||||
telegramAuthData[field] = value;
|
||||
}
|
||||
});
|
||||
|
||||
code = JSON.stringify(telegramAuthData);
|
||||
}
|
||||
|
||||
const redirectUri = `${window.location.origin}/callback`;
|
||||
|
||||
const body = {
|
||||
|
||||
@@ -577,7 +577,7 @@ class LoginPage extends React.Component {
|
||||
} else {
|
||||
const SAMLResponse = res.data;
|
||||
const redirectUri = res.data2.redirectUrl;
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
|
||||
Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${encodeURIComponent(oAuthParams.relayState)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -591,6 +591,7 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
localStorage.setItem("lastLoginOrg", values?.organization || "");
|
||||
this.setState({loginLoading: false});
|
||||
});
|
||||
}
|
||||
@@ -1563,7 +1564,7 @@ class LoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const visibleOAuthProviderItems = (application.providers === null) ? [] : application.providers.filter(providerItem => this.isProviderVisible(providerItem));
|
||||
const visibleOAuthProviderItems = (application.providers === null) ? [] : application.providers.filter(providerItem => this.isProviderVisible(providerItem) && providerItem.provider?.category !== "SAML");
|
||||
if (this.props.preview !== "auto" && !Setting.isPasswordEnabled(application) && !Setting.isCodeSigninEnabled(application) && !Setting.isWebAuthnEnabled(application) && !Setting.isLdapEnabled(application) && visibleOAuthProviderItems.length === 1) {
|
||||
Setting.goToLink(Provider.getAuthUrl(application, visibleOAuthProviderItems[0].provider, "signup"));
|
||||
return (
|
||||
|
||||
@@ -510,8 +510,8 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
} else if (provider.type === "Telegram") {
|
||||
// Telegram uses widget-based authentication
|
||||
// The actual login is handled by Telegram widget on the frontend
|
||||
return `${redirectUri}?state=${state}`;
|
||||
// Redirect to a page that displays the Telegram login widget
|
||||
return `${redirectOrigin}/telegram-login?state=${state}`;
|
||||
} else if (provider.type === "MetaMask") {
|
||||
return `${redirectUri}?state=${state}`;
|
||||
} else if (provider.type === "Web3Onboard") {
|
||||
|
||||
134
web/src/auth/TelegramLogin.js
Normal file
134
web/src/auth/TelegramLogin.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
import React from "react";
|
||||
import {Card} from "antd";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import * as Util from "./Util";
|
||||
import * as Setting from "../Setting";
|
||||
import * as ProviderBackend from "../backend/ProviderBackend";
|
||||
import i18next from "i18next";
|
||||
|
||||
class TelegramLogin extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
applicationName: "",
|
||||
providerName: "",
|
||||
botUsername: "",
|
||||
authUrl: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const params = new URLSearchParams(this.props.location.search);
|
||||
const state = params.get("state");
|
||||
const queryString = Util.getQueryParamsFromState(state);
|
||||
const innerParams = new URLSearchParams(queryString);
|
||||
|
||||
const applicationName = innerParams.get("application");
|
||||
const providerName = innerParams.get("provider");
|
||||
|
||||
// Get provider info to retrieve bot username
|
||||
ProviderBackend.getProvider("admin", providerName).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const provider = res.data;
|
||||
const redirectOrigin = window.location.origin;
|
||||
const redirectUri = `${redirectOrigin}/callback`;
|
||||
|
||||
this.setState({
|
||||
applicationName: applicationName,
|
||||
providerName: providerName,
|
||||
botUsername: provider.clientId,
|
||||
authUrl: `${redirectUri}?state=${state}`,
|
||||
}, () => {
|
||||
this.loadTelegramWidget();
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `Failed to get provider: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadTelegramWidget() {
|
||||
if (!this.state.botUsername || !this.state.authUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any existing Telegram script
|
||||
const existingScript = document.querySelector("script[src*='telegram-widget']");
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Create and load the Telegram widget script
|
||||
// Note: We load from official Telegram domain over HTTPS for security.
|
||||
// SRI is not used as Telegram doesn't provide integrity hashes and the script version may change.
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://telegram.org/js/telegram-widget.js";
|
||||
script.setAttribute("data-telegram-login", this.state.botUsername);
|
||||
script.setAttribute("data-size", "large");
|
||||
script.setAttribute("data-auth-url", this.state.authUrl);
|
||||
script.setAttribute("data-request-access", "write");
|
||||
script.async = true;
|
||||
|
||||
const container = document.getElementById("telegram-login-container");
|
||||
if (container) {
|
||||
container.innerHTML = "";
|
||||
container.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="login-content" style={{margin: "auto"}}>
|
||||
<div style={{marginBottom: "10px", textAlign: "center"}}>
|
||||
<Card
|
||||
style={{
|
||||
width: "400px",
|
||||
margin: "0 auto",
|
||||
marginTop: "100px",
|
||||
}}
|
||||
title={
|
||||
<div>
|
||||
<img
|
||||
width={40}
|
||||
height={40}
|
||||
src={Setting.getProviderLogoURL({type: "Telegram", category: "OAuth"})}
|
||||
alt="Telegram"
|
||||
style={{marginRight: "10px"}}
|
||||
/>
|
||||
{i18next.t("login:Sign in with Telegram")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{textAlign: "center", padding: "20px"}}>
|
||||
<p>{i18next.t("login:Click the button below to sign in with Telegram")}</p>
|
||||
<div
|
||||
id="telegram-login-container"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(TelegramLogin);
|
||||
@@ -200,6 +200,29 @@ export function resetEmailOrPhone(dest, type, code) {
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function impersonateUser(username) {
|
||||
const formData = new FormData();
|
||||
formData.append("username", username);
|
||||
return fetch(`${Setting.ServerUrl}/api/impersonate-user`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function exitImpersonateUser() {
|
||||
return fetch(`${Setting.ServerUrl}/api/exit-impersonate-user`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getCaptcha(owner, name, isCurrentProvider) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-captcha?applicationId=${owner}/${encodeURIComponent(name)}&isCurrentProvider=${isCurrentProvider}`, {
|
||||
method: "GET",
|
||||
|
||||
Reference in New Issue
Block a user