Просмотр исходного кода

Merge pull request #1 from Ehco1996/telegram-login-complete

feat: telegram login and bind
Ehco 1 год назад
Родитель
Сommit
e5cea80103

+ 116 - 0
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
+}

+ 1 - 1
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

+ 15 - 0
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("邮箱地址或密码为空!")

+ 2 - 0
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")
 		{

+ 3 - 2
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",

+ 20 - 11
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 ? (
-                                                <Button
-                                                    type='primary'
-                                                    // icon={<IconTelegram/>} // 假设您有Telegram的图标
-                                                    onClick={onTelegramLoginClicked}
-                                                >
-                                                    Telegram登录
-                                                </Button>
+                                                <TelegramLoginButton dataOnauth={onTelegramLoginClicked} botName={status.telegram_bot_name} />
                                             ) : (
                                                 <></>
                                             )}

+ 6 - 7
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 = () => {
                                         ></Input>
                                     </div>
                                     <div>
-                                        <Button
-                                            disabled={(userState.user && userState.user.telegram_id !== '') || !status.telegram_oauth}
-                                        >
-                                            {
-                                                status.github_oauth?'绑定':'未启用'
-                                            }
-                                        </Button>
+                                        {status.telegram_oauth ?
+                                            userState.user.telegram_id !== '' ? <Button disabled={true}>已绑定</Button>
+                                            : <TelegramLoginButton dataAuthUrl="/api/oauth/telegram/bind" botName={status.telegram_bot_name} />
+                                        : <Button disabled={true}>未启用</Button>
+                                        }
                                     </div>
                                 </div>
                             </div>

+ 5 - 1
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 = () => {
                         <Form.Input
                             label='Telegram Bot Token'
                             name='TelegramBotToken'
+                            onChange={handleInputChange}
                             value={inputs.TelegramBotToken}
                             placeholder='输入你的 Telegram Bot Token'
                         />
                         <Form.Input
                             label='Telegram Bot 名称'
                             name='TelegramBotName'
+                            onChange={handleInputChange}
                             value={inputs.TelegramBotName}
                             placeholder='输入你的 Telegram Bot 名称'
                         />

+ 6 - 0
web/src/pages/Home/index.js

@@ -110,6 +110,12 @@ const Home = () => {
                                                     ? '已启用'
                                                     : '未启用'}
                                             </p>
+                                            <p>
+                                                Telegram 身份验证:
+                                                {statusState?.status?.telegram_oauth === true
+                                                    ? '已启用'
+                                                    : '未启用'}
+                                            </p>
                                         </Card.Description>
                                     </Card.Content>
                                 </Card>