Compare commits

..

7 Commits

Author SHA1 Message Date
JustSong
e123c66bc7 fix: fix SMTPFrom not updated in some cases (close #34) 2023-05-13 22:04:36 +08:00
JustSong
9edc82bde0 fix: fix garbled email subject (#34) 2023-05-13 21:41:52 +08:00
JustSong
d84c2f5c70 feat: able to customize home page now (#24) 2023-05-13 21:27:49 +08:00
JustSong
46e77389a4 fix: support smtp server with port 465 2023-05-13 18:57:27 +08:00
JustSong
f5f4e6fbc6 feat: able to configure smtp from now (close #34) 2023-05-13 18:33:41 +08:00
JustSong
dc4a6cb711 feat: support batch creation of channels (close #58) 2023-05-13 17:08:13 +08:00
JustSong
5798fdac50 refactor: rename file to conform to standards 2023-05-13 15:43:55 +08:00
13 changed files with 232 additions and 76 deletions

View File

@@ -56,7 +56,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
5. 支持设置令牌的过期时间和使用次数。
6. 支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
7. 支持为新用户设置初始配额。
8. 支持发布公告,在线修改关于页面,设置充值链接,自定义页脚。
8. 支持自定义首页,发布公告,自定义关于页面,设置充值链接,自定义页脚。
9. 支持通过系统访问令牌访问管理 API。
10. 多种用户登录注册方式:
+ 邮箱登录注册以及通过邮箱进行密码重置。

View File

@@ -36,6 +36,7 @@ var RegisterEnabled = true
var SMTPServer = ""
var SMTPPort = 587
var SMTPAccount = ""
var SMTPFrom = ""
var SMTPToken = ""
var GitHubClientId = ""

View File

@@ -1,20 +1,67 @@
package common
import (
"crypto/tls"
"encoding/base64"
"fmt"
"net/smtp"
"strings"
)
func SendEmail(subject string, receiver string, content string) error {
if SMTPFrom == "" { // for compatibility
SMTPFrom = SMTPAccount
}
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
mail := []byte(fmt.Sprintf("To: %s\r\n"+
"From: %s<%s>\r\n"+
"Subject: %s\r\n"+
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
receiver, SystemName, SMTPAccount, subject, content))
receiver, SystemName, SMTPFrom, encodedSubject, content))
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
to := strings.Split(receiver, ";")
err := smtp.SendMail(addr, auth, SMTPAccount, to, mail)
var err error
if SMTPPort == 465 {
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: SMTPServer,
}
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig)
if err != nil {
return err
}
client, err := smtp.NewClient(conn, SMTPServer)
if err != nil {
return err
}
defer client.Close()
if err = client.Auth(auth); err != nil {
return err
}
if err = client.Mail(SMTPFrom); err != nil {
return err
}
receiverEmails := strings.Split(receiver, ";")
for _, receiver := range receiverEmails {
if err = client.Rcpt(receiver); err != nil {
return err
}
}
w, err := client.Data()
if err != nil {
return err
}
_, err = w.Write(mail)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
} else {
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
}
return err
}

View File

@@ -6,6 +6,7 @@ import (
"one-api/common"
"one-api/model"
"strconv"
"strings"
)
func GetAllChannels(c *gin.Context) {
@@ -84,7 +85,17 @@ func AddChannel(c *gin.Context) {
}
channel.CreatedTime = common.GetTimestamp()
channel.AccessedTime = common.GetTimestamp()
err = channel.Insert()
keys := strings.Split(channel.Key, "\n")
channels := make([]model.Channel, 0)
for _, key := range keys {
if key == "" {
continue
}
localChannel := channel
localChannel.Key = key
channels = append(channels, localChannel)
}
err = model.BatchInsertChannels(channels)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,

View File

@@ -54,6 +54,17 @@ func GetAbout(c *gin.Context) {
return
}
func GetHomePageContent(c *gin.Context) {
common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": common.OptionMap["HomePageContent"],
})
return
}
func SendEmailVerification(c *gin.Context) {
email := c.Query("email")
if err := common.Validate.Var(email, "required,email"); err != nil {

View File

@@ -53,6 +53,12 @@ func GetRandomChannel() (*Channel, error) {
return &channel, err
}
func BatchInsertChannels(channels []Channel) error {
var err error
err = DB.Create(&channels).Error
return err
}
func (channel *Channel) Insert() error {
var err error
err = DB.Create(channel).Error

View File

@@ -33,11 +33,13 @@ func InitOptionMap() {
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
common.OptionMap["SMTPServer"] = ""
common.OptionMap["SMTPFrom"] = ""
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
common.OptionMap["SMTPAccount"] = ""
common.OptionMap["SMTPToken"] = ""
common.OptionMap["Notice"] = ""
common.OptionMap["About"] = ""
common.OptionMap["HomePageContent"] = ""
common.OptionMap["Footer"] = common.Footer
common.OptionMap["ServerAddress"] = ""
common.OptionMap["GitHubClientId"] = ""
@@ -120,6 +122,8 @@ func updateOptionMap(key string, value string) (err error) {
common.SMTPPort = intValue
case "SMTPAccount":
common.SMTPAccount = value
case "SMTPFrom":
common.SMTPFrom = value
case "SMTPToken":
common.SMTPToken = value
case "ServerAddress":

View File

@@ -15,6 +15,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/status", controller.GetStatus)
apiRouter.GET("/notice", controller.GetNotice)
apiRouter.GET("/about", controller.GetAbout)
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)

View File

@@ -8,6 +8,7 @@ const OtherSetting = () => {
Footer: '',
Notice: '',
About: '',
HomePageContent: '',
});
let originInputs = {};
let [loading, setLoading] = useState(false);
@@ -69,6 +70,10 @@ const OtherSetting = () => {
await updateOption('About', inputs.About);
};
const submitOption = async (key) => {
await updateOption(key, inputs[key]);
};
const openGitHubRelease = () => {
window.location =
'https://github.com/songquanpeng/one-api/releases/latest';
@@ -109,6 +114,17 @@ const OtherSetting = () => {
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
<Divider />
<Header as='h3'>个性化设置</Header>
<Form.Group widths='equal'>
<Form.TextArea
label='首页内容'
placeholder='在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示'
value={inputs.HomePageContent}
name='HomePageContent'
onChange={handleInputChange}
style={{ minHeight: 300, fontFamily: 'JetBrains Mono, Consolas' }}
/>
</Form.Group>
<Form.Button onClick={()=>submitOption('HomePageContent')}>保存首页内容</Form.Button>
<Form.Group widths='equal'>
<Form.TextArea
label='关于'

View File

@@ -14,6 +14,7 @@ const SystemSetting = () => {
SMTPServer: '',
SMTPPort: '',
SMTPAccount: '',
SMTPFrom: '',
SMTPToken: '',
ServerAddress: '',
Footer: '',
@@ -129,6 +130,9 @@ const SystemSetting = () => {
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
await updateOption('SMTPAccount', inputs.SMTPAccount);
}
if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
await updateOption('SMTPFrom', inputs.SMTPFrom);
}
if (
originInputs['SMTPPort'] !== inputs.SMTPPort &&
inputs.SMTPPort !== ''
@@ -298,7 +302,7 @@ const SystemSetting = () => {
配置 SMTP
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
</Header>
<Form.Group widths={4}>
<Form.Group widths={3}>
<Form.Input
label='SMTP 服务器地址'
name='SMTPServer'
@@ -323,6 +327,16 @@ const SystemSetting = () => {
value={inputs.SMTPAccount}
placeholder='通常是邮箱地址'
/>
</Form.Group>
<Form.Group widths={3}>
<Form.Input
label='SMTP 发送者邮箱'
name='SMTPFrom'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.SMTPFrom}
placeholder='通常和邮箱地址保持一致'
/>
<Form.Input
label='SMTP 访问凭证'
name='SMTPToken'

View File

@@ -16,8 +16,10 @@ const EditChannel = () => {
base_url: '',
other: ''
};
const [batch, setBatch] = useState(false);
const [inputs, setInputs] = useState(originInputs);
const handleInputChange = (e, { name, value }) => {
console.log(name, value);
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
@@ -130,17 +132,38 @@ const EditChannel = () => {
autoComplete='new-password'
/>
</Form.Field>
<Form.Field>
<Form.Input
label='密钥'
name='key'
placeholder={'请输入密钥'}
onChange={handleInputChange}
value={inputs.key}
// type='password'
autoComplete='new-password'
/>
</Form.Field>
{
batch ? <Form.Field>
<Form.TextArea
label='密钥'
name='key'
placeholder={'请输入密钥,一行一个'}
onChange={handleInputChange}
value={inputs.key}
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
/>
</Form.Field> : <Form.Field>
<Form.Input
label='密钥'
name='key'
placeholder={'请输入密钥'}
onChange={handleInputChange}
value={inputs.key}
autoComplete='new-password'
/>
</Form.Field>
}
{
!isEdit && (
<Form.Checkbox
checked={batch}
label='批量创建'
name='batch'
onChange={() => setBatch(!batch)}
/>
)
}
<Button onClick={submit}>提交</Button>
</Form>
</Segment>

View File

@@ -1,10 +1,12 @@
import React, { useContext, useEffect } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Card, Grid, Header, Segment } from 'semantic-ui-react';
import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status';
import { marked } from 'marked';
const Home = () => {
const [statusState, statusDispatch] = useContext(StatusContext);
const [homePageContent, setHomePageContent] = useState('');
const displayNotice = async () => {
const res = await API.get('/api/notice');
@@ -20,6 +22,19 @@ const Home = () => {
}
};
const displayHomePageContent = async () => {
const res = await API.get('/api/home_page_content');
const { success, message, data } = res.data;
if (success) {
let HTMLContent = marked.parse(data);
localStorage.setItem('home_page_content', HTMLContent);
setHomePageContent(HTMLContent);
} else {
showError(message);
setHomePageContent('加载首页内容失败...');
}
};
const getStartTimeString = () => {
const timestamp = statusState?.status?.start_time;
return timestamp2string(timestamp);
@@ -27,69 +42,76 @@ const Home = () => {
useEffect(() => {
displayNotice().then();
displayHomePageContent().then();
}, []);
return (
<>
<Segment>
<Header as='h3'>系统状况</Header>
<Grid columns={2} stackable>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>系统信息</Card.Header>
<Card.Meta>系统信息总览</Card.Meta>
<Card.Description>
<p>名称{statusState?.status?.system_name}</p>
<p>版本{statusState?.status?.version}</p>
<p>
源码
<a
href='https://github.com/songquanpeng/one-api'
target='_blank'
>
https://github.com/songquanpeng/one-api
</a>
</p>
<p>启动时间{getStartTimeString()}</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>系统配置</Card.Header>
<Card.Meta>系统配置总览</Card.Meta>
<Card.Description>
<p>
邮箱验证
{statusState?.status?.email_verification === true
? '已启用'
: '未启用'}
</p>
<p>
GitHub 身份验证
{statusState?.status?.github_oauth === true
? '已启用'
: '未启用'}
</p>
<p>
微信身份验证
{statusState?.status?.wechat_login === true
? '已启用'
: '未启用'}
</p>
<p>
Turnstile 用户校验
{statusState?.status?.turnstile_check === true
? '已启用'
: '未启用'}
</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
</Grid>
{
homePageContent === '' ? <>
<Header as='h3'>系统状况</Header>
<Grid columns={2} stackable>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>系统信息</Card.Header>
<Card.Meta>系统信息总览</Card.Meta>
<Card.Description>
<p>名称{statusState?.status?.system_name}</p>
<p>版本{statusState?.status?.version}</p>
<p>
源码
<a
href='https://github.com/songquanpeng/one-api'
target='_blank'
>
https://github.com/songquanpeng/one-api
</a>
</p>
<p>启动时间{getStartTimeString()}</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>系统配置</Card.Header>
<Card.Meta>系统配置总览</Card.Meta>
<Card.Description>
<p>
邮箱验证
{statusState?.status?.email_verification === true
? '已启用'
: '未启用'}
</p>
<p>
GitHub 身份验证
{statusState?.status?.github_oauth === true
? '已启用'
: '未启用'}
</p>
<p>
微信身份验证
{statusState?.status?.wechat_login === true
? '已启用'
: '未启用'}
</p>
<p>
Turnstile 用户校验
{statusState?.status?.turnstile_check === true
? '已启用'
: '未启用'}
</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
</Grid>
</> : <>
<div dangerouslySetInnerHTML={{ __html: homePageContent}}></div>
</>
}
</Segment>
</>
);