feat: integrate Linux DO OAuth authentication

This commit is contained in:
seefs001 2024-11-10 23:56:22 +08:00
parent 3b53a2a5ce
commit 046f859d92
13 changed files with 978 additions and 542 deletions

View File

@ -41,6 +41,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
@ -75,6 +76,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 = ""

265
controller/linuxdo.go Normal file
View File

@ -0,0 +1,265 @@
package controller
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"one-api/common"
"one-api/model"
"strconv"
"strings"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
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 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, c)
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")
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",
})
}
func getLinuxdoUserInfoByCode(code string, c *gin.Context) (*LinuxdoUser, error) {
if code == "" {
return nil, errors.New("invalid code")
}
// Get access token using Basic auth
tokenEndpoint := "https://connect.linux.do/oauth2/token"
credentials := common.LinuxDOClientId + ":" + common.LinuxDOClientSecret
basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
// Get redirect URI from request
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
redirectURI := fmt.Sprintf("%s://%s/api/oauth/linuxdo", scheme, c.Request.Host)
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("code", code)
data.Set("redirect_uri", redirectURI)
req, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", basicAuth)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
client := http.Client{Timeout: 5 * time.Second}
res, err := client.Do(req)
if err != nil {
return nil, errors.New("failed to connect to Linux DO server")
}
defer res.Body.Close()
var tokenRes struct {
AccessToken string `json:"access_token"`
Message string `json:"message"`
}
if err := json.NewDecoder(res.Body).Decode(&tokenRes); err != nil {
return nil, err
}
if tokenRes.AccessToken == "" {
return nil, fmt.Errorf("failed to get access token: %s", tokenRes.Message)
}
// Get user info
userEndpoint := "https://connect.linux.do/api/user"
req, err = http.NewRequest("GET", userEndpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+tokenRes.AccessToken)
req.Header.Set("Accept", "application/json")
res2, err := client.Do(req)
if err != nil {
return nil, errors.New("failed to get user info from Linux DO")
}
defer res2.Body.Close()
var linuxdoUser LinuxdoUser
if err := json.NewDecoder(res2.Body).Decode(&linuxdoUser); err != nil {
return nil, err
}
if linuxdoUser.Id == 0 {
return nil, errors.New("invalid user info returned")
}
return &linuxdoUser, nil
}
func LinuxdoOAuth(c *gin.Context) {
session := sessions.Default(c)
errorCode := c.Query("error")
if errorCode != "" {
errorDescription := c.Query("error_description")
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": errorDescription,
})
return
}
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, c)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
user := model.User{
LinuxDOId: strconv.Itoa(linuxdoUser.Id),
}
// Check if user exists
if model.IsLinuxDOIdAlreadyTaken(user.LinuxDOId) {
err := user.FillUserByLinuxDOId()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
if user.Id == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户已注销",
})
return
}
} else {
if common.RegisterEnabled {
user.Username = "linuxdo_" + strconv.Itoa(model.GetMaxUserId()+1)
user.DisplayName = linuxdoUser.Name
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
if err := user.Insert(0); 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)
}

View File

@ -38,6 +38,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": "无法启用 LinuxDO OAuth请先填入 LinuxDO Client Id 以及 LinuxDO 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{

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,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)
@ -175,6 +176,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":
@ -267,6 +270,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

@ -36,6 +36,7 @@ type User struct {
AffHistoryQuota int `json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"` // 邀请历史额度 AffHistoryQuota int `json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"` // 邀请历史额度
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
} }
func (user *User) GetAccessToken() string { func (user *User) GetAccessToken() string {
@ -537,3 +538,17 @@ func GetUsernameById(id int) (username string, err error) {
err = DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username).Error err = DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username).Error
return username, err return username, err
} }
func IsLinuxDOIdAlreadyTaken(linuxDOId string) bool {
var user User
err := DB.Unscoped().Where("linux_do_id = ?", linuxDOId).First(&user).Error
return !errors.Is(err, gorm.ErrRecordNotFound)
}
func (u *User) FillUserByLinuxDOId() error {
if u.LinuxDOId == "" {
return errors.New("linux do id is empty")
}
err := DB.Where("linux_do_id = ?", u.LinuxDOId).First(u).Error
return err
}

View File

@ -25,6 +25,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

