Преглед изворни кода

fix(payment): 修复微信支付集成多项问题

- 修复订单号超 32 字符限制(微信支付要求),改用时间戳+随机字符格式
- 修复密钥解析:添加 wrapAsPEM 兼容 PEM/Base64/DER 多种输入格式
- 修复前端:RechargeCard 未识别 enableWechatTopUp 导致充值表单不显示
- 修复前端:支付按钮禁用逻辑未区分 epay/wechat_pay
- 移除状态轮询接口的 CriticalRateLimit 防止 429
- 过滤未启用易支付的旧支付方法避免混淆
- 修复 SiLinkedin 图标不存在导致构建失败
- 简化 webhook 冗余条件判断
- 添加密钥解析相关测试

Co-Authored-By: Claude <noreply@anthropic.com>
feat/alipay-payment
fengsilin пре 1 месец
родитељ
комит
cf2a0a7992
8 измењених фајлова са 144 додато и 26 уклоњено
  1. +1
    -2
      controller/subscription_payment_wechat.go
  2. +7
    -2
      controller/topup.go
  3. +31
    -13
      controller/topup_wechat.go
  4. +93
    -0
      controller/topup_wechat_test.go
  5. +1
    -1
      router/api-router.go
  6. +10
    -6
      web/src/components/topup/RechargeCard.jsx
  7. +1
    -0
      web/src/components/topup/index.jsx
  8. +0
    -2
      web/src/helpers/render.jsx

+ 1
- 2
controller/subscription_payment_wechat.go Прегледај датотеку

@@ -73,8 +73,7 @@ func SubscriptionRequestWechatPay(c *gin.Context) {
return return
} }


reference := fmt.Sprintf("wx-sub-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
tradeNo := "wx_sub_" + common.Sha1([]byte(reference))
tradeNo := fmt.Sprintf("ws%d%s", time.Now().UnixMilli(), randstr.String(8))


totalFee := int(math.Round(plan.PriceAmount * 100)) // 元 -> 分 totalFee := int(math.Round(plan.PriceAmount * 100)) // 元 -> 分




+ 7
- 2
controller/topup.go Прегледај датотеку

@@ -23,8 +23,13 @@ import (
) )


