1. add LINUX DO oauth

2. fix oauth reg aff issue

Signed-off-by: wozulong <>
This commit is contained in:
wozulong 2024-03-14 18:53:06 +08:00
parent 299911d4cd
commit 7ddb7c586d
19 changed files with 494 additions and 10 deletions

View File

@ -7,7 +7,7 @@ all: build-frontend start-backend
build-frontend: build-frontend:
@echo "Building frontend..." @echo "Building frontend..."
@cd $(FRONTEND_DIR) && npm install && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build npm run build @cd $(FRONTEND_DIR) && npm install && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
start-backend: start-backend:
@echo "Starting backend dev server..." @echo "Starting backend dev server..."

View File

@ -50,6 +50,7 @@ var PasswordLoginEnabled = true
var PasswordRegisterEnabled = true var PasswordRegisterEnabled = true
var EmailVerificationEnabled = false var EmailVerificationEnabled = false
var GitHubOAuthEnabled = false var GitHubOAuthEnabled = false
var LinuxDoOAuthEnabled = false
var WeChatAuthEnabled = false var WeChatAuthEnabled = false
var TelegramOAuthEnabled = false var TelegramOAuthEnabled = false
var TurnstileCheckEnabled = false var TurnstileCheckEnabled = false
@ -82,6 +83,9 @@ var SMTPToken = ""
var GitHubClientId = "" var GitHubClientId = ""
var GitHubClientSecret = "" var GitHubClientSecret = ""
var LinuxDoClientId = ""
var LinuxDoClientSecret = ""
var WeChatServerAddress = "" var WeChatServerAddress = ""
var WeChatServerToken = "" var WeChatServerToken = ""
var WeChatAccountQRCodeImageURL = "" var WeChatAccountQRCodeImageURL = ""

View File

@ -123,6 +123,8 @@ func GitHubOAuth(c *gin.Context) {
} }
} else { } else {
if common.RegisterEnabled { if common.RegisterEnabled {
user.InviterId, _ = model.GetUserIdByAffCode(c.Query("aff"))
user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1) user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
if githubUser.Name != "" { if githubUser.Name != "" {
user.DisplayName = githubUser.Name user.DisplayName = githubUser.Name
@ -133,7 +135,7 @@ func GitHubOAuth(c *gin.Context) {
user.Role = common.RoleCommonUser user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled user.Status = common.UserStatusEnabled
if err := user.Insert(0); err != nil { if err := user.Insert(user.InviterId); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": err.Error(), "message": err.Error(),

223
controller/linuxdo.go Normal file
View File

@ -0,0 +1,223 @@
package controller
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
"net/url"
"one-api/common"
"one-api/model"
"strconv"
"time"
)
type LinuxDoOAuthResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
type LinuxDoUser struct {
ID int `json:"id"`
Username string `json:"username"`
Name string `json:"name"`
Active bool `json:"active"`
TrustLevel int `json:"trust_level"`
Silenced bool `json:"silenced"`
}
func getLinuxDoUserInfoByCode(code string) (*LinuxDoUser, error) {
if code == "" {
return nil, errors.New("无效的参数")
}
auth := base64.StdEncoding.EncodeToString([]byte(common.LinuxDoClientId + ":" + common.LinuxDoClientSecret))
form := url.Values{
"grant_type": {"authorization_code"},
"code": {code},
}
req, err := http.NewRequest("POST", "https://connect.linux.do/oauth2/token", bytes.NewBufferString(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", "Basic "+auth)
req.Header.Set("Accept", "application/json")
client := http.Client{
Timeout: 5 * time.Second,
}
res, err := client.Do(req)
if err != nil {
common.SysLog(err.Error())
return nil, errors.New("无法连接至 LINUX DO 服务器,请稍后重试!")
}
defer res.Body.Close()
var oAuthResponse LinuxDoOAuthResponse
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
if err != nil {
return nil, err
}
req, err = http.NewRequest("GET", "https://connect.linux.do/api/user", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oAuthResponse.AccessToken))
res2, err := client.Do(req)
if err != nil {
common.SysLog(err.Error())
return nil, errors.New("无法连接至 LINUX DO 服务器,请稍后重试!")
}
defer res2.Body.Close()
var linuxdoUser LinuxDoUser
err = json.NewDecoder(res2.Body).Decode(&linuxdoUser)
if err != nil {
return nil, err
}
if linuxdoUser.ID == 0 {
return nil, errors.New("返回值非法,用户字段为空,请稍后重试!")
}
return &linuxdoUser, nil
}
func LinuxDoOAuth(c *gin.Context) {
session := sessions.Default(c)
state := c.Query("state")
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
})
return
}
username := session.Get("username")
if username != nil {
LinuxDoBind(c)
return
}
if !common.LinuxDoOAuthEnabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员未开启通过 LINUX DO 登录以及注册",
})
return
}
code := c.Query("code")
linuxdoUser, err := getLinuxDoUserInfoByCode(code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
user := model.User{
LinuxDoId: strconv.Itoa(linuxdoUser.ID),
}
if model.IsLinuxDoIdAlreadyTaken(user.LinuxDoId) {
err := user.FillUserByLinuxDoId()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
if common.RegisterEnabled {
affCode := c.Query("aff")
user.InviterId, _ = model.GetUserIdByAffCode(affCode)
user.Username = "linuxdo_" + strconv.Itoa(model.GetMaxUserId()+1)
if linuxdoUser.Name != "" {
user.DisplayName = linuxdoUser.Name
} else {
user.DisplayName = linuxdoUser.Username
}
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
if err := user.Insert(user.InviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员关闭了新用户注册",
})
return
}
}
if user.Status != common.UserStatusEnabled {
c.JSON(http.StatusOK, gin.H{
"message": "用户已被封禁",
"success": false,
})
return
}
setupLogin(&user, c)
}
func LinuxDoBind(c *gin.Context) {
if !common.LinuxDoOAuthEnabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员未开启通过 LINUX DO 登录以及注册",
})
return
}
code := c.Query("code")
linuxdoUser, err := getLinuxDoUserInfoByCode(code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
user := model.User{
LinuxDoId: strconv.Itoa(linuxdoUser.ID),
}
if model.IsLinuxDoIdAlreadyTaken(user.LinuxDoId) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该 LINUX DO 账户已被绑定",
})
return
}
session := sessions.Default(c)
id := session.Get("id")
// id := c.GetInt("id") // critical bug!
user.Id = id.(int)
err = user.FillUserById()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
user.LinuxDoId = strconv.Itoa(linuxdoUser.ID)
err = user.Update(false)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "bind",
})
return
}

