Compare commits

..

4 Commits

Author SHA1 Message Date
JustSong
f97c2b4c22 feat: able to set top up link now 2023-04-27 16:32:21 +08:00
JustSong
54b1e4adef fix: check user status when validating token (#23) 2023-04-27 15:05:33 +08:00
JustSong
9272884381 fix: root user cannot demote itself now (close #30) 2023-04-27 14:45:12 +08:00
JustSong
195e94a75d fix: fix MySQL syntax error (#54) 2023-04-27 11:10:10 +08:00
11 changed files with 85 additions and 15 deletions

View File

@@ -52,14 +52,18 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
+ [x] 自定义渠道 + [x] 自定义渠道
2. 支持通过负载均衡的方式访问多个渠道。 2. 支持通过负载均衡的方式访问多个渠道。
3. 支持单个访问渠道设置多个 API Key利用起来你的多个 API Key。 3. 支持单个访问渠道设置多个 API Key利用起来你的多个 API Key。
4. 支持设置令牌的过期时间和使用次数 4. 支持 HTTP SSE可以通过流式传输实现打字机效果
5. 支持 HTTP SSE 5. 支持设置令牌的过期时间和使用次数
6. 多种用户登录注册方式: 6. 支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
+ 邮箱登录注册以及通过邮箱进行密码重置 7. 支持为新用户设置初始配额
+ [GitHub 开放授权](https://github.com/settings/applications/new) 8. 支持发布公告,在线修改关于页面,设置充值链接,自定义页脚
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server) 9. 支持通过系统访问令牌访问管理 API
7. 支持用户管理。 10. 多种用户登录注册方式:
8. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式 + 邮箱登录注册以及通过邮箱进行密码重置
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
11. 支持用户管理。
12. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
## 部署 ## 部署
### 基于 Docker 进行部署 ### 基于 Docker 进行部署

View File

@@ -11,6 +11,7 @@ var Version = "v0.0.0" // this hard coding will be replaced automatic
var SystemName = "One API" var SystemName = "One API"
var ServerAddress = "http://localhost:3000" var ServerAddress = "http://localhost:3000"
var Footer = "" var Footer = ""
var TopUpLink = ""
var UsingSQLite = false var UsingSQLite = false

View File

@@ -26,6 +26,7 @@ func GetStatus(c *gin.Context) {
"server_address": common.ServerAddress, "server_address": common.ServerAddress,
"turnstile_check": common.TurnstileCheckEnabled, "turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey, "turnstile_site_key": common.TurnstileSiteKey,
"top_up_link": common.TopUpLink,
}, },
}) })
return return

View File

@@ -559,6 +559,13 @@ func ManageUser(c *gin.Context) {
} }
user.Role = common.RoleAdminUser user.Role = common.RoleAdminUser
case "demote": case "demote":
if user.Role == common.RoleRootUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法降级超级管理员用户",
})
return
}
user.Role = common.RoleCommonUser user.Role = common.RoleCommonUser
} }

View File

@@ -98,6 +98,16 @@ func TokenAuth() func(c *gin.Context) {
c.Abort() c.Abort()
return return
} }
if !model.IsUserEnabled(token.UserId) {
c.JSON(http.StatusOK, gin.H{
"error": gin.H{
"message": "用户已被封禁",
"type": "one_api_error",
},
})
c.Abort()
return
}
c.Set("id", token.UserId) c.Set("id", token.UserId)
c.Set("token_id", token.Id) c.Set("token_id", token.Id)
c.Set("unlimited_times", token.UnlimitedTimes) c.Set("unlimited_times", token.UnlimitedTimes)

View File

@@ -47,6 +47,7 @@ func InitOptionMap() {
common.OptionMap["TurnstileSiteKey"] = "" common.OptionMap["TurnstileSiteKey"] = ""
common.OptionMap["TurnstileSecretKey"] = "" common.OptionMap["TurnstileSecretKey"] = ""
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser) common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
common.OptionMap["TopUpLink"] = common.TopUpLink
common.OptionMapRWMutex.Unlock() common.OptionMapRWMutex.Unlock()
options, _ := AllOption() options, _ := AllOption()
for _, option := range options { for _, option := range options {
@@ -134,5 +135,7 @@ func updateOptionMap(key string, value string) {
common.TurnstileSecretKey = value common.TurnstileSecretKey = value
case "QuotaForNewUser": case "QuotaForNewUser":
common.QuotaForNewUser, _ = strconv.Atoi(value) common.QuotaForNewUser, _ = strconv.Atoi(value)
case "TopUpLink":
common.TopUpLink = value
} }
} }

View File

@@ -48,7 +48,7 @@ func Redeem(key string, tokenId int) (quota int, err error) {
return 0, errors.New("未提供 token id") return 0, errors.New("未提供 token id")
} }
redemption := &Redemption{} redemption := &Redemption{}
err = DB.Where("key = ?", key).First(redemption).Error err = DB.Where("`key` = ?", key).First(redemption).Error
if err != nil { if err != nil {
return 0, errors.New("无效的兑换码") return 0, errors.New("无效的兑换码")
} }

