Compare commits

..

22 Commits

Author SHA1 Message Date
haiwu
d7110ff8bf feat: support MetaMask provider (#2084)
* feat: add metamask provider

* feat: add eth login

* feat: check eth sign

* feat: finish metamask signin/signup

* feat: support MetaMask provider link/unlink

* feat: update web/craco.config.js to handle polyfill

* feat: gofumpt idp/metamask.go

* feat: update MetaMask logo path

* feat: support MetaMask avatar
2023-07-20 17:51:36 +08:00
f923a8f0d7 fix: provide detailed description of ldap in swagger (#2094)
* provide detailed description of ldap in swagger

* modify the directory of swagger

fix: provide detailed description of ldap in swagger
2023-07-20 12:32:48 +08:00
Yang Luo
7bfb74ba18 Fix typo 2023-07-19 19:34:43 +08:00
Yang Luo
38f031bc86 Show access secret if isAdminOrSelf is true in get-user and get-account APIs 2023-07-19 19:14:53 +08:00
Yang Luo
5c441d195c Add Effect to Casbin rule of add-permission 2023-07-19 18:52:22 +08:00
Yaodong Yu
0639564d27 fix: check group name cannot be same as organization name (#2090) 2023-07-19 11:37:28 +08:00
Yang Luo
6c647818ca feat: add "Sender number" input for Twilio SMS provider 2023-07-18 22:46:56 +08:00
Yaodong Yu
8bc73d17aa feat: fix bug that themeEditor can not load saved theme data (#2085) 2023-07-17 22:57:55 +08:00
Yang Luo
1f37c80177 feat: refactor code to add getStorageProvider() 2023-07-17 15:59:37 +08:00
Yaodong Yu
7924fca403 fix: hidden bug of "like" query (#2082) 2023-07-16 17:11:32 +08:00
Yang Luo
bd06996bab Improve CorsFilter for login API 2023-07-15 19:29:48 +08:00
Yang Luo
19ab168b12 Fix panic in func (c *ApiController) GetUser() if no user exists in DB 2023-07-14 20:57:59 +08:00
UsherFall
854a74b73e feat: fix the error when user uploads avatar to minio (https) (#2078)
* fix: Error reported when user uploads avatar to minio (https)

* Update provider.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-07-14 15:58:30 +08:00
yehong
beefb0b432 fix: fix event-stream streaming output in prod mode (#2076) 2023-07-14 11:59:26 +08:00
Yang Luo
d8969e6652 Support EnableSigninSession after SAML login 2023-07-14 11:27:18 +08:00
Yang Luo
666ff48837 Use id param in /sync-ldap-users API 2023-07-13 00:14:18 +08:00
Yang Luo
0a0c1b4788 Fix "Groups is immutable" bug when updating a user 2023-07-13 00:03:18 +08:00
Yang Luo
438c999e11 Add password mask to /get-ldaps and /get-ldap APIs 2023-07-12 23:21:47 +08:00
Yang Luo
a193ceb33d Fix bug in TestDeployStaticFiles() 2023-07-12 23:11:02 +08:00
Yang Luo
caec1d1bac Only consider x509 certs in /.well-known/jwks API 2023-07-12 22:39:39 +08:00
Denis Plynskiy
0d48da24dc feat: fix wrong rowKey for tables (#2070) 2023-07-12 21:12:36 +08:00
Yaodong Yu
de9eeaa1ef fix: init groups modify rule with admin (#2054) 2023-07-11 09:49:49 +08:00
58 changed files with 1063 additions and 99 deletions

View File

@@ -129,7 +129,7 @@ func QueryAnswerStream(authToken string, question string, writer io.Writer, buil
fmt.Printf("%s", data)
// Write the streamed data as Server-Sent Events
if _, err = fmt.Fprintf(writer, "data: %s\n\n", data); err != nil {
if _, err = fmt.Fprintf(writer, "event: message\ndata: %s\n\n", data); err != nil {
return err
}
flusher.Flush()

View File

@@ -368,9 +368,11 @@ func (c *ApiController) GetAccount() {
return
}
user.Permissions = object.GetMaskedPermissions(user.Permissions)
user.Roles = object.GetMaskedRoles(user.Roles)
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
if user != nil {
user.Permissions = object.GetMaskedPermissions(user.Permissions)
user.Roles = object.GetMaskedRoles(user.Roles)
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
}
organization, err := object.GetMaskedOrganization(object.GetOrganizationByUser(user))
if err != nil {
@@ -378,7 +380,8 @@ func (c *ApiController) GetAccount() {
return
}
u, err := object.GetMaskedUser(user)
isAdminOrSelf := c.IsAdminOrSelf(user)
u, err := object.GetMaskedUser(user, isAdminOrSelf)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -123,6 +123,11 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
return
}
resp = &Response{Status: "ok", Msg: "", Data: res, Data2: map[string]string{"redirectUrl": redirectUrl, "method": method}}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else if form.Type == ResponseTypeCas {
// not oauth but CAS SSO protocol
service := c.Input().Get("service")
@@ -135,11 +140,11 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
resp.Data = st
}
}
if application.EnableSigninSession || application.HasPromptPage() {
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else {
resp = wrapErrorResponse(fmt.Errorf("unknown response type: %s", form.Type))
}
@@ -417,7 +422,7 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
} else if provider.Category == "OAuth" {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// OAuth
idpInfo := object.FromProviderToIdpInfo(c.Ctx, provider)
idProvider := idp.GetIdProvider(idpInfo, authForm.RedirectUri)
@@ -460,7 +465,7 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
} else if provider.Category == "OAuth" {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
user, err = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if err != nil {
c.ResponseError(err.Error())
@@ -481,7 +486,7 @@ func (c *ApiController) Login() {
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else if provider.Category == "OAuth" {
} else if provider.Category == "OAuth" || provider.Category == "Web3" {
// Sign up via OAuth
if application.EnableLinkWithEmail {
if userInfo.Email != "" {

View File

@@ -55,6 +55,18 @@ func (c *ApiController) IsAdmin() bool {
return isGlobalAdmin || user.IsAdmin
}
func (c *ApiController) IsAdminOrSelf(user2 *object.User) bool {
isGlobalAdmin, user := c.isGlobalAdmin()
if isGlobalAdmin || (user != nil && user.IsAdmin) {
return true
}
if user.Owner == user2.Owner && user.Name == user2.Name {
return true
}
return false
}
func (c *ApiController) isGlobalAdmin() (bool, *object.User) {
username := c.GetSessionUsername()
if strings.HasPrefix(username, "app/") {

View File

@@ -38,8 +38,11 @@ type LdapSyncResp struct {
}
// GetLdapUsers
// @Tag Account API
// @Title GetLdapser
// @Tag Account API
// @Description get ldap users
// Param id string true "id"
// @Success 200 {object} LdapResp The Response object
// @router /get-ldap-users [get]
func (c *ApiController) GetLdapUsers() {
id := c.Input().Get("id")
@@ -94,18 +97,24 @@ func (c *ApiController) GetLdapUsers() {
}
// GetLdaps
// @Tag Account API
// @Title GetLdaps
// @Tag Account API
// @Description get ldaps
// @Param owner query string false "owner"
// @Success 200 {array} object.Ldap The Response object
// @router /get-ldaps [get]
func (c *ApiController) GetLdaps() {
owner := c.Input().Get("owner")
c.ResponseOk(object.GetLdaps(owner))
c.ResponseOk(object.GetMaskedLdaps(object.GetLdaps(owner)))
}
// GetLdap
// @Tag Account API
// @Title GetLdap
// @Tag Account API
// @Description get ldap
// @Param id query string true "id"
// @Success 200 {object} object.Ldap The Response object
// @router /get-ldap [get]
func (c *ApiController) GetLdap() {
id := c.Input().Get("id")
@@ -116,12 +125,15 @@ func (c *ApiController) GetLdap() {
}
_, name := util.GetOwnerAndNameFromId(id)
c.ResponseOk(object.GetLdap(name))
c.ResponseOk(object.GetMaskedLdap(object.GetLdap(name)))
}
// AddLdap
// @Tag Account API
// @Title AddLdap
// @Tag Account API
// @Description add ldap
// @Param body body object.Ldap true "The details of the ldap"
// @Success 200 {object} controllers.Response The Response object
// @router /add-ldap [post]
func (c *ApiController) AddLdap() {
var ldap object.Ldap
@@ -160,8 +172,11 @@ func (c *ApiController) AddLdap() {
}
// UpdateLdap
// @Tag Account API
// @Title UpdateLdap
// @Tag Account API
// @Description update ldap
// @Param body body object.Ldap true "The details of the ldap"
// @Success 200 {object} controllers.Response The Response object
// @router /update-ldap [post]
func (c *ApiController) UpdateLdap() {
var ldap object.Ldap
@@ -198,8 +213,11 @@ func (c *ApiController) UpdateLdap() {
}
// DeleteLdap
// @Tag Account API
// @Title DeleteLdap
// @Tag Account API
// @Description delete ldap
// @Param body body object.Ldap true "The details of the ldap"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-ldap [post]
func (c *ApiController) DeleteLdap() {
var ldap object.Ldap
@@ -222,12 +240,16 @@ func (c *ApiController) DeleteLdap() {
}
// SyncLdapUsers
// @Tag Account API
// @Title SyncLdapUsers
// @Tag Account API
// @Description sync ldap users
// @Param id query string true "id"
// @Success 200 {object} LdapSyncResp The Response object
// @router /sync-ldap-users [post]
func (c *ApiController) SyncLdapUsers() {
owner := c.Input().Get("owner")
ldapId := c.Input().Get("ldapId")
id := c.Input().Get("id")
owner, ldapId := util.GetOwnerAndNameFromId(id)
var users []object.LdapUser
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
if err != nil {

View File

@@ -198,14 +198,18 @@ func (c *ApiController) GetUser() {
return
}
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
if user != nil {
user.MultiFactorAuths = object.GetAllMfaProps(user, true)
}
err = object.ExtendUserWithRolesAndPermissions(user)
if err != nil {
c.ResponseError(err.Error())
return
}
maskedUser, err := object.GetMaskedUser(user)
isAdminOrSelf := c.IsAdminOrSelf(user)
maskedUser, err := object.GetMaskedUser(user, isAdminOrSelf)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -25,6 +25,12 @@ import (
)
func TestDeployStaticFiles(t *testing.T) {
provider := object.GetProvider(util.GetId("admin", "provider_storage_aliyun_oss"))
object.InitConfig()
provider, err := object.GetProvider(util.GetId("admin", "provider_storage_aliyun_oss"))
if err != nil {
panic(err)
}
deployStaticFiles(provider)
}

2
go.sum
View File

@@ -546,8 +546,6 @@ github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sashabaranov/go-openai v1.9.1 h1:3N52HkJKo9Zlo/oe1AVv5ZkCOny0ra58/ACvAxkN3MM=
github.com/sashabaranov/go-openai v1.9.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.12.0 h1:aRNHH0gtVfrpIaEolD0sWrLLRnYQNK4cH/bIAHwL8Rk=
github.com/sashabaranov/go-openai v1.12.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=

80
idp/metamask.go Normal file
View File

@@ -0,0 +1,80 @@
// Copyright 2023 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 idp
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"golang.org/x/oauth2"
)
const Web3AuthTokenKey = "web3AuthToken"
type MetaMaskIdProvider struct {
Client *http.Client
}
type Web3AuthToken struct {
Address string `json:"address"`
Nonce string `json:"nonce"`
CreateAt uint64 `json:"createAt"`
TypedData string `json:"typedData"`
Signature string `json:"signature"` // signature for typed data
}
func NewMetaMaskIdProvider() *MetaMaskIdProvider {
idp := &MetaMaskIdProvider{}
return idp
}
func (idp *MetaMaskIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *MetaMaskIdProvider) GetToken(code string) (*oauth2.Token, error) {
web3AuthToken := Web3AuthToken{}
if err := json.Unmarshal([]byte(code), &web3AuthToken); err != nil {
return nil, err
}
token := &oauth2.Token{
AccessToken: web3AuthToken.Signature,
TokenType: "Bearer",
Expiry: time.Now().AddDate(0, 1, 0),
}
token = token.WithExtra(map[string]interface{}{
Web3AuthTokenKey: web3AuthToken,
})
return token, nil
}
func (idp *MetaMaskIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
// TODO use "github.com/ethereum/go-ethereum" to check address's eth balance or transaction
web3AuthToken, ok := token.Extra(Web3AuthTokenKey).(Web3AuthToken)
if !ok {
return nil, errors.New("invalid web3AuthToken")
}
userInfo := &UserInfo{
Id: web3AuthToken.Address,
Username: web3AuthToken.Address,
DisplayName: web3AuthToken.Address,
AvatarUrl: fmt.Sprintf("metamask:%v", web3AuthToken.Address),
}
return userInfo, nil
}

View File

@@ -109,6 +109,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) IdProvider {
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
case "Bilibili":
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl)
case "MetaMask":
return NewMetaMaskIdProvider()
default:
if isGothSupport(idpInfo.Type) {
return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl)

View File

@@ -107,6 +107,11 @@ func UpdateGroup(id string, group *Group) (bool, error) {
return false, err
}
err = checkGroupName(group.Name)
if err != nil {
return false, err
}
if name != group.Name {
err := GroupChangeTrigger(name, group.Name)
if err != nil {
@@ -123,6 +128,11 @@ func UpdateGroup(id string, group *Group) (bool, error) {
}
func AddGroup(group *Group) (bool, error) {
err := checkGroupName(group.Name)
if err != nil {
return false, err
}
affected, err := adapter.Engine.Insert(group)
if err != nil {
return false, err
@@ -168,6 +178,17 @@ func DeleteGroup(group *Group) (bool, error) {
return affected != 0, nil
}
func checkGroupName(name string) error {
exist, err := adapter.Engine.Exist(&Organization{Owner: "admin", Name: name})
if err != nil {
return err
}
if exist {
return errors.New("group name can't be same as the organization name")
}
return nil
}
func (group *Group) GetId() string {
return fmt.Sprintf("%s/%s", group.Owner, group.Name)
}
@@ -225,7 +246,7 @@ func GetGroupUserCount(groupName string, field, value string) (int64, error) {
func GetPaginationGroupUsers(groupName string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
users := []*User{}
session := adapter.Engine.Table("user").
Where(builder.Like{"`groups`", groupName})
Where(builder.Like{"`groups`", groupName + "\""})
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
@@ -255,7 +276,7 @@ func GetPaginationGroupUsers(groupName string, offset, limit int, field, value,
func GetGroupUsers(groupName string) ([]*User, error) {
users := []*User{}
err := adapter.Engine.Table("user").
Where(builder.Like{"`groups`", groupName}).
Where(builder.Like{"`groups`", groupName + "\""}).
Find(&users)
if err != nil {
return nil, err

View File

@@ -61,7 +61,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},

View File

@@ -103,6 +103,37 @@ func GetLdap(id string) (*Ldap, error) {
}
}
func GetMaskedLdap(ldap *Ldap, errs ...error) (*Ldap, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
if ldap == nil {
return nil, nil
}
if ldap.Password != "" {
ldap.Password = "***"
}
return ldap, nil
}
func GetMaskedLdaps(ldaps []*Ldap, errs ...error) ([]*Ldap, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
var err error
for _, ldap := range ldaps {
ldap, err = GetMaskedLdap(ldap)
if err != nil {
return nil, err
}
}
return ldaps, nil
}
func UpdateLdap(ldap *Ldap) (bool, error) {
if l, err := GetLdap(ldap.Id); err != nil {
return false, nil

View File

@@ -123,6 +123,10 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
// link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
// or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
for _, cert := range certs {
if cert.Type != "x509" {
continue
}
certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)

View File

@@ -109,10 +109,10 @@ func getPolicies(permission *Permission) [][]string {
for _, action := range permission.Actions {
if domainExist {
for _, domain := range permission.Domains {
policies = append(policies, []string{user, domain, resource, strings.ToLower(action), "", permissionId})
policies = append(policies, []string{user, domain, resource, strings.ToLower(action), permission.Effect, permissionId})
}
} else {
policies = append(policies, []string{user, resource, strings.ToLower(action), "", "", permissionId})
policies = append(policies, []string{user, resource, strings.ToLower(action), permission.Effect, "", permissionId})
}
}
}

View File

@@ -229,7 +229,7 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
session = session.Omit("client_secret2")
}
if provider.Type != "Keycloak" {
if provider.Type == "Tencent Cloud COS" {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
}
@@ -243,7 +243,7 @@ func UpdateProvider(id string, provider *Provider) (bool, error) {
}
func AddProvider(provider *Provider) (bool, error) {
if provider.Type != "Keycloak" {
if provider.Type == "Tencent Cloud COS" {
provider.Endpoint = util.GetEndPoint(provider.Endpoint)
provider.IntranetEndpoint = util.GetEndPoint(provider.IntranetEndpoint)
}

View File

@@ -49,7 +49,7 @@ func (pi *ProviderItem) IsProviderVisible() bool {
if pi.Provider == nil {
return false
}
return pi.Provider.Category == "OAuth" || pi.Provider.Category == "SAML"
return pi.Provider.Category == "OAuth" || pi.Provider.Category == "SAML" || pi.Provider.Category == "Web3"
}
func (pi *ProviderItem) isProviderPrompted() bool {

View File

@@ -161,7 +161,8 @@ func SendWebhooks(record *Record) error {
if matched {
if webhook.IsUserExtended {
user, err := GetMaskedUser(getUser(record.Organization, record.User))
user, err := getUser(record.Organization, record.User)
user, err = GetMaskedUser(user, false, err)
if err != nil {
return err
}

View File

@@ -42,7 +42,11 @@ func SendSms(provider *Provider, content string, phoneNumbers ...string) error {
return err
}
if provider.Type == sender.Aliyun {
if provider.Type == sender.Twilio {
if provider.AppId != "" {
phoneNumbers = append([]string{provider.AppId}, phoneNumbers...)
}
} else if provider.Type == sender.Aliyun {
for i, number := range phoneNumbers {
phoneNumbers[i] = strings.TrimPrefix(number, "+86")
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/storage"
"github.com/casdoor/casdoor/util"
"github.com/casdoor/oss"
)
var isCloudIntranet bool
@@ -102,11 +103,11 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
return fileUrl, objectKey
}
func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer, lang string) (string, string, error) {
func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface, error) {
endpoint := getProviderEndpoint(provider)
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint)
if storageProvider == nil {
return "", "", fmt.Errorf(i18n.Translate(lang, "storage:The provider type: %s is not supported"), provider.Type)
return nil, fmt.Errorf(i18n.Translate(lang, "storage:The provider type: %s is not supported"), provider.Type)
}
if provider.Domain == "" {
@@ -114,9 +115,18 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
UpdateProvider(provider.GetId(), provider)
}
return storageProvider, nil
}
func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffer, lang string) (string, string, error) {
storageProvider, err := getStorageProvider(provider, lang)
if err != nil {
return "", "", err
}
fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true)
_, err := storageProvider.Put(objectKey, fileBuffer)
_, err = storageProvider.Put(objectKey, fileBuffer)
if err != nil {
return "", "", err
}
@@ -154,15 +164,9 @@ func DeleteFile(provider *Provider, objectKey string, lang string) error {
return fmt.Errorf(i18n.Translate(lang, "storage:The objectKey: %s is not allowed"), objectKey)
}
endpoint := getProviderEndpoint(provider)
storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, endpoint)
if storageProvider == nil {
return fmt.Errorf(i18n.Translate(lang, "storage:The provider type: %s is not supported"), provider.Type)
}
if provider.Domain == "" {
provider.Domain = storageProvider.GetEndpoint()
UpdateProvider(provider.GetId(), provider)
storageProvider, err := getStorageProvider(provider, lang)
if err != nil {
return err
}
return storageProvider.Delete(objectKey)

View File

@@ -156,6 +156,7 @@ type User struct {
Yammer string `xorm:"yammer varchar(100)" json:"yammer"`
Yandex string `xorm:"yandex varchar(100)" json:"yandex"`
Zoom string `xorm:"zoom varchar(100)" json:"zoom"`
MetaMask string `xorm:"metamask varchar(100)" json:"metamask"`
Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
@@ -418,7 +419,7 @@ func GetUserNoCheck(id string) (*User, error) {
return getUser(owner, name)
}
func GetMaskedUser(user *User, errs ...error) (*User, error) {
func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error) {
if len(errs) > 0 && errs[0] != nil {
return nil, errs[0]
}
@@ -430,9 +431,13 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
if user.Password != "" {
user.Password = "***"
}
if user.AccessSecret != "" {
user.AccessSecret = "***"
if !isAdminOrSelf {
if user.AccessSecret != "" {
user.AccessSecret = "***"
}
}
if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts {
manageAccount.Password = "***"
@@ -456,7 +461,7 @@ func GetMaskedUsers(users []*User, errs ...error) ([]*User, error) {
var err error
for _, user := range users {
user, err = GetMaskedUser(user)
user, err = GetMaskedUser(user, false)
if err != nil {
return nil, err
}

View File

@@ -293,7 +293,13 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
itemsChanged = append(itemsChanged, item)
}
if oldUser.Groups == nil {
oldUser.Groups = []string{}
}
oldUserGroupsJson, _ := json.Marshal(oldUser.Groups)
if newUser.Groups == nil {
newUser.Groups = []string{}
}
newUserGroupsJson, _ := json.Marshal(newUser.Groups)
if string(oldUserGroupsJson) != string(newUserGroupsJson) {
item := GetAccountItemByName("Groups", organization)

View File

@@ -33,6 +33,13 @@ func CorsFilter(ctx *context.Context) {
origin := ctx.Input.Header(headerOrigin)
originConf := conf.GetConfigString("origin")
if ctx.Request.Method == "POST" && ctx.Request.RequestURI == "/api/login/oauth/access_token" {
ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
return
}
if origin != "" && originConf != "" && origin != originConf {
ok, err := object.IsOriginAllowed(origin)
if err != nil {

View File

@@ -188,7 +188,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.AddLdap"
"description": "add ldap",
"operationId": "ApiController.AddLdap",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the ldap",
"required": true,
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/add-message": {
@@ -1086,7 +1106,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.DeleteLdap"
"description": "delete ldap",
"operationId": "ApiController.DeleteLdap",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the ldap",
"required": true,
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/delete-message": {
@@ -2098,7 +2138,25 @@
"tags": [
"Account API"
],
"operationId": "ApiController.GetLdap"
"description": "get ldap",
"operationId": "ApiController.GetLdap",
"parameters": [
{
"in": "query",
"name": "id",
"description": "id",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
}
}
},
"/api/get-ldap-users": {
@@ -2106,7 +2164,16 @@
"tags": [
"Account API"
],
"operationId": "ApiController.GetLdapser"
"description": "get ldap users",
"operationId": "ApiController.GetLdapser",
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/LdapResp"
}
}
}
}
},
"/api/get-ldaps": {
@@ -2114,7 +2181,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.GetLdaps"
"description": "get ldaps",
"operationId": "ApiController.GetLdaps",
"parameters": [
{
"in": "query",
"name": "owner",
"description": "owner",
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/object.Ldap"
}
}
}
}
}
},
"/api/get-message": {
@@ -4139,7 +4226,25 @@
"tags": [
"Account API"
],
"operationId": "ApiController.SyncLdapUsers"
"description": "sync ldap users",
"operationId": "ApiController.SyncLdapUsers",
"parameters": [
{
"in": "query",
"name": "id",
"description": "id",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/LdapSyncResp"
}
}
}
}
},
"/api/unlink": {
@@ -4329,7 +4434,27 @@
"tags": [
"Account API"
],
"operationId": "ApiController.UpdateLdap"
"description": "update ldap",
"operationId": "ApiController.UpdateLdap",
"parameters": [
{
"in": "body",
"name": "body",
"description": "The details of the ldap",
"required": true,
"schema": {
"$ref": "#/definitions/object.Ldap"
}
}
],
"responses": {
"200": {
"description": "The Response object",
"schema": {
"$ref": "#/definitions/controllers.Response"
}
}
}
}
},
"/api/update-message": {
@@ -5152,6 +5277,14 @@
"title": "LaravelResponse",
"type": "object"
},
"LdapResp": {
"title": "LdapResp",
"type": "object"
},
"LdapSyncResp": {
"title": "LdapSyncResp",
"type": "object"
},
"Response": {
"title": "Response",
"type": "object"
@@ -5681,6 +5814,59 @@
}
}
},
"object.Ldap": {
"title": "Ldap",
"type": "object",
"properties": {
"autoSync": {
"type": "integer",
"format": "int64"
},
"baseDn": {
"type": "string"
},
"createdTime": {
"type": "string"
},
"enableSsl": {
"type": "boolean"
},
"filter": {
"type": "string"
},
"filterFields": {
"type": "array",
"items": {
"type": "string"
}
},
"host": {
"type": "string"
},
"id": {
"type": "string"
},
"lastSync": {
"type": "string"
},
"owner": {
"type": "string"
},
"password": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int64"
},
"serverName": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"object.ManagedAccount": {
"title": "ManagedAccount",
"type": "object",

View File

@@ -122,7 +122,20 @@ paths:
post:
tags:
- Account API
description: add ldap
operationId: ApiController.AddLdap
parameters:
- in: body
name: body
description: The details of the ldap
required: true
schema:
$ref: '#/definitions/object.Ldap'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-message:
post:
tags:
@@ -702,7 +715,20 @@ paths:
post:
tags:
- Account API
description: delete ldap
operationId: ApiController.DeleteLdap
parameters:
- in: body
name: body
description: The details of the ldap
required: true
schema:
$ref: '#/definitions/object.Ldap'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-message:
post:
tags:
@@ -1360,17 +1386,48 @@ paths:
get:
tags:
- Account API
description: get ldap
operationId: ApiController.GetLdap
parameters:
- in: query
name: id
description: id
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Ldap'
/api/get-ldap-users:
get:
tags:
- Account API
description: get ldap users
operationId: ApiController.GetLdapser
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/LdapResp'
/api/get-ldaps:
get:
tags:
- Account API
description: get ldaps
operationId: ApiController.GetLdaps
parameters:
- in: query
name: owner
description: owner
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Ldap'
/api/get-message:
get:
tags:
@@ -2703,7 +2760,19 @@ paths:
post:
tags:
- Account API
description: sync ldap users
operationId: ApiController.SyncLdapUsers
parameters:
- in: query
name: id
description: id
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/LdapSyncResp'
/api/unlink:
post:
tags:
@@ -2827,7 +2896,20 @@ paths:
post:
tags:
- Account API
description: update ldap
operationId: ApiController.UpdateLdap
parameters:
- in: body
name: body
description: The details of the ldap
required: true
schema:
$ref: '#/definitions/object.Ldap'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-message:
post:
tags:
@@ -3367,6 +3449,12 @@ definitions:
LaravelResponse:
title: LaravelResponse
type: object
LdapResp:
title: LdapResp
type: object
LdapSyncResp:
title: LdapSyncResp
type: object
Response:
title: Response
type: object
@@ -3725,6 +3813,42 @@ definitions:
type: string
username:
type: string
object.Ldap:
title: Ldap
type: object
properties:
autoSync:
type: integer
format: int64
baseDn:
type: string
createdTime:
type: string
enableSsl:
type: boolean
filter:
type: string
filterFields:
type: array
items:
type: string
host:
type: string
id:
type: string
lastSync:
type: string
owner:
type: string
password:
type: string
port:
type: integer
format: int64
serverName:
type: string
username:
type: string
object.ManagedAccount:
title: ManagedAccount
type: object

View File

@@ -50,4 +50,32 @@ module.exports = {
},
},
],
webpack: {
// use polyfill Buffer with Webpack 5
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
// https://craco.js.org/docs/configuration/webpack/
configure: (webpackConfig, { env, paths }) => {
webpackConfig.resolve.fallback = {
// "process": require.resolve('process/browser'),
// "util": require.resolve("util/"),
// "url": require.resolve("url/"),
// "zlib": require.resolve("browserify-zlib"),
// "stream": require.resolve("stream-browserify"),
// "http": require.resolve("stream-http"),
// "https": require.resolve("https-browserify"),
// "assert": require.resolve("assert/"),
"buffer": require.resolve('buffer/'),
"process": false,
"util": false,
"url": false,
"zlib": false,
"stream": false,
"http": false,
"https": false,
"assert": false,
"buffer": false,
};
return webpackConfig;
},
}
};

View File

@@ -9,11 +9,13 @@
"@crowdin/cli": "^3.7.10",
"@ctrl/tinycolor": "^3.5.0",
"@emotion/react": "^11.10.5",
"@metamask/eth-sig-util": "^6.0.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"antd": "5.2.3",
"antd-token-previewer": "^1.1.0-22",
"buffer": "^6.0.3",
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0",
@@ -34,6 +36,7 @@
"react-helmet": "^6.1.0",
"react-highlight-words": "^0.18.0",
"react-i18next": "^11.8.7",
"react-metamask-avatar": "^1.2.1",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.1",
"react-social-login-buttons": "^3.4.0"

View File

@@ -89,6 +89,8 @@ import {withTranslation} from "react-i18next";
import LanguageSelect from "./common/select/LanguageSelect";
import ThemeSelect from "./common/select/ThemeSelect";
import OrganizationSelect from "./common/select/OrganizationSelect";
import {clearWeb3AuthToken} from "./auth/Web3Auth";
import AccountAvatar from "./account/AccountAvatar";
const {Header, Footer, Content} = Layout;
@@ -312,12 +314,11 @@ class App extends Component {
.then((res) => {
if (res.status === "ok") {
const owner = this.state.account.owner;
this.setState({
account: null,
themeAlgorithm: ["default"],
});
clearWeb3AuthToken();
Setting.showMessage("success", i18next.t("application:Logged out successfully"));
const redirectUri = res.data2;
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
@@ -348,7 +349,9 @@ class App extends Component {
);
} else {
return (
<Avatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size="large">
<Avatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size="large"
icon={<AccountAvatar src={this.state.account.avatar} style={{verticalAlign: "middle"}} size={40} />}
>
{Setting.getShortName(this.state.account.name)}
</Avatar>
);

View File

@@ -158,6 +158,7 @@ class CertEditPage extends React.Component {
{
[
{id: "x509", name: "x509"},
{id: "Payment", name: "Payment"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>

View File

@@ -151,6 +151,7 @@ class CertListPage extends BaseListPage {
filterMultiple: false,
filters: [
{text: "x509", value: "x509"},
{text: "Payment", value: "Payment"},
],
width: "110px",
sorter: true,
@@ -213,7 +214,7 @@ class CertListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={certs} rowKey="name" size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={columns} dataSource={certs} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Certs")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -74,7 +74,7 @@ class OrganizationListPage extends BaseListPage {
{name: "Ranking", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "API key", label: i18next.t("general:API key"), modifyRule: "Self"},
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},

View File

@@ -196,7 +196,7 @@ class PlanListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={plans} rowKey="name" size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={columns} dataSource={plans} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Plans")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -165,7 +165,7 @@ class PricingListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={pricings} rowKey="name" size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={columns} dataSource={pricings} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Pricings")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -236,7 +236,10 @@ class ProviderEditPage extends React.Component {
tooltip = i18next.t("provider:Agent ID - Tooltip");
}
} else if (provider.category === "SMS") {
if (provider.type === "Tencent Cloud SMS") {
if (provider.type === "Twilio SMS") {
text = i18next.t("provider:Sender number");
tooltip = i18next.t("provider:Sender number - Tooltip");
} else if (provider.type === "Tencent Cloud SMS") {
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
} else if (provider.type === "Volc Engine SMS") {
@@ -355,6 +358,8 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("type", "Default");
} else if (value === "AI") {
this.updateProviderField("type", "OpenAI API - GPT");
} else if (value === "Web3") {
this.updateProviderField("type", "MetaMask");
}
})}>
{
@@ -367,6 +372,7 @@ class ProviderEditPage extends React.Component {
{id: "SAML", name: "SAML"},
{id: "SMS", name: "SMS"},
{id: "Storage", name: "Storage"},
{id: "Web3", name: "Web3"},
]
.sort((a, b) => a.name.localeCompare(b.name))
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
@@ -521,7 +527,7 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.category === "Captcha" && this.state.provider.type === "Default" ? null : (
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") || this.state.provider.category === "Web3" ? null : (
<React.Fragment>
{
this.state.provider.category === "AI" ? null : (
@@ -674,6 +680,7 @@ class ProviderEditPage extends React.Component {
) : null}
</div>
) : null}
{this.getAppIdRow(this.state.provider)}
{
this.state.provider.category === "Email" ? (
<React.Fragment>
@@ -919,7 +926,6 @@ class ProviderEditPage extends React.Component {
</Row>
) : null
}
{this.getAppIdRow(this.state.provider)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :

View File

@@ -216,6 +216,12 @@ export const OtherProviderInfo = {
url: "https://platform.openai.com",
},
},
Web3: {
"MetaMask": {
logo: `${StaticBaseUrl}/img/social_metamask.svg`,
url: "https://metamask.io/",
},
},
};
export function initCountries() {
@@ -288,7 +294,7 @@ export function isProviderVisible(providerItem) {
return false;
}
if (providerItem.provider.category !== "OAuth" && providerItem.provider.category !== "SAML") {
if (!["OAuth", "SAML", "Web3"].includes(providerItem.provider.category)) {
return false;
}
@@ -891,6 +897,10 @@ export function getProviderTypeOptions(category) {
return ([
{id: "OpenAI API - GPT", name: "OpenAI API - GPT"},
]);
} else if (category === "Web3") {
return ([
{id: "MetaMask", name: "MetaMask"},
]);
} else {
return [];
}

View File

@@ -215,7 +215,7 @@ class SubscriptionListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={subscriptions} rowKey="name" size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={columns} dataSource={subscriptions} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Subscriptions")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -36,6 +36,7 @@ import PopconfirmModal from "./common/modal/PopconfirmModal";
import {DeleteMfa} from "./backend/MfaBackend";
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
import * as MfaBackend from "./backend/MfaBackend";
import AccountAvatar from "./account/AccountAvatar";
const {Option} = Select;
@@ -791,10 +792,23 @@ class UserEditPage extends React.Component {
{
(this.state.application === null || this.state.user === null) ? null : (
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem) =>
(providerItem.provider.category === "OAuth") ? (
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} account={this.props.account} onUnlinked={() => {return this.unlinked();}} />
(providerItem.provider.category === "OAuth" || providerItem.provider.category === "Web3") ? (
<OAuthWidget
key={providerItem.name}
labelSpan={(Setting.isMobile()) ? 10 : 3}
user={this.state.user}
application={this.state.application}
providerItem={providerItem}
account={this.props.account}
onUnlinked={() => {return this.unlinked();}} />
) : (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => {return this.unlinked();}} />
<SamlWidget
key={providerItem.name}
labelSpan={(Setting.isMobile()) ? 10 : 3}
user={this.state.user}
application={this.state.application}
providerItem={providerItem}
onUnlinked={() => {return this.unlinked();}} />
)
)
)
@@ -971,12 +985,12 @@ class UserEditPage extends React.Component {
{
imgUrl ?
<a target="_blank" rel="noreferrer" href={imgUrl} style={{marginBottom: "10px"}}>
<img src={imgUrl} alt={imgUrl} height={90} style={{marginBottom: "20px"}} />
<AccountAvatar src={imgUrl} alt={imgUrl} size={90} style={{marginBottom: "20px"}} />
</a>
:
<Col style={{height: "78%", border: "1px dotted grey", borderRadius: 3, marginBottom: 5}}>
<div style={{fontSize: 30, margin: 10}}>+</div>
<div style={{verticalAlign: "middle", marginBottom: 10}}>{`请上传${title}...`}</div>
<div style={{verticalAlign: "middle", marginBottom: 10}}>{`Upload ${title}...`}</div>
</Col>
}
<CropperDivModal disabled={disabled} tag={tag} setTitle={set} buttonText={`${title}...`} title={title} user={this.state.user} organization={this.state.organizations.find(organization => organization.name === this.state.organizationName)} />

View File

@@ -23,6 +23,7 @@ import * as UserBackend from "./backend/UserBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import AccountAvatar from "./account/AccountAvatar";
class UserListPage extends BaseListPage {
constructor(props) {
@@ -270,7 +271,7 @@ class UserListPage extends BaseListPage {
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
<img referrerPolicy="no-referrer" src={text} alt={text} width={50} />
<AccountAvatar referrerPolicy="no-referrer" src={text} alt={text} size={50} />
</a>
);
},

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 {MetaMaskAvatar} from "react-metamask-avatar";
class AccountAvatar extends React.Component {
render() {
const {src, size} = this.props;
// The avatar for Metamask account is directly generated by an algorithm based on the address
// src = "metamask:0xC304b2cC0Be8E9ce10fF3Afd34820Ed306A23600";
const matchMetaMask = src.match(/^metamask:(\w+)$/);
if (matchMetaMask) {
const address = matchMetaMask[1];
return (
<MetaMaskAvatar address={address} size={size} />
);
}
return (
<img width={size} height={size} src={src} />
);
}
}
export default AccountAvatar;

View File

@@ -95,6 +95,12 @@ class AuthCallback extends React.Component {
if (code === null) {
code = params.get("authCode");
}
// The code for Metamask is the JSON-serialized string of Web3AuthToken
// Due to the limited length of URLs, we only pass the web3AuthTokenKey
if (code === null) {
code = params.get("web3AuthTokenKey");
code = localStorage.getItem(code);
}
// Steam don't use code, so we should use all params as code.
if (isSteam !== null && code === null) {
code = this.props.location.search;

View File

@@ -317,6 +317,10 @@ const authInfo = {
scope: "user:read",
endpoint: "https://zoom.us/oauth/authorize",
},
MetaMask: {
scope: "",
endpoint: "",
},
};
export function getProviderUrl(provider) {
@@ -459,5 +463,7 @@ export function getAuthUrl(application, provider, method) {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}&grant_options[]=per-user`;
} else if (provider.type === "Twitter" || provider.type === "Fitbit") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
} else if (provider.type === "MetaMask") {
return `${redirectUri}?state=${state}`;
}
}

View File

@@ -17,6 +17,7 @@ import i18next from "i18next";
import * as Provider from "./Provider";
import {getProviderLogoURL} from "../Setting";
import {GithubLoginButton, GoogleLoginButton} from "react-social-login-buttons";
import {authViaMetaMask} from "./Web3Auth";
import QqLoginButton from "./QqLoginButton";
import FacebookLoginButton from "./FacebookLoginButton";
import WeiboLoginButton from "./WeiboLoginButton";
@@ -117,6 +118,12 @@ function goToSamlUrl(provider, location) {
});
}
export function goToWeb3Url(application, provider, method) {
if (provider.type === "MetaMask") {
authViaMetaMask(application, provider, method);
}
}
export function renderProviderLogo(provider, application, width, margin, size, location) {
if (size === "small") {
if (provider.category === "OAuth") {
@@ -153,6 +160,12 @@ export function renderProviderLogo(provider, application, width, margin, size, l
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
</a>
);
} else if (provider.category === "Web3") {
return (
<a key={provider.displayName} onClick={() => goToWeb3Url(application, provider, "signup")}>
<img width={width} height={width} src={getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
</a>
);
}
} else if (provider.type === "Custom") {
// style definition
@@ -192,6 +205,16 @@ export function renderProviderLogo(provider, application, width, margin, size, l
</a>
</div>
);
} else if (provider.category === "Web3") {
return (
<div key={provider.displayName} style={{marginBottom: "10px"}}>
<a onClick={() => goToWeb3Url(application, provider, "signup")}>
{
getSigninButton(provider)
}
</a>
</div>
);
} else {
return (
<div key={provider.displayName} style={{marginBottom: "10px"}}>

149
web/src/auth/Web3Auth.js Normal file
View File

@@ -0,0 +1,149 @@
// // Copyright 2023 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 {goToLink, showMessage} from "../Setting";
import i18next from "i18next";
import {v4 as uuidv4} from "uuid";
import {SignTypedDataVersion, recoverTypedSignature} from "@metamask/eth-sig-util";
import {getAuthUrl} from "./Provider";
import {Buffer} from "buffer";
// import {toChecksumAddress} from "ethereumjs-util";
global.Buffer = Buffer;
export function generateNonce() {
const nonce = uuidv4();
return nonce;
}
export function getWeb3AuthTokenKey(address) {
return `Web3AuthToken_${address}`;
}
export function setWeb3AuthToken(token) {
const key = getWeb3AuthTokenKey(token.address);
localStorage.setItem(key, JSON.stringify(token));
}
export function getWeb3AuthToken(address) {
const key = getWeb3AuthTokenKey(address);
return JSON.parse(localStorage.getItem(key));
}
export function delWeb3AuthToken(address) {
const key = getWeb3AuthTokenKey(address);
localStorage.removeItem(key);
}
export function clearWeb3AuthToken() {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith("Web3AuthToken_")) {
localStorage.removeItem(key);
}
});
}
export function detectMetaMaskPlugin() {
// check if ethereum extension MetaMask is installed
return window.ethereum && window.ethereum.isMetaMask;
}
export function requestEthereumAccount() {
const method = "eth_requestAccounts";
const selectedAccount = window.ethereum.request({method})
.then((accounts) => {
return accounts[0];
});
return selectedAccount;
}
export function signEthereumTypedData(from, nonce) {
// https://docs.metamask.io/wallet/how-to/sign-data/
const date = new Date();
const typedData = JSON.stringify({
domain: {
chainId: window.ethereum.chainId,
name: "Casdoor",
version: "1",
},
message: {
prompt: "In order to authenticate to this website, sign this request and your public address will be sent to the server in a verifiable way.",
nonce: nonce,
createAt: `${date.toLocaleString()}`,
},
primaryType: "AuthRequest",
types: {
EIP712Domain: [
{name: "name", type: "string"},
{name: "version", type: "string"},
{name: "chainId", type: "uint256"},
],
AuthRequest: [
{name: "prompt", type: "string"},
{name: "nonce", type: "string"},
{name: "createAt", type: "string"},
],
},
});
const method = "eth_signTypedData_v4";
const params = [from, typedData];
return window.ethereum.request({method, params})
.then((sign) => {
return {
address: from,
createAt: Math.floor(date.getTime() / 1000),
typedData: typedData,
signature: sign,
};
});
}
export function checkEthereumSignedTypedData(token) {
if (token === undefined || token === null) {
return false;
}
if (token.address && token.typedData && token.signature) {
const recoveredAddr = recoverTypedSignature({
data: JSON.parse(token.typedData),
signature: token.signature,
version: SignTypedDataVersion.V4,
});
// const recoveredAddr = token.address;
return recoveredAddr === token.address;
// return toChecksumAddress(recoveredAddr) === toChecksumAddress(token.address);
}
return false;
}
export async function authViaMetaMask(application, provider, method) {
if (!detectMetaMaskPlugin()) {
showMessage("error", `${i18next.t("login:MetaMask plugin not detected")}`);
return;
}
try {
const account = await requestEthereumAccount();
let token = getWeb3AuthToken(account);
if (!checkEthereumSignedTypedData(token)) {
const nonce = generateNonce();
token = await signEthereumTypedData(account, nonce);
setWeb3AuthToken(token);
}
const redirectUri = `${getAuthUrl(application, provider, method)}&web3AuthTokenKey=${getWeb3AuthTokenKey(account)}`;
goToLink(redirectUri);
} catch (err) {
showMessage("error", `${i18next.t("login:Failed to obtain MetaMask authorization")}: ${err.message}`);
}
}

View File

@@ -19,6 +19,9 @@ import * as UserBackend from "../backend/UserBackend";
import * as Setting from "../Setting";
import * as Provider from "../auth/Provider";
import * as AuthBackend from "../auth/AuthBackend";
import {goToWeb3Url} from "../auth/ProviderButton";
import {delWeb3AuthToken} from "../auth/Web3Auth";
import AccountAvatar from "../account/AccountAvatar";
class OAuthWidget extends React.Component {
constructor(props) {
@@ -88,12 +91,15 @@ class OAuthWidget extends React.Component {
return user.properties[key];
}
unlinkUser(providerType) {
unlinkUser(providerType, linkedValue) {
const body = {
providerType: providerType,
// should add the unlink user's info, cause the user may not be logged in, but a admin want to unlink the user.
user: this.props.user,
};
if (providerType === "MetaMask") {
delWeb3AuthToken(linkedValue);
}
AuthBackend.unlink(body)
.then((res) => {
if (res.status === "ok") {
@@ -151,7 +157,7 @@ class OAuthWidget extends React.Component {
</span>
</Col>
<Col span={24 - this.props.labelSpan} >
<img style={{marginRight: "10px"}} width={30} height={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
<AccountAvatar style={{marginRight: "10px"}} size={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
<span style={{width: this.props.labelSpan === 3 ? "300px" : "200px", display: (Setting.isMobile()) ? "inline" : "inline-block"}}>
{
linkedValue === "" ? (
@@ -169,11 +175,15 @@ class OAuthWidget extends React.Component {
</span>
{
linkedValue === "" ? (
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
</a>
provider.category === "Web3" ? (
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id} onClick={() => goToWeb3Url(application, provider, "link")}>{i18next.t("user:Link")}</Button>
) : (
<a key={provider.displayName} href={user.id !== account.id ? null : Provider.getAuthUrl(application, provider, "link")}>
<Button style={{marginLeft: "20px", width: linkButtonWidth}} type="primary" disabled={user.id !== account.id}>{i18next.t("user:Link")}</Button>
</a>
)
) : (
<Button disabled={!providerItem.canUnlink && !account.isGlobalAdmin} style={{marginLeft: "20px", width: linkButtonWidth}} onClick={() => this.unlinkUser(provider.type)}>{i18next.t("user:Unlink")}</Button>
<Button disabled={!providerItem.canUnlink && !account.isGlobalAdmin} style={{marginLeft: "20px", width: linkButtonWidth}} onClick={() => this.unlinkUser(provider.type, linkedValue)}>{i18next.t("user:Unlink")}</Button>
)
}
</Col>

View File

@@ -17,7 +17,7 @@ import ThemePicker from "./ThemePicker";
import ColorPicker, {GREEN_COLOR, PINK_COLOR} from "./ColorPicker";
import RadiusPicker from "./RadiusPicker";
import * as React from "react";
import {useEffect} from "react";
import {useEffect, useLayoutEffect} from "react";
import {Content} from "antd/es/layout/layout";
import i18next from "i18next";
import * as Conf from "../../Conf";
@@ -58,6 +58,11 @@ export default function ThemeEditor(props) {
}, [isLight, isCompact]);
useEffect(() => {
onThemeChange(null, themeData);
form.setFieldsValue(themeData);
}, []);
useLayoutEffect(() => {
const mergedData = Object.assign(Object.assign(Object.assign({}, Conf.ThemeDefault), {themeType}), ThemesInfo[themeType]);
onThemeChange(null, mergedData);
form.setFieldsValue(mergedData);

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Automatische Anmeldung",
"Continue with": "Weitermachen mit",
"Email or phone": "E-Mail oder Telefon",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "Passwort vergessen?",
"Loading": "Laden",
"Logging out...": "Ausloggen...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "Kein Konto?",
"Or sign in with another account": "Oder mit einem anderen Konto anmelden",
"Please input your Email or Phone!": "Bitte geben Sie Ihre E-Mail oder Telefonnummer ein!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Auto sign in",
"Continue with": "Continue with",
"Email or phone": "Email or phone",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "Forgot password?",
"Loading": "Loading",
"Logging out...": "Logging out...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "No account?",
"Or sign in with another account": "Or sign in with another account",
"Please input your Email or Phone!": "Please input your Email or Phone!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Inicio de sesión automático",
"Continue with": "Continúe con",
"Email or phone": "Correo electrónico o teléfono",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "¿Olvidaste tu contraseña?",
"Loading": "Cargando",
"Logging out...": "Cerrando sesión...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "¿No tienes cuenta?",
"Or sign in with another account": "O inicia sesión con otra cuenta",
"Please input your Email or Phone!": "¡Por favor introduzca su correo electrónico o teléfono!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Connexion automatique",
"Continue with": "Continuer avec",
"Email or phone": "Email ou téléphone",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "Mot de passe oublié ?",
"Loading": "Chargement",
"Logging out...": "Déconnexion...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "Aucun compte ?",
"Or sign in with another account": "Ou connectez-vous avec un autre compte",
"Please input your Email or Phone!": "S'il vous plaît, entrez votre adresse e-mail ou votre numéro de téléphone !",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Masuk otomatis",
"Continue with": "Lanjutkan dengan",
"Email or phone": "Email atau telepon",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "Lupa kata sandi?",
"Loading": "Memuat",
"Logging out...": "Keluar...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "Tidak memiliki akun?",
"Or sign in with another account": "Atau masuk dengan akun lain",
"Please input your Email or Phone!": "Silahkan masukkan email atau nomor telepon Anda!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "自動サインイン",
"Continue with": "続ける",
"Email or phone": "メールまたは電話",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "パスワードを忘れましたか?",
"Loading": "ローディング",
"Logging out...": "ログアウト中...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "アカウントがありませんか?",
"Or sign in with another account": "別のアカウントでサインインする",
"Please input your Email or Phone!": "あなたのメールアドレスまたは電話番号を入力してください!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "자동 로그인",
"Continue with": "계속하다",
"Email or phone": "이메일 또는 전화",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "비밀번호를 잊으셨나요?",
"Loading": "로딩 중입니다",
"Logging out...": "로그아웃 중...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "계정이 없나요?",
"Or sign in with another account": "다른 계정으로 로그인하세요",
"Please input your Email or Phone!": "이메일 또는 전화번호를 입력해주세요!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Entrar automaticamente",
"Continue with": "Continuar com",
"Email or phone": "Email ou telefone",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "Esqueceu a senha?",
"Loading": "Carregando",
"Logging out...": "Saindo...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "Não possui uma conta?",
"Or sign in with another account": "Ou entre com outra conta",
"Please input your Email or Phone!": "Por favor, informe seu email ou telefone!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Автоматическая авторизация",
"Continue with": "Продолжайте с",
"Email or phone": "Электронная почта или телефон",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "Забыли пароль?",
"Loading": "Загрузка",
"Logging out...": "Выход...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "Нет аккаунта?",
"Or sign in with another account": "Или войти с другой учетной записью",
"Please input your Email or Phone!": "Пожалуйста, введите свой адрес электронной почты или номер телефона!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "Tự động đăng nhập",
"Continue with": "Tiếp tục với",
"Email or phone": "Email hoặc điện thoại",
"Failed to obtain MetaMask authorization": "Failed to obtain MetaMask authorization",
"Forgot password?": "Quên mật khẩu?",
"Loading": "Đang tải",
"Logging out...": "Đăng xuất ...",
"MetaMask plugin not detected": "MetaMask plugin not detected",
"No account?": "Không có tài khoản?",
"Or sign in with another account": "Hoặc đăng nhập bằng tài khoản khác",
"Please input your Email or Phone!": "Vui lòng nhập địa chỉ Email hoặc số điện thoại của bạn!",

View File

@@ -392,9 +392,11 @@
"Auto sign in": "下次自动登录",
"Continue with": "使用以下账号继续",
"Email or phone": "Email或手机号",
"Failed to obtain MetaMask authorization": "获取MetaMask授权失败",
"Forgot password?": "忘记密码?",
"Loading": "加载中",
"Logging out...": "正在退出登录...",
"MetaMask plugin not detected": "未检测到MetaMask插件",
"No account?": "没有账号?",
"Or sign in with another account": "或者,登录其他账号",
"Please input your Email or Phone!": "请输入您的Email或手机号!",
@@ -746,6 +748,8 @@
"Token URL - Tooltip": "自定义OAuth的Token URL",
"Type": "类型",
"Type - Tooltip": "类型",
"User mapping": "User mapping",
"User mapping - Tooltip": "User mapping - Tooltip",
"UserInfo URL": "UserInfo URL",
"UserInfo URL - Tooltip": "自定义OAuth的UserInfo URL",
"admin (Shared)": "admin共享"

View File

@@ -110,7 +110,7 @@ class ProviderTable extends React.Component {
key: "canSignUp",
width: "120px",
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
return null;
}
@@ -127,7 +127,7 @@ class ProviderTable extends React.Component {
key: "canSignIn",
width: "120px",
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
return null;
}
@@ -144,7 +144,7 @@ class ProviderTable extends React.Component {
key: "canUnlink",
width: "120px",
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
return null;
}
@@ -161,7 +161,7 @@ class ProviderTable extends React.Component {
key: "prompted",
width: "120px",
render: (text, record, index) => {
if (record.provider?.category !== "OAuth") {
if (!["OAuth", "Web3"].includes(record.provider?.category)) {
return null;
}

View File

@@ -173,7 +173,7 @@
lru-cache "^5.1.1"
semver "^6.3.0"
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.5":
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz#2192a1970ece4685fbff85b48da2c32fcb130b7c"
integrity sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==
@@ -433,16 +433,6 @@
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
"@babel/plugin-proposal-private-property-in-object@^7.21.11":
version "7.21.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c"
integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
"@babel/helper-create-class-features-plugin" "^7.21.0"
"@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e"
@@ -1590,6 +1580,20 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.43.0.tgz#559ca3d9ddbd6bf907ad524320a0d14b85586af0"
integrity sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==
"@ethereumjs/rlp@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41"
integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==
"@ethereumjs/util@^8.0.6":
version "8.1.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4"
integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==
dependencies:
"@ethereumjs/rlp" "^4.0.1"
ethereum-cryptography "^2.0.0"
micro-ftch "^0.3.1"
"@humanwhocodes/config-array@^0.10.4":
version "0.10.7"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.7.tgz#6d53769fd0c222767e6452e8ebda825c22e9f0dc"
@@ -1931,6 +1935,18 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@metamask/eth-sig-util@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-6.0.0.tgz#083321dc7285a9aa6e066db7c49be6e94c5e03a3"
integrity sha512-M0ezVz8lirXG1P6rHPzx+9i4zfhebCgVHE8XQT8VWxy/eUWllHQGcBcE8QmOusC7su55M4CMr9AyMIu0lx452g==
dependencies:
"@ethereumjs/util" "^8.0.6"
bn.js "^4.12.0"
ethereum-cryptography "^2.0.0"
ethjs-util "^0.1.6"
tweetnacl "^1.0.3"
tweetnacl-util "^0.15.1"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@@ -1938,6 +1954,18 @@
dependencies:
eslint-scope "5.1.1"
"@noble/curves@1.1.0", "@noble/curves@~1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
dependencies:
"@noble/hashes" "1.3.1"
"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -2094,6 +2122,28 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz#31b9c510d8cada9683549e1dbb4284cca5001faf"
integrity sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==
"@scure/base@~1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@scure/bip32@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10"
integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==
dependencies:
"@noble/curves" "~1.1.0"
"@noble/hashes" "~1.3.1"
"@scure/base" "~1.1.0"
"@scure/bip39@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a"
integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==
dependencies:
"@noble/hashes" "~1.3.0"
"@scure/base" "~1.1.0"
"@sheerun/mutationobserver-shim@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25"
@@ -3674,6 +3724,11 @@ bluebird@^3.5.5, bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.12.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
body-parser@1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
@@ -3769,6 +3824,14 @@ buffer@^5.6.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
builtin-modules@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
@@ -5480,6 +5543,24 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
ethereum-cryptography@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67"
integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==
dependencies:
"@noble/curves" "1.1.0"
"@noble/hashes" "1.3.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
ethjs-util@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536"
integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==
dependencies:
is-hex-prefixed "1.0.0"
strip-hex-prefix "1.0.0"
eventemitter2@6.4.7:
version "6.4.7"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
@@ -6463,7 +6544,7 @@ identity-obj-proxy@^3.0.0:
dependencies:
harmony-reflect "^1.4.6"
ieee754@^1.1.13:
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -6668,6 +6749,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-hex-prefixed@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554"
integrity sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==
is-installed-globally@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
@@ -7978,6 +8064,11 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
micro-ftch@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f"
integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
@@ -10150,6 +10241,11 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-metamask-avatar@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-metamask-avatar/-/react-metamask-avatar-1.2.1.tgz#f0623e00ebc90ec24b8ac91cad3a25f653e7bc25"
integrity sha512-EQhaW27PdqGKLxCnDgpCTWnEs1bjba+l5b/ZQc1V/GSWrCznAcrQ2HrcjSPgmuud2rvDChYyrzgRB5sBU33gSw==
react-refresh@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
@@ -11194,6 +11290,13 @@ strip-final-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
strip-hex-prefix@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f"
integrity sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==
dependencies:
is-hex-prefixed "1.0.0"
strip-indent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
@@ -11731,11 +11834,21 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tweetnacl-util@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b"
integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
tweetnacl@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"