View File

@ -36,6 +36,8 @@ func GetStatus(c *gin.Context) {
"email_verification": common.EmailVerificationEnabled, "email_verification": common.EmailVerificationEnabled,
"github_oauth": common.GitHubOAuthEnabled, "github_oauth": common.GitHubOAuthEnabled,
"github_client_id": common.GitHubClientId, "github_client_id": common.GitHubClientId,
"linuxdo_oauth": common.LinuxDoOAuthEnabled,
"linuxdo_client_id": common.LinuxDoClientId,
"telegram_oauth": common.TelegramOAuthEnabled, "telegram_oauth": common.TelegramOAuthEnabled,
"telegram_bot_name": common.TelegramBotName, "telegram_bot_name": common.TelegramBotName,
"system_name": common.SystemName, "system_name": common.SystemName,

View File

@ -50,6 +50,14 @@ func UpdateOption(c *gin.Context) {
}) })
return return
} }
case "LinuxDoOAuthEnabled":
if option.Value == "true" && common.LinuxDoClientId == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法启用 LINUX DO OAuth请先填入 LINUX DO Client Id 以及 LINUX DO Client Secret",
})
return
}
case "EmailDomainRestrictionEnabled": case "EmailDomainRestrictionEnabled":
if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 { if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{

View File

@ -30,6 +30,7 @@ func InitOptionMap() {
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled) common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled) common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled) common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
common.OptionMap["LinuxDoOAuthEnabled"] = strconv.FormatBool(common.LinuxDoOAuthEnabled)
common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled) common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled)
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled) common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
@ -65,6 +66,8 @@ func InitOptionMap() {
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString() common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientId"] = ""
common.OptionMap["GitHubClientSecret"] = "" common.OptionMap["GitHubClientSecret"] = ""
common.OptionMap["LinuxDoClientId"] = ""
common.OptionMap["LinuxDoClientSecret"] = ""
common.OptionMap["TelegramBotToken"] = "" common.OptionMap["TelegramBotToken"] = ""
common.OptionMap["TelegramBotName"] = "" common.OptionMap["TelegramBotName"] = ""
common.OptionMap["WeChatServerAddress"] = "" common.OptionMap["WeChatServerAddress"] = ""
@ -155,6 +158,8 @@ func updateOptionMap(key string, value string) (err error) {
common.EmailVerificationEnabled = boolValue common.EmailVerificationEnabled = boolValue
case "GitHubOAuthEnabled": case "GitHubOAuthEnabled":
common.GitHubOAuthEnabled = boolValue common.GitHubOAuthEnabled = boolValue
case "LinuxDoOAuthEnabled":
common.LinuxDoOAuthEnabled = boolValue
case "WeChatAuthEnabled": case "WeChatAuthEnabled":
common.WeChatAuthEnabled = boolValue common.WeChatAuthEnabled = boolValue
case "TelegramOAuthEnabled": case "TelegramOAuthEnabled":
@ -217,6 +222,10 @@ func updateOptionMap(key string, value string) (err error) {
common.GitHubClientId = value common.GitHubClientId = value
case "GitHubClientSecret": case "GitHubClientSecret":
common.GitHubClientSecret = value common.GitHubClientSecret = value
case "LinuxDoClientId":
common.LinuxDoClientId = value
case "LinuxDoClientSecret":
common.LinuxDoClientSecret = value
case "Footer": case "Footer":
common.Footer = value common.Footer = value
case "SystemName": case "SystemName":

View File

@ -21,6 +21,7 @@ type User struct {
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
Email string `json:"email" gorm:"index" validate:"max=50"` Email string `json:"email" gorm:"index" validate:"max=50"`
GitHubId string `json:"github_id" gorm:"column:github_id;index"` GitHubId string `json:"github_id" gorm:"column:github_id;index"`
LinuxDoId string `json:"linuxdo_id" gorm:"column:linuxdo_id;index"`
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
TelegramId string `json:"telegram_id" gorm:"column:telegram_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! VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
@ -272,6 +273,14 @@ func (user *User) FillUserByGitHubId() error {
return nil return nil
} }
func (user *User) FillUserByLinuxDoId() error {
if user.LinuxDoId == "" {
return errors.New("LINUX DO id 为空!")
}
DB.Where(User{LinuxDoId: user.LinuxDoId}).First(user)
return nil
}
func (user *User) FillUserByWeChatId() error { func (user *User) FillUserByWeChatId() error {
if user.WeChatId == "" { if user.WeChatId == "" {
return errors.New("WeChat id 为空!") return errors.New("WeChat id 为空!")
@ -311,6 +320,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool {
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1 return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
} }
func IsLinuxDoIdAlreadyTaken(linuxdoId string) bool {
return DB.Where("linuxdo_id = ?", linuxdoId).Find(&User{}).RowsAffected == 1
}
func IsUsernameAlreadyTaken(username string) bool { func IsUsernameAlreadyTaken(username string) bool {
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
} }

View File

@ -23,6 +23,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth) apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
apiRouter.GET("/oauth/linuxdo", middleware.CriticalRateLimit(), controller.LinuxDoOAuth)
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode) apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)

View File

@ -11,6 +11,7 @@ import EditUser from './pages/User/EditUser';
import { API, getLogo, getSystemName, showError, showNotice } from './helpers'; import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
import PasswordResetForm from './components/PasswordResetForm'; import PasswordResetForm from './components/PasswordResetForm';
import GitHubOAuth from './components/GitHubOAuth'; import GitHubOAuth from './components/GitHubOAuth';
import LinuxDoOAuth from "./components/LinuxDoOAuth";
import PasswordResetConfirm from './components/PasswordResetConfirm'; import PasswordResetConfirm from './components/PasswordResetConfirm';
import { UserContext } from './context/User'; import { UserContext } from './context/User';
import { StatusContext } from './context/Status'; import { StatusContext } from './context/Status';
@ -170,6 +171,14 @@ function App() {
</Suspense> </Suspense>
} }
/> />
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>}>
<LinuxDoOAuth />
</Suspense>
}
/>
<Route <Route
path='/setting' path='/setting'
element={ element={

View File

@ -14,7 +14,8 @@ const GitHubOAuth = () => {
let navigate = useNavigate(); let navigate = useNavigate();
const sendCode = async (code, state, count) => { const sendCode = async (code, state, count) => {
const res = await API.get(`/api/oauth/github?code=${code}&state=${state}`); let aff = localStorage.getItem('aff');
const res = await API.get(`/api/oauth/github?code=${code}&state=${state}&aff=${aff}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (message === 'bind') { if (message === 'bind') {
@ -41,6 +42,14 @@ const GitHubOAuth = () => {
}; };
useEffect(() => { useEffect(() => {
let error = searchParams.get('error');
if (error) {
let errorDescription = searchParams.get('error_description');
showError(`授权错误:${error}: ${errorDescription}`);
navigate('/setting');
return;
}
let code = searchParams.get('code'); let code = searchParams.get('code');
let state = searchParams.get('state'); let state = searchParams.get('state');
sendCode(code, state, 0).then(); sendCode(code, state, 0).then();

View File

@ -0,0 +1,21 @@
import React from 'react';
import {Icon} from '@douyinfe/semi-ui';
const LinuxDoIcon = (props) => {
function CustomIcon() {
return <svg className='icon' viewBox='0 0 24 24' version='1.1'
xmlns='http://www.w3.org/2000/svg' width='16' height='16' {...props}>
<path
d="M19.7,17.6c-0.1-0.2-0.2-0.4-0.2-0.6c0-0.4-0.2-0.7-0.5-1c-0.1-0.1-0.3-0.2-0.4-0.2c0.6-1.8-0.3-3.6-1.3-4.9c0,0,0,0,0,0c-0.8-1.2-2-2.1-1.9-3.7c0-1.9,0.2-5.4-3.3-5.1C8.5,2.3,9.5,6,9.4,7.3c0,1.1-0.5,2.2-1.3,3.1c-0.2,0.2-0.4,0.5-0.5,0.7c-1,1.2-1.5,2.8-1.5,4.3c-0.2,0.2-0.4,0.4-0.5,0.6c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.1-0.3,0.2-0.5,0.3c-0.4,0.1-0.7,0.3-0.9,0.7c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,0.2,0.1,0.4,0,0.7c-0.2,0.4-0.2,0.9,0,1.4c0.3,0.4,0.8,0.5,1.5,0.6c0.5,0,1.1,0.2,1.6,0.4l0,0c0.5,0.3,1.1,0.5,1.7,0.5c0.3,0,0.7-0.1,1-0.2c0.3-0.2,0.5-0.4,0.6-0.7c0.4,0,1-0.2,1.7-0.2c0.6,0,1.2,0.2,2,0.1c0,0.1,0,0.2,0.1,0.3c0.2,0.5,0.7,0.9,1.3,1c0.1,0,0.1,0,0.2,0c0.8-0.1,1.6-0.5,2.1-1.1l0,0c0.4-0.4,0.9-0.7,1.4-0.9c0.6-0.3,1-0.5,1.1-1C20.3,18.6,20.1,18.2,19.7,17.6z M12.8,4.8c0.6,0.1,1.1,0.6,1,1.2c0,0.3-0.1,0.6-0.3,0.9c0,0,0,0-0.1,0c-0.2-0.1-0.3-0.1-0.4-0.2c0.1-0.1,0.1-0.3,0.2-0.5c0-0.4-0.2-0.7-0.4-0.7c-0.3,0-0.5,0.3-0.5,0.7c0,0,0,0.1,0,0.1c-0.1-0.1-0.3-0.1-0.4-0.2c0,0,0-0.1,0-0.1C11.8,5.5,12.2,4.9,12.8,4.8z M12.5,6.8c0.1,0.1,0.3,0.2,0.4,0.2c0.1,0,0.3,0.1,0.4,0.2c0.2,0.1,0.4,0.2,0.4,0.5c0,0.3-0.3,0.6-0.9,0.8c-0.2,0.1-0.3,0.1-0.4,0.2c-0.3,0.2-0.6,0.3-1,0.3c-0.3,0-0.6-0.2-0.8-0.4c-0.1-0.1-0.2-0.2-0.4-0.3C10.1,8.2,9.9,8,9.8,7.7c0-0.1,0.1-0.2,0.2-0.3c0.3-0.2,0.4-0.3,0.5-0.4l0.1-0.1c0.2-0.3,0.6-0.5,1-0.5C11.9,6.5,12.2,6.6,12.5,6.8z M10.4,5c0.4,0,0.7,0.4,0.8,1.1c0,0.1,0,0.1,0,0.2c-0.1,0-0.3,0.1-0.4,0.2c0,0,0-0.1,0-0.2c0-0.3-0.2-0.6-0.4-0.5c-0.2,0-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.4l0,0c0,0-0.1,0.1-0.2,0.1C9.9,6.7,9.7,6.4,9.7,6.1C9.7,5.5,10,5,10.4,5z M9.4,21.1c-0.7,0.3-1.6,0.2-2.2-0.2c-0.6-0.3-1.1-0.4-1.8-0.4c-0.5-0.1-1-0.1-1.1-0.3c-0.1-0.2-0.1-0.5,0.1-1c0.1-0.3,0.1-0.6,0-0.9c-0.1-0.3-0.1-0.5,0-0.8C4.5,17.2,4.7,17.1,5,17c0.3-0.1,0.5-0.2,0.7-0.4c0.1-0.1,0.2-0.2,0.3-0.4c0.3-0.4,0.5-0.6,0.8-0.6c0.6,0.1,1.1,1,1.5,1.9c0.2,0.3,0.4,0.7,0.7,1c0.4,0.5,0.9,1.2,0.9,1.6C9.9,20.6,9.7,20.9,9.4,21.1z M14.3,18.9c0,0.1,0,0.1-0.1,0.2c-1.2,0.9-2.8,1-4.1,0.3c-0.2-0.3-0.4-0.6-0.6-0.9c0.9-0.1,0.7-1.3-1.2-2.5c-2-1.3-0.6-3.7,0.1-4.8c0.1-0.1,0.1,0-0.3,0.8c-0.3,0.6-0.9,2.1-0.1,3.2c0-0.8,0.2-1.6,0.5-2.4c0.7-1.3,1.2-2.8,1.5-4.3c0.1,0.1,0.1,0.1,0.2,0.1c0.1,0.1,0.2,0.2,0.3,0.2c0.2,0.3,0.6,0.4,0.9,0.4c0,0,0.1,0,0.1,0c0.4,0,0.8-0.1,1.1-0.4c0.1-0.1,0.2-0.2,0.4-0.2c0.3-0.1,0.6-0.3,0.9-0.6c0.4,1.3,0.8,2.5,1.4,3.6c0.4,0.8,0.7,1.6,0.9,2.5c0.3,0,0.7,0.1,1,0.3c0.8,0.4,1.1,0.7,1,1.2c-0.1,0-0.1,0-0.2,0c0-0.3-0.2-0.6-0.9-0.9c-0.7-0.3-1.3-0.3-1.5,0.4c-0.1,0-0.2,0.1-0.3,0.1c-0.8,0.4-0.8,1.5-0.9,2.6C14.5,18.2,14.4,18.5,14.3,18.9z M18.9,19.5c-0.6,0.2-1.1,0.6-1.5,1.1c-0.4,0.6-1.1,1-1.9,0.9c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.6-0.1-1.2,0.2-1.8c0.1-0.4,0.2-0.7,0.3-1.1c0.1-1.2,0.1-1.9,0.6-2.2h0c0,0.5,0.3,0.8,0.7,1c0.5,0,1-0.1,1.4-0.5c0.1,0,0.1,0,0.2,0c0.3,0,0.5,0,0.7,0.2c0.2,0.2,0.3,0.5,0.3,0.7c0,0.3,0.2,0.6,0.3,0.9c0.5,0.5,0.5,0.8,0.5,0.9C19.7,19.1,19.3,19.3,18.9,19.5z M9.9,7.5c-0.1,0-0.1,0-0.1,0.1c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0.1,0,0.1,0.1,0.1,0.1c0.3,0.4,0.8,0.6,1.4,0.7c0.5-0.1,1-0.2,1.5-0.6c0.2-0.1,0.4-0.2,0.6-0.3c0.1,0,0.1-0.1,0.1-0.1c0-0.1,0-0.1-0.1-0.1l0,0c-0.2,0.1-0.5,0.2-0.7,0.3c-0.4,0.3-0.9,0.5-1.4,0.5c-0.5,0-0.9-0.3-1.2-0.6C10.1,7.6,10,7.5,9.9,7.5z"
fill="currentColor"/>
</svg>;
}
return (
<div>
<Icon svg={<CustomIcon/>}/>
</div>
);
};
export default LinuxDoIcon;

View File

@ -0,0 +1,67 @@
import React, { useContext, useEffect, useState } from 'react';
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers';
import { UserContext } from '../context/User';
const LinuxDoOAuth = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [userState, userDispatch] = useContext(UserContext);
const [prompt, setPrompt] = useState('处理中...');
const [processing, setProcessing] = useState(true);
let navigate = useNavigate();
const sendCode = async (code, state, count) => {
let aff = localStorage.getItem('aff');
const res = await API.get(`/api/oauth/linuxdo?code=${code}&state=${state}&aff=${aff}`);
const { success, message, data } = res.data;
if (success) {
if (message === 'bind') {
showSuccess('绑定成功!');
navigate('/setting');
} else {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
showSuccess('登录成功!');
navigate('/');
}
} else {
showError(message);
if (count === 0) {
setPrompt(`操作失败,重定向至登录界面中...`);
navigate('/setting'); // in case this is failed to bind GitHub
return;
}
count++;
setPrompt(`出现错误,第 ${count} 次重试中...`);
await new Promise((resolve) => setTimeout(resolve, count * 2000));
await sendCode(code, state, count);
}
};
useEffect(() => {
let error = searchParams.get('error');
if (error) {
let errorDescription = searchParams.get('error_description');
showError(`授权错误:${error}: ${errorDescription}`);
navigate('/setting');
return;
}
let code = searchParams.get('code');
let state = searchParams.get('state');
sendCode(code, state, 0).then();
}, []);
return (
<Segment style={{ minHeight: '300px' }}>
<Dimmer active inverted>
<Loader size='large'>{prompt}</Loader>
</Dimmer>
</Segment>
);
};
export default LinuxDoOAuth;

View File

@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { API, getLogo, isMobile, showError, showInfo, showSuccess, showWarning } from '../helpers'; import { API, getLogo, isMobile, showError, showInfo, showSuccess, showWarning } from '../helpers';
import { onGitHubOAuthClicked } from './utils'; import { onGitHubOAuthClicked, onLinuxDoOAuthClicked } from './utils';
import Turnstile from "react-turnstile"; import Turnstile from "react-turnstile";
import { Layout, Card, Image, Form, Button, Divider, Modal, Icon } from '@douyinfe/semi-ui'; import { Layout, Card, Image, Form, Button, Divider, Modal, Icon } from '@douyinfe/semi-ui';
import Title from "@douyinfe/semi-ui/lib/es/typography/title"; import Title from "@douyinfe/semi-ui/lib/es/typography/title";
@ -10,6 +10,7 @@ import Text from "@douyinfe/semi-ui/lib/es/typography/text";
import TelegramLoginButton from 'react-telegram-login'; import TelegramLoginButton from 'react-telegram-login';
import { IconGithubLogo } from '@douyinfe/semi-icons'; import { IconGithubLogo } from '@douyinfe/semi-icons';
import LinuxDoIcon from './LinuxDoIcon';
import WeChatIcon from './WeChatIcon'; import WeChatIcon from './WeChatIcon';
const LoginForm = () => { const LoginForm = () => {
@ -165,7 +166,7 @@ const LoginForm = () => {
忘记密码 <Link to='/reset'>点击重置</Link> 忘记密码 <Link to='/reset'>点击重置</Link>
</Text> </Text>
</div> </div>
{status.github_oauth || status.wechat_login || status.telegram_oauth ? ( {status.github_oauth || status.linuxdo_oauth || status.wechat_login || status.telegram_oauth ? (
<> <>
<Divider margin='12px' align='center'> <Divider margin='12px' align='center'>
第三方登录 第三方登录
@ -180,6 +181,16 @@ const LoginForm = () => {
) : ( ) : (
<></> <></>
)} )}
{status.linuxdo_oauth ? (
<Button
type='primary'
icon={<LinuxDoIcon />}
style={{color: '#000'}}
onClick={() => onLinuxDoOAuthClicked(status.linuxdo_client_id)}
/>
) : (
<></>
)}
{status.wechat_login ? ( {status.wechat_login ? (
<Button <Button
type='primary' type='primary'

View File

@ -3,7 +3,7 @@ import {Link, useNavigate} from 'react-router-dom';
import {API, copy, isRoot, showError, showInfo, showNotice, showSuccess} from '../helpers'; import {API, copy, isRoot, showError, showInfo, showNotice, showSuccess} from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import {UserContext} from '../context/User'; import {UserContext} from '../context/User';
import {onGitHubOAuthClicked} from './utils'; import {onGitHubOAuthClicked, onLinuxDoOAuthClicked} from './utils';
import { import {
Avatar, Banner, Avatar, Banner,
Button, Button,
@ -443,6 +443,27 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}>
<Typography.Text strong>LINUX DO</Typography.Text>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<div>
<Input
value={userState.user && userState.user.linuxdo_id !== '' ? userState.user.linuxdo_id : '未绑定'}
readonly={true}
></Input>
</div>
<div>
<Button
onClick={() => {onLinuxDoOAuthClicked(status.linuxdo_client_id)}}
disabled={(userState.user && userState.user.linuxdo_id !== '') || !status.linuxdo_oauth}
>
{
status.linuxdo_oauth ? '绑定' : '未启用'
}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}> <div style={{marginTop: 10}}>
<Typography.Text strong>Telegram</Typography.Text> <Typography.Text strong>Telegram</Typography.Text>

View File

@ -10,6 +10,9 @@ const SystemSetting = () => {
GitHubOAuthEnabled: '', GitHubOAuthEnabled: '',
GitHubClientId: '', GitHubClientId: '',
GitHubClientSecret: '', GitHubClientSecret: '',
LinuxDoOAuthEnabled: '',
LinuxDoClientId: '',
LinuxDoClientSecret: '',
Notice: '', Notice: '',
SMTPServer: '', SMTPServer: '',
SMTPPort: '', SMTPPort: '',
@ -82,6 +85,7 @@ const SystemSetting = () => {
case 'PasswordRegisterEnabled': case 'PasswordRegisterEnabled':
case 'EmailVerificationEnabled': case 'EmailVerificationEnabled':
case 'GitHubOAuthEnabled': case 'GitHubOAuthEnabled':
case 'LinuxDoOAuthEnabled':
case 'WeChatAuthEnabled': case 'WeChatAuthEnabled':
case 'TelegramOAuthEnabled': case 'TelegramOAuthEnabled':
case 'TurnstileCheckEnabled': case 'TurnstileCheckEnabled':
@ -129,6 +133,8 @@ const SystemSetting = () => {
name === 'PayAddress' || name === 'PayAddress' ||
name === 'GitHubClientId' || name === 'GitHubClientId' ||
name === 'GitHubClientSecret' || name === 'GitHubClientSecret' ||
name === 'LinuxDoClientId' ||
name === 'LinuxDoClientSecret' ||
name === 'WeChatServerAddress' || name === 'WeChatServerAddress' ||
name === 'WeChatServerToken' || name === 'WeChatServerToken' ||
name === 'WeChatAccountQRCodeImageURL' || name === 'WeChatAccountQRCodeImageURL' ||
@ -243,6 +249,18 @@ const SystemSetting = () => {
} }
}; };
const submitLinuxDoOAuth = async () => {
if (originInputs['LinuxDoClientId'] !== inputs.LinuxDoClientId) {
await updateOption('LinuxDoClientId', inputs.LinuxDoClientId);
}
if (
originInputs['LinuxDoClientSecret'] !== inputs.LinuxDoClientSecret &&
inputs.LinuxDoClientSecret !== ''
) {
await updateOption('LinuxDoClientSecret', inputs.LinuxDoClientSecret);
}
};
const submitTelegramSettings = async () => { const submitTelegramSettings = async () => {
// await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled); // await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
await updateOption('TelegramBotToken', inputs.TelegramBotToken); await updateOption('TelegramBotToken', inputs.TelegramBotToken);
@ -412,6 +430,12 @@ const SystemSetting = () => {
name='GitHubOAuthEnabled' name='GitHubOAuthEnabled'
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Form.Checkbox
checked={inputs.LinuxDoOAuthEnabled === 'true'}
label='允许通过 LINUX DO 账户登录 & 注册'
name='LinuxDoOAuthEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox <Form.Checkbox
checked={inputs.WeChatAuthEnabled === 'true'} checked={inputs.WeChatAuthEnabled === 'true'}
label='允许通过微信登录 & 注册' label='允许通过微信登录 & 注册'
@ -577,6 +601,44 @@ const SystemSetting = () => {
保存 GitHub OAuth 设置 保存 GitHub OAuth 设置
</Form.Button> </Form.Button>
<Divider /> <Divider />
<Header as='h3'>
配置 LINUX DO Oauth
<Header.Subheader>
用以支持通过 LINUX DO 进行登录注册
<a href='https://connect.linux.do' target='_blank'>
点击此处
</a>
管理你的 LINUX DO OAuth
</Header.Subheader>
</Header>
<Message>
Homepage URL <code>{inputs.ServerAddress}</code>
Authorization callback URL {' '}
<code>{`${inputs.ServerAddress}/oauth/linuxdo`}</code>
</Message>
<Form.Group widths={3}>
<Form.Input
label='LINUX DO Client ID'
name='LinuxDoClientId'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.LinuxDoClientId}
placeholder='输入你注册的 LINUX DO OAuth 的 ID'
/>
<Form.Input
label='LINUX DO Client Secret'
name='LinuxDoClientSecret'
onChange={handleInputChange}
type='password'
autoComplete='new-password'
value={inputs.LinuxDoClientSecret}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitLinuxDoOAuth}>
保存 LINUX DO OAuth 设置
</Form.Button>
<Divider />
<Header as='h3'> <Header as='h3'>
配置 WeChat Server 配置 WeChat Server
<Header.Subheader> <Header.Subheader>

View File

@ -14,7 +14,11 @@ export async function getOAuthState() {
export async function onGitHubOAuthClicked(github_client_id) { export async function onGitHubOAuthClicked(github_client_id) {
const state = await getOAuthState(); const state = await getOAuthState();
if (!state) return; if (!state) return;
window.open( location.href = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email` }
);
export async function onLinuxDoOAuthClicked(linuxdo_client_id) {
const state = await getOAuthState();
if (!state) return;
location.href = `https://connect.linux.do/oauth2/authorize?client_id=${linuxdo_client_id}&response_type=code&state=${state}&scope=user:profile`;
} }

View File

@ -95,6 +95,10 @@ const Home = () => {
GitHub 身份验证 GitHub 身份验证
{statusState?.status?.github_oauth === true ? '已启用' : '未启用'} {statusState?.status?.github_oauth === true ? '已启用' : '未启用'}
</p> </p>
<p>
LINUX DO 身份验证
{statusState?.status?.linuxdo_oauth === true ? '已启用' : '未启用'}
</p>
<p> <p>
微信身份验证 微信身份验证
{statusState?.status?.wechat_login === true ? '已启用' : '未启用'} {statusState?.status?.wechat_login === true ? '已启用' : '未启用'}

View File

@ -13,13 +13,14 @@ const EditUser = (props) => {
display_name: '', display_name: '',
password: '', password: '',
github_id: '', github_id: '',
linuxdo_id: '',
wechat_id: '', wechat_id: '',
email: '', email: '',
quota: 0, quota: 0,
group: 'default' group: 'default'
}); });
const [groupOptions, setGroupOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]);
const { username, display_name, password, github_id, wechat_id, telegram_id, email, quota, group } = const { username, display_name, password, github_id, linuxdo_id, wechat_id, telegram_id, email, quota, group } =
inputs; inputs;
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
@ -184,6 +185,16 @@ const EditUser = (props) => {
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly readonly
/> />
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的 LINUX DO 账户</Typography.Text>
</div>
<Input
name='linuxdo_id'
value={linuxdo_id}
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的微信账户</Typography.Text> <Typography.Text>已绑定的微信账户</Typography.Text>
</div> </div>
@ -194,6 +205,9 @@ const EditUser = (props) => {
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly readonly
/> />
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的 Telegram 账户</Typography.Text>
</div>
<Input <Input
name='telegram_id' name='telegram_id'
value={telegram_id} value={telegram_id}