forked from casdoor/casdoor
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d883db907b | ||
|
|
8e7efe5c23 | ||
|
|
bf75508d95 | ||
|
|
986b94cc90 | ||
|
|
890f528556 | ||
|
|
b46e779235 | ||
|
|
5c80948a06 | ||
|
|
1467199159 | ||
|
|
64c2b8f0c2 | ||
|
|
8f7ea7f0a0 | ||
|
|
2ab85c0c44 | ||
|
|
bf67be2af6 | ||
|
|
bc94735a8d | ||
|
|
89c6ef5aae | ||
|
|
21da9f5ff2 | ||
|
|
3b11e778e7 | ||
|
|
ad240a373f | ||
|
|
01000f7022 |
@@ -143,6 +143,10 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
|
||||
return false
|
||||
}
|
||||
|
||||
if user.IsGlobalAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
if user.IsAdmin && (subOwner == objOwner || (objOwner == "admin")) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ func (c *ApiController) UpdateApplication() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateApplication(id, &application, c.IsGlobalAdmin()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ func (c *ApiController) UploadResource() {
|
||||
}
|
||||
|
||||
applicationObj.TermsOfUse = fileUrl
|
||||
_, err = object.UpdateApplication(applicationId, applicationObj)
|
||||
_, err = object.UpdateApplication(applicationId, applicationObj, true)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
||||
@@ -59,8 +59,10 @@ func (c *ApiController) HandleSamlRedirect() {
|
||||
|
||||
relayState := c.Input().Get("RelayState")
|
||||
samlRequest := c.Input().Get("SAMLRequest")
|
||||
username := c.Input().Get("username")
|
||||
loginHint := c.Input().Get("login_hint")
|
||||
|
||||
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host)
|
||||
targetURL := object.GetSamlRedirectAddress(owner, application, relayState, samlRequest, host, username, loginHint)
|
||||
|
||||
c.Redirect(targetURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func (c *ApiController) GetSessions() {
|
||||
// @Title GetSingleSession
|
||||
// @Tag Session API
|
||||
// @Description Get session for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param sessionPkId query string true "The id(organization/user/application) of session"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /get-session [get]
|
||||
func (c *ApiController) GetSingleSession() {
|
||||
@@ -87,7 +87,7 @@ func (c *ApiController) GetSingleSession() {
|
||||
// @Title UpdateSession
|
||||
// @Tag Session API
|
||||
// @Description Update session for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param id query string true "The id(organization/user/application) of session"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /update-session [post]
|
||||
func (c *ApiController) UpdateSession() {
|
||||
@@ -106,7 +106,7 @@ func (c *ApiController) UpdateSession() {
|
||||
// @Title AddSession
|
||||
// @Tag Session API
|
||||
// @Description Add session for one user in one application. If there are other existing sessions, join the session into the list.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param id query string true "The id(organization/user/application) of session"
|
||||
// @Param sessionId query string true "sessionId to be added"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /add-session [post]
|
||||
@@ -126,7 +126,7 @@ func (c *ApiController) AddSession() {
|
||||
// @Title DeleteSession
|
||||
// @Tag Session API
|
||||
// @Description Delete session for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param id query string true "The id(organization/user/application) of session"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /delete-session [post]
|
||||
func (c *ApiController) DeleteSession() {
|
||||
@@ -145,7 +145,7 @@ func (c *ApiController) DeleteSession() {
|
||||
// @Title IsSessionDuplicated
|
||||
// @Tag Session API
|
||||
// @Description Check if there are other different sessions for one user in one application.
|
||||
// @Param id query string true "The id(organization/application/user) of session"
|
||||
// @Param sessionPkId query string true "The id(organization/user/application) of session"
|
||||
// @Param sessionId query string true "sessionId to be checked"
|
||||
// @Success 200 {array} string The Response object
|
||||
// @router /is-session-duplicated [get]
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *ApiController) UpdateSyncer() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSyncer(id, &syncer))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateSyncer(id, &syncer, c.IsGlobalAdmin()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ func (c *ApiController) UpdateToken() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token, c.IsGlobalAdmin()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ func (c *ApiController) UpdateWebhook() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateWebhook(id, &webhook))
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateWebhook(id, &webhook, c.IsGlobalAdmin()))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,11 @@ type SamlItem struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type JwtItem struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
@@ -107,6 +112,7 @@ type Application struct {
|
||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||
TokenSigningMethod string `xorm:"varchar(100)" json:"tokenSigningMethod"`
|
||||
TokenFields []string `xorm:"varchar(1000)" json:"tokenFields"`
|
||||
TokenAttributes []*JwtItem `xorm:"mediumtext" json:"tokenAttributes"`
|
||||
ExpireInHours int `json:"expireInHours"`
|
||||
RefreshExpireInHours int `json:"refreshExpireInHours"`
|
||||
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
|
||||
@@ -267,6 +273,14 @@ func extendApplicationWithSigninItems(application *Application) (err error) {
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Verification code",
|
||||
Visible: true,
|
||||
CustomCss: ".verification-code {}\n.verification-code-input{}",
|
||||
Placeholder: "",
|
||||
Rule: "None",
|
||||
}
|
||||
application.SigninItems = append(application.SigninItems, signinItem)
|
||||
signinItem = &SigninItem{
|
||||
Name: "Agreement",
|
||||
Visible: true,
|
||||
@@ -551,7 +565,6 @@ func GetMaskedApplication(application *Application, userId string) *Application
|
||||
application.Providers = providerItems
|
||||
|
||||
application.GrantTypes = nil
|
||||
application.Tags = nil
|
||||
application.RedirectUris = nil
|
||||
application.TokenFormat = "***"
|
||||
application.TokenFields = nil
|
||||
@@ -627,13 +640,17 @@ func GetAllowedApplications(applications []*Application, userId string, lang str
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func UpdateApplication(id string, application *Application) (bool, error) {
|
||||
func UpdateApplication(id string, application *Application, isGlobalAdmin bool) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
oldApplication, err := getApplication(owner, name)
|
||||
if oldApplication == nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !isGlobalAdmin && oldApplication.Organization != application.Organization {
|
||||
return false, fmt.Errorf("auth:Unauthorized operation")
|
||||
}
|
||||
|
||||
if name == "app-built-in" {
|
||||
application.Name = name
|
||||
}
|
||||
@@ -710,7 +727,7 @@ func AddApplication(application *Application) (bool, error) {
|
||||
}
|
||||
|
||||
func deleteApplication(application *Application) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{application.Owner, application.Name}).Delete(&Application{})
|
||||
affected, err := ormer.Engine.ID(core.PK{application.Owner, application.Name}).Where("organization = ?", application.Organization).Delete(&Application{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ type InitData struct {
|
||||
Sessions []*Session `json:"sessions"`
|
||||
Subscriptions []*Subscription `json:"subscriptions"`
|
||||
Transactions []*Transaction `json:"transactions"`
|
||||
|
||||
EnforcerPolicies map[string][][]string `json:"enforcerPolicies"`
|
||||
}
|
||||
|
||||
var initDataNewOnly bool
|
||||
@@ -85,9 +87,6 @@ func InitFromFile() {
|
||||
for _, model := range initData.Models {
|
||||
initDefinedModel(model)
|
||||
}
|
||||
for _, permission := range initData.Permissions {
|
||||
initDefinedPermission(permission)
|
||||
}
|
||||
for _, payment := range initData.Payments {
|
||||
initDefinedPayment(payment)
|
||||
}
|
||||
@@ -116,7 +115,11 @@ func InitFromFile() {
|
||||
initDefinedAdapter(adapter)
|
||||
}
|
||||
for _, enforcer := range initData.Enforcers {
|
||||
initDefinedEnforcer(enforcer)
|
||||
policies := initData.EnforcerPolicies[enforcer.GetId()]
|
||||
initDefinedEnforcer(enforcer, policies)
|
||||
}
|
||||
for _, permission := range initData.Permissions {
|
||||
initDefinedPermission(permission)
|
||||
}
|
||||
for _, plan := range initData.Plans {
|
||||
initDefinedPlan(plan)
|
||||
@@ -175,6 +178,8 @@ func readInitDataFromFile(filePath string) (*InitData, error) {
|
||||
Sessions: []*Session{},
|
||||
Subscriptions: []*Subscription{},
|
||||
Transactions: []*Transaction{},
|
||||
|
||||
EnforcerPolicies: map[string][][]string{},
|
||||
}
|
||||
err := util.JsonToStruct(s, data)
|
||||
if err != nil {
|
||||
@@ -694,7 +699,7 @@ func initDefinedAdapter(adapter *Adapter) {
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedEnforcer(enforcer *Enforcer) {
|
||||
func initDefinedEnforcer(enforcer *Enforcer, policies [][]string) {
|
||||
existed, err := getEnforcer(enforcer.Owner, enforcer.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -716,6 +721,27 @@ func initDefinedEnforcer(enforcer *Enforcer) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = enforcer.InitEnforcer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if enforcer.HasPolicy(policy) {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = enforcer.AddPolicy(policy)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = enforcer.SavePolicy()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDefinedPlan(plan *Plan) {
|
||||
|
||||
@@ -146,6 +146,16 @@ func writeInitDataToFile(filePath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
enforcerPolicies := make(map[string][][]string)
|
||||
for _, enforcer := range enforcers {
|
||||
err = enforcer.InitEnforcer()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
enforcerPolicies[enforcer.GetId()] = enforcer.GetPolicy()
|
||||
}
|
||||
|
||||
data := &InitData{
|
||||
Organizations: organizations,
|
||||
Applications: applications,
|
||||
@@ -172,6 +182,8 @@ func writeInitDataToFile(filePath string) error {
|
||||
Sessions: sessions,
|
||||
Subscriptions: subscriptions,
|
||||
Transactions: transactions,
|
||||
|
||||
EnforcerPolicies: enforcerPolicies,
|
||||
}
|
||||
|
||||
text := util.StructToJsonFormatted(data)
|
||||
|
||||
@@ -43,6 +43,8 @@ type Record struct {
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func maskPassword(recordString string) string {
|
||||
@@ -74,6 +76,19 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if action != "buy-product" {
|
||||
resp.Data = nil
|
||||
}
|
||||
|
||||
dataResp := ""
|
||||
if resp.Data != nil {
|
||||
dataByte, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataResp = fmt.Sprintf(", data:%s", string(dataByte))
|
||||
}
|
||||
|
||||
language := ctx.Request.Header.Get("Accept-Language")
|
||||
if len(language) > 2 {
|
||||
language = language[0:2]
|
||||
@@ -91,7 +106,7 @@ func NewRecord(ctx *context.Context) (*casvisorsdk.Record, error) {
|
||||
Language: languageCode,
|
||||
Object: object,
|
||||
StatusCode: 200,
|
||||
Response: fmt.Sprintf("{status:\"%s\", msg:\"%s\"}", resp.Status, resp.Msg),
|
||||
Response: fmt.Sprintf("{status:\"%s\", msg:\"%s\"%s}", resp.Status, resp.Msg, dataResp),
|
||||
IsTriggered: false,
|
||||
}
|
||||
return &record, nil
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -124,25 +125,7 @@ func NewSamlResponse(application *Application, user *User, host string, certific
|
||||
role.CreateAttr("Name", item.Name)
|
||||
role.CreateAttr("NameFormat", item.NameFormat)
|
||||
|
||||
valueList := []string{item.Value}
|
||||
if strings.Contains(item.Value, "$user.roles") {
|
||||
valueList = replaceSamlAttributeValuesWithList("$user.roles", getUserRoleNames(user), valueList)
|
||||
}
|
||||
|
||||
if strings.Contains(item.Value, "$user.permissions") {
|
||||
valueList = replaceSamlAttributeValuesWithList("$user.permissions", getUserPermissionNames(user), valueList)
|
||||
}
|
||||
|
||||
if strings.Contains(item.Value, "$user.groups") {
|
||||
valueList = replaceSamlAttributeValuesWithList("$user.groups", user.Groups, valueList)
|
||||
}
|
||||
|
||||
valueList = replaceSamlAttributeValues("$user.owner", user.Owner, valueList)
|
||||
valueList = replaceSamlAttributeValues("$user.name", user.Name, valueList)
|
||||
valueList = replaceSamlAttributeValues("$user.email", user.Email, valueList)
|
||||
valueList = replaceSamlAttributeValues("$user.id", user.Id, valueList)
|
||||
valueList = replaceSamlAttributeValues("$user.phone", user.Phone, valueList)
|
||||
|
||||
valueList := replaceAttributeValue(user, item.Value)
|
||||
for _, value := range valueList {
|
||||
av := role.CreateElement("saml:AttributeValue")
|
||||
av.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
@@ -162,26 +145,6 @@ func NewSamlResponse(application *Application, user *User, host string, certific
|
||||
return samlResponse, nil
|
||||
}
|
||||
|
||||
func replaceSamlAttributeValues(val string, replaceVal string, values []string) []string {
|
||||
newValues := []string{}
|
||||
for _, value := range values {
|
||||
newValues = append(newValues, strings.ReplaceAll(value, val, replaceVal))
|
||||
}
|
||||
|
||||
return newValues
|
||||
}
|
||||
|
||||
func replaceSamlAttributeValuesWithList(val string, replaceVals []string, values []string) []string {
|
||||
newValues := []string{}
|
||||
for _, value := range values {
|
||||
for _, rVal := range replaceVals {
|
||||
newValues = append(newValues, strings.ReplaceAll(value, val, rVal))
|
||||
}
|
||||
}
|
||||
|
||||
return newValues
|
||||
}
|
||||
|
||||
type X509Key struct {
|
||||
X509Certificate string
|
||||
PrivateKey string
|
||||
@@ -547,7 +510,14 @@ func NewSamlResponse11(application *Application, user *User, requestID string, h
|
||||
return samlResponse, nil
|
||||
}
|
||||
|
||||
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string) string {
|
||||
func GetSamlRedirectAddress(owner string, application string, relayState string, samlRequest string, host string, username string, loginHint string) string {
|
||||
originF, _ := getOriginFromHost(host)
|
||||
return fmt.Sprintf("%s/login/saml/authorize/%s/%s?relayState=%s&samlRequest=%s", originF, owner, application, relayState, samlRequest)
|
||||
baseURL := fmt.Sprintf("%s/login/saml/authorize/%s/%s?relayState=%s&samlRequest=%s", originF, owner, application, relayState, samlRequest)
|
||||
if username != "" {
|
||||
baseURL += fmt.Sprintf("&username=%s", url.QueryEscape(username))
|
||||
}
|
||||
if loginHint != "" {
|
||||
baseURL += fmt.Sprintf("&login_hint=%s", url.QueryEscape(loginHint))
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
|
||||
@@ -153,13 +153,15 @@ func GetMaskedSyncers(syncers []*Syncer, errs ...error) ([]*Syncer, error) {
|
||||
return syncers, nil
|
||||
}
|
||||
|
||||
func UpdateSyncer(id string, syncer *Syncer) (bool, error) {
|
||||
func UpdateSyncer(id string, syncer *Syncer, isGlobalAdmin bool) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
s, err := getSyncer(owner, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if s == nil {
|
||||
return false, nil
|
||||
} else if !isGlobalAdmin && s.Organization != syncer.Organization {
|
||||
return false, fmt.Errorf("auth:Unauthorized operation")
|
||||
}
|
||||
|
||||
session := ormer.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
@@ -218,7 +220,7 @@ func AddSyncer(syncer *Syncer) (bool, error) {
|
||||
}
|
||||
|
||||
func DeleteSyncer(syncer *Syncer) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{syncer.Owner, syncer.Name}).Delete(&Syncer{})
|
||||
affected, err := ormer.Engine.ID(core.PK{syncer.Owner, syncer.Name}).Where("organization = ?", syncer.Organization).Delete(&Syncer{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -273,11 +275,16 @@ func (syncer *Syncer) getKeyColumn() *TableColumn {
|
||||
return column
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getKey() string {
|
||||
func (syncer *Syncer) getLocalPrimaryKey() string {
|
||||
column := syncer.getKeyColumn()
|
||||
return util.CamelToSnakeCase(column.CasdoorName)
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getTargetTablePrimaryKey() string {
|
||||
column := syncer.getKeyColumn()
|
||||
return column.Name
|
||||
}
|
||||
|
||||
func RunSyncer(syncer *Syncer) error {
|
||||
err := syncer.initAdapter()
|
||||
if err != nil {
|
||||
|
||||
@@ -65,7 +65,7 @@ func (syncer *Syncer) syncUsers() error {
|
||||
}
|
||||
}
|
||||
|
||||
key := syncer.getKey()
|
||||
key := syncer.getLocalPrimaryKey()
|
||||
|
||||
myUsers := map[string]*User{}
|
||||
for _, m := range users {
|
||||
|
||||
@@ -75,7 +75,7 @@ func (syncer *Syncer) getCasdoorColumns() []string {
|
||||
}
|
||||
|
||||
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
|
||||
key := syncer.getKey()
|
||||
key := syncer.getTargetTablePrimaryKey()
|
||||
m := syncer.getMapFromOriginalUser(user)
|
||||
pkValue := m[key]
|
||||
delete(m, key)
|
||||
|
||||
@@ -180,12 +180,14 @@ func (token *Token) popularHashes() {
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateToken(id string, token *Token) (bool, error) {
|
||||
func UpdateToken(id string, token *Token, isGlobalAdmin bool) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if t, err := getToken(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if t == nil {
|
||||
return false, nil
|
||||
} else if !isGlobalAdmin && t.Organization != token.Organization {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
token.popularHashes()
|
||||
@@ -210,7 +212,7 @@ func AddToken(token *Token) (bool, error) {
|
||||
}
|
||||
|
||||
func DeleteToken(token *Token) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Delete(&Token{})
|
||||
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Where("organization = ?", token.Organization).Delete(&Token{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -67,6 +67,14 @@ type CasAttributes struct {
|
||||
LongTermAuthenticationRequestTokenUsed bool `xml:"cas:longTermAuthenticationRequestTokenUsed"`
|
||||
IsFromNewLogin bool `xml:"cas:isFromNewLogin"`
|
||||
MemberOf []string `xml:"cas:memberOf"`
|
||||
FirstName string `xml:"cas:firstName,omitempty"`
|
||||
LastName string `xml:"cas:lastName,omitempty"`
|
||||
Title string `xml:"cas:title,omitempty"`
|
||||
Email string `xml:"cas:email,omitempty"`
|
||||
Affiliation string `xml:"cas:affiliation,omitempty"`
|
||||
Avatar string `xml:"cas:avatar,omitempty"`
|
||||
Phone string `xml:"cas:phone,omitempty"`
|
||||
DisplayName string `xml:"cas:displayName,omitempty"`
|
||||
UserAttributes *CasUserAttributes
|
||||
ExtraAttributes []*CasAnyAttribute `xml:",any"`
|
||||
}
|
||||
@@ -240,6 +248,24 @@ func GenerateCasToken(userId string, service string) (string, error) {
|
||||
} else {
|
||||
value = escapedValue
|
||||
}
|
||||
switch k {
|
||||
case "firstName":
|
||||
authenticationSuccess.Attributes.FirstName = value
|
||||
case "lastName":
|
||||
authenticationSuccess.Attributes.LastName = value
|
||||
case "title":
|
||||
authenticationSuccess.Attributes.Title = value
|
||||
case "email":
|
||||
authenticationSuccess.Attributes.Email = value
|
||||
case "affiliation":
|
||||
authenticationSuccess.Attributes.Affiliation = value
|
||||
case "avatar":
|
||||
authenticationSuccess.Attributes.Avatar = value
|
||||
case "phone":
|
||||
authenticationSuccess.Attributes.Phone = value
|
||||
case "displayName":
|
||||
authenticationSuccess.Attributes.DisplayName = value
|
||||
}
|
||||
authenticationSuccess.Attributes.UserAttributes.Attributes = append(authenticationSuccess.Attributes.UserAttributes.Attributes, &CasNamedAttribute{
|
||||
Name: k,
|
||||
Value: value,
|
||||
|
||||
@@ -330,7 +330,7 @@ func getClaimsWithoutThirdIdp(claims Claims) ClaimsWithoutThirdIdp {
|
||||
return res
|
||||
}
|
||||
|
||||
func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
|
||||
func getClaimsCustom(claims Claims, tokenField []string, tokenAttributes []*JwtItem) jwt.MapClaims {
|
||||
res := make(jwt.MapClaims)
|
||||
|
||||
userValue := reflect.ValueOf(claims.User).Elem()
|
||||
@@ -370,6 +370,12 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
|
||||
res[fieldName] = finalField.Interface()
|
||||
}
|
||||
|
||||
} else if field == "permissionNames" {
|
||||
permissionNames := []string{}
|
||||
for _, val := range claims.User.Permissions {
|
||||
permissionNames = append(permissionNames, val.Name)
|
||||
}
|
||||
res[util.SnakeToCamel(util.CamelToSnakeCase(field))] = permissionNames
|
||||
} else { // Use selected user field as claims.
|
||||
userField := userValue.FieldByName(field)
|
||||
if userField.IsValid() {
|
||||
@@ -379,6 +385,16 @@ func getClaimsCustom(claims Claims, tokenField []string) jwt.MapClaims {
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range tokenAttributes {
|
||||
valueList := replaceAttributeValue(claims.User, item.Value)
|
||||
|
||||
if len(valueList) == 1 {
|
||||
res[item.Name] = valueList[0]
|
||||
} else {
|
||||
res[item.Name] = valueList
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -491,10 +507,10 @@ func generateJwtToken(application *Application, user *User, provider string, sig
|
||||
claimsShort.TokenType = "refresh-token"
|
||||
refreshToken = jwt.NewWithClaims(jwtMethod, claimsShort)
|
||||
} else if application.TokenFormat == "JWT-Custom" {
|
||||
claimsCustom := getClaimsCustom(claims, application.TokenFields)
|
||||
claimsCustom := getClaimsCustom(claims, application.TokenFields, application.TokenAttributes)
|
||||
|
||||
token = jwt.NewWithClaims(jwtMethod, claimsCustom)
|
||||
refreshClaims := getClaimsCustom(claims, application.TokenFields)
|
||||
refreshClaims := getClaimsCustom(claims, application.TokenFields, application.TokenAttributes)
|
||||
refreshClaims["exp"] = jwt.NewNumericDate(refreshExpireTime)
|
||||
refreshClaims["TokenType"] = "refresh-token"
|
||||
refreshToken = jwt.NewWithClaims(jwtMethod, refreshClaims)
|
||||
|
||||
@@ -827,3 +827,49 @@ func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func replaceAttributeValue(user *User, value string) []string {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
valueList := []string{value}
|
||||
if strings.Contains(value, "$user.roles") {
|
||||
valueList = replaceAttributeValuesWithList("$user.roles", getUserRoleNames(user), valueList)
|
||||
}
|
||||
|
||||
if strings.Contains(value, "$user.permissions") {
|
||||
valueList = replaceAttributeValuesWithList("$user.permissions", getUserPermissionNames(user), valueList)
|
||||
}
|
||||
|
||||
if strings.Contains(value, "$user.groups") {
|
||||
valueList = replaceAttributeValuesWithList("$user.groups", user.Groups, valueList)
|
||||
}
|
||||
|
||||
valueList = replaceAttributeValues("$user.owner", user.Owner, valueList)
|
||||
valueList = replaceAttributeValues("$user.name", user.Name, valueList)
|
||||
valueList = replaceAttributeValues("$user.email", user.Email, valueList)
|
||||
valueList = replaceAttributeValues("$user.id", user.Id, valueList)
|
||||
valueList = replaceAttributeValues("$user.phone", user.Phone, valueList)
|
||||
|
||||
return valueList
|
||||
}
|
||||
|
||||
func replaceAttributeValues(val string, replaceVal string, values []string) []string {
|
||||
var newValues []string
|
||||
for _, value := range values {
|
||||
newValues = append(newValues, strings.ReplaceAll(value, val, replaceVal))
|
||||
}
|
||||
|
||||
return newValues
|
||||
}
|
||||
|
||||
func replaceAttributeValuesWithList(val string, replaceVals []string, values []string) []string {
|
||||
var newValues []string
|
||||
for _, value := range values {
|
||||
for _, rVal := range replaceVals {
|
||||
newValues = append(newValues, strings.ReplaceAll(value, val, rVal))
|
||||
}
|
||||
}
|
||||
|
||||
return newValues
|
||||
}
|
||||
|
||||
@@ -104,12 +104,14 @@ func GetWebhook(id string) (*Webhook, error) {
|
||||
return getWebhook(owner, name)
|
||||
}
|
||||
|
||||
func UpdateWebhook(id string, webhook *Webhook) (bool, error) {
|
||||
func UpdateWebhook(id string, webhook *Webhook, isGlobalAdmin bool) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if w, err := getWebhook(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if w == nil {
|
||||
return false, nil
|
||||
} else if !isGlobalAdmin && w.Organization != webhook.Organization {
|
||||
return false, fmt.Errorf("auth:Unauthorized operation")
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(webhook)
|
||||
@@ -130,7 +132,7 @@ func AddWebhook(webhook *Webhook) (bool, error) {
|
||||
}
|
||||
|
||||
func DeleteWebhook(webhook *Webhook) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{webhook.Owner, webhook.Name}).Delete(&Webhook{})
|
||||
affected, err := ormer.Engine.ID(core.PK{webhook.Owner, webhook.Name}).Where("organization = ?", webhook.Organization).Delete(&Webhook{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type Object struct {
|
||||
Name string `json:"name"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
AccessSecret string `json:"accessSecret"`
|
||||
Organization string `json:"organization"`
|
||||
}
|
||||
|
||||
func getUsername(ctx *context.Context) (username string) {
|
||||
@@ -110,6 +111,15 @@ func getObject(ctx *context.Context) (string, string, error) {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, "-application") || strings.HasSuffix(path, "-token") ||
|
||||
strings.HasSuffix(path, "-syncer") || strings.HasSuffix(path, "-webhook") {
|
||||
return obj.Organization, obj.Name, nil
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, "-organization") {
|
||||
return obj.Name, obj.Name, nil
|
||||
}
|
||||
|
||||
if path == "/api/delete-resource" {
|
||||
tokens := strings.Split(obj.Name, "/")
|
||||
if len(tokens) >= 5 {
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id(organization/application/user) of session",
|
||||
"description": "The id(organization/user/application) of session",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1448,7 +1448,7 @@
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id(organization/application/user) of session",
|
||||
"description": "The id(organization/user/application) of session",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
@@ -3318,8 +3318,8 @@
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id(organization/application/user) of session",
|
||||
"name": "sessionPkId",
|
||||
"description": "The id(organization/user/application) of session",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
@@ -4034,8 +4034,8 @@
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id(organization/application/user) of session",
|
||||
"name": "sessionPkId",
|
||||
"description": "The id(organization/user/application) of session",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5457,7 +5457,7 @@
|
||||
{
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"description": "The id(organization/application/user) of session",
|
||||
"description": "The id(organization/user/application) of session",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id(organization/application/user) of session
|
||||
description: The id(organization/user/application) of session
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
@@ -935,7 +935,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id(organization/application/user) of session
|
||||
description: The id(organization/user/application) of session
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
@@ -2159,8 +2159,8 @@ paths:
|
||||
operationId: ApiController.GetSingleSession
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id(organization/application/user) of session
|
||||
name: sessionPkId
|
||||
description: The id(organization/user/application) of session
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
@@ -2629,8 +2629,8 @@ paths:
|
||||
operationId: ApiController.IsSessionDuplicated
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id(organization/application/user) of session
|
||||
name: sessionPkId
|
||||
description: The id(organization/user/application) of session
|
||||
required: true
|
||||
type: string
|
||||
- in: query
|
||||
@@ -3567,7 +3567,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: id
|
||||
description: The id(organization/application/user) of session
|
||||
description: The id(organization/user/application) of session
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
|
||||
19
util/log.go
19
util/log.go
@@ -16,6 +16,7 @@ package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -28,20 +29,12 @@ func getIpInfo(clientIp string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
ips := strings.Split(clientIp, ",")
|
||||
res := strings.TrimSpace(ips[0])
|
||||
//res := ""
|
||||
//for i := range ips {
|
||||
// ip := strings.TrimSpace(ips[i])
|
||||
// ipstr := fmt.Sprintf("%s: %s", ip, "")
|
||||
// if i != len(ips)-1 {
|
||||
// res += ipstr + " -> "
|
||||
// } else {
|
||||
// res += ipstr
|
||||
// }
|
||||
//}
|
||||
first := strings.TrimSpace(strings.Split(clientIp, ",")[0])
|
||||
if host, _, err := net.SplitHostPort(first); err == nil {
|
||||
return strings.Trim(host, "[]")
|
||||
}
|
||||
|
||||
return res
|
||||
return strings.Trim(first, "[]")
|
||||
}
|
||||
|
||||
func GetClientIpFromRequest(req *http.Request) string {
|
||||
|
||||
@@ -20,26 +20,34 @@ module.exports = {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/cas/serviceValidate": {
|
||||
"/cas/**/serviceValidate": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/cas/proxyValidate": {
|
||||
"/cas/**/proxyValidate": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/cas/proxy": {
|
||||
"/cas/**/proxy": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/cas/validate": {
|
||||
"/cas/**/validate": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/cas/**/p3/serviceValidate": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/cas/**/p3/proxyValidate": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/scim": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@@ -37,6 +37,7 @@ import ThemeEditor from "./common/theme/ThemeEditor";
|
||||
import SigninTable from "./table/SigninTable";
|
||||
import Editor from "./common/Editor";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
import TokenAttributeTable from "./table/TokenAttributeTable";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@@ -116,6 +117,7 @@ class ApplicationEditPage extends React.Component {
|
||||
providers: [],
|
||||
uploading: false,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
tokenAttributes: [],
|
||||
samlAttributes: [],
|
||||
samlMetadata: null,
|
||||
isAuthorized: true,
|
||||
@@ -463,11 +465,26 @@ class ApplicationEditPage extends React.Component {
|
||||
<Select virtual={false} disabled={this.state.application.tokenFormat !== "JWT-Custom"} mode="tags" showSearch style={{width: "100%"}} value={this.state.application.tokenFields} onChange={(value => {this.updateApplicationField("tokenFields", value);})}>
|
||||
<Option key={"provider"} value={"provider"}>{"Provider"}</Option>)
|
||||
{
|
||||
Setting.getUserCommonFields().map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
[...Setting.getUserCommonFields(), "permissionNames"].map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.application.tokenFormat === "JWT-Custom" ? (<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Token attributes"), i18next.t("general:Token attributes - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<TokenAttributeTable
|
||||
title={i18next.t("general:Token attributes")}
|
||||
table={this.state.application.tokenAttributes}
|
||||
application={this.state.application}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("tokenAttributes", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Order"), i18next.t("application:Order - Tooltip"))} :
|
||||
|
||||
@@ -367,6 +367,19 @@ class ProviderEditPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getDomainLabel(provider) {
|
||||
switch (provider.category) {
|
||||
case "OAuth":
|
||||
if (provider.type === "AzureAD" || provider.type === "AzureADB2C") {
|
||||
return Setting.getLabel(i18next.t("provider:Tenant ID"), i18next.t("provider:Tenant ID - Tooltip"));
|
||||
} else {
|
||||
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
|
||||
}
|
||||
default:
|
||||
return Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"));
|
||||
}
|
||||
}
|
||||
|
||||
getProviderSubTypeOptions(type) {
|
||||
if (type === "WeCom" || type === "Infoflow") {
|
||||
return (
|
||||
@@ -963,7 +976,7 @@ class ProviderEditPage extends React.Component {
|
||||
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && (this.state.provider.type !== "Casdoor" && this.state.category !== "Storage") && this.state.provider.type !== "Okta" && this.state.provider.type !== "Nextcloud" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||
{this.getDomainLabel(this.state.provider)} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.provider.domain} onChange={e => {
|
||||
|
||||
@@ -171,6 +171,9 @@ class WebhookEditPage extends React.Component {
|
||||
if (obj === "payment") {
|
||||
res.push("invoice-payment", "notify-payment");
|
||||
}
|
||||
if (obj === "product") {
|
||||
res.push("buy-product");
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class LoginPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.captchaRef = React.createRef();
|
||||
const urlParams = new URLSearchParams(this.props.location?.search);
|
||||
this.state = {
|
||||
classes: props,
|
||||
type: props.type,
|
||||
@@ -70,6 +71,7 @@ class LoginPage extends React.Component {
|
||||
loginLoading: false,
|
||||
userCode: props.userCode ?? (props.match?.params?.userCode ?? null),
|
||||
userCodeStatus: "",
|
||||
prefilledUsername: urlParams.get("username") || urlParams.get("login_hint"),
|
||||
};
|
||||
|
||||
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
||||
@@ -825,6 +827,13 @@ class LoginPage extends React.Component {
|
||||
{this.renderPasswordOrCodeInput(signinItem)}
|
||||
</div>
|
||||
);
|
||||
} else if (signinItem.name === "Verification code") {
|
||||
return (
|
||||
<div key={resultItemKey}>
|
||||
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
|
||||
{this.renderCodeInput(signinItem)}
|
||||
</div>
|
||||
);
|
||||
} else if (signinItem.name === "Forgot password?") {
|
||||
return (
|
||||
<div key={resultItemKey}>
|
||||
@@ -1011,7 +1020,7 @@ class LoginPage extends React.Component {
|
||||
organization: application.organization,
|
||||
application: application.name,
|
||||
autoSignin: !application?.signinItems.map(signinItem => signinItem.name === "Forgot password?" && signinItem.rule === "Auto sign in - False")?.includes(true),
|
||||
username: Conf.ShowGithubCorner ? "admin" : "",
|
||||
username: this.state.prefilledUsername || (Conf.ShowGithubCorner ? "admin" : ""),
|
||||
password: Conf.ShowGithubCorner ? "123" : "",
|
||||
}}
|
||||
onFinish={(values) => {
|
||||
@@ -1283,6 +1292,14 @@ class LoginPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
hasVerificationCodeSigninItem(application) {
|
||||
const targetApp = application || this.getApplicationObj();
|
||||
if (!targetApp || !targetApp.signinItems) {
|
||||
return false;
|
||||
}
|
||||
return targetApp.signinItems.some(item => item.name === "Verification code");
|
||||
}
|
||||
|
||||
renderPasswordOrCodeInput(signinItem) {
|
||||
const application = this.getApplicationObj();
|
||||
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
|
||||
@@ -1306,7 +1323,7 @@ class LoginPage extends React.Component {
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
} else if (this.state.loginMethod?.includes("verificationCode")) {
|
||||
} else if (this.state.loginMethod?.includes("verificationCode") && !this.hasVerificationCodeSigninItem(application)) {
|
||||
return (
|
||||
<Col span={24}>
|
||||
<div className="login-password">
|
||||
@@ -1329,6 +1346,31 @@ class LoginPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderCodeInput(signinItem) {
|
||||
const application = this.getApplicationObj();
|
||||
if (this.hasVerificationCodeSigninItem(application) && this.state.loginMethod?.includes("verificationCode")) {
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label={signinItem.label ? signinItem.label : null}
|
||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||
className="verification-code"
|
||||
>
|
||||
<SendCodeInput
|
||||
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
||||
method={"login"}
|
||||
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
||||
application={application}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderMethodChoiceBox() {
|
||||
const application = this.getApplicationObj();
|
||||
const items = [];
|
||||
|
||||
@@ -125,6 +125,10 @@ export function setPassword(userOwner, userName, oldPassword, newPassword, code
|
||||
}
|
||||
|
||||
export function sendCode(captchaType, captchaToken, clientSecret, method, countryCode = "", dest, type, applicationId, checkUser = "") {
|
||||
if (Setting.isValidEmail(dest) && type !== "email") {
|
||||
type = "email";
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("captchaType", captchaType);
|
||||
formData.append("captchaToken", captchaToken);
|
||||
|
||||
@@ -15,14 +15,26 @@
|
||||
import React from "react";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import GridCards from "./GridCards";
|
||||
import i18next from "i18next";
|
||||
import {Tag} from "antd";
|
||||
|
||||
const AppListPage = (props) => {
|
||||
const [applications, setApplications] = React.useState(null);
|
||||
const [selectedTags, setSelectedTags] = React.useState([]);
|
||||
const [allTags, setAllTags] = React.useState([]);
|
||||
|
||||
const sort = (applications) => {
|
||||
applications.sort((a, b) => {
|
||||
return a.order - b.order;
|
||||
return [...applications].sort((a, b) => a.order - b.order);
|
||||
};
|
||||
|
||||
const extractTags = (applications) => {
|
||||
const tagsSet = new Set();
|
||||
applications.forEach(application => {
|
||||
if (application.tags && Array.isArray(application.tags)) {
|
||||
application.tags.forEach(tag => tagsSet.add(tag));
|
||||
}
|
||||
});
|
||||
return Array.from(tagsSet);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -32,36 +44,107 @@ const AppListPage = (props) => {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", props.account.owner)
|
||||
.then((res) => {
|
||||
const applications = res.data || [];
|
||||
sort(applications);
|
||||
setApplications(applications);
|
||||
const sortedApps = sort(applications);
|
||||
setApplications(sortedApps);
|
||||
setAllTags(extractTags(sortedApps));
|
||||
});
|
||||
}, [props.account]);
|
||||
|
||||
const handleTagChange = (tag, checked) => {
|
||||
setSelectedTags(prev =>
|
||||
checked
|
||||
? [...prev, tag]
|
||||
: prev.filter(t => t !== tag)
|
||||
);
|
||||
};
|
||||
|
||||
const filterByTags = (applications) => {
|
||||
if (selectedTags.length === 0) {return applications;}
|
||||
|
||||
return applications.filter(application => {
|
||||
if (!application.tags || !Array.isArray(application.tags)) {return false;}
|
||||
|
||||
return selectedTags.every(tag => application.tags.includes(tag));
|
||||
});
|
||||
};
|
||||
|
||||
const generateTagColor = (tag) => {
|
||||
const colors = [
|
||||
"#ff4d4f", "#f5222d", "#ff7a45", "#fa541c",
|
||||
"#ffa940", "#fa8c16", "#ffc53d", "#faad14",
|
||||
"#ffec3d", "#fadb14", "#bae637", "#a0d911",
|
||||
"#73d13d", "#52c41a", "#36cfc9", "#13c2c2",
|
||||
"#40a9ff", "#1890ff", "#f759ab", "#eb2f96",
|
||||
];
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < tag.length; i++) {
|
||||
hash = (hash * 33) ^ tag.charCodeAt(i);
|
||||
}
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
};
|
||||
|
||||
const getItems = () => {
|
||||
if (applications === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return applications.map(application => {
|
||||
const filteredApps = filterByTags(applications);
|
||||
|
||||
return filteredApps.map(application => {
|
||||
let homepageUrl = application.homepageUrl;
|
||||
if (homepageUrl === "<custom-url>") {
|
||||
homepageUrl = props.account.homepage;
|
||||
}
|
||||
|
||||
const tagObjects = application.tags ? application.tags.map(tag => ({
|
||||
name: tag,
|
||||
color: generateTagColor(tag),
|
||||
})) : [];
|
||||
|
||||
return {
|
||||
link: homepageUrl,
|
||||
name: application.displayName,
|
||||
description: application.description,
|
||||
logo: application.logo,
|
||||
createdTime: "",
|
||||
tags: tagObjects,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const TagFilterArea = () => {
|
||||
return (
|
||||
<div style={{marginBottom: "20px", display: "flex", flexWrap: "wrap", gap: "8px"}}>
|
||||
<span style={{marginRight: "8px", fontWeight: "bold"}}>{i18next.t("organization:Tags")}</span>
|
||||
{allTags.map(tag => (
|
||||
<Tag.CheckableTag
|
||||
key={tag}
|
||||
checked={selectedTags.includes(tag)}
|
||||
onChange={(checked) => handleTagChange(tag, checked)}
|
||||
style={{backgroundColor: selectedTags.includes(tag) ? generateTagColor(tag) : "white", borderColor: generateTagColor(tag)}}
|
||||
>
|
||||
{tag}
|
||||
</Tag.CheckableTag>
|
||||
))}
|
||||
|
||||
{selectedTags.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTags([])}
|
||||
style={{marginLeft: "10px", padding: "2px 8px", background: "#ffffff", border: "2px solid #ddd", borderRadius: "4px", cursor: "pointer"}}
|
||||
>
|
||||
{i18next.t("forget:Reset")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
|
||||
<GridCards items={getItems()} />
|
||||
<div style={{padding: "20px"}}>
|
||||
{allTags.length > 0 && TagFilterArea()}
|
||||
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
|
||||
<GridCards items={getItems()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,12 +32,12 @@ const GridCards = (props) => {
|
||||
return (
|
||||
Setting.isMobile() ? (
|
||||
<Card styles={{body: {padding: 0}}}>
|
||||
{items.map(item => <SingleCard key={item.link} logo={item.logo} link={item.link} title={item.name} desc={item.description} isSingle={items.length === 1} />)}
|
||||
{items.map(item => <SingleCard key={item.link} logo={item.logo} link={item.link} title={item.name} desc={item.description} tags = {item.tags} isSingle={items.length === 1} />)}
|
||||
</Card>
|
||||
) : (
|
||||
<div style={{width: "100%", padding: "0 100px"}}>
|
||||
<Row style={{justifyContent: "center"}}>
|
||||
{items.map(item => <SingleCard logo={item.logo} link={item.link} title={item.name} desc={item.description} time={item.createdTime} isSingle={items.length === 1} key={item.name} />)}
|
||||
{items.map(item => <SingleCard logo={item.logo} link={item.link} title={item.name} desc={item.description} tags = {item.tags} time={item.createdTime} isSingle={items.length === 1} key={item.name} />)}
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Card, Col} from "antd";
|
||||
import {Card, Col, Tag} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import {withRouter} from "react-router-dom";
|
||||
|
||||
@@ -34,7 +34,7 @@ class SingleCard extends React.Component {
|
||||
return link;
|
||||
}
|
||||
|
||||
renderCardMobile(logo, link, title, desc, time, isSingle) {
|
||||
renderCardMobile(logo, link, title, desc, time, tags, isSingle) {
|
||||
const gridStyle = {
|
||||
width: "100vw",
|
||||
textAlign: "center",
|
||||
@@ -50,11 +50,28 @@ class SingleCard extends React.Component {
|
||||
description={desc}
|
||||
style={{justifyContent: "center"}}
|
||||
/>
|
||||
{this.renderTags(tags)}
|
||||
</Card.Grid>
|
||||
);
|
||||
}
|
||||
|
||||
renderCard(logo, link, title, desc, time, isSingle) {
|
||||
renderTags(tags) {
|
||||
if (!tags || !Array.isArray(tags) || tags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginTop: "8px"}}>
|
||||
{tags.map(tag => (
|
||||
<Tag key={tag.name} color={tag.color} style={{marginRight: "4px"}}>
|
||||
{tag.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCard(logo, link, title, desc, time, tags, isSingle) {
|
||||
const silentSigninLink = this.wrappedAsSilentSigninLink(link);
|
||||
|
||||
return (
|
||||
@@ -68,7 +85,7 @@ class SingleCard extends React.Component {
|
||||
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%"}}
|
||||
>
|
||||
<Meta title={title} description={desc} />
|
||||
<br />
|
||||
{this.renderTags(tags)}
|
||||
<br />
|
||||
<Meta title={""} description={Setting.getFormattedDateShort(time)} />
|
||||
</Card>
|
||||
@@ -78,9 +95,9 @@ class SingleCard extends React.Component {
|
||||
|
||||
render() {
|
||||
if (Setting.isMobile()) {
|
||||
return this.renderCardMobile(this.props.logo, this.props.link, this.props.title, this.props.desc, this.props.time, this.props.isSingle);
|
||||
return this.renderCardMobile(this.props.logo, this.props.link, this.props.title, this.props.desc, this.props.time, this.props.tags, this.props.isSingle);
|
||||
} else {
|
||||
return this.renderCard(this.props.logo, this.props.link, this.props.title, this.props.desc, this.props.time, this.props.isSingle);
|
||||
return this.renderCard(this.props.logo, this.props.link, this.props.title, this.props.desc, this.props.time, this.props.tags, this.props.isSingle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export const SendCodeInput = ({value, disabled, textBefore, onChange, onButtonCl
|
||||
value={value}
|
||||
prefix={<SafetyOutlined />}
|
||||
placeholder={i18next.t("code:Enter your code")}
|
||||
className="verification-code-input"
|
||||
onChange={e => onChange(e.target.value)}
|
||||
enterButton={
|
||||
<Button style={{fontSize: 14}} type={"primary"} disabled={disabled || buttonLeftTime > 0} loading={buttonLoading}>
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "معرف الفريق - تلميح",
|
||||
"Template code": "رمز القالب",
|
||||
"Template code - Tooltip": "رمز القالب",
|
||||
"Tenant ID": "معرف العميل",
|
||||
"Tenant ID - Tooltip": "معرف العميل",
|
||||
"Test Email": "اختبار البريد الإلكتروني",
|
||||
"Test Email - Tooltip": "عنوان البريد الإلكتروني لتلقي الرسائل التجريبية",
|
||||
"Test SMTP Connection": "اختبار اتصال SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Komanda ID",
|
||||
"Template code": "Şablon kodu",
|
||||
"Template code - Tooltip": "Şablon kodu",
|
||||
"Tenant ID": "Tenant ID",
|
||||
"Tenant ID - Tooltip": "Tenant ID",
|
||||
"Test Email": "Test Email",
|
||||
"Test Email - Tooltip": "Test emailləri qəbul edəcək email ünvanı",
|
||||
"Test SMTP Connection": "SMTP Bağlantısını Test Et",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Nápověda k ID týmu",
|
||||
"Template code": "Kód šablony",
|
||||
"Template code - Tooltip": "Nápověda ke kódu šablony",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Testovací e-mail",
|
||||
"Test Email - Tooltip": "E-mailová adresa pro příjem testovacích e-mailů",
|
||||
"Test SMTP Connection": "Testovat SMTP připojení",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Template-Code",
|
||||
"Template code - Tooltip": "Template-Code",
|
||||
"Tenant ID": "Tenant-ID",
|
||||
"Tenant ID - Tooltip": "Tenant-ID",
|
||||
"Test Email": "Test E-Mail",
|
||||
"Test Email - Tooltip": "E-Mail-Adresse zum Empfangen von Test-E-Mails",
|
||||
"Test SMTP Connection": "Testen Sie die SMTP-Verbindung",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID",
|
||||
"Template code": "Template code",
|
||||
"Template code - Tooltip": "Template code",
|
||||
"Tenant ID": "Tenant ID",
|
||||
"Tenant ID - Tooltip": "Tenant ID",
|
||||
"Test Email": "Test Email",
|
||||
"Test Email - Tooltip": "Email address to receive test mails",
|
||||
"Test SMTP Connection": "Test SMTP Connection",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Código de plantilla",
|
||||
"Template code - Tooltip": "Código de plantilla",
|
||||
"Tenant ID": "ID de tenant",
|
||||
"Tenant ID - Tooltip": "ID de tenant",
|
||||
"Test Email": "Correo de prueba",
|
||||
"Test Email - Tooltip": "Dirección de correo electrónico para recibir mensajes de prueba",
|
||||
"Test SMTP Connection": "Prueba de conexión SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "شناسه تیم",
|
||||
"Template code": "کد قالب",
|
||||
"Template code - Tooltip": "کد قالب",
|
||||
"Tenant ID": "شناسه تیم",
|
||||
"Tenant ID - Tooltip": "شناسه تیم",
|
||||
"Test Email": "ایمیل تست",
|
||||
"Test Email - Tooltip": "آدرس ایمیل برای دریافت ایمیلهای تست",
|
||||
"Test SMTP Connection": "تست اتصال SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Tiimin tunnus – ohje",
|
||||
"Template code": "Mallikoodi",
|
||||
"Template code - Tooltip": "Mallikoodi",
|
||||
"Tenant ID": "Tenant-tunnus",
|
||||
"Tenant ID - Tooltip": "Tenant-tunnus",
|
||||
"Test Email": "Testisähköposti",
|
||||
"Test Email - Tooltip": "Sähköpostiosoite testiviestien vastaanottamiseksi",
|
||||
"Test SMTP Connection": "Testaa SMTP-yhteyttä",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Code modèle",
|
||||
"Template code - Tooltip": "Code de modèle",
|
||||
"Tenant ID": "ID de tenant",
|
||||
"Tenant ID - Tooltip": "ID de tenant",
|
||||
"Test Email": "E-mail de test",
|
||||
"Test Email - Tooltip": "Adresse e-mail pour recevoir des courriels de test",
|
||||
"Test SMTP Connection": "Test de connexion SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "מזהה צוות - תיאור",
|
||||
"Template code": "קוד תבנית",
|
||||
"Template code - Tooltip": "קוד תבנית",
|
||||
"Tenant ID": "מזהה תפריט",
|
||||
"Tenant ID - Tooltip": "מזהה תפריט",
|
||||
"Test Email": "אימייל ניסיון",
|
||||
"Test Email - Tooltip": "כתובת אימייל לקבלת דואר ניסיון",
|
||||
"Test SMTP Connection": "בדוק חיבור SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Kode template",
|
||||
"Template code - Tooltip": "Kode template",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Email Uji Coba",
|
||||
"Test Email - Tooltip": "Alamat email untuk menerima email percobaan",
|
||||
"Test SMTP Connection": "Tes Koneksi SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Codice modello",
|
||||
"Template code - Tooltip": "Codice modello",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Email di Test",
|
||||
"Test Email - Tooltip": "Indirizzo Email per ricevere Email di test",
|
||||
"Test SMTP Connection": "Test Connessione SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "テンプレートコード",
|
||||
"Template code - Tooltip": "テンプレートコード",
|
||||
"Tenant ID": "テナントID",
|
||||
"Tenant ID - Tooltip": "テナントID",
|
||||
"Test Email": "テストメール",
|
||||
"Test Email - Tooltip": "テストメールを受け取るためのメールアドレス",
|
||||
"Test SMTP Connection": "SMTP接続をテストする",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Команда ID-сі - Құралдың түсіндірмесі",
|
||||
"Template code": "Үлгі коды",
|
||||
"Template code - Tooltip": "Үлгі коды",
|
||||
"Tenant ID": "Tenant ID",
|
||||
"Tenant ID - Tooltip": "Tenant ID",
|
||||
"Test Email": "Тест электрондық поштасы",
|
||||
"Test Email - Tooltip": "Тест электрондық поштасын алу мекенжайы",
|
||||
"Test SMTP Connection": "SMTP байланысын тестілеу",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "템플릿 코드",
|
||||
"Template code - Tooltip": "템플릿 코드",
|
||||
"Tenant ID": "Tenant ID",
|
||||
"Tenant ID - Tooltip": "Tenant ID",
|
||||
"Test Email": "테스트 이메일",
|
||||
"Test Email - Tooltip": "테스트 메일을 받을 이메일 주소",
|
||||
"Test SMTP Connection": "테스트 SMTP 연결",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "ID Pasukan - Tooltip",
|
||||
"Template code": "Kod templat",
|
||||
"Template code - Tooltip": "Kod templat",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Emel Ujian",
|
||||
"Test Email - Tooltip": "Alamat emel untuk menerima emel ujian",
|
||||
"Test SMTP Connection": "Ujian Sambungan SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team-ID - Tooltip",
|
||||
"Template code": "Sjablooncode",
|
||||
"Template code - Tooltip": "Sjablooncode",
|
||||
"Tenant ID": "Tenant-ID",
|
||||
"Tenant ID - Tooltip": "Tenant-ID",
|
||||
"Test Email": "Test-e-mail",
|
||||
"Test Email - Tooltip": "E-mailadres om testmails te ontvangen",
|
||||
"Test SMTP Connection": "SMTP-verbinding testen",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "ID zespołu - Tooltip",
|
||||
"Template code": "Kod szablonu",
|
||||
"Template code - Tooltip": "Kod szablonu",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Testowy e-mail",
|
||||
"Test Email - Tooltip": "Adres e-mail do odbierania testowych wiadomości",
|
||||
"Test SMTP Connection": "Testuj połączenie SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Código do modelo",
|
||||
"Template code - Tooltip": "Código do modelo",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Testar E-mail",
|
||||
"Test Email - Tooltip": "Endereço de e-mail para receber e-mails de teste",
|
||||
"Test SMTP Connection": "Testar Conexão SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Шаблонный код",
|
||||
"Template code - Tooltip": "Шаблонный код",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Тестовое письмо",
|
||||
"Test Email - Tooltip": "Адрес электронной почты для получения тестовых писем",
|
||||
"Test SMTP Connection": "Тестирование соединения SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "ID tímu",
|
||||
"Template code": "Kód šablóny",
|
||||
"Template code - Tooltip": "Kód šablóny",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Testovací e-mail",
|
||||
"Test Email - Tooltip": "E-mailová adresa na prijímanie testovacích e-mailov",
|
||||
"Test SMTP Connection": "Testovať SMTP pripojenie",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Şablon kodu",
|
||||
"Template code - Tooltip": "Şablon kodu",
|
||||
"Tenant ID": "Tenant ID",
|
||||
"Tenant ID - Tooltip": "Tenant ID",
|
||||
"Test Email": "Test E-postası",
|
||||
"Test Email - Tooltip": "Test e-postalarını almak için E-posta adresi",
|
||||
"Test SMTP Connection": "SMTP Bağlantısını Test Et",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "ID команди – підказка",
|
||||
"Template code": "Код шаблону",
|
||||
"Template code - Tooltip": "Код шаблону",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Перевірити електронну пошту",
|
||||
"Test Email - Tooltip": "Електронна адреса для отримання тестових листів",
|
||||
"Test SMTP Connection": "Перевірте з'єднання SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID - Tooltip",
|
||||
"Template code": "Mã mẫu của template",
|
||||
"Template code - Tooltip": "Mã mẫu của template",
|
||||
"Tenant ID": "ID Tenant",
|
||||
"Tenant ID - Tooltip": "ID Tenant",
|
||||
"Test Email": "Thư Email kiểm tra",
|
||||
"Test Email - Tooltip": "Địa chỉ email để nhận thư kiểm tra",
|
||||
"Test SMTP Connection": "Kiểm tra kết nối SMTP",
|
||||
|
||||
@@ -994,6 +994,8 @@
|
||||
"Team ID - Tooltip": "Team ID",
|
||||
"Template code": "模板代码",
|
||||
"Template code - Tooltip": "模板代码",
|
||||
"Tenant ID": "Tenant ID",
|
||||
"Tenant ID - Tooltip": "Tenant ID",
|
||||
"Test Email": "测试Email配置",
|
||||
"Test Email - Tooltip": "接收测试邮件的Email邮箱",
|
||||
"Test SMTP Connection": "测试SMTP连接",
|
||||
|
||||
@@ -28,6 +28,7 @@ export const SigninTableDefaultCssMap = {
|
||||
"Signin methods": ".signin-methods {}",
|
||||
"Username": ".login-username {}\n.login-username-input{}",
|
||||
"Password": ".login-password {}\n.login-password-input{}",
|
||||
"Verification code": ".verification-code {}\n.verification-code-input{}",
|
||||
"Agreement": ".login-agreement {}",
|
||||
"Forgot password?": ".login-forget-password {\n display: inline-flex;\n justify-content: space-between;\n width: 320px;\n margin-bottom: 25px;\n}",
|
||||
"Login button": ".login-button-box {\n margin-bottom: 5px;\n}\n.login-button {\n width: 100%;\n}",
|
||||
@@ -112,6 +113,7 @@ class SigninTable extends React.Component {
|
||||
{name: "Languages", displayName: i18next.t("general:Languages")},
|
||||
{name: "Username", displayName: i18next.t("signup:Username")},
|
||||
{name: "Password", displayName: i18next.t("general:Password")},
|
||||
{name: "Verification code", displayName: i18next.t("login:Verification code")},
|
||||
{name: "Providers", displayName: i18next.t("general:Providers")},
|
||||
{name: "Agreement", displayName: i18next.t("signup:Agreement")},
|
||||
{name: "Forgot password?", displayName: i18next.t("login:Forgot password?")},
|
||||
@@ -176,17 +178,17 @@ class SigninTable extends React.Component {
|
||||
return (
|
||||
<Popover placement="right" content={
|
||||
<div style={{width: "900px", height: "300px"}} >
|
||||
<Editor value={text} lang="html" fillHeight dark onChange={value => {
|
||||
this.updateField(table, index, "label", value);
|
||||
<Editor value={record.customCss} lang="html" fillHeight dark onChange={value => {
|
||||
this.updateField(table, index, "customCss", value);
|
||||
}} />
|
||||
</div>
|
||||
} title={i18next.t("signup:Label HTML")} trigger="click">
|
||||
<Input value={text} style={{marginBottom: "10px"}} onChange={e => {
|
||||
this.updateField(table, index, "label", e.target.value);
|
||||
<Input value={record.customCss} style={{marginBottom: "10px"}} onChange={e => {
|
||||
this.updateField(table, index, "customCss", e.target.value);
|
||||
}} />
|
||||
</Popover>
|
||||
);
|
||||
} else if (["Username", "Password", "Signup link", "Forgot password?", "Login button"].includes(record.name)) {
|
||||
} else if (["Username", "Password", "Verification code", "Signup link", "Forgot password?", "Login button"].includes(record.name)) {
|
||||
return <Input value={text} style={{marginBottom: "10px"}} onChange={e => {
|
||||
this.updateField(table, index, "label", e.target.value);
|
||||
}} />;
|
||||
|
||||
139
web/src/table/TokenAttributeTable.js
Normal file
139
web/src/table/TokenAttributeTable.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Input, Row, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class TokenAttributeTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
};
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {Name: "", nameFormat: "", value: ""};
|
||||
if (table === undefined || table === null) {
|
||||
table = [];
|
||||
}
|
||||
table = Setting.addRow(table, row);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
deleteRow(table, i) {
|
||||
table = Setting.deleteRow(table, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
upRow(table, i) {
|
||||
table = Setting.swapRow(table, i - 1, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
downRow(table, i) {
|
||||
table = Setting.swapRow(table, i, i + 1);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "200px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "name", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("webhook:Value"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
width: "200px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "value", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: "20px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
|
||||
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table title={() => (
|
||||
<div>
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
columns={columns} dataSource={table} rowKey="key" size="middle" bordered
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={24}>
|
||||
{
|
||||
this.renderTable(this.props.table)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TokenAttributeTable;
|
||||
Reference in New Issue
Block a user