@ -1,8 +1,15 @@
import React, { useContext, useEffect, useState } from 'react'; 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, showError, showInfo, showSuccess, updateAPI } from '../helpers'; import {
import { onGitHubOAuthClicked } from './utils'; API,
getLogo,
showError,
showInfo,
showSuccess,
updateAPI,
} from '../helpers';
import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { import {
Button, Button,
@ -17,7 +24,7 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; 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, IconAlarm } from '@douyinfe/semi-icons';
import WeChatIcon from './WeChatIcon'; import WeChatIcon from './WeChatIcon';
import { setUserData } from '../helpers/data.js'; import { setUserData } from '../helpers/data.js';
@ -72,7 +79,7 @@ const LoginForm = () => {
userDispatch({ type: 'login', payload: data }); userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data)); localStorage.setItem('user', JSON.stringify(data));
setUserData(data); setUserData(data);
updateAPI() updateAPI();
navigate('/'); navigate('/');
showSuccess('登录成功!'); showSuccess('登录成功!');
setShowWeChatLoginModal(false); setShowWeChatLoginModal(false);
@ -103,7 +110,7 @@ const LoginForm = () => {
if (success) { if (success) {
userDispatch({ type: 'login', payload: data }); userDispatch({ type: 'login', payload: data });
setUserData(data); setUserData(data);
updateAPI() updateAPI();
showSuccess('登录成功!'); showSuccess('登录成功!');
if (username === 'root' && password === '123456') { if (username === 'root' && password === '123456') {
Modal.error({ Modal.error({
@ -146,7 +153,7 @@ const LoginForm = () => {
localStorage.setItem('user', JSON.stringify(data)); localStorage.setItem('user', JSON.stringify(data));
showSuccess('登录成功!'); showSuccess('登录成功!');
setUserData(data); setUserData(data);
updateAPI() updateAPI();
navigate('/'); navigate('/');
} else { } else {
showError(message); showError(message);
@ -214,7 +221,8 @@ const LoginForm = () => {
</div> </div>
{status.github_oauth || {status.github_oauth ||
status.wechat_login || status.wechat_login ||
status.telegram_oauth ? ( status.telegram_oauth ||
status.linuxdo_oauth ? (
<> <>
<Divider margin='12px' align='center'> <Divider margin='12px' align='center'>
第三方登录 第三方登录
@ -237,6 +245,17 @@ const LoginForm = () => {
) : ( ) : (
<></> <></>
)} )}
{status.linuxdo_oauth ? (
<Button
type='primary'
icon={<IconAlarm />}
onClick={() =>
onLinuxDOOAuthClicked(status.linuxdo_client_id)
}
/>
) : (
<></>
)}
{status.wechat_login ? ( {status.wechat_login ? (
<Button <Button
type='primary' type='primary'

View File

@ -10,7 +10,7 @@ import {
} from '../helpers'; } 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, Avatar,
Banner, Banner,
@ -519,7 +519,6 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>Telegram</Typography.Text> <Typography.Text strong>Telegram</Typography.Text>
<div <div
@ -551,7 +550,36 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>LinuxDO</Typography.Text>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<div>
<Input
value={
userState.user && userState.user.linux_do_id !== ''
? userState.user.linux_do_id
: '未绑定'
}
readonly={true}
></Input>
</div>
<div>
<Button
onClick={() => {
onLinuxDOOAuthClicked(status.linuxdo_client_id);
}}
disabled={
(userState.user && userState.user.linux_do_id !== '') ||
!status.linuxdo_oauth
}
>
{status.linuxdo_oauth ? '绑定' : '未启用'}
</Button>
</div>
</div>
</div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Space> <Space>
<Button onClick={generateAccessToken}> <Button onClick={generateAccessToken}>

View File

@ -53,6 +53,9 @@ const SystemSetting = () => {
TelegramOAuthEnabled: '', TelegramOAuthEnabled: '',
TelegramBotToken: '', TelegramBotToken: '',
TelegramBotName: '', TelegramBotName: '',
LinuxDOOAuthEnabled: '',
LinuxDOClientId: '',
LinuxDOClientSecret: '',
}); });
const [originInputs, setOriginInputs] = useState({}); const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
@ -103,6 +106,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':
@ -163,7 +167,9 @@ const SystemSetting = () => {
name === 'EmailDomainWhitelist' || name === 'EmailDomainWhitelist' ||
name === 'TopupGroupRatio' || name === 'TopupGroupRatio' ||
name === 'TelegramBotToken' || name === 'TelegramBotToken' ||
name === 'TelegramBotName' name === 'TelegramBotName' ||
name === 'LinuxDOClientId' ||
name === 'LinuxDOClientSecret'
) { ) {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
} else { } else {
@ -182,7 +188,7 @@ const SystemSetting = () => {
if (inputs.WorkerValidKey !== '') { if (inputs.WorkerValidKey !== '') {
await updateOption('WorkerValidKey', inputs.WorkerValidKey); await updateOption('WorkerValidKey', inputs.WorkerValidKey);
} }
} };
const submitPayAddress = async () => { const submitPayAddress = async () => {
if (inputs.ServerAddress === '') { if (inputs.ServerAddress === '') {
@ -320,6 +326,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);
}
};
return ( return (
<Grid columns={1}> <Grid columns={1}>
<Grid.Column> <Grid.Column>
@ -340,7 +358,15 @@ const SystemSetting = () => {
更新服务器地址 更新服务器地址
</Form.Button> </Form.Button>
<Header as='h3' inverted={isDark}> <Header as='h3' inverted={isDark}>
代理设置支持 <a href='https://github.com/Calcium-Ion/new-api-worker' target='_blank' rel='noreferrer'>new-api-worker</a> 代理设置支持{' '}
<a
href='https://github.com/Calcium-Ion/new-api-worker'
target='_blank'
rel='noreferrer'
>
new-api-worker
</a>
</Header> </Header>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.Input <Form.Input
@ -358,9 +384,7 @@ const SystemSetting = () => {
onChange={handleInputChange} onChange={handleInputChange}
/> />
</Form.Group> </Form.Group>
<Form.Button onClick={submitWorker}> <Form.Button onClick={submitWorker}>更新Worker设置</Form.Button>
更新Worker设置
</Form.Button>
<Divider /> <Divider />
<Header as='h3' inverted={isDark}> <Header as='h3' inverted={isDark}>
支付设置当前仅支持易支付接口默认使用上方服务器地址作为回调地址 支付设置当前仅支持易支付接口默认使用上方服务器地址作为回调地址
@ -483,6 +507,12 @@ const SystemSetting = () => {
name='GitHubOAuthEnabled' name='GitHubOAuthEnabled'
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Form.Checkbox
checked={inputs.LinuxDOOAuthEnabled === 'true'}
label='允许通过 LinuxDO 账户登录 & 注册'
name='LinuxDOOAuthEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox <Form.Checkbox
checked={inputs.WeChatAuthEnabled === 'true'} checked={inputs.WeChatAuthEnabled === 'true'}
label='允许通过微信登录 & 注册' label='允许通过微信登录 & 注册'
@ -781,6 +811,48 @@ const SystemSetting = () => {
<Form.Button onClick={submitTurnstile}> <Form.Button onClick={submitTurnstile}>
保存 Turnstile 设置 保存 Turnstile 设置
</Form.Button> </Form.Button>
<Divider />
<Header as='h3' inverted={isDark}>
配置 LinuxDO OAuth App
<Header.Subheader>
用以支持通过 LinuxDO 进行登录注册
<a
href='https://connect.linux.do/'
target='_blank'
rel='noreferrer'
>
点击此处
</a>
管理你的 LinuxDO OAuth App
</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='LinuxDO Client ID'
name='LinuxDOClientId'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.LinuxDOClientId}
placeholder='输入你注册的 LinuxDO OAuth APP 的 ID'
/>
<Form.Input
label='LinuxDO Client Secret'
name='LinuxDOClientSecret'
onChange={handleInputChange}
type='password'
autoComplete='new-password'
value={inputs.LinuxDOClientSecret}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitLinuxDOOAuth}>
保存 LinuxDO OAuth 设置
</Form.Button>
</Form> </Form>
</Grid.Column> </Grid.Column>
</Grid> </Grid>

View File

@ -19,6 +19,14 @@ export async function onGitHubOAuthClicked(github_client_id) {
); );
} }
export async function onLinuxDOOAuthClicked(linuxdo_client_id) {
const state = await getOAuthState();
if (!state) return;
window.open(
`https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`,
);
}
let channelModels = undefined; let channelModels = undefined;
export async function loadChannelModels() { export async function loadChannelModels() {
const res = await API.get('/api/models'); const res = await API.get('/api/models');

View File

@ -150,6 +150,12 @@ const Home = () => {
? '已启用' ? '已启用'
: '未启用'} : '未启用'}
</p> </p>
<p>
Linux DO 身份验证
{statusState?.status?.linuxdo_oauth === true
? '已启用'
: '未启用'}
</p>
</Card> </Card>
</Col> </Col>
</Row> </Row>