From 140e843d93b956e236ca9e03ffe96905b8fbb7bf Mon Sep 17 00:00:00 2001 From: Ehco1996 Date: Fri, 1 Mar 2024 20:39:28 +0800 Subject: [PATCH 1/6] Add Telegram bot token and name --- common/constants.go | 3 ++ docker-compose.yml | 2 +- makefile | 17 +++++++ model/option.go | 4 ++ web/src/components/SystemSetting.js | 75 +++++++++++++++++++++-------- 5 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 makefile diff --git a/common/constants.go b/common/constants.go index 26684e3..55cc793 100644 --- a/common/constants.go +++ b/common/constants.go @@ -82,6 +82,9 @@ var WeChatAccountQRCodeImageURL = "" var TurnstileSiteKey = "" var TurnstileSecretKey = "" +var TelegramBotToken = "" +var TelegramBotName = "" + var QuotaForNewUser = 0 var QuotaForInviter = 0 var QuotaForInvitee = 0 diff --git a/docker-compose.yml b/docker-compose.yml index 40da248..403f372 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: - ./data:/data - ./logs:/app/logs environment: - - SQL_DSN=root:123456@tcp(host.docker.internal:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库 + # - SQL_DSN=root:123456@tcp(host.docker.internal:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库 - REDIS_CONN_STRING=redis://redis - SESSION_SECRET=random_string # 修改为随机字符串 - TZ=Asia/Shanghai diff --git a/makefile b/makefile new file mode 100644 index 0000000..1df2b5c --- /dev/null +++ b/makefile @@ -0,0 +1,17 @@ +FRONTEND_DIR = ./web +BACKEND_DIR = . + +.PHONY: all start-frontend start-backend + +all: start-frontend start-backend + +# 启动前端开发服务器 +start-frontend: + @echo "Starting frontend dev server..." + @cd $(FRONTEND_DIR) && npm start & + +# 启动后端开发服务器 +start-backend: + @echo "Starting backend dev server..." + @cd $(BACKEND_DIR) && go run main.go & + diff --git a/model/option.go b/model/option.go index a651b85..dcb4c34 100644 --- a/model/option.go +++ b/model/option.go @@ -215,6 +215,10 @@ func updateOptionMap(key string, value string) (err error) { common.WeChatServerToken = value case "WeChatAccountQRCodeImageURL": common.WeChatAccountQRCodeImageURL = value + case "TelegramBotToken": + common.TelegramBotToken = value + case "TelegramBotName": + common.TelegramBotName = value case "TurnstileSiteKey": common.TurnstileSiteKey = value case "TurnstileSecretKey": diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 197050e..a8956b7 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -1,6 +1,6 @@ -import React, {useEffect, useState} from 'react'; -import {Button, Divider, Form, Grid, Header, Modal, Message} from 'semantic-ui-react'; -import {API, removeTrailingSlash, showError, verifyJSON} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react'; +import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers'; const SystemSetting = () => { let [inputs, setInputs] = useState({ @@ -32,7 +32,11 @@ const SystemSetting = () => { TurnstileSecretKey: '', RegisterEnabled: '', EmailDomainRestrictionEnabled: '', - EmailDomainWhitelist: '' + EmailDomainWhitelist: '', + // telegram login + TelegramLoginEnabled: '', + TelegramBotToken: '', + TelegramBotName: '', }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); @@ -42,7 +46,7 @@ const SystemSetting = () => { const getOptions = async () => { const res = await API.get('/api/option/'); - const {success, message, data} = res.data; + const { success, message, data } = res.data; if (success) { let newInputs = {}; data.forEach((item) => { @@ -58,7 +62,7 @@ const SystemSetting = () => { setOriginInputs(newInputs); setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => { - return {key: item, text: item, value: item}; + return { key: item, text: item, value: item }; })); } else { showError(message); @@ -77,6 +81,7 @@ const SystemSetting = () => { case 'EmailVerificationEnabled': case 'GitHubOAuthEnabled': case 'WeChatAuthEnabled': + case 'TelegramLoginEnabled': case 'TurnstileCheckEnabled': case 'EmailDomainRestrictionEnabled': case 'RegisterEnabled': @@ -89,7 +94,7 @@ const SystemSetting = () => { key, value }); - const {success, message} = res.data; + const { success, message } = res.data; if (success) { if (key === 'EmailDomainWhitelist') { value = value.split(','); @@ -106,7 +111,7 @@ const SystemSetting = () => { setLoading(false); }; - const handleInputChange = async (e, {name, value}) => { + const handleInputChange = async (e, { name, value }) => { if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') { // block disabling password login setShowPasswordWarningModal(true); @@ -130,7 +135,7 @@ const SystemSetting = () => { name === 'EmailDomainWhitelist' || name === 'TopupGroupRatio' ) { - setInputs((inputs) => ({...inputs, [name]: value})); + setInputs((inputs) => ({ ...inputs, [name]: value })); } else { await updateOption(name, value); } @@ -234,6 +239,12 @@ const SystemSetting = () => { } }; + const submitTelegramSettings = async () => { + await updateOption('TelegramLoginEnabled', inputs.TelegramLoginEnabled); + await updateOption('TelegramBotToken', inputs.TelegramBotToken); + await updateOption('TelegramBotName', inputs.TelegramBotName); + }; + const submitTurnstile = async () => { if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); @@ -279,7 +290,7 @@ const SystemSetting = () => { 更新服务器地址 - +
支付设置(当前仅支持易支付接口,使用上方服务器地址作为回调地址!)
{ label='充值分组倍率' name='TopupGroupRatio' onChange={handleInputChange} - style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}} + style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} autoComplete='new-password' value={inputs.TopupGroupRatio} placeholder='为一个 JSON 文本,键为组名称,值为倍率' @@ -327,7 +338,7 @@ const SystemSetting = () => { 更新支付设置 - +
配置登录注册
{ open={showPasswordWarningModal} onClose={() => setShowPasswordWarningModal(false)} size={'tiny'} - style={{maxWidth: '450px'}} + style={{ maxWidth: '450px' }} > 警告 @@ -401,7 +412,33 @@ const SystemSetting = () => { onChange={handleInputChange} /> - + +
配置 Telegram 登录
+ + + + + + + 保存 Telegram 登录设置 +
配置邮箱域名白名单 用以防止恶意用户利用临时邮箱批量注册 @@ -443,13 +480,13 @@ const SystemSetting = () => { autoComplete='new-password' placeholder='输入新的允许的邮箱域名' value={restrictedDomainInput} - onChange={(e, {value}) => { + onChange={(e, { value }) => { setRestrictedDomainInput(value); }} /> 保存邮箱域名白名单设置 - +
配置 SMTP 用以支持系统的邮件发送 @@ -500,7 +537,7 @@ const SystemSetting = () => { /> 保存 SMTP 设置 - +
配置 GitHub OAuth App @@ -538,7 +575,7 @@ const SystemSetting = () => { 保存 GitHub OAuth 设置 - +
配置 WeChat Server @@ -582,7 +619,7 @@ const SystemSetting = () => { 保存 WeChat Server 设置 - +
配置 Turnstile From 690d2e6ba2c35bfee01464aa4e0a7b07acafc9d1 Mon Sep 17 00:00:00 2001 From: Ehco1996 Date: Fri, 1 Mar 2024 21:54:24 +0800 Subject: [PATCH 2/6] Add Telegram OAuth support --- common/constants.go | 1 + controller/misc.go | 2 ++ makefile | 4 +++ model/option.go | 5 +++ web/src/components/SystemSetting.js | 53 +++++++++++++++-------------- 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/common/constants.go b/common/constants.go index 55cc793..9ce2003 100644 --- a/common/constants.go +++ b/common/constants.go @@ -45,6 +45,7 @@ var PasswordRegisterEnabled = true var EmailVerificationEnabled = false var GitHubOAuthEnabled = false var WeChatAuthEnabled = false +var TelegramOAuthEnabled = false var TurnstileCheckEnabled = false var RegisterEnabled = true diff --git a/controller/misc.go b/controller/misc.go index f99baa8..1aabb02 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -20,6 +20,8 @@ func GetStatus(c *gin.Context) { "email_verification": common.EmailVerificationEnabled, "github_oauth": common.GitHubOAuthEnabled, "github_client_id": common.GitHubClientId, + "telegram_oauth": common.TelegramOAuthEnabled, + "telegram_bot_name": common.TelegramBotName, "system_name": common.SystemName, "logo": common.Logo, "footer_html": common.Footer, diff --git a/makefile b/makefile index 1df2b5c..603f5aa 100644 --- a/makefile +++ b/makefile @@ -5,6 +5,10 @@ BACKEND_DIR = . all: start-frontend start-backend +build-frontend: + @echo "Building frontend..." + @cd $(FRONTEND_DIR) && npm install && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build npm run build + # 启动前端开发服务器 start-frontend: @echo "Starting frontend dev server..." diff --git a/model/option.go b/model/option.go index dcb4c34..2feb711 100644 --- a/model/option.go +++ b/model/option.go @@ -30,6 +30,7 @@ func InitOptionMap() { common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled) common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled) common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled) + common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled) common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled) common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled) @@ -62,6 +63,8 @@ func InitOptionMap() { common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString() common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientSecret"] = "" + common.OptionMap["TelegramBotToken"] = "" + common.OptionMap["TelegramBotName"] = "" common.OptionMap["WeChatServerAddress"] = "" common.OptionMap["WeChatServerToken"] = "" common.OptionMap["WeChatAccountQRCodeImageURL"] = "" @@ -151,6 +154,8 @@ func updateOptionMap(key string, value string) (err error) { common.GitHubOAuthEnabled = boolValue case "WeChatAuthEnabled": common.WeChatAuthEnabled = boolValue + case "TelegramOAuthEnabled": + common.TelegramOAuthEnabled = boolValue case "TurnstileCheckEnabled": common.TurnstileCheckEnabled = boolValue case "RegisterEnabled": diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index a8956b7..f5d87be 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -413,32 +413,6 @@ const SystemSetting = () => { /> -
配置 Telegram 登录
- - - - - - - 保存 Telegram 登录设置 -
配置邮箱域名白名单 用以防止恶意用户利用临时邮箱批量注册 @@ -620,6 +594,33 @@ const SystemSetting = () => { 保存 WeChat Server 设置 +
配置 Telegram 登录
+ + + + + + + 保存 Telegram 登录设置 + +
配置 Turnstile From 194ff1ac5749e53bdf0ff376b13a5d9c3d1d45fe Mon Sep 17 00:00:00 2001 From: Ehco1996 Date: Fri, 1 Mar 2024 22:33:30 +0800 Subject: [PATCH 3/6] Update makefile and user model, add Telegram integration --- makefile | 8 +- model/user.go | 4 +- web/src/components/LoginForm.js | 67 ++++--- web/src/components/PersonalSetting.js | 21 +++ web/src/components/SystemSetting.js | 20 +-- web/src/pages/User/EditUser.js | 248 +++++++++++++------------- 6 files changed, 204 insertions(+), 164 deletions(-) diff --git a/makefile b/makefile index 603f5aa..1e8ab1b 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ FRONTEND_DIR = ./web BACKEND_DIR = . -.PHONY: all start-frontend start-backend +.PHONY: all build-frontend start-backend all: start-frontend start-backend @@ -9,12 +9,6 @@ build-frontend: @echo "Building frontend..." @cd $(FRONTEND_DIR) && npm install && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build npm run build -# 启动前端开发服务器 -start-frontend: - @echo "Starting frontend dev server..." - @cd $(FRONTEND_DIR) && npm start & - -# 启动后端开发服务器 start-backend: @echo "Starting backend dev server..." @cd $(BACKEND_DIR) && go run main.go & diff --git a/model/user.go b/model/user.go index 3e727a0..7b70bfe 100644 --- a/model/user.go +++ b/model/user.go @@ -3,10 +3,11 @@ package model import ( "errors" "fmt" - "gorm.io/gorm" "one-api/common" "strings" "time" + + "gorm.io/gorm" ) // User if you add sensitive fields, don't forget to clean them in setupLogin function. @@ -21,6 +22,7 @@ type User struct { Email string `json:"email" gorm:"index" validate:"max=50"` GitHubId string `json:"github_id" gorm:"column:github_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` + TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"` VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management Quota int `json:"quota" gorm:"type:int;default:0"` diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index 03aec65..ec49ece 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -1,14 +1,14 @@ -import React, {useContext, useEffect, useState} from 'react'; -import {Link, useNavigate, useSearchParams} from 'react-router-dom'; -import {UserContext} from '../context/User'; -import {API, getLogo, isMobile, showError, showInfo, showSuccess, showWarning} from '../helpers'; -import {onGitHubOAuthClicked} from './utils'; +import React, { useContext, useEffect, useState } from 'react'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { UserContext } from '../context/User'; +import { API, getLogo, isMobile, showError, showInfo, showSuccess, showWarning } from '../helpers'; +import { onGitHubOAuthClicked } from './utils'; import Turnstile from "react-turnstile"; -import {Layout, Card, Image, Form, Button, Divider, Modal} from "@douyinfe/semi-ui"; +import { Layout, Card, Image, Form, Button, Divider, Modal } from "@douyinfe/semi-ui"; import Title from "@douyinfe/semi-ui/lib/es/typography/title"; import Text from "@douyinfe/semi-ui/lib/es/typography/text"; -import {IconGithubLogo} from '@douyinfe/semi-icons'; +import { IconGithubLogo } from '@douyinfe/semi-icons'; const LoginForm = () => { const [inputs, setInputs] = useState({ @@ -18,7 +18,7 @@ const LoginForm = () => { }); const [searchParams, setSearchParams] = useSearchParams(); const [submitted, setSubmitted] = useState(false); - const {username, password} = inputs; + const { username, password } = inputs; const [userState, userDispatch] = useContext(UserContext); const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); @@ -56,9 +56,9 @@ const LoginForm = () => { const res = await API.get( `/api/oauth/wechat?code=${inputs.wechat_verification_code}` ); - const {success, message, data} = res.data; + const { success, message, data } = res.data; if (success) { - userDispatch({type: 'login', payload: data}); + userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); navigate('/'); showSuccess('登录成功!'); @@ -69,7 +69,7 @@ const LoginForm = () => { }; function handleChange(name, value) { - setInputs((inputs) => ({...inputs, [name]: value})); + setInputs((inputs) => ({ ...inputs, [name]: value })); } async function handleSubmit(e) { @@ -83,13 +83,13 @@ const LoginForm = () => { username, password }); - const {success, message, data} = res.data; + const { success, message, data } = res.data; if (success) { - userDispatch({type: 'login', payload: data}); + userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); showSuccess('登录成功!'); if (username === 'root' && password === '123456') { - Modal.error({title: '您正在使用默认密码!', content: '请立刻修改默认密码!', centered: true}); + Modal.error({ title: '您正在使用默认密码!', content: '请立刻修改默认密码!', centered: true }); } navigate('/token'); } else { @@ -100,16 +100,23 @@ const LoginForm = () => { } } + // 添加Telegram登录处理函数 + const onTelegramLoginClicked = async () => { + // 这里调用后端API进行Telegram登录 + // 例如: const res = await API.get(`/api/oauth/telegram`); + // 根据响应处理登录逻辑 + }; + return (
-
-
+
+
- + <Title heading={2} style={{ textAlign: 'center' }}> 用户登录
@@ -129,12 +136,12 @@ const LoginForm = () => { onChange={(value) => handleChange('password', value)} /> -
-
+
没有账号请先 注册账号 @@ -142,16 +149,16 @@ const LoginForm = () => { 忘记密码 点击重置
- {status.github_oauth || status.wechat_login ? ( + {status.github_oauth || status.wechat_login || status.telegram_oauth ? ( <> 第三方登录 -
+
{status.github_oauth ? ( + ) : ( + <> + )}
) : ( @@ -208,7 +227,7 @@ const LoginForm = () => { {/**/} {turnstileEnabled ? ( -
+
{ diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 78d3240..3c67a3f 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -443,6 +443,27 @@ const PersonalSetting = () => {
+
+ Telegram +
+
+ +
+
+ +
+
+
+
diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index f5d87be..436cc67 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -34,7 +34,7 @@ const SystemSetting = () => { EmailDomainRestrictionEnabled: '', EmailDomainWhitelist: '', // telegram login - TelegramLoginEnabled: '', + TelegramOAuthEnabled: '', TelegramBotToken: '', TelegramBotName: '', }); @@ -81,7 +81,7 @@ const SystemSetting = () => { case 'EmailVerificationEnabled': case 'GitHubOAuthEnabled': case 'WeChatAuthEnabled': - case 'TelegramLoginEnabled': + case 'TelegramOAuthEnabled': case 'TurnstileCheckEnabled': case 'EmailDomainRestrictionEnabled': case 'RegisterEnabled': @@ -240,7 +240,7 @@ const SystemSetting = () => { }; const submitTelegramSettings = async () => { - await updateOption('TelegramLoginEnabled', inputs.TelegramLoginEnabled); + await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled); await updateOption('TelegramBotToken', inputs.TelegramBotToken); await updateOption('TelegramBotName', inputs.TelegramBotName); }; @@ -397,6 +397,12 @@ const SystemSetting = () => { name='WeChatAuthEnabled' onChange={handleInputChange} /> + {
配置 Telegram 登录
- diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index 705f7a2..6d79127 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import {API, isMobile, showError, showSuccess} from '../../helpers'; +import { API, isMobile, showError, showSuccess } from '../../helpers'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; import Title from "@douyinfe/semi-ui/lib/es/typography/title"; -import {SideSheet, Space, Button, Spin, Input, Typography, Select, Divider} from "@douyinfe/semi-ui"; +import { SideSheet, Space, Button, Spin, Input, Typography, Select, Divider } from "@douyinfe/semi-ui"; const EditUser = (props) => { const userId = props.editingUser.id; @@ -19,8 +19,8 @@ const EditUser = (props) => { group: 'default' }); const [groupOptions, setGroupOptions] = useState([]); - const { username, display_name, password, github_id, wechat_id, email, quota, group } = - inputs; + const { username, display_name, password, github_id, wechat_id, telegram_id, email, quota, group } = + inputs; const handleInputChange = (name, value) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; @@ -88,126 +88,132 @@ const EditUser = (props) => { }; return ( - <> - {'编辑用户'}} - headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - visible={props.visible} - footer={ -
- - - - + <> + {'编辑用户'}} + headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + visible={props.visible} + footer={ +
+ + + + +
+ } + closeIcon={null} + onCancel={() => handleCancel()} + width={isMobile() ? '100%' : 600} + > + +
+ 用户名 +
+ handleInputChange('username', value)} + value={username} + autoComplete='new-password' + /> +
+ 密码 +
+ handleInputChange('password', value)} + value={password} + autoComplete='new-password' + /> +
+ 显示名称 +
+ handleInputChange('display_name', value)} + value={display_name} + autoComplete='new-password' + /> + { + userId && <> +
+ 分组
- } - closeIcon={null} - onCancel={() => handleCancel()} - width={isMobile() ? '100%' : 600} - > - -
- 用户名 -
- handleInputChange('username', value)} - value={username} + handleInputChange('password', value)} - value={password} + optionList={groupOptions} + /> +
+ {`剩余额度${renderQuotaWithPrompt(quota)}`} +
+ handleInputChange('quota', value)} + value={quota} + type={'number'} autoComplete='new-password' - /> -
- 显示名称 -
- handleInputChange('display_name', value)} - value={display_name} - autoComplete='new-password' - /> - { - userId && <> -
- 分组 -
- handleInputChange('quota', value)} - value={quota} - type={'number'} - autoComplete='new-password' - /> - - } - 以下信息不可修改 -
- 已绑定的 GitHub 账户 -
- -
- 已绑定的微信账户 -
- -
- 已绑定的邮箱账户 -
- -
- -
- + /> + + } + 以下信息不可修改 +
+ 已绑定的 GitHub 账户 +
+ +
+ 已绑定的微信账户 +
+ + +
+ 已绑定的邮箱账户 +
+ + + + ); }; From 84144306a899055071f32d5ad73f5da0c0bd50f5 Mon Sep 17 00:00:00 2001 From: sljeff Date: Sat, 2 Mar 2024 17:15:52 +0800 Subject: [PATCH 4/6] feat: telegram login and bind --- controller/telegram.go | 116 ++++++++++++++++++++++++++ docker-compose.yml | 2 +- model/user.go | 15 ++++ router/api-router.go | 2 + web/package.json | 5 +- web/src/components/LoginForm.js | 31 ++++--- web/src/components/PersonalSetting.js | 13 ++- web/src/components/SystemSetting.js | 6 +- web/src/pages/Home/index.js | 6 ++ 9 files changed, 174 insertions(+), 22 deletions(-) create mode 100644 controller/telegram.go diff --git a/controller/telegram.go b/controller/telegram.go new file mode 100644 index 0000000..b5bc0c0 --- /dev/null +++ b/controller/telegram.go @@ -0,0 +1,116 @@ +package controller + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "io" + "one-api/common" + "one-api/model" + "sort" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +func TelegramBind(c *gin.Context) { + if !common.TelegramOAuthEnabled { + c.JSON(200, gin.H{ + "message": "管理员未开启通过 Telegram 登录以及注册", + "success": false, + }) + return + } + params := c.Request.URL.Query() + if !checkTelegramAuthorization(params, common.TelegramBotToken) { + c.JSON(200, gin.H{ + "message": "无效的请求", + "success": false, + }) + return + } + telegramId := params["id"][0] + if model.IsTelegramIdAlreadyTaken(telegramId) { + c.JSON(200, gin.H{ + "message": "该 Telegram 账户已被绑定", + "success": false, + }) + return + } + + session := sessions.Default(c) + id := session.Get("id") + user := model.User{Id: id.(int)} + if err := user.FillUserById(); err != nil { + c.JSON(200, gin.H{ + "message": err.Error(), + "success": false, + }) + return + } + user.TelegramId = telegramId + if err := user.Update(false); err != nil { + c.JSON(200, gin.H{ + "message": err.Error(), + "success": false, + }) + return + } + + c.Redirect(302, "/setting") +} + +func TelegramLogin(c *gin.Context) { + if !common.TelegramOAuthEnabled { + c.JSON(200, gin.H{ + "message": "管理员未开启通过 Telegram 登录以及注册", + "success": false, + }) + return + } + params := c.Request.URL.Query() + if !checkTelegramAuthorization(params, common.TelegramBotToken) { + c.JSON(200, gin.H{ + "message": "无效的请求", + "success": false, + }) + return + } + + telegramId := params["id"][0] + user := model.User{TelegramId: telegramId} + if err := user.FillUserByTelegramId(); err != nil { + c.JSON(200, gin.H{ + "message": err.Error(), + "success": false, + }) + return + } + setupLogin(&user, c) +} + +func checkTelegramAuthorization(params map[string][]string, token string) bool { + strs := []string{} + var hash = "" + for k, v := range params { + if k == "hash" { + hash = v[0] + continue + } + strs = append(strs, k+"="+v[0]) + } + sort.Strings(strs) + var imploded = "" + for _, s := range strs { + if imploded != "" { + imploded += "\n" + } + imploded += s + } + sha256hash := sha256.New() + io.WriteString(sha256hash, token) + hmachash := hmac.New(sha256.New, sha256hash.Sum(nil)) + io.WriteString(hmachash, imploded) + ss := hex.EncodeToString(hmachash.Sum(nil)) + return hash == ss +} diff --git a/docker-compose.yml b/docker-compose.yml index 403f372..1a1ff21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.4' services: new-api: - image: calciumion/new-api:latest + build: . container_name: new-api restart: always command: --log-dir /app/logs diff --git a/model/user.go b/model/user.go index 7b70bfe..00294b2 100644 --- a/model/user.go +++ b/model/user.go @@ -288,6 +288,17 @@ func (user *User) FillUserByUsername() error { return nil } +func (user *User) FillUserByTelegramId() error { + if user.TelegramId == "" { + return errors.New("Telegram id 为空!") + } + err := DB.Where(User{TelegramId: user.TelegramId}).First(user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("该 Telegram 账户未绑定") + } + return nil +} + func IsEmailAlreadyTaken(email string) bool { return DB.Where("email = ?", email).Find(&User{}).RowsAffected == 1 } @@ -304,6 +315,10 @@ func IsUsernameAlreadyTaken(username string) bool { return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 } +func IsTelegramIdAlreadyTaken(telegramId string) bool { + return DB.Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1 +} + func ResetUserPasswordByEmail(email string, password string) error { if email == "" || password == "" { return errors.New("邮箱地址或密码为空!") diff --git a/router/api-router.go b/router/api-router.go index 04d3490..f72b9b3 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -26,6 +26,8 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) + apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin) + apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.TelegramBind) userRoute := apiRouter.Group("/user") { diff --git a/web/package.json b/web/package.json index 009092a..d6d7ad5 100644 --- a/web/package.json +++ b/web/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "dependencies": { - "@douyinfe/semi-ui": "^2.46.1", "@douyinfe/semi-icons": "^2.46.1", - "@visactor/vchart": "~1.8.8", + "@douyinfe/semi-ui": "^2.46.1", "@visactor/react-vchart": "~1.8.8", + "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", "axios": "^0.27.2", "history": "^5.3.0", @@ -17,6 +17,7 @@ "react-fireworks": "^1.0.4", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-telegram-login": "^1.1.2", "react-toastify": "^9.0.8", "react-turnstile": "^1.0.5", "semantic-ui-css": "^2.5.0", diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index ec49ece..eb94784 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -7,6 +7,7 @@ import Turnstile from "react-turnstile"; import { Layout, Card, Image, Form, Button, Divider, Modal } from "@douyinfe/semi-ui"; import Title from "@douyinfe/semi-ui/lib/es/typography/title"; import Text from "@douyinfe/semi-ui/lib/es/typography/text"; +import TelegramLoginButton from 'react-telegram-login'; import { IconGithubLogo } from '@douyinfe/semi-icons'; @@ -101,10 +102,24 @@ const LoginForm = () => { } // 添加Telegram登录处理函数 - const onTelegramLoginClicked = async () => { - // 这里调用后端API进行Telegram登录 - // 例如: const res = await API.get(`/api/oauth/telegram`); - // 根据响应处理登录逻辑 + const onTelegramLoginClicked = async (response) => { + const fields = ["id", "first_name", "last_name", "username", "photo_url", "auth_date", "hash", "lang"]; + const params = {}; + fields.forEach((field) => { + if (response[field]) { + params[field] = response[field]; + } + }); + const res = await API.get(`/api/oauth/telegram/login`, { params }); + const { success, message, data } = res.data; + if (success) { + userDispatch({ type: 'login', payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + showSuccess('登录成功!'); + navigate('/'); + } else { + showError(message); + } }; return ( @@ -176,13 +191,7 @@ const LoginForm = () => { {/*)}*/} {status.telegram_oauth ? ( - + ) : ( <> )} diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 3c67a3f..1216b99 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -21,6 +21,7 @@ import {getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor} from import EditToken from "../pages/Token/EditToken"; import EditUser from "../pages/User/EditUser"; import passwordResetConfirm from "./PasswordResetConfirm"; +import TelegramLoginButton from 'react-telegram-login'; const PersonalSetting = () => { const [userState, userDispatch] = useContext(UserContext); @@ -453,13 +454,11 @@ const PersonalSetting = () => { >
- + {status.telegram_oauth ? + userState.user.telegram_id !== '' ? + : + : + }
diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 436cc67..4d2bbc1 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -133,7 +133,9 @@ const SystemSetting = () => { name === 'TurnstileSiteKey' || name === 'TurnstileSecretKey' || name === 'EmailDomainWhitelist' || - name === 'TopupGroupRatio' + name === 'TopupGroupRatio' || + name === 'TelegramBotToken' || + name === 'TelegramBotName' ) { setInputs((inputs) => ({ ...inputs, [name]: value })); } else { @@ -605,12 +607,14 @@ const SystemSetting = () => { diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js index 6009486..5ee9f0a 100644 --- a/web/src/pages/Home/index.js +++ b/web/src/pages/Home/index.js @@ -110,6 +110,12 @@ const Home = () => { ? '已启用' : '未启用'}

+

+ Telegram 身份验证: + {statusState?.status?.telegram_oauth === true + ? '已启用' + : '未启用'} +

From cb92d6fd5f90e6ddf11ab823dbc8160c2df3df6f Mon Sep 17 00:00:00 2001 From: Ehco1996 Date: Sat, 2 Mar 2024 19:58:36 +0800 Subject: [PATCH 5/6] revert compose --- docker-compose.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1a1ff21..fff2716 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,8 @@ version: '3.4' services: new-api: - build: . + image: calciumion/new-api:latest + # build: . container_name: new-api restart: always command: --log-dir /app/logs @@ -12,7 +13,7 @@ services: - ./data:/data - ./logs:/app/logs environment: - # - SQL_DSN=root:123456@tcp(host.docker.internal:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库 + - SQL_DSN=root:123456@tcp(host.docker.internal:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库 - REDIS_CONN_STRING=redis://redis - SESSION_SECRET=random_string # 修改为随机字符串 - TZ=Asia/Shanghai From 699fe256d0d85b9b7bae6dd6483fd9a2e793a579 Mon Sep 17 00:00:00 2001 From: Ehco1996 Date: Sun, 3 Mar 2024 19:41:15 +0800 Subject: [PATCH 6/6] address comment --- makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/makefile b/makefile index 1e8ab1b..f846d30 100644 --- a/makefile +++ b/makefile @@ -3,7 +3,7 @@ BACKEND_DIR = . .PHONY: all build-frontend start-backend -all: start-frontend start-backend +all: build-frontend start-backend build-frontend: @echo "Building frontend..." @@ -12,4 +12,3 @@ build-frontend: start-backend: @echo "Starting backend dev server..." @cd $(BACKEND_DIR) && go run main.go & -