Compare commits

...

12 Commits

22 changed files with 355 additions and 66 deletions

View File

@@ -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

View File

@@ -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, *, *

View File

@@ -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())

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

4
go.mod
View File

@@ -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
@@ -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

View File

@@ -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

View File

@@ -17,6 +17,7 @@ package mcp
import (
"encoding/json"
"fmt"
"net/http"
"github.com/beego/beego/v2/server/web"
"github.com/casdoor/casdoor/object"
@@ -120,32 +121,26 @@ func (c *McpController) Prepare() {
c.EnableRender = false
}
// SendMcpResponse sends a successful MCP response
func (c *McpController) SendMcpResponse(id interface{}, result interface{}) {
resp := GetMcpResponse(id, result, nil)
// Set proper HTTP headers for MCP responses
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()
}
// SendMcpError sends an MCP error response
func (c *McpController) SendMcpError(id interface{}, code int, message string, data interface{}) {
resp := GetMcpResponse(id, nil, &McpError{
func (c *McpController) McpResponseError(id interface{}, code int, message string, data interface{}) {
resp := BuildMcpResponse(id, nil, &McpError{
Code: code,
Message: message,
Data: data,
})
// Set proper HTTP headers for MCP responses
c.Ctx.Output.Header("Content-Type", "application/json")
c.Data["json"] = resp
c.ServeJSON()
}
// GetMcpResponse returns a McpResponse object
func GetMcpResponse(id interface{}, result interface{}, err *McpError) McpResponse {
func BuildMcpResponse(id interface{}, result interface{}, err *McpError) McpResponse {
resp := McpResponse{
JSONRPC: "2.0",
ID: id,
@@ -157,7 +152,7 @@ func GetMcpResponse(id interface{}, result interface{}, err *McpError) McpRespon
// 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
@@ -170,7 +165,7 @@ func (c *McpController) SendToolResult(id interface{}, text string) {
},
},
}
c.SendMcpResponse(id, result)
c.McpResponseOk(id, result)
}
// SendToolErrorResult sends a tool execution error result
@@ -184,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
@@ -214,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
}
@@ -231,7 +226,7 @@ func (c *McpController) HandleMcp() {
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))
}
}
@@ -258,17 +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(202)
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.SendMcpResponse(req.ID, map[string]interface{}{})
c.McpResponseOk(req.ID, map[string]interface{}{})
}
func (c *McpController) handleToolsList(req McpRequest) {
@@ -353,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) {
@@ -402,6 +398,6 @@ func (c *McpController) handleToolsCall(req McpRequest) {
}
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))
}
}

View File

@@ -35,16 +35,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,7 +49,6 @@ 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())
order := &Order{
@@ -73,8 +62,8 @@ func PlaceOrder(productId string, user *User, pricingName string, planName strin
PlanName: planName,
User: user.Name,
Payment: "", // Payment will be set when user pays
Price: price,
Currency: userBalanceCurrency,
Price: productPrice,
Currency: productCurrency,
State: "Created",
Message: "",
StartTime: util.GetCurrentTime(),

View File

@@ -137,6 +137,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 +152,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 +165,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 +195,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 +224,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

View File

@@ -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
}

View File

@@ -258,8 +258,41 @@ func getExtraInfo(ctx *context.Context, urlPath string) map[string]interface{} {
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)
extraInfo := getExtraInfo(ctx, urlPath)

View File

@@ -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
//}

View File

@@ -40,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) {
@@ -79,10 +84,11 @@ func denyMcpRequest(ctx *context.Context) {
if req.ID == nil {
ctx.Output.SetStatus(http.StatusAccepted)
ctx.Output.Body([]byte{})
return
}
resp := mcp.GetMcpResponse(req.ID, nil, &mcp.McpError{
resp := mcp.BuildMcpResponse(req.ID, nil, &mcp.McpError{
Code: -32001,
Message: "Unauthorized",
Data: T(ctx, "auth:Unauthorized operation"),

View File

@@ -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")

View File

@@ -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,

View File

@@ -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 />&nbsp;&nbsp;{i18next.t("account:Logout")}</>,
"/logout"));
const curCookie = Cookie.parse(document.cookie);
if (curCookie["impersonateUser"]) {
items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Exit impersonation")}</>,
"/exit-impersonation"));
} else {
items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{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);
}
});
}
};

View File

@@ -289,7 +289,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,

View File

@@ -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}`);

View File

@@ -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)}`);
}
}
};
@@ -1563,7 +1563,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 (

View File

@@ -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",