View File

@@ -39,7 +39,7 @@ func ValidateUserToken(key string) (token *Token, err error) {
} }
key = strings.Replace(key, "Bearer ", "", 1) key = strings.Replace(key, "Bearer ", "", 1)
token = &Token{} token = &Token{}
err = DB.Where("key = ?", key).First(token).Error err = DB.Where("`key` = ?", key).First(token).Error
if err == nil { if err == nil {
if token.Status != common.TokenStatusEnabled { if token.Status != common.TokenStatusEnabled {
return nil, errors.New("该 token 状态不可用") return nil, errors.New("该 token 状态不可用")

View File

@@ -195,6 +195,19 @@ func IsAdmin(userId int) bool {
return user.Role >= common.RoleAdminUser return user.Role >= common.RoleAdminUser
} }
func IsUserEnabled(userId int) bool {
if userId == 0 {
return false
}
var user User
err := DB.Where("id = ?", userId).Select("status").Find(&user).Error
if err != nil {
common.SysError("No such user " + err.Error())
return false
}
return user.Status == common.UserStatusEnabled
}
func ValidateAccessToken(token string) (user *User) { func ValidateAccessToken(token string) (user *User) {
if token == "" { if token == "" {
return nil return nil

View File

@@ -25,6 +25,7 @@ const SystemSetting = () => {
TurnstileSecretKey: '', TurnstileSecretKey: '',
RegisterEnabled: '', RegisterEnabled: '',
QuotaForNewUser: 0, QuotaForNewUser: 0,
TopUpLink: ''
}); });
let originInputs = {}; let originInputs = {};
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
@@ -65,7 +66,7 @@ const SystemSetting = () => {
} }
const res = await API.put('/api/option', { const res = await API.put('/api/option', {
key, key,
value, value
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
@@ -88,7 +89,8 @@ const SystemSetting = () => {
name === 'WeChatAccountQRCodeImageURL' || name === 'WeChatAccountQRCodeImageURL' ||
name === 'TurnstileSiteKey' || name === 'TurnstileSiteKey' ||
name === 'TurnstileSecretKey' || name === 'TurnstileSecretKey' ||
name === 'QuotaForNewUser' name === 'QuotaForNewUser' ||
name === 'TopUpLink'
) { ) {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
} else { } else {
@@ -101,6 +103,15 @@ const SystemSetting = () => {
await updateOption('ServerAddress', ServerAddress); await updateOption('ServerAddress', ServerAddress);
}; };
const submitOperationConfig = async () => {
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
}
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
await updateOption('TopUpLink', inputs.TopUpLink);
}
}
const submitSMTP = async () => { const submitSMTP = async () => {
if (originInputs['SMTPServer'] !== inputs.SMTPServer) { if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
await updateOption('SMTPServer', inputs.SMTPServer); await updateOption('SMTPServer', inputs.SMTPServer);
@@ -244,10 +255,17 @@ const SystemSetting = () => {
min='0' min='0'
placeholder='例如100' placeholder='例如100'
/> />
<Form.Input
label='充值链接'
name='TopUpLink'
onChange={handleInputChange}
autoComplete='off'
value={inputs.TopUpLink}
type='link'
placeholder='例如发卡网站的购买链接'
/>
</Form.Group> </Form.Group>
<Form.Button onClick={()=>{ <Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button>
updateOption('QuotaForNewUser', inputs.QuotaForNewUser).then();
}}>保存运营设置</Form.Button>
<Divider /> <Divider />
<Header as='h3'> <Header as='h3'>
配置 SMTP 配置 SMTP

View File

@@ -37,6 +37,7 @@ const TokensTable = () => {
const [showTopUpModal, setShowTopUpModal] = useState(false); const [showTopUpModal, setShowTopUpModal] = useState(false);
const [targetTokenIdx, setTargetTokenIdx] = useState(0); const [targetTokenIdx, setTargetTokenIdx] = useState(0);
const [redemptionCode, setRedemptionCode] = useState(''); const [redemptionCode, setRedemptionCode] = useState('');
const [topUpLink, setTopUpLink] = useState('');
const loadTokens = async (startIdx) => { const loadTokens = async (startIdx) => {
const res = await API.get(`/api/token/?p=${startIdx}`); const res = await API.get(`/api/token/?p=${startIdx}`);
@@ -71,6 +72,13 @@ const TokensTable = () => {
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
if (status.top_up_link) {
setTopUpLink(status.top_up_link);
}
}
}, []); }, []);
const manageToken = async (id, action, idx) => { const manageToken = async (id, action, idx) => {
@@ -342,6 +350,11 @@ const TokensTable = () => {
<Modal.Content> <Modal.Content>
<Modal.Description> <Modal.Description>
{/*<Image src={status.wechat_qrcode} fluid />*/} {/*<Image src={status.wechat_qrcode} fluid />*/}
{
topUpLink && <p>
<a target='_blank' href={topUpLink}>点击此处获取兑换码</a>
</p>
}
<Form size='large'> <Form size='large'>
<Form.Input <Form.Input
fluid fluid