Skip to content

Commit

Permalink
feat: support email domain whitelist (#337)
Browse files Browse the repository at this point in the history
* feat: support email domain restriction

* fix(SMTPToken): disable password auto complete

* chore: update implementation

---------

Co-authored-by: JustSong <[email protected]>
  • Loading branch information
ckt1031 and songquanpeng authored Jul 30, 2023
1 parent 8cbbeb7 commit 3fca6ff
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 9 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
19. 支持通过系统访问令牌访问管理 API。
20. 支持 Cloudflare Turnstile 用户校验。
21. 支持用户管理,支持**多种用户登录注册方式**
+ 邮箱登录注册以及通过邮箱进行密码重置
+ 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置
+ [GitHub 开放授权](https://github.com/settings/applications/new)
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。

Expand Down
13 changes: 13 additions & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ var WeChatAuthEnabled = false
var TurnstileCheckEnabled = false
var RegisterEnabled = true

var EmailDomainRestrictionEnabled = false
var EmailDomainWhitelist = []string{
"gmail.com",
"163.com",
"126.com",
"qq.com",
"outlook.com",
"hotmail.com",
"icloud.com",
"yahoo.com",
"foxmail.com",
}

var LogConsumeEnabled = true

var SMTPServer = ""
Expand Down
20 changes: 19 additions & 1 deletion controller/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package controller
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strings"

"github.com/gin-gonic/gin"
)

func GetStatus(c *gin.Context) {
Expand Down Expand Up @@ -78,6 +80,22 @@ func SendEmailVerification(c *gin.Context) {
})
return
}
if common.EmailDomainRestrictionEnabled {
allowed := false
for _, domain := range common.EmailDomainWhitelist {
if strings.HasSuffix(email, "@"+domain) {
allowed = true
break
}
}
if !allowed {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员启用了邮箱域名白名单,您的邮箱地址的域名不在白名单中",
})
return
}
}
if model.IsEmailAlreadyTaken(email) {
c.JSON(http.StatusOK, gin.H{
"success": false,
Expand Down
11 changes: 10 additions & 1 deletion controller/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package controller

import (
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strings"

"github.com/gin-gonic/gin"
)

func GetOptions(c *gin.Context) {
Expand Down Expand Up @@ -49,6 +50,14 @@ func UpdateOption(c *gin.Context) {
})
return
}
case "EmailDomainRestrictionEnabled":
if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法启用邮箱域名限制,请先填入限制的邮箱域名!",
})
return
}
case "WeChatAuthEnabled":
if option.Value == "true" && common.WeChatServerAddress == "" {
c.JSON(http.StatusOK, gin.H{
Expand Down
6 changes: 6 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func InitOptionMap() {
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
common.OptionMap["EmailDomainRestrictionEnabled"] = strconv.FormatBool(common.EmailDomainRestrictionEnabled)
common.OptionMap["EmailDomainWhitelist"] = strings.Join(common.EmailDomainWhitelist, ",")
common.OptionMap["SMTPServer"] = ""
common.OptionMap["SMTPFrom"] = ""
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
Expand Down Expand Up @@ -141,6 +143,8 @@ func updateOptionMap(key string, value string) (err error) {
common.TurnstileCheckEnabled = boolValue
case "RegisterEnabled":
common.RegisterEnabled = boolValue
case "EmailDomainRestrictionEnabled":
common.EmailDomainRestrictionEnabled = boolValue
case "AutomaticDisableChannelEnabled":
common.AutomaticDisableChannelEnabled = boolValue
case "ApproximateTokenEnabled":
Expand All @@ -154,6 +158,8 @@ func updateOptionMap(key string, value string) (err error) {
}
}
switch key {
case "EmailDomainWhitelist":
common.EmailDomainWhitelist = strings.Split(value, ",")
case "SMTPServer":
common.SMTPServer = value
case "SMTPPort":
Expand Down
104 changes: 98 additions & 6 deletions web/src/components/SystemSetting.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
import { Button, Divider, Form, Grid, Header, Input, Message } from 'semantic-ui-react';
import { API, removeTrailingSlash, showError } from '../helpers';

const SystemSetting = () => {
let [inputs, setInputs] = useState({
Expand All @@ -26,9 +26,13 @@ const SystemSetting = () => {
TurnstileSiteKey: '',
TurnstileSecretKey: '',
RegisterEnabled: '',
EmailDomainRestrictionEnabled: '',
EmailDomainWhitelist: ''
});
const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false);
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');

const getOptions = async () => {
const res = await API.get('/api/option/');
Expand All @@ -38,8 +42,15 @@ const SystemSetting = () => {
data.forEach((item) => {
newInputs[item.key] = item.value;
});
setInputs(newInputs);
setInputs({
...newInputs,
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
});
setOriginInputs(newInputs);

setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
return { key: item, text: item, value: item };
}));
} else {
showError(message);
}
Expand All @@ -58,6 +69,7 @@ const SystemSetting = () => {
case 'GitHubOAuthEnabled':
case 'WeChatAuthEnabled':
case 'TurnstileCheckEnabled':
case 'EmailDomainRestrictionEnabled':
case 'RegisterEnabled':
value = inputs[key] === 'true' ? 'false' : 'true';
break;
Expand All @@ -70,7 +82,12 @@ const SystemSetting = () => {
});
const { success, message } = res.data;
if (success) {
setInputs((inputs) => ({ ...inputs, [key]: value }));
if (key === 'EmailDomainWhitelist') {
value = value.split(',');
}
setInputs((inputs) => ({
...inputs, [key]: value
}));
} else {
showError(message);
}
Expand All @@ -88,7 +105,8 @@ const SystemSetting = () => {
name === 'WeChatServerToken' ||
name === 'WeChatAccountQRCodeImageURL' ||
name === 'TurnstileSiteKey' ||
name === 'TurnstileSecretKey'
name === 'TurnstileSecretKey' ||
name === 'EmailDomainWhitelist'
) {
setInputs((inputs) => ({ ...inputs, [name]: value }));
} else {
Expand Down Expand Up @@ -125,6 +143,16 @@ const SystemSetting = () => {
}
};


const submitEmailDomainWhitelist = async () => {
if (
originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
inputs.SMTPToken !== ''
) {
await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
}
};

const submitWeChat = async () => {
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
await updateOption(
Expand Down Expand Up @@ -173,6 +201,22 @@ const SystemSetting = () => {
}
};

const submitNewRestrictedDomain = () => {
const localDomainList = inputs.EmailDomainWhitelist;
if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
setRestrictedDomainInput('');
setInputs({
...inputs,
EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
});
setEmailDomainWhitelist([...EmailDomainWhitelist, {
key: restrictedDomainInput,
text: restrictedDomainInput,
value: restrictedDomainInput,
}]);
}
}

return (
<Grid columns={1}>
<Grid.Column>
Expand Down Expand Up @@ -239,6 +283,54 @@ const SystemSetting = () => {
/>
</Form.Group>
<Divider />
<Header as='h3'>
配置邮箱域名白名单
<Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Checkbox
label='启用邮箱域名白名单'
name='EmailDomainRestrictionEnabled'
onChange={handleInputChange}
checked={inputs.EmailDomainRestrictionEnabled === 'true'}
/>
</Form.Group>
<Form.Group widths={2}>
<Form.Dropdown
label='允许的邮箱域名'
placeholder='允许的邮箱域名'
name='EmailDomainWhitelist'
required
fluid
multiple
selection
onChange={handleInputChange}
value={inputs.EmailDomainWhitelist}
autoComplete='new-password'
options={EmailDomainWhitelist}
/>
<Form.Input
label='添加新的允许的邮箱域名'
action={
<Button type='button' onClick={() => {
submitNewRestrictedDomain();
}}>填入</Button>
}
onKeyDown={(e) => {
if (e.key === 'Enter') {
submitNewRestrictedDomain();
}
}}
autoComplete='new-password'
placeholder='输入新的允许的邮箱域名'
value={restrictedDomainInput}
onChange={(e, { value }) => {
setRestrictedDomainInput(value);
}}
/>
</Form.Group>
<Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button>
<Divider />
<Header as='h3'>
配置 SMTP
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
Expand Down Expand Up @@ -284,7 +376,7 @@ const SystemSetting = () => {
onChange={handleInputChange}
type='password'
autoComplete='new-password'
value={inputs.SMTPToken}
checked={inputs.RegisterEnabled === 'true'}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
Expand Down

0 comments on commit 3fca6ff

Please sign in to comment.