func GetTopUpInfo(c *gin.Context) { func GetTopUpInfo(c *gin.Context) {
// 获取支付方式
enableOnlineTopup := operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != ""

// 获取支付方式:易支付未启用时过滤掉旧支付方法
payMethods := operation_setting.PayMethods payMethods := operation_setting.PayMethods
if !enableOnlineTopup {
payMethods = []map[string]string{}
}


// 如果启用了 Stripe 支付,添加到支付方法列表 // 如果启用了 Stripe 支付,添加到支付方法列表
if setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "" { if setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "" {
@@ -69,7 +74,7 @@ func GetTopUpInfo(c *gin.Context) {
} }


data := gin.H{ data := gin.H{
"enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
"enable_online_topup": enableOnlineTopup,
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "", "enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
"enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]", "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
"enable_wechat_topup": setting.IsWechatPayConfigured(), "enable_wechat_topup": setting.IsWechatPayConfigured(),


+ 31
- 13
controller/topup_wechat.go Прегледај датотеку

@@ -3,6 +3,7 @@ package controller
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@@ -10,6 +11,7 @@ import (
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"


@@ -61,7 +63,7 @@ func getWechatPayClient() (*wechat.ClientV3, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("解析商户私钥 Base64 失败: %w", err) return nil, fmt.Errorf("解析商户私钥 Base64 失败: %w", err)
} }
privateKey = string(keyBytes)
privateKey = wrapAsPEM(keyBytes, "PRIVATE KEY")
} else { } else {
keyBytes, err := os.ReadFile(setting.WechatPayKeyPath) keyBytes, err := os.ReadFile(setting.WechatPayKeyPath)
if err != nil { if err != nil {
@@ -81,7 +83,7 @@ func getWechatPayClient() (*wechat.ClientV3, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("解析微信公钥 Base64 失败: %w", err) return nil, fmt.Errorf("解析微信公钥 Base64 失败: %w", err)
} }
if err := client.AutoVerifySignByPublicKey(pubKeyBytes, setting.WechatPayPubKeyID); err != nil {
if err := client.AutoVerifySignByPublicKey([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY")), setting.WechatPayPubKeyID); err != nil {
return nil, fmt.Errorf("开启公钥验签失败: %w", err) return nil, fmt.Errorf("开启公钥验签失败: %w", err)
} }
} else { } else {
@@ -89,7 +91,7 @@ func getWechatPayClient() (*wechat.ClientV3, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("读取微信公钥文件失败: %w", err) return nil, fmt.Errorf("读取微信公钥文件失败: %w", err)
} }
if err := client.AutoVerifySignByPublicKey(pubKeyBytes, setting.WechatPayPubKeyID); err != nil {
if err := client.AutoVerifySignByPublicKey([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY")), setting.WechatPayPubKeyID); err != nil {
return nil, fmt.Errorf("开启公钥验签失败: %w", err) return nil, fmt.Errorf("开启公钥验签失败: %w", err)
} }
} }
@@ -98,6 +100,29 @@ func getWechatPayClient() (*wechat.ClientV3, error) {
return wechatPayClient, nil return wechatPayClient, nil
} }


// wrapAsPEM 确保 keyBytes 是 PEM 格式,兼容多种输入:
// 1. 已是 PEM(含 -----BEGIN)→ 原样返回
// 2. 是 DER base64 文本(如 PEM 内层内容)→ 解码为 DER 后包装为 PEM
// 3. 是原始 DER 字节 → 直接包装为 PEM
func wrapAsPEM(keyBytes []byte, keyType string) string {
content := string(keyBytes)
if strings.Contains(content, "-----BEGIN") {
return content
}
// 尝试 base64 解码:内容可能是 PEM 文件内层的 base64 文本
if derBytes, err := base64.StdEncoding.DecodeString(content); err == nil && len(derBytes) > 0 {
return string(pem.EncodeToMemory(&pem.Block{
Type: keyType,
Bytes: derBytes,
}))
}
// 当作原始 DER 字节
return string(pem.EncodeToMemory(&pem.Block{
Type: keyType,
Bytes: keyBytes,
}))
}

// WechatPayRequest 微信支付请求参数 // WechatPayRequest 微信支付请求参数
type WechatPayRequest struct { type WechatPayRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
@@ -205,8 +230,7 @@ func RequestWechatPay(c *gin.Context) {
} }
chargedMoney := float64(req.Amount) * topupGroupRatio chargedMoney := float64(req.Amount) * topupGroupRatio


reference := fmt.Sprintf("wx-pay-%d-%d-%s", id, time.Now().UnixMilli(), randstr.String(4))
tradeNo := "wx_" + common.Sha1([]byte(reference))
tradeNo := fmt.Sprintf("wx%d%s", time.Now().UnixMilli(), randstr.String(8))


payMoney := getWechatPayMoney(float64(req.Amount), group) payMoney := getWechatPayMoney(float64(req.Amount), group)
totalFee := int(math.Round(payMoney * 100)) // 元 -> 分 totalFee := int(math.Round(payMoney * 100)) // 元 -> 分
@@ -235,7 +259,7 @@ func RequestWechatPay(c *gin.Context) {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"message": "success", "message": "success",
"data": gin.H{ "data": gin.H{
"trade_no": tradeNo,
"trade_no": tradeNo,
"qr_code_url": codeUrl, "qr_code_url": codeUrl,
}, },
}) })
@@ -273,7 +297,6 @@ func WechatPayStatus(c *gin.Context) {


// WechatPayWebhook 处理微信支付回调通知 // WechatPayWebhook 处理微信支付回调通知
func WechatPayWebhook(c *gin.Context) { func WechatPayWebhook(c *gin.Context) {
// 解析 V3 回调请求
notifyReq, err := wechat.V3ParseNotify(c.Request) notifyReq, err := wechat.V3ParseNotify(c.Request)
if err != nil { if err != nil {
log.Printf("解析微信支付回调失败: %v", err) log.Printf("解析微信支付回调失败: %v", err)
@@ -288,14 +311,12 @@ func WechatPayWebhook(c *gin.Context) {
return return
} }


// 验证签名
if err := notifyReq.VerifySignByPK(client.WxPublicKey()); err != nil { if err := notifyReq.VerifySignByPK(client.WxPublicKey()); err != nil {
log.Printf("微信支付回调验签失败: %v", err) log.Printf("微信支付回调验签失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "验签失败"}) c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "验签失败"})
return return
} }


// 解密回调数据
result, err := notifyReq.DecryptPayCipherText(setting.WechatPayAPIv3Key) result, err := notifyReq.DecryptPayCipherText(setting.WechatPayAPIv3Key)
if err != nil { if err != nil {
log.Printf("解密微信支付回调数据失败: %v", err) log.Printf("解密微信支付回调数据失败: %v", err)
@@ -303,7 +324,6 @@ func WechatPayWebhook(c *gin.Context) {
return return
} }


// 只处理支付成功
if result.TradeState != "SUCCESS" { if result.TradeState != "SUCCESS" {
log.Printf("微信支付回调非成功状态: %s, 订单号: %s", result.TradeState, result.OutTradeNo) log.Printf("微信支付回调非成功状态: %s, 订单号: %s", result.TradeState, result.OutTradeNo)
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
@@ -312,7 +332,6 @@ func WechatPayWebhook(c *gin.Context) {


tradeNo := result.OutTradeNo tradeNo := result.OutTradeNo


// 先尝试完成订阅订单
LockOrder(tradeNo) LockOrder(tradeNo)
defer UnlockOrder(tradeNo) defer UnlockOrder(tradeNo)


@@ -320,13 +339,12 @@ func WechatPayWebhook(c *gin.Context) {
log.Printf("微信支付订阅订单完成: %s", tradeNo) log.Printf("微信支付订阅订单完成: %s", tradeNo)
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
return return
} else if err != nil && !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
} else if !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
log.Printf("微信支付订阅订单处理失败: %s, err: %s", tradeNo, err.Error()) log.Printf("微信支付订阅订单处理失败: %s, err: %s", tradeNo, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"}) c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"})
return return
} }


// 处理充值订单
if err := model.RechargeWechat(tradeNo); err != nil { if err := model.RechargeWechat(tradeNo); err != nil {
log.Printf("微信支付充值失败: %s, err: %s", tradeNo, err.Error()) log.Printf("微信支付充值失败: %s, err: %s", tradeNo, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"}) c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"})


+ 93
- 0
controller/topup_wechat_test.go Прегледај датотеку

@@ -2,9 +2,12 @@ package controller


import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"testing" "testing"


"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
@@ -12,6 +15,7 @@ import (
"github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-pay/gopay/wechat/v3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gorm.io/gorm" "gorm.io/gorm"
@@ -372,3 +376,92 @@ func TestResetWechatPayClient_CallbackRegistered(t *testing.T) {
func TestPaymentMethodWechatPay(t *testing.T) { func TestPaymentMethodWechatPay(t *testing.T) {
assert.Equal(t, "wechat_pay", PaymentMethodWechatPay) assert.Equal(t, "wechat_pay", PaymentMethodWechatPay)
} }

// ========== Base64 密钥解析测试 ==========

func TestBase64PemDecoding(t *testing.T) {
// 测试:PEM 文件整体 Base64 编码 → 解码后应能得到有效 PEM
keyPath := "C:/Users/28221/Downloads/WXCertUtil/cert/1602799864_20260402_cert/apiclient_key.pem"
pubKeyPath := "C:/Users/28221/Downloads/pub_key.pem"

// 读取原始文件
origKeyBytes, err := os.ReadFile(keyPath)
require.NoError(t, err, "读取私钥文件失败")
t.Logf("原始私钥前30字符: %s", string(origKeyBytes[:30]))

// 模拟 getWechatPayClient 的逻辑:base64 编码 → 存入配置 → 解码
encoded := base64.StdEncoding.EncodeToString(origKeyBytes)
t.Logf("Base64 编码后长度: %d", len(encoded))

// 解码
decoded, err := base64.StdEncoding.DecodeString(encoded)
require.NoError(t, err, "Base64 解码失败")
assert.Equal(t, string(origKeyBytes), string(decoded), "解码后应与原始一致")

// 验证解码后确实是 PEM 格式
assert.Contains(t, string(decoded), "-----BEGIN", "解码后应包含 PEM 头")
t.Logf("解码后前30字符: %s", string(decoded[:30]))

// 测试 gopay 能否接受这个格式
mchID := "1602799864"
serialNo := "1ABE61AF2D4FA785F2782320B001D8B2E5517840"
apiV3Key := "Pr79nX6Ej4Xw4eReJ7KM7NhE4Hezec3y"

client, err := wechat.NewClientV3(mchID, serialNo, apiV3Key, string(decoded))
if err != nil {
t.Errorf("NewClientV3 失败: %v", err)
} else {
t.Log("NewClientV3 成功")
_ = client
}

// 读取公钥
pubKeyBytes, err := os.ReadFile(pubKeyPath)
require.NoError(t, err, "读取公钥文件失败")
t.Logf("原始公钥前30字符: %s", string(pubKeyBytes[:30]))

// Base64 编码公钥
encodedPub := base64.StdEncoding.EncodeToString(pubKeyBytes)
decodedPub, err := base64.StdEncoding.DecodeString(encodedPub)
require.NoError(t, err)
assert.Contains(t, string(decodedPub), "-----BEGIN", "解码后应包含 PEM 头")
}

func TestWrapAsPEM(t *testing.T) {
// Case 1: PEM 格式输入应原样返回
pemInput := "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkq\n-----END PRIVATE KEY-----"
result := wrapAsPEM([]byte(pemInput), "PRIVATE KEY")
assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")

// Case 2: 裸 DER 字节应自动包装
rawDER := []byte{0x30, 0x82, 0x02, 0x5c}
result = wrapAsPEM(rawDER, "PRIVATE KEY")
assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")
assert.Contains(t, result, "-----END PRIVATE KEY-----")

// Case 3: PEM 内层 base64 文本(双重编码场景)应正确解码为 DER 再包装
// 模拟:原始 DER → base64 得到 "MIIEvAI..." 文本 → 作为输入
innerB64 := base64.StdEncoding.EncodeToString(rawDER)
result = wrapAsPEM([]byte(innerB64), "PRIVATE KEY")
assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")
// 验证内层内容解码后等于原始 DER
block, _ := pem.Decode([]byte(result))
assert.Equal(t, rawDER, block.Bytes)

// Case 4: 用实际私钥文件测试内层 base64 场景
keyPath := "C:/Users/28221/Downloads/WXCertUtil/cert/1602799864_20260402_cert/apiclient_key.pem"
if keyBytes, err := os.ReadFile(keyPath); err == nil {
// 提取 PEM 内层的 base64 文本
block, _ := pem.Decode(keyBytes)
require.NotNil(t, block, "原始 PEM 解析失败")
innerB64 := base64.StdEncoding.EncodeToString(block.Bytes)

// 用内层 base64 文本作为输入
result := wrapAsPEM([]byte(innerB64), "PRIVATE KEY")
assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")

// 验证 gopay 能接受
_, err := wechat.NewClientV3("test_mch", "test_serial", "test_key", result)
t.Logf("gopay NewClientV3 with inner-b64 input: err=%v", err)
}
}

+ 1
- 1
router/api-router.go Прегледај датотеку

@@ -92,7 +92,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay) selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay)
selfRoute.POST("/wechat/pay/amount", controller.RequestWechatPayAmount) selfRoute.POST("/wechat/pay/amount", controller.RequestWechatPayAmount)
selfRoute.POST("/wechat/pay", middleware.CriticalRateLimit(), controller.RequestWechatPay) selfRoute.POST("/wechat/pay", middleware.CriticalRateLimit(), controller.RequestWechatPay)
selfRoute.GET("/wechat/pay/status", middleware.CriticalRateLimit(), controller.WechatPayStatus)
selfRoute.GET("/wechat/pay/status", controller.WechatPayStatus)
selfRoute.POST("/aff_transfer", controller.TransferAffQuota) selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
selfRoute.PUT("/setting", controller.UpdateUserSetting) selfRoute.PUT("/setting", controller.UpdateUserSetting)




+ 10
- 6
web/src/components/topup/RechargeCard.jsx Прегледај датотеку

@@ -57,6 +57,7 @@ const RechargeCard = ({
enableOnlineTopUp, enableOnlineTopUp,
enableStripeTopUp, enableStripeTopUp,
enableCreemTopUp, enableCreemTopUp,
enableWechatTopUp,
creemProducts, creemProducts,
creemPreTopUp, creemPreTopUp,
presetAmounts, presetAmounts,
@@ -224,19 +225,19 @@ const RechargeCard = ({
<div className='py-8 flex justify-center'> <div className='py-8 flex justify-center'>
<Spin size='large' /> <Spin size='large' />
</div> </div>
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? (
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWechatTopUp ? (
<Form <Form
getFormApi={(api) => (onlineFormApiRef.current = api)} getFormApi={(api) => (onlineFormApiRef.current = api)}
initValues={{ topUpCount: topUpCount }} initValues={{ topUpCount: topUpCount }}
> >
<div className='space-y-6'> <div className='space-y-6'>
{(enableOnlineTopUp || enableStripeTopUp) && (
{(enableOnlineTopUp || enableStripeTopUp || enableWechatTopUp) && (
<Row gutter={12}> <Row gutter={12}>
<Col xs={24} sm={24} md={24} lg={10} xl={10}> <Col xs={24} sm={24} md={24} lg={10} xl={10}>
<Form.InputNumber <Form.InputNumber
field='topUpCount' field='topUpCount'
label={t('充值数量')} label={t('充值数量')}
disabled={!enableOnlineTopUp && !enableStripeTopUp}
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWechatTopUp}
placeholder={ placeholder={
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp) t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
} }
@@ -295,9 +296,12 @@ const RechargeCard = ({
{payMethods.map((payMethod) => { {payMethods.map((payMethod) => {
const minTopupVal = Number(payMethod.min_topup) || 0; const minTopupVal = Number(payMethod.min_topup) || 0;
const isStripe = payMethod.type === 'stripe'; const isStripe = payMethod.type === 'stripe';
const isWechatPay = payMethod.type === 'wechat_pay';
const isEpay = !isStripe && !isWechatPay;
const disabled = const disabled =
(!enableOnlineTopUp && !isStripe) ||
(!enableOnlineTopUp && isEpay) ||
(!enableStripeTopUp && isStripe) || (!enableStripeTopUp && isStripe) ||
(!enableWechatTopUp && isWechatPay) ||
minTopupVal > Number(topUpCount || 0); minTopupVal > Number(topUpCount || 0);


const buttonEl = ( const buttonEl = (
@@ -313,7 +317,7 @@ const RechargeCard = ({
icon={ icon={
payMethod.type === 'alipay' ? ( payMethod.type === 'alipay' ? (
<SiAlipay size={18} color='#1677FF' /> <SiAlipay size={18} color='#1677FF' />
) : payMethod.type === 'wxpay' ? (
) : payMethod.type === 'wxpay' || payMethod.type === 'wechat_pay' ? (
<SiWechat size={18} color='#07C160' /> <SiWechat size={18} color='#07C160' />
) : payMethod.type === 'stripe' ? ( ) : payMethod.type === 'stripe' ? (
<SiStripe size={18} color='#635BFF' /> <SiStripe size={18} color='#635BFF' />
@@ -362,7 +366,7 @@ const RechargeCard = ({
</Row> </Row>
)} )}


{(enableOnlineTopUp || enableStripeTopUp) && (
{(enableOnlineTopUp || enableStripeTopUp || enableWechatTopUp) && (
<Form.Slot <Form.Slot
label={ label={
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>


+ 1
- 0
web/src/components/topup/index.jsx Прегледај датотеку

@@ -792,6 +792,7 @@ const TopUp = () => {
enableOnlineTopUp={enableOnlineTopUp} enableOnlineTopUp={enableOnlineTopUp}
enableStripeTopUp={enableStripeTopUp} enableStripeTopUp={enableStripeTopUp}
enableCreemTopUp={enableCreemTopUp} enableCreemTopUp={enableCreemTopUp}
enableWechatTopUp={enableWechatTopUp}
creemProducts={creemProducts} creemProducts={creemProducts}
creemPreTopUp={creemPreTopUp} creemPreTopUp={creemPreTopUp}
presetAmounts={presetAmounts} presetAmounts={presetAmounts}


+ 0
- 2
web/src/helpers/render.jsx Прегледај датотеку

@@ -89,7 +89,6 @@ import {
SiGitlab, SiGitlab,
SiGoogle, SiGoogle,
SiKeycloak, SiKeycloak,
SiLinkedin,
SiNextcloud, SiNextcloud,
SiNotion, SiNotion,
SiOkta, SiOkta,
@@ -504,7 +503,6 @@ const oauthProviderIconMap = {
google: SiGoogle, google: SiGoogle,
discord: SiDiscord, discord: SiDiscord,
facebook: SiFacebook, facebook: SiFacebook,
linkedin: SiLinkedin,
x: SiX, x: SiX,
twitter: SiX, twitter: SiX,
slack: SiSlack, slack: SiSlack,


Loading…
Откажи
Сачувај