mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-05 00:03:42 +08:00
Compare commits
11 Commits
v0.5.0-alp
...
v0.5.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eea096654 | ||
|
|
4ab3211c0e | ||
|
|
3da119efba | ||
|
|
dccd66b852 | ||
|
|
2fcd6852e0 | ||
|
|
9b4d1964d4 | ||
|
|
806bf8241c | ||
|
|
ce93c9b6b2 | ||
|
|
4ec4289565 | ||
|
|
3dc5a0f91d | ||
|
|
80a846673a |
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# One API
|
# One API
|
||||||
|
|
||||||
_✨ An OpenAI key management & redistribution system, easy to deploy & use ✨_
|
_✨ Access all LLM through the standard OpenAI API format, easy to deploy & use ✨_
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
# One API
|
# One API
|
||||||
|
|
||||||
_✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用✨_
|
_✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 ✨_
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -58,13 +58,13 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
1. 支持多种 API 访问渠道:
|
1. 支持多种大模型:
|
||||||
+ [x] OpenAI 官方通道(支持配置镜像)
|
+ [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference))
|
||||||
+ [x] **Azure OpenAI API**
|
|
||||||
+ [x] [Anthropic Claude 系列模型](https://anthropic.com)
|
+ [x] [Anthropic Claude 系列模型](https://anthropic.com)
|
||||||
+ [x] [Google PaLM2 系列模型](https://developers.generativeai.google)
|
+ [x] [Google PaLM2 系列模型](https://developers.generativeai.google)
|
||||||
+ [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
+ [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
||||||
+ [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
|
+ [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
|
||||||
|
2. 支持配置镜像以及众多第三方代理服务:
|
||||||
+ [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
|
+ [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
|
||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||||
+ [x] [API2D](https://api2d.com/r/197971)
|
+ [x] [API2D](https://api2d.com/r/197971)
|
||||||
@@ -72,27 +72,27 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
|
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
|
||||||
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
|
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
|
||||||
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
|
+ [x] 自定义渠道:例如各种未收录的第三方代理服务
|
||||||
2. 支持通过**负载均衡**的方式访问多个渠道。
|
3. 支持通过**负载均衡**的方式访问多个渠道。
|
||||||
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||||
4. 支持**多机部署**,[详见此处](#多机部署)。
|
5. 支持**多机部署**,[详见此处](#多机部署)。
|
||||||
5. 支持**令牌管理**,设置令牌的过期时间和额度。
|
6. 支持**令牌管理**,设置令牌的过期时间和额度。
|
||||||
6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
|
7. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。
|
||||||
7. 支持**通道管理**,批量创建通道。
|
8. 支持**通道管理**,批量创建通道。
|
||||||
8. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。
|
9. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。
|
||||||
9. 支持渠道**设置模型列表**。
|
10. 支持渠道**设置模型列表**。
|
||||||
10. 支持**查看额度明细**。
|
11. 支持**查看额度明细**。
|
||||||
11. 支持**用户邀请奖励**。
|
12. 支持**用户邀请奖励**。
|
||||||
12. 支持以美元为单位显示额度。
|
13. 支持以美元为单位显示额度。
|
||||||
13. 支持发布公告,设置充值链接,设置新用户初始额度。
|
14. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||||
14. 支持模型映射,重定向用户的请求模型。
|
15. 支持模型映射,重定向用户的请求模型。
|
||||||
15. 支持失败自动重试。
|
16. 支持失败自动重试。
|
||||||
16. 支持绘图接口。
|
17. 支持绘图接口。
|
||||||
17. 支持丰富的**自定义**设置,
|
18. 支持丰富的**自定义**设置,
|
||||||
1. 支持自定义系统名称,logo 以及页脚。
|
1. 支持自定义系统名称,logo 以及页脚。
|
||||||
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||||
18. 支持通过系统访问令牌访问管理 API。
|
19. 支持通过系统访问令牌访问管理 API。
|
||||||
19. 支持 Cloudflare Turnstile 用户校验。
|
20. 支持 Cloudflare Turnstile 用户校验。
|
||||||
20. 支持用户管理,支持**多种用户登录注册方式**:
|
21. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
||||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
|||||||
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
||||||
var RequestInterval = time.Duration(requestInterval) * time.Second
|
var RequestInterval = time.Duration(requestInterval) * time.Second
|
||||||
|
|
||||||
|
var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleGuestUser = 0
|
RoleGuestUser = 0
|
||||||
RoleCommonUser = 1
|
RoleCommonUser = 1
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ func GetSubscription(c *gin.Context) {
|
|||||||
var usedQuota int
|
var usedQuota int
|
||||||
var err error
|
var err error
|
||||||
var token *model.Token
|
var token *model.Token
|
||||||
|
var expiredTime int64
|
||||||
if common.DisplayTokenStatEnabled {
|
if common.DisplayTokenStatEnabled {
|
||||||
tokenId := c.GetInt("token_id")
|
tokenId := c.GetInt("token_id")
|
||||||
token, err = model.GetTokenById(tokenId)
|
token, err = model.GetTokenById(tokenId)
|
||||||
|
expiredTime = token.ExpiredTime
|
||||||
remainQuota = token.RemainQuota
|
remainQuota = token.RemainQuota
|
||||||
usedQuota = token.UsedQuota
|
usedQuota = token.UsedQuota
|
||||||
} else {
|
} else {
|
||||||
@@ -21,6 +23,9 @@ func GetSubscription(c *gin.Context) {
|
|||||||
remainQuota, err = model.GetUserQuota(userId)
|
remainQuota, err = model.GetUserQuota(userId)
|
||||||
usedQuota, err = model.GetUserUsedQuota(userId)
|
usedQuota, err = model.GetUserUsedQuota(userId)
|
||||||
}
|
}
|
||||||
|
if expiredTime <= 0 {
|
||||||
|
expiredTime = 0
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
openAIError := OpenAIError{
|
openAIError := OpenAIError{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
@@ -45,6 +50,7 @@ func GetSubscription(c *gin.Context) {
|
|||||||
SoftLimitUSD: amount,
|
SoftLimitUSD: amount,
|
||||||
HardLimitUSD: amount,
|
HardLimitUSD: amount,
|
||||||
SystemHardLimitUSD: amount,
|
SystemHardLimitUSD: amount,
|
||||||
|
AccessUntil: expiredTime,
|
||||||
}
|
}
|
||||||
c.JSON(200, subscription)
|
c.JSON(200, subscription)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type OpenAISubscriptionResponse struct {
|
|||||||
SoftLimitUSD float64 `json:"soft_limit_usd"`
|
SoftLimitUSD float64 `json:"soft_limit_usd"`
|
||||||
HardLimitUSD float64 `json:"hard_limit_usd"`
|
HardLimitUSD float64 `json:"hard_limit_usd"`
|
||||||
SystemHardLimitUSD float64 `json:"system_hard_limit_usd"`
|
SystemHardLimitUSD float64 `json:"system_hard_limit_usd"`
|
||||||
|
AccessUntil int64 `json:"access_until"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIUsageDailyCost struct {
|
type OpenAIUsageDailyCost struct {
|
||||||
@@ -84,7 +85,6 @@ func GetAuthHeader(token string) http.Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest(method, url, nil)
|
req, err := http.NewRequest(method, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -92,10 +92,13 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
|
|||||||
for k := range headers {
|
for k := range headers {
|
||||||
req.Header.Add(k, headers.Get(k))
|
req.Header.Add(k, headers.Get(k))
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("status code: %d", res.StatusCode)
|
||||||
|
}
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr
|
|||||||
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
client := &http.Client{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return err, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,8 +127,9 @@ func SendPasswordResetEmail(c *gin.Context) {
|
|||||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
||||||
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||||
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
|
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
||||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
|
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
||||||
|
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
|
||||||
err := common.SendEmail(subject, email, content)
|
err := common.SendEmail(subject, email, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -109,8 +109,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
|||||||
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
||||||
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
||||||
|
|
||||||
client := &http.Client{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope
|
|||||||
}
|
}
|
||||||
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
||||||
// And then we will have to send an error response, but in this case, the header has already been set.
|
// And then we will have to send an error response, but in this case, the header has already been set.
|
||||||
// So the client will be confused by the response.
|
// So the httpClient will be confused by the response.
|
||||||
// For example, Postman will report error, and we cannot check the response at all.
|
// For example, Postman will report error, and we cannot check the response at all.
|
||||||
for k, v := range resp.Header {
|
for k, v := range resp.Header {
|
||||||
c.Writer.Header().Set(k, v[0])
|
c.Writer.Header().Set(k, v[0])
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const (
|
|||||||
APITypeZhipu
|
APITypeZhipu
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var httpClient *http.Client
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
httpClient = &http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||||
channelType := c.GetInt("channel")
|
channelType := c.GetInt("channel")
|
||||||
tokenId := c.GetInt("token_id")
|
tokenId := c.GetInt("token_id")
|
||||||
@@ -244,8 +250,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
||||||
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
|
||||||
//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
|
//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
|
||||||
client := &http.Client{}
|
resp, err := httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -477,6 +478,16 @@ func DeleteUser(c *gin.Context) {
|
|||||||
|
|
||||||
func DeleteSelf(c *gin.Context) {
|
func DeleteSelf(c *gin.Context) {
|
||||||
id := c.GetInt("id")
|
id := c.GetInt("id")
|
||||||
|
user, _ := model.GetUserById(id, false)
|
||||||
|
|
||||||
|
if user.Role == common.RoleRootUser {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "不能删除超级管理员账户",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := model.DeleteUserById(id)
|
err := model.DeleteUserById(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -54,6 +54,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
|
common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
|
||||||
}
|
}
|
||||||
|
common.SyncFrequency = frequency
|
||||||
go model.SyncOptions(frequency)
|
go model.SyncOptions(frequency)
|
||||||
if common.RedisEnabled {
|
if common.RedisEnabled {
|
||||||
go model.SyncChannelCache(frequency)
|
go model.SyncChannelCache(frequency)
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
TokenCacheSeconds = 60 * 60
|
TokenCacheSeconds = common.SyncFrequency
|
||||||
UserId2GroupCacheSeconds = 60 * 60
|
UserId2GroupCacheSeconds = common.SyncFrequency
|
||||||
UserId2QuotaCacheSeconds = 10 * 60
|
UserId2QuotaCacheSeconds = common.SyncFrequency
|
||||||
UserId2StatusCacheSeconds = 60 * 60
|
UserId2StatusCacheSeconds = common.SyncFrequency
|
||||||
)
|
)
|
||||||
|
|
||||||
func CacheGetTokenByKey(key string) (*Token, error) {
|
func CacheGetTokenByKey(key string) (*Token, error) {
|
||||||
@@ -35,7 +35,7 @@ func CacheGetTokenByKey(key string) (*Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), TokenCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), time.Duration(TokenCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set token error: " + err.Error())
|
common.SysError("Redis set token error: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func CacheGetUserGroup(id int) (group string, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, UserId2GroupCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_group:%d", id), group, time.Duration(UserId2GroupCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user group error: " + err.Error())
|
common.SysError("Redis set user group error: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func CacheGetUserQuota(id int) (quota int, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user quota error: " + err.Error())
|
common.SysError("Redis set user quota error: " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func CacheUpdateUserQuota(id int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), UserId2QuotaCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_quota:%d", id), fmt.Sprintf("%d", quota), time.Duration(UserId2QuotaCacheSeconds)*time.Second)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ func CacheIsUserEnabled(userId int) bool {
|
|||||||
status = common.UserStatusEnabled
|
status = common.UserStatusEnabled
|
||||||
}
|
}
|
||||||
enabled = fmt.Sprintf("%d", status)
|
enabled = fmt.Sprintf("%d", status)
|
||||||
err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, UserId2StatusCacheSeconds*time.Second)
|
err = common.RedisSet(fmt.Sprintf("user_enabled:%d", userId), enabled, time.Duration(UserId2StatusCacheSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Redis set user enabled error: " + err.Error())
|
common.SysError("Redis set user enabled error: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
selfRoute.GET("/self", controller.GetSelf)
|
selfRoute.GET("/self", controller.GetSelf)
|
||||||
selfRoute.PUT("/self", controller.UpdateSelf)
|
selfRoute.PUT("/self", controller.UpdateSelf)
|
||||||
selfRoute.DELETE("/self", controller.DeleteSelf)
|
selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf)
|
||||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||||
selfRoute.GET("/aff", controller.GetAffCode)
|
selfRoute.GET("/aff", controller.GetAffCode)
|
||||||
selfRoute.POST("/topup", controller.TopUp)
|
selfRoute.POST("/topup", controller.TopUp)
|
||||||
|
|||||||
@@ -1,36 +1,25 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import {
|
import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react';
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
Form,
|
|
||||||
Grid,
|
|
||||||
Header,
|
|
||||||
Image,
|
|
||||||
Message,
|
|
||||||
Modal,
|
|
||||||
Segment,
|
|
||||||
} from 'semantic-ui-react';
|
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { API, getLogo, showError, showSuccess, showInfo } from '../helpers';
|
import { API, getLogo, showError, showSuccess } from '../helpers';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
wechat_verification_code: '',
|
wechat_verification_code: ''
|
||||||
});
|
});
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const { username, password } = inputs;
|
const { username, password } = inputs;
|
||||||
const [userState, userDispatch] = useContext(UserContext);
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
const logo = getLogo();
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get("expired")) {
|
if (searchParams.get('expired')) {
|
||||||
showError('未登录或登录已过期,请重新登录!');
|
showError('未登录或登录已过期,请重新登录!');
|
||||||
}
|
}
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@@ -78,7 +67,7 @@ const LoginForm = () => {
|
|||||||
if (username && password) {
|
if (username && password) {
|
||||||
const res = await API.post(`/api/user/login`, {
|
const res = await API.post(`/api/user/login`, {
|
||||||
username,
|
username,
|
||||||
password,
|
password
|
||||||
});
|
});
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -93,44 +82,44 @@ const LoginForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid textAlign="center" style={{ marginTop: '48px' }}>
|
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
<Header as="h2" color="" textAlign="center">
|
<Header as='h2' color='' textAlign='center'>
|
||||||
<Image src={logo} /> 用户登录
|
<Image src={logo} /> 用户登录
|
||||||
</Header>
|
</Header>
|
||||||
<Form size="large">
|
<Form size='large'>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
icon="user"
|
icon='user'
|
||||||
iconPosition="left"
|
iconPosition='left'
|
||||||
placeholder="用户名"
|
placeholder='用户名'
|
||||||
name="username"
|
name='username'
|
||||||
value={username}
|
value={username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
icon="lock"
|
icon='lock'
|
||||||
iconPosition="left"
|
iconPosition='left'
|
||||||
placeholder="密码"
|
placeholder='密码'
|
||||||
name="password"
|
name='password'
|
||||||
type="password"
|
type='password'
|
||||||
value={password}
|
value={password}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Button color="" fluid size="large" onClick={handleSubmit}>
|
<Button color='green' fluid size='large' onClick={handleSubmit}>
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
</Form>
|
</Form>
|
||||||
<Message>
|
<Message>
|
||||||
忘记密码?
|
忘记密码?
|
||||||
<Link to="/reset" className="btn btn-link">
|
<Link to='/reset' className='btn btn-link'>
|
||||||
点击重置
|
点击重置
|
||||||
</Link>
|
</Link>
|
||||||
; 没有账户?
|
; 没有账户?
|
||||||
<Link to="/register" className="btn btn-link">
|
<Link to='/register' className='btn btn-link'>
|
||||||
点击注册
|
点击注册
|
||||||
</Link>
|
</Link>
|
||||||
</Message>
|
</Message>
|
||||||
@@ -140,8 +129,8 @@ const LoginForm = () => {
|
|||||||
{status.github_oauth ? (
|
{status.github_oauth ? (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color="black"
|
color='black'
|
||||||
icon="github"
|
icon='github'
|
||||||
onClick={onGitHubOAuthClicked}
|
onClick={onGitHubOAuthClicked}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -150,8 +139,8 @@ const LoginForm = () => {
|
|||||||
{status.wechat_login ? (
|
{status.wechat_login ? (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color="green"
|
color='green'
|
||||||
icon="wechat"
|
icon='wechat'
|
||||||
onClick={onWeChatLoginClicked}
|
onClick={onWeChatLoginClicked}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -175,18 +164,18 @@ const LoginForm = () => {
|
|||||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Form size="large">
|
<Form size='large'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
fluid
|
fluid
|
||||||
placeholder="验证码"
|
placeholder='验证码'
|
||||||
name="wechat_verification_code"
|
name='wechat_verification_code'
|
||||||
value={inputs.wechat_verification_code}
|
value={inputs.wechat_verification_code}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color=""
|
color=''
|
||||||
fluid
|
fluid
|
||||||
size="large"
|
size='large'
|
||||||
onClick={onSubmitWeChatVerificationCode}
|
onClick={onSubmitWeChatVerificationCode}
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ const PasswordResetConfirm = () => {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let token = searchParams.get('token');
|
let token = searchParams.get('token');
|
||||||
@@ -22,7 +27,21 @@ const PasswordResetConfirm = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let countdownInterval = null;
|
||||||
|
if (disableButton && countdown > 0) {
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
setCountdown(countdown - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else if (countdown === 0) {
|
||||||
|
setDisableButton(false);
|
||||||
|
setCountdown(30);
|
||||||
|
}
|
||||||
|
return () => clearInterval(countdownInterval);
|
||||||
|
}, [disableButton, countdown]);
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
|
setDisableButton(true);
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.post(`/api/user/reset`, {
|
const res = await API.post(`/api/user/reset`, {
|
||||||
@@ -32,14 +51,15 @@ const PasswordResetConfirm = () => {
|
|||||||
const { success, message } = res.data;
|
const { success, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let password = res.data.data;
|
let password = res.data.data;
|
||||||
|
setNewPassword(password);
|
||||||
await copy(password);
|
await copy(password);
|
||||||
showNotice(`密码已重置并已复制到剪贴板:${password}`);
|
showNotice(`新密码已复制到剪贴板:${password}`);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
@@ -57,20 +77,37 @@ const PasswordResetConfirm = () => {
|
|||||||
value={email}
|
value={email}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
{newPassword && (
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
icon='lock'
|
||||||
|
iconPosition='left'
|
||||||
|
placeholder='新密码'
|
||||||
|
name='newPassword'
|
||||||
|
value={newPassword}
|
||||||
|
readOnly
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.select();
|
||||||
|
navigator.clipboard.writeText(newPassword);
|
||||||
|
showNotice(`密码已复制到剪贴板:${newPassword}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
color=''
|
color='green'
|
||||||
fluid
|
fluid
|
||||||
size='large'
|
size='large'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
disabled={disableButton}
|
||||||
>
|
>
|
||||||
提交
|
{disableButton ? `密码重置完成` : '提交'}
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordResetConfirm;
|
export default PasswordResetConfirm;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile';
|
|||||||
|
|
||||||
const PasswordResetForm = () => {
|
const PasswordResetForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
email: '',
|
email: ''
|
||||||
});
|
});
|
||||||
const { email } = inputs;
|
const { email } = inputs;
|
||||||
|
|
||||||
@@ -13,24 +13,29 @@ const PasswordResetForm = () => {
|
|||||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let countdownInterval = null;
|
||||||
if (status) {
|
if (disableButton && countdown > 0) {
|
||||||
status = JSON.parse(status);
|
countdownInterval = setInterval(() => {
|
||||||
if (status.turnstile_check) {
|
setCountdown(countdown - 1);
|
||||||
setTurnstileEnabled(true);
|
}, 1000);
|
||||||
setTurnstileSiteKey(status.turnstile_site_key);
|
} else if (countdown === 0) {
|
||||||
}
|
setDisableButton(false);
|
||||||
|
setCountdown(30);
|
||||||
}
|
}
|
||||||
}, []);
|
return () => clearInterval(countdownInterval);
|
||||||
|
}, [disableButton, countdown]);
|
||||||
|
|
||||||
function handleChange(e) {
|
function handleChange(e) {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs(inputs => ({ ...inputs, [name]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
|
setDisableButton(true);
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
if (turnstileEnabled && turnstileToken === '') {
|
if (turnstileEnabled && turnstileToken === '') {
|
||||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||||
@@ -78,13 +83,14 @@ const PasswordResetForm = () => {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
color=''
|
color='green'
|
||||||
fluid
|
fluid
|
||||||
size='large'
|
size='large'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
disabled={disableButton}
|
||||||
>
|
>
|
||||||
提交
|
{disableButton ? `重试 (${countdown})` : '提交'}
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
const PersonalSetting = () => {
|
const PersonalSetting = () => {
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
wechat_verification_code: '',
|
wechat_verification_code: '',
|
||||||
email_verification_code: '',
|
email_verification_code: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
self_account_deletion_confirmation: ''
|
||||||
});
|
});
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||||||
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
||||||
|
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
|
||||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@@ -30,6 +38,19 @@ const PersonalSetting = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let countdownInterval = null;
|
||||||
|
if (disableButton && countdown > 0) {
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
setCountdown(countdown - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else if (countdown === 0) {
|
||||||
|
setDisableButton(false);
|
||||||
|
setCountdown(30);
|
||||||
|
}
|
||||||
|
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||||||
|
}, [disableButton, countdown]);
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
@@ -57,6 +78,26 @@ const PersonalSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteAccount = async () => {
|
||||||
|
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||||||
|
showError('请输入你的账户名以确认删除!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await API.delete('/api/user/self');
|
||||||
|
const { success, message } = res.data;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showSuccess('账户已删除!');
|
||||||
|
await API.get('/api/user/logout');
|
||||||
|
userDispatch({ type: 'logout' });
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
navigate('/login');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const bindWeChat = async () => {
|
const bindWeChat = async () => {
|
||||||
if (inputs.wechat_verification_code === '') return;
|
if (inputs.wechat_verification_code === '') return;
|
||||||
const res = await API.get(
|
const res = await API.get(
|
||||||
@@ -78,6 +119,7 @@ const PersonalSetting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
const sendVerificationCode = async () => {
|
||||||
|
setDisableButton(true);
|
||||||
if (inputs.email === '') return;
|
if (inputs.email === '') return;
|
||||||
if (turnstileEnabled && turnstileToken === '') {
|
if (turnstileEnabled && turnstileToken === '') {
|
||||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||||
@@ -123,6 +165,9 @@ const PersonalSetting = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
<Button onClick={getAffLink}>复制邀请链接</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setShowAccountDeleteModal(true);
|
||||||
|
}}>删除个人账户</Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>账号绑定</Header>
|
<Header as='h3'>账号绑定</Header>
|
||||||
{
|
{
|
||||||
@@ -195,8 +240,8 @@ const PersonalSetting = () => {
|
|||||||
name='email'
|
name='email'
|
||||||
type='email'
|
type='email'
|
||||||
action={
|
action={
|
||||||
<Button onClick={sendVerificationCode} disabled={loading}>
|
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
||||||
获取验证码
|
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -230,6 +275,47 @@ const PersonalSetting = () => {
|
|||||||
</Modal.Description>
|
</Modal.Description>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
onClose={() => setShowAccountDeleteModal(false)}
|
||||||
|
onOpen={() => setShowAccountDeleteModal(true)}
|
||||||
|
open={showAccountDeleteModal}
|
||||||
|
size={'tiny'}
|
||||||
|
style={{ maxWidth: '450px' }}
|
||||||
|
>
|
||||||
|
<Modal.Header>确认删除自己的帐户</Modal.Header>
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Description>
|
||||||
|
<Form size='large'>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
||||||
|
name='self_account_deletion_confirmation'
|
||||||
|
value={inputs.self_account_deletion_confirmation}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
{turnstileEnabled ? (
|
||||||
|
<Turnstile
|
||||||
|
sitekey={turnstileSiteKey}
|
||||||
|
onVerify={(token) => {
|
||||||
|
setTurnstileToken(token);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
color='red'
|
||||||
|
fluid
|
||||||
|
size='large'
|
||||||
|
onClick={deleteAccount}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Modal.Description>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react';
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Grid,
|
|
||||||
Header,
|
|
||||||
Image,
|
|
||||||
Message,
|
|
||||||
Segment,
|
|
||||||
} from 'semantic-ui-react';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
@@ -18,7 +10,7 @@ const RegisterForm = () => {
|
|||||||
password: '',
|
password: '',
|
||||||
password2: '',
|
password2: '',
|
||||||
email: '',
|
email: '',
|
||||||
verification_code: '',
|
verification_code: ''
|
||||||
});
|
});
|
||||||
const { username, password, password2 } = inputs;
|
const { username, password, password2 } = inputs;
|
||||||
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
||||||
@@ -178,7 +170,7 @@ const RegisterForm = () => {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
color=''
|
color='green'
|
||||||
fluid
|
fluid
|
||||||
size='large'
|
size='large'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ const About = () => {
|
|||||||
about.startsWith('https://') ? <iframe
|
about.startsWith('https://') ? <iframe
|
||||||
src={about}
|
src={about}
|
||||||
style={{ width: '100%', height: '100vh', border: 'none' }}
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
/> : <Segment>
|
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
|
||||||
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
|
|
||||||
</Segment>
|
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,26 +215,12 @@ const EditChannel = () => {
|
|||||||
</Form.Field>
|
</Form.Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
inputs.type !== 3 && inputs.type !== 8 && (
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='镜像'
|
|
||||||
name='base_url'
|
|
||||||
placeholder={'此项可选,输入镜像站地址,格式为:https://domain.com'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={inputs.base_url}
|
|
||||||
autoComplete='new-password'
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='名称'
|
label='名称'
|
||||||
required
|
required
|
||||||
name='name'
|
name='name'
|
||||||
placeholder={'请输入名称'}
|
placeholder={'请为渠道命名'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.name}
|
value={inputs.name}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@@ -243,7 +229,7 @@ const EditChannel = () => {
|
|||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Dropdown
|
<Form.Dropdown
|
||||||
label='分组'
|
label='分组'
|
||||||
placeholder={'请选择分组'}
|
placeholder={'请选择可以使用该渠道的分组'}
|
||||||
name='groups'
|
name='groups'
|
||||||
required
|
required
|
||||||
fluid
|
fluid
|
||||||
@@ -260,7 +246,7 @@ const EditChannel = () => {
|
|||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Dropdown
|
<Form.Dropdown
|
||||||
label='模型'
|
label='模型'
|
||||||
placeholder={'请选择该通道所支持的模型'}
|
placeholder={'请选择该渠道所支持的模型'}
|
||||||
name='models'
|
name='models'
|
||||||
required
|
required
|
||||||
fluid
|
fluid
|
||||||
@@ -312,7 +298,7 @@ const EditChannel = () => {
|
|||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='模型映射'
|
label='模型映射'
|
||||||
placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||||
name='model_mapping'
|
name='model_mapping'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.model_mapping}
|
value={inputs.model_mapping}
|
||||||
@@ -337,7 +323,7 @@ const EditChannel = () => {
|
|||||||
label='密钥'
|
label='密钥'
|
||||||
name='key'
|
name='key'
|
||||||
required
|
required
|
||||||
placeholder={inputs.type === 15 ? "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次" : '请输入密钥'}
|
placeholder={inputs.type === 15 ? "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次" : '请输入渠道对应的鉴权密钥'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.key}
|
value={inputs.key}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@@ -354,6 +340,20 @@ const EditChannel = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
inputs.type !== 3 && inputs.type !== 8 && (
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='镜像'
|
||||||
|
name='base_url'
|
||||||
|
placeholder={'此项可选,用于通过镜像站来进行 API 调用,请输入镜像站地址,格式为:https://domain.com'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.base_url}
|
||||||
|
autoComplete='new-password'
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button>
|
<Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|||||||
@@ -7,24 +7,32 @@ const TopUp = () => {
|
|||||||
const [redemptionCode, setRedemptionCode] = useState('');
|
const [redemptionCode, setRedemptionCode] = useState('');
|
||||||
const [topUpLink, setTopUpLink] = useState('');
|
const [topUpLink, setTopUpLink] = useState('');
|
||||||
const [userQuota, setUserQuota] = useState(0);
|
const [userQuota, setUserQuota] = useState(0);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const topUp = async () => {
|
const topUp = async () => {
|
||||||
if (redemptionCode === '') {
|
if (redemptionCode === '') {
|
||||||
showInfo('请输入充值码!')
|
showInfo('请输入充值码!')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await API.post('/api/user/topup', {
|
setIsSubmitting(true);
|
||||||
key: redemptionCode
|
try {
|
||||||
});
|
const res = await API.post('/api/user/topup', {
|
||||||
const { success, message, data } = res.data;
|
key: redemptionCode
|
||||||
if (success) {
|
|
||||||
showSuccess('充值成功!');
|
|
||||||
setUserQuota((quota) => {
|
|
||||||
return quota + data;
|
|
||||||
});
|
});
|
||||||
setRedemptionCode('');
|
const { success, message, data } = res.data;
|
||||||
} else {
|
if (success) {
|
||||||
showError(message);
|
showSuccess('充值成功!');
|
||||||
|
setUserQuota((quota) => {
|
||||||
|
return quota + data;
|
||||||
|
});
|
||||||
|
setRedemptionCode('');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showError('请求失败');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,8 +82,8 @@ const TopUp = () => {
|
|||||||
<Button color='green' onClick={openTopUpLink}>
|
<Button color='green' onClick={openTopUpLink}>
|
||||||
获取兑换码
|
获取兑换码
|
||||||
</Button>
|
</Button>
|
||||||
<Button color='yellow' onClick={topUp}>
|
<Button color='yellow' onClick={topUp} disabled={isSubmitting}>
|
||||||
充值
|
{isSubmitting ? '兑换中...' : '兑换'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
@@ -92,5 +100,4 @@ const TopUp = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default TopUp;
|
||||||
export default TopUp;
|
|
||||||
Reference in New Issue
Block a user