feat: support feishu login now

This commit is contained in:
JustSong 2024-04-05 12:10:43 +08:00
parent 68605800af
commit 4d61b9937b
17 changed files with 404 additions and 28 deletions

View File

@ -105,6 +105,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
21. 支持 Cloudflare Turnstile 用户校验。 21. 支持 Cloudflare Turnstile 用户校验。
22. 支持用户管理,支持**多种用户登录注册方式** 22. 支持用户管理,支持**多种用户登录注册方式**
+ 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。 + 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。
+ 支持使用飞书进行授权登录。
+ [GitHub 开放授权](https://github.com/settings/applications/new)。 + [GitHub 开放授权](https://github.com/settings/applications/new)。
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。 23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。

View File

@ -66,6 +66,9 @@ var SMTPToken = ""
var GitHubClientId = "" var GitHubClientId = ""
var GitHubClientSecret = "" var GitHubClientSecret = ""
var LarkClientId = ""
var LarkClientSecret = ""
var WeChatServerAddress = "" var WeChatServerAddress = ""
var WeChatServerToken = "" var WeChatServerToken = ""
var WeChatAccountQRCodeImageURL = "" var WeChatAccountQRCodeImageURL = ""

View File

@ -1,4 +1,4 @@
package controller package auth
import ( import (
"bytes" "bytes"
@ -11,6 +11,7 @@ import (
"github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/config"
"github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/helper"
"github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/controller"
"github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/model"
"net/http" "net/http"
"strconv" "strconv"
@ -159,7 +160,7 @@ func GitHubOAuth(c *gin.Context) {
}) })
return return
} }
setupLogin(&user, c) controller.SetupLogin(&user, c)
} }
func GitHubBind(c *gin.Context) { func GitHubBind(c *gin.Context) {

201
controller/auth/lark.go Normal file
View File

@ -0,0 +1,201 @@
package auth
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/config"
"github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/controller"
"github.com/songquanpeng/one-api/model"
"net/http"
"strconv"
"time"
)
type LarkOAuthResponse struct {
AccessToken string `json:"access_token"`
}
type LarkUser struct {
Name string `json:"name"`
OpenID string `json:"open_id"`
}
func getLarkUserInfoByCode(code string) (*LarkUser, error) {
if code == "" {
return nil, errors.New("无效的参数")
}
values := map[string]string{
"client_id": config.LarkClientId,
"client_secret": config.LarkClientSecret,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": fmt.Sprintf("%s/oauth/lark", config.ServerAddress),
}
jsonData, err := json.Marshal(values)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "https://passport.feishu.cn/suite/passport/oauth/token", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := http.Client{
Timeout: 5 * time.Second,
}
res, err := client.Do(req)
if err != nil {
logger.SysLog(err.Error())
return nil, errors.New("无法连接至飞书服务器,请稍后重试!")
}
defer res.Body.Close()
var oAuthResponse LarkOAuthResponse
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
if err != nil {
return nil, err
}
req, err = http.NewRequest("GET", "https://passport.feishu.cn/suite/passport/oauth/userinfo", 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 {
logger.SysLog(err.Error())
return nil, errors.New("无法连接至飞书服务器,请稍后重试!")
}
var larkUser LarkUser
err = json.NewDecoder(res2.Body).Decode(&larkUser)
if err != nil {
return nil, err
}
return &larkUser, nil
}
func LarkOAuth(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 {
LarkBind(c)
return
}
code := c.Query("code")
larkUser, err := getLarkUserInfoByCode(code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
user := model.User{
LarkId: larkUser.OpenID,
}
if model.IsLarkIdAlreadyTaken(user.LarkId) {
err := user.FillUserByLarkId()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
if config.RegisterEnabled {
user.Username = "lark_" + strconv.Itoa(model.GetMaxUserId()+1)
if larkUser.Name != "" {
user.DisplayName = larkUser.Name
} else {
user.DisplayName = "Lark User"
}
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
}
controller.SetupLogin(&user, c)
}
func LarkBind(c *gin.Context) {
code := c.Query("code")
larkUser, err := getLarkUserInfoByCode(code)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
user := model.User{
LarkId: larkUser.OpenID,
}
if model.IsLarkIdAlreadyTaken(user.LarkId) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该飞书账户已被绑定",
})
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.LarkId = larkUser.OpenID
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

@ -1,4 +1,4 @@
package controller package auth
import ( import (
"encoding/json" "encoding/json"
@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/config"
"github.com/songquanpeng/one-api/controller"
"github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/model"
"net/http" "net/http"
"strconv" "strconv"
@ -109,7 +110,7 @@ func WeChatAuth(c *gin.Context) {
}) })
return return
} }
setupLogin(&user, c) controller.SetupLogin(&user, c)
} }
func WeChatBind(c *gin.Context) { func WeChatBind(c *gin.Context) {

View File

@ -23,6 +23,7 @@ func GetStatus(c *gin.Context) {
"email_verification": config.EmailVerificationEnabled, "email_verification": config.EmailVerificationEnabled,
"github_oauth": config.GitHubOAuthEnabled, "github_oauth": config.GitHubOAuthEnabled,
"github_client_id": config.GitHubClientId, "github_client_id": config.GitHubClientId,
"lark_client_id": config.LarkClientId,
"system_name": config.SystemName, "system_name": config.SystemName,
"logo": config.Logo, "logo": config.Logo,
"footer_html": config.Footer, "footer_html": config.Footer,

View File

@ -58,11 +58,11 @@ func Login(c *gin.Context) {
}) })
return return
} }
setupLogin(&user, c) SetupLogin(&user, c)
} }
// setup session & cookies and then return user info // setup session & cookies and then return user info
func setupLogin(user *model.User, c *gin.Context) { func SetupLogin(user *model.User, c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
session.Set("id", user.Id) session.Set("id", user.Id)
session.Set("username", user.Username) session.Set("username", user.Username)

View File

@ -172,6 +172,10 @@ func updateOptionMap(key string, value string) (err error) {
config.GitHubClientId = value config.GitHubClientId = value
case "GitHubClientSecret": case "GitHubClientSecret":
config.GitHubClientSecret = value config.GitHubClientSecret = value
case "LarkClientId":
config.LarkClientId = value
case "LarkClientSecret":
config.LarkClientSecret = value
case "Footer": case "Footer":
config.Footer = value config.Footer = value
case "SystemName": case "SystemName":

View File

@ -24,6 +24,7 @@ type User struct {
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"`
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
LarkId string `json:"lark_id" gorm:"column:lark_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!
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
Quota int64 `json:"quota" gorm:"bigint;default:0"` Quota int64 `json:"quota" gorm:"bigint;default:0"`
@ -41,21 +42,21 @@ func GetMaxUserId() int {
} }
func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) { func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) {
query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted) query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted)
switch order { switch order {
case "quota": case "quota":
query = query.Order("quota desc") query = query.Order("quota desc")
case "used_quota": case "used_quota":
query = query.Order("used_quota desc") query = query.Order("used_quota desc")
case "request_count": case "request_count":
query = query.Order("request_count desc") query = query.Order("request_count desc")
default: default:
query = query.Order("id desc") query = query.Order("id desc")
} }
err = query.Find(&users).Error err = query.Find(&users).Error
return users, err return users, err
} }
func SearchUsers(keyword string) (users []*User, err error) { func SearchUsers(keyword string) (users []*User, err error) {
@ -206,6 +207,14 @@ func (user *User) FillUserByGitHubId() error {
return nil return nil
} }
func (user *User) FillUserByLarkId() error {
if user.LarkId == "" {
return errors.New("lark id 为空!")
}
DB.Where(User{LarkId: user.LarkId}).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 为空!")
@ -234,6 +243,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 IsLarkIdAlreadyTaken(githubId string) bool {
return DB.Where("lark_id = ?", githubId).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

@ -2,6 +2,7 @@ package router
import ( import (
"github.com/songquanpeng/one-api/controller" "github.com/songquanpeng/one-api/controller"
"github.com/songquanpeng/one-api/controller/auth"
"github.com/songquanpeng/one-api/middleware" "github.com/songquanpeng/one-api/middleware"
"github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
@ -21,10 +22,11 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
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(), auth.GitHubOAuth)
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode) apiRouter.GET("/oauth/lark", middleware.CriticalRateLimit(), auth.LarkOAuth)
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), auth.GenerateOAuthCode)
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), auth.WeChatAuth)
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), auth.WeChatBind)
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
apiRouter.POST("/topup", middleware.AdminAuth(), controller.AdminTopUp) apiRouter.POST("/topup", middleware.AdminAuth(), controller.AdminTopUp)

View File

@ -24,6 +24,7 @@ import EditRedemption from './pages/Redemption/EditRedemption';
import TopUp from './pages/TopUp'; import TopUp from './pages/TopUp';
import Log from './pages/Log'; import Log from './pages/Log';
import Chat from './pages/Chat'; import Chat from './pages/Chat';
import LarkOAuth from './components/LarkOAuth';
const Home = lazy(() => import('./pages/Home')); const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About')); const About = lazy(() => import('./pages/About'));
@ -239,6 +240,14 @@ function App() {
</Suspense> </Suspense>
} }
/> />
<Route
path='/oauth/lark'
element={
<Suspense fallback={<Loading></Loading>}>
<LarkOAuth />
</Suspense>
}
/>
<Route <Route
path='/setting' path='/setting'
element={ element={

View File

@ -0,0 +1,58 @@
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 LarkOAuth = () => {
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) => {
const res = await API.get(`/api/oauth/lark?code=${code}&state=${state}`);
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 lark
return;
}
count++;
setPrompt(`出现错误,第 ${count} 次重试中...`);
await new Promise((resolve) => setTimeout(resolve, count * 2000));
await sendCode(code, state, count);
}
};
useEffect(() => {
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 LarkOAuth;

View File

@ -3,7 +3,8 @@ import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } f
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, showSuccess, showWarning } from '../helpers'; import { API, getLogo, showError, showSuccess, showWarning } from '../helpers';
import { onGitHubOAuthClicked } from './utils'; import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils';
import larkIcon from '../images/lark.svg';
const LoginForm = () => { const LoginForm = () => {
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@ -124,7 +125,7 @@ const LoginForm = () => {
点击注册 点击注册
</Link> </Link>
</Message> </Message>
{status.github_oauth || status.wechat_login ? ( {status.github_oauth || status.wechat_login || status.lark_client_id ? (
<> <>
<Divider horizontal>Or</Divider> <Divider horizontal>Or</Divider>
{status.github_oauth ? ( {status.github_oauth ? (
@ -137,6 +138,18 @@ const LoginForm = () => {
) : ( ) : (
<></> <></>
)} )}
{status.lark_client_id ? (
<Button
// circular
color=''
onClick={() => onLarkOAuthClicked(status.lark_client_id)}
style={{ padding: 0, width: 36, height: 36 }}
>
<img src={larkIcon} width={36} height={36} />
</Button>
) : (
<></>
)}
{status.wechat_login ? ( {status.wechat_login ? (
<Button <Button
circular circular

View File

@ -4,7 +4,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; import { API, copy, 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, onLarkOAuthClicked } from './utils';
const PersonalSetting = () => { const PersonalSetting = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
@ -247,6 +247,11 @@ const PersonalSetting = () => {
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button> <Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
) )
} }
{
status.lark_client_id && (
<Button onClick={()=>{onLarkOAuthClicked(status.lark_client_id)}}>绑定飞书账号</Button>
)
}
<Button <Button
onClick={() => { onClick={() => {
setShowEmailBindModal(true); setShowEmailBindModal(true);

View File

@ -10,6 +10,8 @@ const SystemSetting = () => {
GitHubOAuthEnabled: '', GitHubOAuthEnabled: '',
GitHubClientId: '', GitHubClientId: '',
GitHubClientSecret: '', GitHubClientSecret: '',
LarkClientId: '',
LarkClientSecret: '',
Notice: '', Notice: '',
SMTPServer: '', SMTPServer: '',
SMTPPort: '', SMTPPort: '',
@ -109,6 +111,8 @@ const SystemSetting = () => {
name === 'ServerAddress' || name === 'ServerAddress' ||
name === 'GitHubClientId' || name === 'GitHubClientId' ||
name === 'GitHubClientSecret' || name === 'GitHubClientSecret' ||
name === 'LarkClientId' ||
name === 'LarkClientSecret' ||
name === 'WeChatServerAddress' || name === 'WeChatServerAddress' ||
name === 'WeChatServerToken' || name === 'WeChatServerToken' ||
name === 'WeChatAccountQRCodeImageURL' || name === 'WeChatAccountQRCodeImageURL' ||
@ -212,6 +216,18 @@ const SystemSetting = () => {
} }
}; };
const submitLarkOAuth = async () => {
if (originInputs['LarkClientId'] !== inputs.LarkClientId) {
await updateOption('LarkClientId', inputs.LarkClientId);
}
if (
originInputs['LarkClientSecret'] !== inputs.LarkClientSecret &&
inputs.LarkClientSecret !== ''
) {
await updateOption('LarkClientSecret', inputs.LarkClientSecret);
}
};
const submitTurnstile = async () => { const submitTurnstile = async () => {
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
@ -469,6 +485,44 @@ const SystemSetting = () => {
保存 GitHub OAuth 设置 保存 GitHub OAuth 设置
</Form.Button> </Form.Button>
<Divider /> <Divider />
<Header as='h3'>
配置飞书授权登录
<Header.Subheader>
用以支持通过飞书进行登录注册
<a href='https://open.feishu.cn/app' target='_blank'>
点击此处
</a>
管理你的飞书应用
</Header.Subheader>
</Header>
<Message>
主页链接填 <code>{inputs.ServerAddress}</code>
重定向 URL {' '}
<code>{`${inputs.ServerAddress}/oauth/lark`}</code>
</Message>
<Form.Group widths={3}>
<Form.Input
label='App ID'
name='LarkClientId'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.LarkClientId}
placeholder='输入 App ID'
/>
<Form.Input
label='App Secret'
name='LarkClientSecret'
onChange={handleInputChange}
type='password'
autoComplete='new-password'
value={inputs.LarkClientSecret}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitLarkOAuth}>
保存飞书 OAuth 设置
</Form.Button>
<Divider />
<Header as='h3'> <Header as='h3'>
配置 WeChat Server 配置 WeChat Server
<Header.Subheader> <Header.Subheader>

View File

@ -18,3 +18,12 @@ export async function onGitHubOAuthClicked(github_client_id) {
`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 onLarkOAuthClicked(lark_client_id) {
const state = await getOAuthState();
if (!state) return;
let redirect_uri = `${window.location.origin}/oauth/lark`;
window.open(
`https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB