Compare commits

...

18 Commits

Author SHA1 Message Date
DacongDA
d883db907b feat: improve authz_filter (#4195) 2025-09-18 23:46:00 +08:00
Attack825
8e7efe5c23 feat: add code verification label in signin items (#4187) 2025-09-18 10:41:47 +08:00
DacongDA
bf75508d95 feat: add token attribute table to provide a more flexible Jwt-custom token in application edit page (#4191) 2025-09-17 21:57:17 +08:00
Mirko Rapisarda
986b94cc90 feat: improve domain field text in provider edit page (#4181) 2025-09-16 20:57:40 +08:00
Attack825
890f528556 feat: separate getLocalPrimaryKey() and getTargetTablePrimaryKey() in DB syncer (#4180) 2025-09-15 17:45:18 +08:00
Robin Ye
b46e779235 feat: persist custom signin item label in signin items table (#4179) 2025-09-15 17:43:20 +08:00
Attack825
5c80948a06 feat: add tag filtering in app list page (#4163) 2025-09-14 15:32:13 +08:00
DacongDA
1467199159 feat: add webhook for buy-product and add resp data (#4177) 2025-09-12 23:53:59 +08:00
DacongDA
64c2b8f0c2 feat: fix issue that init will add duplicate policy and not add permission policies to adapter (#4175) 2025-09-11 21:21:07 +08:00
DacongDA
8f7ea7f0a0 feat: fix Data Missing From casbin_rule Table After Importing init_data.json (#4167) 2025-09-09 21:20:25 +08:00
DacongDA
2ab85c0c44 feat: fix bug that send code type will be "phone" when logged-in via autofill (#4164) 2025-09-08 18:13:52 +08:00
Dev Hjz
bf67be2af6 feat: add username and loginHint to redirect URL in HandleSamlRedirect of SAML IdP (#4162) 2025-09-07 14:18:35 +08:00
DacongDA
bc94735a8d feat: add the username parameter in SAML or OAuth2 (#4161) 2025-09-06 22:03:18 +08:00
DacongDA
89c6ef5aae feat: support "permissionNames" field in JWT-Custom token (#4154) 2025-09-06 00:05:47 +08:00
anhuv
21da9f5ff2 feat: remove port from client IP in getIpInfo() (#4145) 2025-09-04 08:10:42 +08:00
karatekaneen
3b11e778e7 feat(i18n): Update faulty Swedish translations (#4149) 2025-09-03 20:45:57 +08:00
Attack825
ad240a373f feat: fix non-standard CAS bug (#4146) 2025-09-03 20:20:08 +08:00
amaankm
01000f7022 feat: update parameter descriptions in Session API (#4140) 2025-09-02 16:31:06 +08:00
63 changed files with 1114 additions and 585 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ func (syncer *Syncer) syncUsers() error {
}
}
key := syncer.getKey()
key := syncer.getLocalPrimaryKey()
myUsers := map[string]*User{}
for _, m := range users {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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接続をテストする",

View File

@@ -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 байланысын тестілеу",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;