mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-15 01:43:49 +08:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
df2a5681cc | |||
ac102480c7 | |||
feff47d2dc | |||
79b934d6c2 | |||
365449695b | |||
55a52093e8 | |||
e65fdeb1e0 | |||
a46c1cc775 | |||
5629343466 | |||
3718d2dc04 | |||
38b9ad1d9f | |||
5a92411006 | |||
52eaf6c822 | |||
cc84709151 | |||
22fca78be9 | |||
effd257040 | |||
a38747d90e | |||
da70682cd1 | |||
4a3bd84f84 | |||
7f2869cecb | |||
cef2ab213b | |||
cc979c310e | |||
13d73732ce | |||
5686fe5d22 | |||
d8cb82f67a | |||
cad2e1bcc3 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ./web/yarn.lock
|
||||
- run: yarn install && CI=false yarn run build
|
||||
@ -101,7 +101,7 @@ jobs:
|
||||
working-directory: ./
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ./web/yarn.lock
|
||||
- run: yarn install
|
||||
@ -138,7 +138,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Fetch Previous version
|
||||
id: get-previous-tag
|
||||
|
@ -665,6 +665,11 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsSignupItemRequired("Invitation code") {
|
||||
c.ResponseError(c.T("check:Invitation code cannot be blank"))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle username conflicts
|
||||
var tmpUser *object.User
|
||||
tmpUser, err = object.GetUser(util.GetId(application.Organization, userInfo.Username))
|
||||
|
@ -16,6 +16,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@ -163,11 +164,17 @@ func (c *ApiController) GetPolicies() {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
if adapter == nil {
|
||||
c.ResponseError(fmt.Sprintf(c.T("the adapter: %s is not found"), adapterId))
|
||||
return
|
||||
}
|
||||
|
||||
err = adapter.InitAdapter()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk()
|
||||
return
|
||||
}
|
||||
|
@ -46,10 +46,10 @@ func (c *ApiController) GetSystemInfo() {
|
||||
// @Success 200 {object} util.VersionInfo The Response object
|
||||
// @router /get-version-info [get]
|
||||
func (c *ApiController) GetVersionInfo() {
|
||||
errInfo := ""
|
||||
versionInfo, err := util.GetVersionInfo()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
errInfo = "Git error: " + err.Error()
|
||||
}
|
||||
|
||||
if versionInfo.Version != "" {
|
||||
@ -59,9 +59,11 @@ func (c *ApiController) GetVersionInfo() {
|
||||
|
||||
versionInfo, err = util.GetVersionInfoFromFile()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
errInfo = errInfo + ", File error: " + err.Error()
|
||||
c.ResponseError(errInfo)
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(versionInfo)
|
||||
}
|
||||
|
||||
|
@ -333,6 +333,35 @@ func (c *ApiController) IntrospectToken() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = &object.IntrospectionResponse{
|
||||
Active: true,
|
||||
Scope: jwtToken.Scope,
|
||||
ClientId: clientId,
|
||||
Username: token.User,
|
||||
TokenType: token.TokenType,
|
||||
Exp: jwtToken.ExpiresAt.Unix(),
|
||||
Iat: jwtToken.IssuedAt.Unix(),
|
||||
Nbf: jwtToken.NotBefore.Unix(),
|
||||
Sub: jwtToken.Subject,
|
||||
Aud: jwtToken.Audience,
|
||||
Iss: jwtToken.Issuer,
|
||||
Jti: jwtToken.ID,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
|
||||
if err != nil || jwtToken.Valid() != nil {
|
||||
// and token revoked case. but we not implement
|
||||
|
18
idp/lark.go
18
idp/lark.go
@ -22,6 +22,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nyaruka/phonenumbers"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@ -199,12 +200,25 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var phoneNumber string
|
||||
var countryCode string
|
||||
if len(larkUserInfo.Data.Mobile) != 0 {
|
||||
phoneNumberParsed, err := phonenumbers.Parse(larkUserInfo.Data.Mobile, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
countryCode = phonenumbers.GetRegionCodeForNumber(phoneNumberParsed)
|
||||
phoneNumber = fmt.Sprintf("%d", phoneNumberParsed.GetNationalNumber())
|
||||
}
|
||||
|
||||
userInfo := UserInfo{
|
||||
Id: larkUserInfo.Data.OpenId,
|
||||
DisplayName: larkUserInfo.Data.EnName,
|
||||
Username: larkUserInfo.Data.Name,
|
||||
DisplayName: larkUserInfo.Data.Name,
|
||||
Username: larkUserInfo.Data.UserId,
|
||||
Email: larkUserInfo.Data.Email,
|
||||
AvatarUrl: larkUserInfo.Data.AvatarUrl,
|
||||
Phone: phoneNumber,
|
||||
CountryCode: countryCode,
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
@ -59,7 +59,15 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
|
||||
}
|
||||
|
||||
bindPassword := string(r.AuthenticationSimple())
|
||||
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
|
||||
|
||||
enableCaptcha := false
|
||||
isSigninViaLdap := false
|
||||
isPasswordWithLdapEnabled := false
|
||||
if bindPassword != "" {
|
||||
isPasswordWithLdapEnabled = true
|
||||
}
|
||||
|
||||
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en", enableCaptcha, isSigninViaLdap, isPasswordWithLdapEnabled)
|
||||
if err != nil {
|
||||
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
|
||||
res.SetResultCode(ldap.LDAPResultInvalidCredentials)
|
||||
@ -122,6 +130,9 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
|
||||
e.AddAttribute("homeDirectory", message.AttributeValue("/home/"+user.Name))
|
||||
e.AddAttribute("cn", message.AttributeValue(user.Name))
|
||||
e.AddAttribute("uid", message.AttributeValue(user.Id))
|
||||
for _, group := range user.Groups {
|
||||
e.AddAttribute(ldapMemberOfAttr, message.AttributeValue(group))
|
||||
}
|
||||
attrs := r.Attributes()
|
||||
for _, attr := range attrs {
|
||||
if string(attr) == "*" {
|
||||
|
21
ldap/util.go
21
ldap/util.go
@ -79,6 +79,8 @@ var ldapAttributesMapping = map[string]FieldRelation{
|
||||
},
|
||||
}
|
||||
|
||||
const ldapMemberOfAttr = "memberOf"
|
||||
|
||||
var AdditionalLdapAttributes []message.LDAPString
|
||||
|
||||
func init() {
|
||||
@ -180,7 +182,22 @@ func buildUserFilterCondition(filter interface{}) (builder.Cond, error) {
|
||||
}
|
||||
return builder.Not{cond}, nil
|
||||
case message.FilterEqualityMatch:
|
||||
field, err := getUserFieldFromAttribute(string(f.AttributeDesc()))
|
||||
attr := string(f.AttributeDesc())
|
||||
|
||||
if attr == ldapMemberOfAttr {
|
||||
groupId := string(f.AssertionValue())
|
||||
users, err := object.GetGroupUsers(groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
for _, user := range users {
|
||||
names = append(names, user.Name)
|
||||
}
|
||||
return builder.In("name", names), nil
|
||||
}
|
||||
|
||||
field, err := getUserFieldFromAttribute(attr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -246,7 +263,7 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
|
||||
return nil, code
|
||||
}
|
||||
|
||||
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org'
|
||||
if name == "*" { // get all users from organization 'org'
|
||||
if m.Client.IsGlobalAdmin && org == "*" {
|
||||
filteredUsers, err = object.GetGlobalUsersWithFilter(buildSafeCondition(r.Filter()))
|
||||
if err != nil {
|
||||
|
@ -78,6 +78,7 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,6 +110,7 @@ func initBuiltInOrganization() bool {
|
||||
EnableSoftDeletion: false,
|
||||
IsProfilePublic: false,
|
||||
UseEmailAsUsername: false,
|
||||
EnableTour: true,
|
||||
}
|
||||
_, err = AddOrganization(organization)
|
||||
if err != nil {
|
||||
|
@ -73,6 +73,7 @@ type Organization struct {
|
||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||
IsProfilePublic bool `json:"isProfilePublic"`
|
||||
UseEmailAsUsername bool `json:"useEmailAsUsername"`
|
||||
EnableTour bool `json:"enableTour"`
|
||||
|
||||
MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"`
|
||||
AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"`
|
||||
@ -355,6 +356,11 @@ func GetDefaultApplication(id string) (*Application, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = extendApplicationWithSigninMethods(defaultApplication)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return defaultApplication, nil
|
||||
}
|
||||
|
||||
|
@ -201,7 +201,7 @@ func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp
|
||||
}
|
||||
|
||||
if payment.IsRecharge {
|
||||
err = updateUserBalance(payment.Owner, payment.User, payment.Price)
|
||||
err = UpdateUserBalance(payment.Owner, payment.User, payment.Price)
|
||||
return payment, notifyResult, err
|
||||
}
|
||||
|
||||
@ -222,6 +222,19 @@ func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transaction, err := GetTransaction(payment.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if transaction != nil {
|
||||
transaction.State = payment.State
|
||||
_, err = UpdateTransaction(transaction.GetId(), transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payment, nil
|
||||
|
@ -181,15 +181,15 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if oldPermission.Adapter != "" && oldPermission.Adapter != permission.Adapter {
|
||||
isEmpty, _ := ormer.Engine.IsTableEmpty(oldPermission.Adapter)
|
||||
if isEmpty {
|
||||
err = ormer.Engine.DropTables(oldPermission.Adapter)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// if oldPermission.Adapter != "" && oldPermission.Adapter != permission.Adapter {
|
||||
// isEmpty, _ := ormer.Engine.IsTableEmpty(oldPermission.Adapter)
|
||||
// if isEmpty {
|
||||
// err = ormer.Engine.DropTables(oldPermission.Adapter)
|
||||
// if err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
err = addGroupingPolicies(permission)
|
||||
if err != nil {
|
||||
@ -312,15 +312,15 @@ func DeletePermission(permission *Permission) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if permission.Adapter != "" && permission.Adapter != "permission_rule" {
|
||||
isEmpty, _ := ormer.Engine.IsTableEmpty(permission.Adapter)
|
||||
if isEmpty {
|
||||
err = ormer.Engine.DropTables(permission.Adapter)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// if permission.Adapter != "" && permission.Adapter != "permission_rule" {
|
||||
// isEmpty, _ := ormer.Engine.IsTableEmpty(permission.Adapter)
|
||||
// if isEmpty {
|
||||
// err = ormer.Engine.DropTables(permission.Adapter)
|
||||
// if err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return affected, nil
|
||||
|
@ -227,13 +227,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
||||
NotifyUrl: notifyUrl,
|
||||
PaymentEnv: paymentEnv,
|
||||
}
|
||||
|
||||
// custom process for WeChat & WeChat Pay
|
||||
if provider.Type == "WeChat Pay" {
|
||||
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if provider.Type == "Balance" {
|
||||
payReq.PayerId = user.GetId()
|
||||
}
|
||||
|
||||
payResp, err := pProvider.Pay(payReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -264,12 +268,46 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
||||
OutOrderId: payResp.OrderId,
|
||||
}
|
||||
|
||||
transaction := &Transaction{
|
||||
Owner: payment.Owner,
|
||||
Name: payment.Name,
|
||||
DisplayName: payment.DisplayName,
|
||||
Provider: provider.Name,
|
||||
Category: provider.Category,
|
||||
Type: provider.Type,
|
||||
|
||||
ProductName: product.Name,
|
||||
ProductDisplayName: product.DisplayName,
|
||||
Detail: product.Detail,
|
||||
Tag: product.Tag,
|
||||
Currency: product.Currency,
|
||||
Amount: payment.Price,
|
||||
ReturnUrl: payment.ReturnUrl,
|
||||
|
||||
User: payment.User,
|
||||
Application: owner,
|
||||
Payment: payment.GetId(),
|
||||
|
||||
State: pp.PaymentStateCreated,
|
||||
}
|
||||
|
||||
if provider.Type == "Dummy" {
|
||||
payment.State = pp.PaymentStatePaid
|
||||
err = updateUserBalance(user.Owner, user.Name, payment.Price)
|
||||
err = UpdateUserBalance(user.Owner, user.Name, payment.Price)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if provider.Type == "Balance" {
|
||||
if product.Price > user.Balance {
|
||||
return nil, nil, fmt.Errorf("insufficient user balance")
|
||||
}
|
||||
transaction.Amount = -transaction.Amount
|
||||
err = UpdateUserBalance(user.Owner, user.Name, -product.Price)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
payment.State = pp.PaymentStatePaid
|
||||
transaction.State = pp.PaymentStatePaid
|
||||
}
|
||||
|
||||
affected, err := AddPayment(payment)
|
||||
@ -280,6 +318,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
||||
if !affected {
|
||||
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
||||
}
|
||||
|
||||
if product.IsRecharge || provider.Type == "Balance" {
|
||||
affected, err = AddTransaction(transaction)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !affected {
|
||||
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(payment))
|
||||
}
|
||||
}
|
||||
|
||||
return payment, payResp.AttachInfo, nil
|
||||
}
|
||||
|
||||
|
@ -309,6 +309,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "Balance" {
|
||||
pp, err := pp.NewBalancePaymentProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
|
||||
}
|
||||
|
@ -139,6 +139,15 @@ type ClaimsShort struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type OIDCAddress struct {
|
||||
Formatted string `json:"formatted"`
|
||||
StreetAddress string `json:"street_address"`
|
||||
Locality string `json:"locality"`
|
||||
Region string `json:"region"`
|
||||
PostalCode string `json:"postal_code"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
type ClaimsWithoutThirdIdp struct {
|
||||
*UserWithoutThirdIdp
|
||||
TokenType string `json:"tokenType,omitempty"`
|
||||
@ -386,6 +395,13 @@ func generateJwtToken(application *Application, user *User, nonce string, scope
|
||||
refreshClaims["exp"] = jwt.NewNumericDate(refreshExpireTime)
|
||||
refreshClaims["TokenType"] = "refresh-token"
|
||||
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, refreshClaims)
|
||||
} else if application.TokenFormat == "JWT-Standard" {
|
||||
claimsStandard := getStandardClaims(claims)
|
||||
|
||||
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsStandard)
|
||||
claimsStandard.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
|
||||
claimsStandard.TokenType = "refresh-token"
|
||||
refreshToken = jwt.NewWithClaims(jwt.SigningMethodRS256, claimsStandard)
|
||||
} else {
|
||||
return "", "", "", fmt.Errorf("unknown application TokenFormat: %s", application.TokenFormat)
|
||||
}
|
||||
|
@ -309,6 +309,15 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
}, nil
|
||||
}
|
||||
|
||||
if application.TokenFormat == "JWT-Standard" {
|
||||
_, err = ParseStandardJwtToken(refreshToken, cert)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
Error: InvalidGrant,
|
||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
} else {
|
||||
_, err = ParseJwtToken(refreshToken, cert)
|
||||
if err != nil {
|
||||
return &TokenError{
|
||||
@ -316,6 +325,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// generate a new token
|
||||
user, err := getUser(application.Organization, token.User)
|
||||
|
106
object/token_standard_jwt.go
Normal file
106
object/token_standard_jwt.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type ClaimsStandard struct {
|
||||
*UserShort
|
||||
Gender string `json:"gender,omitempty"`
|
||||
TokenType string `json:"tokenType,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Address OIDCAddress `json:"address,omitempty"`
|
||||
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func getStreetAddress(user *User) string {
|
||||
var addrs string
|
||||
for _, addr := range user.Address {
|
||||
addrs += addr + "\n"
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
func getStandardClaims(claims Claims) ClaimsStandard {
|
||||
res := ClaimsStandard{
|
||||
UserShort: getShortUser(claims.User),
|
||||
TokenType: claims.TokenType,
|
||||
Nonce: claims.Nonce,
|
||||
Scope: claims.Scope,
|
||||
RegisteredClaims: claims.RegisteredClaims,
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
|
||||
if strings.Contains(claims.Scope, ",") {
|
||||
scopes = strings.Split(claims.Scope, ",")
|
||||
} else {
|
||||
scopes = strings.Split(claims.Scope, " ")
|
||||
}
|
||||
|
||||
for _, scope := range scopes {
|
||||
if scope == "address" {
|
||||
res.Address = OIDCAddress{StreetAddress: getStreetAddress(claims.User)}
|
||||
} else if scope == "profile" {
|
||||
res.Gender = claims.User.Gender
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func ParseStandardJwtToken(token string, cert *Cert) (*ClaimsStandard, error) {
|
||||
t, err := jwt.ParseWithClaims(token, &ClaimsStandard{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
if cert.Certificate == "" {
|
||||
return nil, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
|
||||
}
|
||||
|
||||
// RSA certificate
|
||||
certificate, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert.Certificate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certificate, nil
|
||||
})
|
||||
|
||||
if t != nil {
|
||||
if claims, ok := t.Claims.(*ClaimsStandard); ok && t.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func ParseStandardJwtTokenByApplication(token string, application *Application) (*ClaimsStandard, error) {
|
||||
cert, err := getCertByApplication(application)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseStandardJwtToken(token, cert)
|
||||
}
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/pp"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
@ -43,7 +44,7 @@ type Transaction struct {
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
Payment string `xorm:"varchar(100)" json:"payment"`
|
||||
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
|
||||
}
|
||||
|
||||
func GetTransactionCount(owner, field, value string) (int64, error) {
|
||||
|
@ -204,6 +204,7 @@ type User struct {
|
||||
SigninWrongTimes int `json:"signinWrongTimes"`
|
||||
|
||||
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
||||
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
||||
NeedUpdatePassword bool `json:"needUpdatePassword"`
|
||||
}
|
||||
|
||||
@ -230,6 +231,12 @@ type ManagedAccount struct {
|
||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||
}
|
||||
|
||||
type MfaAccount struct {
|
||||
AccountName string `xorm:"varchar(100)" json:"accountName"`
|
||||
Issuer string `xorm:"varchar(100)" json:"issuer"`
|
||||
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
|
||||
}
|
||||
|
||||
type FaceId struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
FaceIdData []float64 `json:"faceIdData"`
|
||||
@ -603,6 +610,12 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
|
||||
}
|
||||
}
|
||||
|
||||
if user.MfaAccounts != nil {
|
||||
for _, mfaAccount := range user.MfaAccounts {
|
||||
mfaAccount.SecretKey = "***"
|
||||
}
|
||||
}
|
||||
|
||||
if user.TotpSecret != "" {
|
||||
user.TotpSecret = ""
|
||||
}
|
||||
@ -675,7 +688,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
columns = []string{
|
||||
"owner", "display_name", "avatar", "first_name", "last_name",
|
||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids",
|
||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
||||
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
|
||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
||||
@ -1158,7 +1171,7 @@ func GenerateIdForNewUser(application *Application) (string, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func updateUserBalance(owner string, name string, balance float64) error {
|
||||
func UpdateUserBalance(owner string, name string, balance float64) error {
|
||||
user, err := getUser(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -393,6 +393,20 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.Address == nil {
|
||||
oldUser.Address = []string{}
|
||||
}
|
||||
oldUserAddressJson, _ := json.Marshal(oldUser.Address)
|
||||
|
||||
if newUser.Address == nil {
|
||||
newUser.Address = []string{}
|
||||
}
|
||||
newUserAddressJson, _ := json.Marshal(newUser.Address)
|
||||
if string(oldUserAddressJson) != string(newUserAddressJson) {
|
||||
item := GetAccountItemByName("Address", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if newUser.FaceIds != nil {
|
||||
item := GetAccountItemByName("Face ID", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
@ -426,6 +440,31 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.Karma != newUser.Karma {
|
||||
item := GetAccountItemByName("Karma", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.Language != newUser.Language {
|
||||
item := GetAccountItemByName("Language", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.Ranking != newUser.Ranking {
|
||||
item := GetAccountItemByName("Ranking", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.Currency != newUser.Currency {
|
||||
item := GetAccountItemByName("Currency", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.Hash != newUser.Hash {
|
||||
item := GetAccountItemByName("Hash", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
for _, accountItem := range itemsChanged {
|
||||
|
||||
if pass, err := CheckAccountItemModifyRule(accountItem, isAdmin, lang); !pass {
|
||||
|
50
pp/balance.go
Normal file
50
pp/balance.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type BalancePaymentProvider struct{}
|
||||
|
||||
func NewBalancePaymentProvider() (*BalancePaymentProvider, error) {
|
||||
pp := &BalancePaymentProvider{}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *BalancePaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
owner, _ := util.GetOwnerAndNameFromId(r.PayerId)
|
||||
return &PayResp{
|
||||
PayUrl: r.ReturnUrl,
|
||||
OrderId: fmt.Sprintf("%s/%s", owner, r.PaymentName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *BalancePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||
return &NotifyResult{
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *BalancePaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *BalancePaymentProvider) GetResponseError(err error) string {
|
||||
return ""
|
||||
}
|
@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
@ -27,6 +28,14 @@ import (
|
||||
"layeh.com/radius/rfc2866"
|
||||
)
|
||||
|
||||
var StateMap map[string]AccessStateContent
|
||||
|
||||
const StateExpiredTime = time.Second * 120
|
||||
|
||||
type AccessStateContent struct {
|
||||
ExpiredAt time.Time
|
||||
}
|
||||
|
||||
func StartRadiusServer() {
|
||||
secret := conf.GetConfigString("radiusSecret")
|
||||
server := radius.PacketServer{
|
||||
@ -55,6 +64,7 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
|
||||
username := rfc2865.UserName_GetString(r.Packet)
|
||||
password := rfc2865.UserPassword_GetString(r.Packet)
|
||||
organization := rfc2865.Class_GetString(r.Packet)
|
||||
state := rfc2865.State_GetString(r.Packet)
|
||||
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
|
||||
|
||||
if organization == "" {
|
||||
@ -62,12 +72,75 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := object.CheckUserPassword(organization, username, password, "en")
|
||||
var user *object.User
|
||||
var err error
|
||||
|
||||
if state == "" {
|
||||
user, err = object.CheckUserPassword(organization, username, password, "en")
|
||||
} else {
|
||||
user, err = object.GetUser(fmt.Sprintf("%s/%s", organization, username))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsMfaEnabled() {
|
||||
mfaProp := user.GetMfaProps(object.TotpType, false)
|
||||
if mfaProp == nil {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
|
||||
if StateMap == nil {
|
||||
StateMap = map[string]AccessStateContent{}
|
||||
}
|
||||
|
||||
if state != "" {
|
||||
stateContent, ok := StateMap[state]
|
||||
if !ok {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
|
||||
delete(StateMap, state)
|
||||
if stateContent.ExpiredAt.Before(time.Now()) {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
|
||||
mfaUtil := object.GetMfaUtil(mfaProp.MfaType, mfaProp)
|
||||
if mfaUtil.Verify(password) != nil {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(r.Response(radius.CodeAccessAccept))
|
||||
return
|
||||
}
|
||||
|
||||
responseState := util.GenerateId()
|
||||
StateMap[responseState] = AccessStateContent{
|
||||
time.Now().Add(StateExpiredTime),
|
||||
}
|
||||
|
||||
err = rfc2865.State_Set(r.Packet, []byte(responseState))
|
||||
if err != nil {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
|
||||
err = rfc2865.ReplyMessage_Set(r.Packet, []byte("please enter OTP"))
|
||||
if err != nil {
|
||||
w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
|
||||
r.Packet.Code = radius.CodeAccessChallenge
|
||||
w.Write(r.Packet)
|
||||
}
|
||||
|
||||
w.Write(r.Response(radius.CodeAccessAccept))
|
||||
}
|
||||
|
||||
|
@ -354,9 +354,16 @@ func StringToInterfaceArray(array []string) []interface{} {
|
||||
func StringToInterfaceArray2d(arrays [][]string) [][]interface{} {
|
||||
var interfaceArrays [][]interface{}
|
||||
for _, req := range arrays {
|
||||
var interfaceArray []interface{}
|
||||
for _, r := range req {
|
||||
interfaceArray = append(interfaceArray, r)
|
||||
var (
|
||||
interfaceArray []interface{}
|
||||
elem interface{}
|
||||
)
|
||||
for _, elem = range req {
|
||||
jStruct, err := TryJsonToAnonymousStruct(elem.(string))
|
||||
if err == nil {
|
||||
elem = jStruct
|
||||
}
|
||||
interfaceArray = append(interfaceArray, elem)
|
||||
}
|
||||
interfaceArrays = append(interfaceArrays, interfaceArray)
|
||||
}
|
||||
|
@ -252,8 +252,8 @@ class AdapterEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("provider:DB test"), i18next.t("provider:DB test - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={2} >
|
||||
<Button type={"primary"} onClick={() => {
|
||||
AdapterBackend.getPolicies("", "", `${this.state.organizationName}/${this.state.adapterName}`)
|
||||
<Button disabled={this.state.organizationName !== this.state.adapter.owner} type={"primary"} onClick={() => {
|
||||
AdapterBackend.getPolicies("", "", `${this.state.adapter.owner}/${this.state.adapter.name}`)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("syncer:Connect successfully"));
|
||||
@ -279,13 +279,14 @@ class AdapterEditPage extends React.Component {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
organizationName: this.state.adapter.owner,
|
||||
adapterName: this.state.adapter.name,
|
||||
});
|
||||
|
||||
if (exitAfterSave) {
|
||||
this.props.history.push("/adapters");
|
||||
} else {
|
||||
this.props.history.push(`/adapters/${this.state.organizationName}/${this.state.adapter.name}`);
|
||||
this.props.history.push(`/adapters/${this.state.adapter.owner}/${this.state.adapter.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
|
@ -16,6 +16,7 @@ import React, {Component, Suspense, lazy} from "react";
|
||||
import "./App.less";
|
||||
import {Helmet} from "react-helmet";
|
||||
import * as Setting from "./Setting";
|
||||
import {setOrgIsTourVisible, setTourLogo} from "./TourConfig";
|
||||
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
|
||||
import {GithubOutlined, InfoCircleFilled, ShareAltOutlined} from "@ant-design/icons";
|
||||
import {Alert, Button, ConfigProvider, Drawer, FloatButton, Layout, Result, Tooltip} from "antd";
|
||||
@ -247,6 +248,8 @@ class App extends Component {
|
||||
|
||||
this.setLanguage(account);
|
||||
this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm);
|
||||
setTourLogo(account.organization.logo);
|
||||
setOrgIsTourVisible(account.organization.enableTour);
|
||||
} else {
|
||||
if (res.data !== "Please login first") {
|
||||
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
|
||||
|
@ -384,7 +384,7 @@ class ApplicationEditPage extends React.Component {
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.application.tokenFormat} onChange={(value => {this.updateApplicationField("tokenFormat", value);})}
|
||||
options={["JWT", "JWT-Empty", "JWT-Custom"].map((item) => Setting.getOption(item, item))}
|
||||
options={["JWT", "JWT-Empty", "JWT-Custom", "JWT-Standard"].map((item) => Setting.getOption(item, item))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -199,7 +199,7 @@ class InvitationEditPage extends React.Component {
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.invitation.application}
|
||||
onChange={(value => {this.updateInvitationField("application", value);})}
|
||||
options={[
|
||||
{label: "All", value: i18next.t("general:All")},
|
||||
{label: i18next.t("general:All"), value: "All"},
|
||||
...this.state.applications.map((application) => Setting.getOption(application.name, application.name)),
|
||||
]} />
|
||||
</Col>
|
||||
|
@ -446,6 +446,16 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Enable tour"), i18next.t("general:Enable tour - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.organization.enableTour} onChange={checked => {
|
||||
this.updateOrganizationField("enableTour", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
|
||||
|
@ -44,6 +44,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
defaultPassword: "",
|
||||
enableSoftDeletion: false,
|
||||
isProfilePublic: true,
|
||||
enableTour: true,
|
||||
accountItems: [
|
||||
{name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
@ -87,6 +88,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
|
||||
payment: payment,
|
||||
});
|
||||
if (payment.state === "Created") {
|
||||
if (["PayPal", "Stripe", "Alipay", "WeChat Pay"].includes(payment.type)) {
|
||||
if (["PayPal", "Stripe", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
|
||||
this.setState({
|
||||
timeout: setTimeout(async() => {
|
||||
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
|
||||
|
@ -725,7 +725,7 @@ class ProviderEditPage extends React.Component {
|
||||
(this.state.provider.category === "Web3") ||
|
||||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
|
||||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
|
||||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP")) ? null : (
|
||||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
|
||||
<React.Fragment>
|
||||
{
|
||||
(this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") ||
|
||||
|
@ -247,6 +247,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/payment_paypal.png`,
|
||||
url: "",
|
||||
},
|
||||
"Balance": {
|
||||
logo: `${StaticBaseUrl}/img/payment_balance.svg`,
|
||||
url: "",
|
||||
},
|
||||
"Alipay": {
|
||||
logo: `${StaticBaseUrl}/img/payment_alipay.png`,
|
||||
url: "https://www.alipay.com/",
|
||||
@ -1067,6 +1071,7 @@ export function getProviderTypeOptions(category) {
|
||||
} else if (category === "Payment") {
|
||||
return ([
|
||||
{id: "Dummy", name: "Dummy"},
|
||||
{id: "Balance", name: "Balance"},
|
||||
{id: "Alipay", name: "Alipay"},
|
||||
{id: "WeChat Pay", name: "WeChat Pay"},
|
||||
{id: "PayPal", name: "PayPal"},
|
||||
|
@ -203,13 +203,24 @@ export function getNextUrl(pathName = window.location.pathname) {
|
||||
return TourUrlList[TourUrlList.indexOf(pathName.replace("/", "")) + 1] || "";
|
||||
}
|
||||
|
||||
let orgIsTourVisible = true;
|
||||
|
||||
export function setOrgIsTourVisible(visible) {
|
||||
orgIsTourVisible = visible;
|
||||
}
|
||||
|
||||
export function setIsTourVisible(visible) {
|
||||
localStorage.setItem("isTourVisible", visible);
|
||||
window.dispatchEvent(new Event("storageTourChanged"));
|
||||
}
|
||||
|
||||
export function setTourLogo(tourLogoSrc) {
|
||||
if (tourLogoSrc !== "") {
|
||||
TourObj["home"][0]["cover"] = (<img alt="casdoor.png" src={tourLogoSrc} />);
|
||||
}
|
||||
}
|
||||
|
||||
export function getTourVisible() {
|
||||
return localStorage.getItem("isTourVisible") !== "false";
|
||||
return localStorage.getItem("isTourVisible") !== "false" && orgIsTourVisible;
|
||||
}
|
||||
|
||||
export function getNextButtonChild(nextPathName) {
|
||||
|
@ -41,6 +41,7 @@ import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-de
|
||||
import * as MfaBackend from "./backend/MfaBackend";
|
||||
import AccountAvatar from "./account/AccountAvatar";
|
||||
import FaceIdTable from "./table/FaceIdTable";
|
||||
import MfaAccountTable from "./table/MfaAccountTable";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
@ -1039,6 +1040,21 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "MFA accounts") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("user:MFA accounts"), i18next.t("user:MFA accounts"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<MfaAccountTable
|
||||
title={i18next.t("user:MFA accounts")}
|
||||
table={this.state.user.mfaAccounts}
|
||||
onUpdateTable={(table) => {this.updateUserField("mfaAccounts", table);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Need update password") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
|
@ -167,6 +167,9 @@ class WebhookEditPage extends React.Component {
|
||||
["add", "update", "delete"].forEach(action => {
|
||||
res.push(`${action}-${obj}`);
|
||||
});
|
||||
if (obj === "payment") {
|
||||
res.push("invoice-payment", "notify-payment");
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React, {Suspense, lazy} from "react";
|
||||
import {Button, Checkbox, Col, Form, Input, Result, Spin, Tabs} from "antd";
|
||||
import {Button, Checkbox, Col, Form, Input, Result, Spin, Tabs, message} from "antd";
|
||||
import {ArrowLeftOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
||||
@ -23,7 +23,6 @@ import * as AuthBackend from "./AuthBackend";
|
||||
import * as OrganizationBackend from "../backend/OrganizationBackend";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import * as Provider from "./Provider";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
import * as Util from "./Util";
|
||||
import * as Setting from "../Setting";
|
||||
import * as AgreementModal from "../common/modal/AgreementModal";
|
||||
@ -36,6 +35,7 @@ import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
|
||||
import RedirectForm from "../common/RedirectForm";
|
||||
import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
|
||||
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
@ -746,8 +746,21 @@ class LoginPage extends React.Component {
|
||||
<div dangerouslySetInnerHTML={{__html: ("<style>" + signinItem.customCss?.replaceAll("<style>", "").replaceAll("</style>", "") + "</style>")}} />
|
||||
<Form.Item>
|
||||
{
|
||||
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
|
||||
return ProviderButton.renderProviderLogo(providerItem.provider, application, null, null, signinItem.rule, this.props.location);
|
||||
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map((providerItem, id) => {
|
||||
return (
|
||||
<span key ={id} onClick={(e) => {
|
||||
const agreementChecked = this.form.current.getFieldValue("agreement");
|
||||
|
||||
if (agreementChecked !== undefined && typeof agreementChecked === "boolean" && !agreementChecked) {
|
||||
e.preventDefault();
|
||||
message.error(i18next.t("signup:Please accept the agreement!"));
|
||||
}
|
||||
}}>
|
||||
{
|
||||
ProviderButton.renderProviderLogo(providerItem.provider, application, null, null, signinItem.rule, this.props.location)
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
|
@ -61,9 +61,9 @@ const authInfo = {
|
||||
},
|
||||
WeCom: {
|
||||
scope: "snsapi_userinfo",
|
||||
endpoint: "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect",
|
||||
endpoint: "https://login.work.weixin.qq.com/wwlogin/sso/login",
|
||||
silentEndpoint: "https://open.weixin.qq.com/connect/oauth2/authorize",
|
||||
internalEndpoint: "https://open.work.weixin.qq.com/wwopen/sso/qrConnect",
|
||||
internalEndpoint: "https://login.work.weixin.qq.com/wwlogin/sso/login",
|
||||
},
|
||||
Lark: {
|
||||
// scope: "email",
|
||||
@ -433,7 +433,7 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&scope=${scope}&response_type=code#wechat_redirect`;
|
||||
} else if (provider.method === "Normal") {
|
||||
endpoint = authInfo[provider.type].internalEndpoint;
|
||||
return `${endpoint}?appid=${provider.clientId}&agentid=${provider.appId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`;
|
||||
return `${endpoint}?login_type=CorpApp&appid=${provider.clientId}&agentid=${provider.appId}&redirect_uri=${redirectUri}&state=${state}`;
|
||||
} else {
|
||||
return `https://error:not-supported-provider-method:${provider.method}`;
|
||||
}
|
||||
@ -442,7 +442,8 @@ export function getAuthUrl(application, provider, method, code) {
|
||||
endpoint = authInfo[provider.type].silentEndpoint;
|
||||
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&scope=${scope}&response_type=code#wechat_redirect`;
|
||||
} else if (provider.method === "Normal") {
|
||||
return `${endpoint}?appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&usertype=member`;
|
||||
endpoint = authInfo[provider.type].endpoint;
|
||||
return `${endpoint}?login_type=ServiceApp&appid=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}`;
|
||||
} else {
|
||||
return `https://error:not-supported-provider-method:${provider.method}`;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Form, Input, Radio, Result, Row} from "antd";
|
||||
import {Button, Form, Input, Radio, Result, Row, message} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import * as AuthBackend from "./AuthBackend";
|
||||
import * as ProviderButton from "./ProviderButton";
|
||||
@ -653,8 +653,21 @@ class SignupPage extends React.Component {
|
||||
}
|
||||
return (
|
||||
|
||||
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map(providerItem => {
|
||||
return ProviderButton.renderProviderLogo(providerItem.provider, application, null, null, signupItem.rule, this.props.location);
|
||||
application.providers.filter(providerItem => this.isProviderVisible(providerItem)).map((providerItem, id) => {
|
||||
return (
|
||||
<span key={id} onClick={(e) => {
|
||||
const agreementChecked = this.form.current.getFieldValue("agreement");
|
||||
|
||||
if (agreementChecked !== undefined && typeof agreementChecked === "boolean" && !agreementChecked) {
|
||||
e.preventDefault();
|
||||
message.error(i18next.t("signup:Please accept the agreement!"));
|
||||
}
|
||||
}}>
|
||||
{
|
||||
ProviderButton.renderProviderLogo(providerItem.provider, application, null, null, signupItem.rule, this.props.location)
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
|
||||
);
|
||||
|
@ -108,6 +108,7 @@ class AccountTable extends React.Component {
|
||||
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
|
||||
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
|
||||
{name: "Face ID", label: i18next.t("user:Face ID")},
|
||||
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
|
||||
];
|
||||
};
|
||||
|
||||
|
182
web/src/table/MfaAccountTable.js
Normal file
182
web/src/table/MfaAccountTable.js
Normal file
@ -0,0 +1,182 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Image, Input, Row, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class MfaAccountTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
mfaAccounts: this.props.table !== null ? this.props.table.map((item, index) => {
|
||||
item.key = index;
|
||||
return item;
|
||||
}) : [],
|
||||
};
|
||||
}
|
||||
|
||||
count = this.props.table?.length ?? 0;
|
||||
|
||||
updateTable(table) {
|
||||
this.setState({
|
||||
mfaAccounts: table,
|
||||
});
|
||||
|
||||
this.props.onUpdateTable([...table].map((item) => {
|
||||
const newItem = Setting.deepCopy(item);
|
||||
delete newItem.key;
|
||||
return newItem;
|
||||
}));
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {key: this.count, accountName: "", issuer: "", secretKey: ""};
|
||||
if (table === undefined || table === null) {
|
||||
table = [];
|
||||
}
|
||||
|
||||
this.count += 1;
|
||||
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("mfaAccount:Account Name"),
|
||||
dataIndex: "accountName",
|
||||
key: "accountName",
|
||||
width: "400px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "accountName", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("mfaAccount:Issuer"),
|
||||
dataIndex: "issuer",
|
||||
key: "issuer",
|
||||
width: "300px",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "issuer", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("mfaAccount:Secret Key"),
|
||||
dataIndex: "secretKey",
|
||||
key: "secretKey",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input.Password value={text} onChange={e => {
|
||||
this.updateField(table, index, "secretKey", e.target.value);
|
||||
}} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Logo"),
|
||||
dataIndex: "issuer",
|
||||
key: "logo",
|
||||
width: "60px",
|
||||
render: (text, record, index) => (
|
||||
<Tooltip>
|
||||
{text ? (
|
||||
<Image width={36} height={36} preview={false} src={`https://cdn.casbin.org/img/social_${text.toLowerCase()}.png`}
|
||||
fallback="https://cdn.casbin.org/img/social_default.png" alt={text} />
|
||||
) : (
|
||||
<Image width={36} height={36} preview={false} src={"https://cdn.casbin.org/img/social_default.png"} alt="default" />
|
||||
)}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
width: "100px",
|
||||
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 scroll={{x: "max-content"}} rowKey="key" columns={columns} dataSource={table} size="middle" bordered pagination={false}
|
||||
title={() => (
|
||||
<div>
|
||||
{this.props.title}
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col span={24}>
|
||||
{
|
||||
this.renderTable(this.state.mfaAccounts)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MfaAccountTable;
|
Reference in New Issue
Block a user