mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-17 19:13:42 +08:00
Compare commits
9 Commits
v0.2.7.2-a
...
v0.2.3-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4267de5642 | ||
|
|
8b55116563 | ||
|
|
f35e63e3f3 | ||
|
|
17c409de23 | ||
|
|
e4753e7411 | ||
|
|
9adefa80b9 | ||
|
|
4ce2381182 | ||
|
|
62afc21ea5 | ||
|
|
7ddb7c586d |
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 项目群聊
|
- name: 交流社区
|
||||||
url: https://private-user-images.githubusercontent.com/61247483/283011625-de536a8a-0161-47a7-a0a2-66ef6de81266.jpeg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTEiLCJleHAiOjE3MDIyMjQzOTAsIm5iZiI6MTcwMjIyNDA5MCwicGF0aCI6Ii82MTI0NzQ4My8yODMwMTE2MjUtZGU1MzZhOGEtMDE2MS00N2E3LWEwYTItNjZlZjZkZTgxMjY2LmpwZWc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBSVdOSllBWDRDU1ZFSDUzQSUyRjIwMjMxMjEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDIzMTIxMFQxNjAxMzBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT02MGIxYmM3ZDQyYzBkOTA2ZTYyYmVmMzQ1NjY4NjM1YjY0NTUzNTM5NjE1NDZkYTIzODdhYTk4ZjZjODJmYzY2JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.TJ8CTfOSwR0-CHS1KLfomqgL0e4YH1luy8lSLrkv5Zg
|
url: https://linux.do
|
||||||
about: QQ 群:629454374
|
about: 项目交流社区
|
||||||
|
|||||||
2
.github/workflows/docker-image-amd64.yml
vendored
2
.github/workflows/docker-image-amd64.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
calciumion/new-api
|
pengzhile/new-api
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
|
|||||||
2
.github/workflows/docker-image-arm64.yml
vendored
2
.github/workflows/docker-image-arm64.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
calciumion/new-api
|
pengzhile/new-api
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
|
|||||||
23
Dockerfile
23
Dockerfile
@@ -1,32 +1,29 @@
|
|||||||
FROM node:16 as builder
|
FROM node:16-slim as builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY web/package.json .
|
COPY web/package.json .
|
||||||
RUN npm install
|
COPY web/yarn.lock .
|
||||||
|
RUN yarn install --network-timeout 1000000
|
||||||
COPY ./web .
|
COPY ./web .
|
||||||
COPY ./VERSION .
|
COPY ./VERSION .
|
||||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
|
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) yarn build
|
||||||
|
|
||||||
FROM golang AS builder2
|
|
||||||
|
|
||||||
|
FROM golang:1.19-alpine AS builder2
|
||||||
|
RUN apk add --no-cache build-base
|
||||||
ENV GO111MODULE=on \
|
ENV GO111MODULE=on \
|
||||||
CGO_ENABLED=1 \
|
CGO_ENABLED=1 \
|
||||||
GOOS=linux
|
GOOS=linux
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ADD go.mod go.sum ./
|
#ADD go.mod go.sum ./
|
||||||
RUN go mod download
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=builder /build/build ./web/build
|
COPY --from=builder /build/build ./web/build
|
||||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
|
||||||
|
RUN go mod tidy \
|
||||||
|
&& go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
|
||||||
RUN apk update \
|
|
||||||
&& apk upgrade \
|
|
||||||
&& apk add --no-cache ca-certificates tzdata \
|
|
||||||
&& update-ca-certificates 2>/dev/null || true
|
|
||||||
|
|
||||||
COPY --from=builder2 /build/one-api /
|
COPY --from=builder2 /build/one-api /
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
|||||||
@@ -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) && yarn install --network-timeout 1000000 && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) yarn build
|
||||||
|
|
||||||
start-backend:
|
start-backend:
|
||||||
@echo "Starting backend dev server..."
|
@echo "Starting backend dev server..."
|
||||||
@@ -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,10 @@ var SMTPToken = ""
|
|||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
|
var LinuxDoClientId = ""
|
||||||
|
var LinuxDoClientSecret = ""
|
||||||
|
var LinuxDoMinLevel = 0
|
||||||
|
|
||||||
var WeChatServerAddress = ""
|
var WeChatServerAddress = ""
|
||||||
var WeChatServerToken = ""
|
var WeChatServerToken = ""
|
||||||
var WeChatAccountQRCodeImageURL = ""
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
239
controller/linuxdo.go
Normal file
239
controller/linuxdo.go
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
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("返回值非法,用户字段为空,请稍后重试!")
|
||||||
|
}
|
||||||
|
if linuxdoUser.TrustLevel < common.LinuxDoMinLevel {
|
||||||
|
return nil, errors.New("用户 LINUX DO 信任等级不足!")
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
LinuxDoLevel: linuxdoUser.TrustLevel,
|
||||||
|
}
|
||||||
|
if model.IsLinuxDoIdAlreadyTaken(user.LinuxDoId) {
|
||||||
|
err := user.FillUserByLinuxDoId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.LinuxDoLevel = linuxdoUser.TrustLevel
|
||||||
|
err = user.Update(false)
|
||||||
|
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),
|
||||||
|
LinuxDoLevel: linuxdoUser.TrustLevel,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
user.LinuxDoLevel = linuxdoUser.TrustLevel
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -37,6 +37,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,
|
||||||
@@ -61,6 +63,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||||
"enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "",
|
"enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "",
|
||||||
"mj_notify_enabled": constant.MjNotifyEnabled,
|
"mj_notify_enabled": constant.MjNotifyEnabled,
|
||||||
|
"version": common.Version,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func setupLogin(user *model.User, c *gin.Context) {
|
|||||||
session.Set("username", user.Username)
|
session.Set("username", user.Username)
|
||||||
session.Set("role", user.Role)
|
session.Set("role", user.Role)
|
||||||
session.Set("status", user.Status)
|
session.Set("status", user.Status)
|
||||||
|
session.Set("linuxdo_enable", user.LinuxDoId == "" || user.LinuxDoLevel >= common.LinuxDoMinLevel)
|
||||||
err := session.Save()
|
err := session.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -2,18 +2,17 @@ version: '3.4'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
new-api:
|
new-api:
|
||||||
image: calciumion/new-api:latest
|
image: pengzhile/new-api:latest
|
||||||
# build: .
|
|
||||||
container_name: new-api
|
container_name: new-api
|
||||||
restart: always
|
restart: always
|
||||||
command: --log-dir /app/logs
|
command: --log-dir /app/logs
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data/new-api:/data
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- SQL_DSN=root:123456@tcp(host.docker.internal:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库
|
- SQL_DSN=newapi:123456@tcp(db:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库
|
||||||
- REDIS_CONN_STRING=redis://redis
|
- REDIS_CONN_STRING=redis://redis
|
||||||
- SESSION_SECRET=random_string # 修改为随机字符串
|
- SESSION_SECRET=random_string # 修改为随机字符串
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
@@ -23,13 +22,22 @@ services:
|
|||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
healthcheck:
|
- db
|
||||||
test: [ "CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $2}'" ]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
container_name: redis
|
container_name: redis
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:8.2.0
|
||||||
|
container_name: mysql
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./data/mysql:/var/lib/mysql # 挂载目录,持久化存储
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai # 设置时区
|
||||||
|
MYSQL_ROOT_PASSWORD: 'OneAPI@justsong' # 设置 root 用户的密码
|
||||||
|
MYSQL_USER: newapi # 创建专用用户
|
||||||
|
MYSQL_PASSWORD: '123456' # 设置专用用户密码
|
||||||
|
MYSQL_DATABASE: new-api # 自动创建数据库
|
||||||
@@ -15,6 +15,7 @@ func authHelper(c *gin.Context, minRole int) {
|
|||||||
role := session.Get("role")
|
role := session.Get("role")
|
||||||
id := session.Get("id")
|
id := session.Get("id")
|
||||||
status := session.Get("status")
|
status := session.Get("status")
|
||||||
|
linuxDoEnable := session.Get("linuxdo_enable")
|
||||||
if username == nil {
|
if username == nil {
|
||||||
// Check access token
|
// Check access token
|
||||||
accessToken := c.Request.Header.Get("Authorization")
|
accessToken := c.Request.Header.Get("Authorization")
|
||||||
@@ -33,6 +34,7 @@ func authHelper(c *gin.Context, minRole int) {
|
|||||||
role = user.Role
|
role = user.Role
|
||||||
id = user.Id
|
id = user.Id
|
||||||
status = user.Status
|
status = user.Status
|
||||||
|
linuxDoEnable = user.LinuxDoId == "" || user.LinuxDoLevel >= common.LinuxDoMinLevel
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -50,6 +52,14 @@ func authHelper(c *gin.Context, minRole int) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if nil != linuxDoEnable && !linuxDoEnable.(bool) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "用户 LINUX DO 信任等级不足",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
if role.(int) < minRole {
|
if role.(int) < minRole {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -112,6 +122,15 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
abortWithOpenAiMessage(c, http.StatusForbidden, "用户已被封禁")
|
abortWithOpenAiMessage(c, http.StatusForbidden, "用户已被封禁")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
linuxDoEnabled, err := model.CacheIsLinuxDoEnabled(token.UserId)
|
||||||
|
if err != nil {
|
||||||
|
abortWithOpenAiMessage(c, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !linuxDoEnabled {
|
||||||
|
abortWithOpenAiMessage(c, http.StatusForbidden, "用户 LINUX DO 信任等级不足")
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Set("id", token.UserId)
|
c.Set("id", token.UserId)
|
||||||
c.Set("token_id", token.Id)
|
c.Set("token_id", token.Id)
|
||||||
c.Set("token_name", token.Name)
|
c.Set("token_name", token.Name)
|
||||||
|
|||||||
@@ -204,6 +204,30 @@ func CacheIsUserEnabled(userId int) (bool, error) {
|
|||||||
return userEnabled, err
|
return userEnabled, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CacheIsLinuxDoEnabled(userId int) (bool, error) {
|
||||||
|
if !common.RedisEnabled {
|
||||||
|
return IsLinuxDoEnabled(userId)
|
||||||
|
}
|
||||||
|
enabled, err := common.RedisGet(fmt.Sprintf("linuxdo_enabled:%d", userId))
|
||||||
|
if err == nil {
|
||||||
|
return enabled == "1", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxDoEnabled, err := IsLinuxDoEnabled(userId)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
enabled = "0"
|
||||||
|
if linuxDoEnabled {
|
||||||
|
enabled = "1"
|
||||||
|
}
|
||||||
|
err = common.RedisSet(fmt.Sprintf("linuxdo_enabled:%d", userId), enabled, time.Duration(UserId2StatusCacheSeconds)*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("Redis set linuxdo enabled error: " + err.Error())
|
||||||
|
}
|
||||||
|
return linuxDoEnabled, err
|
||||||
|
}
|
||||||
|
|
||||||
var group2model2channels map[string]map[string][]*Channel
|
var group2model2channels map[string]map[string][]*Channel
|
||||||
var channelsIDM map[int]*Channel
|
var channelsIDM map[int]*Channel
|
||||||
var channelSyncLock sync.RWMutex
|
var channelSyncLock sync.RWMutex
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -66,6 +67,9 @@ 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["LinuxDoMinLevel"] = strconv.Itoa(common.LinuxDoMinLevel)
|
||||||
common.OptionMap["TelegramBotToken"] = ""
|
common.OptionMap["TelegramBotToken"] = ""
|
||||||
common.OptionMap["TelegramBotName"] = ""
|
common.OptionMap["TelegramBotName"] = ""
|
||||||
common.OptionMap["WeChatServerAddress"] = ""
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
@@ -157,6 +161,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":
|
||||||
@@ -221,6 +227,12 @@ 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 "LinuxDoMinLevel":
|
||||||
|
common.LinuxDoMinLevel, _ = strconv.Atoi(value)
|
||||||
case "Footer":
|
case "Footer":
|
||||||
common.Footer = value
|
common.Footer = value
|
||||||
case "SystemName":
|
case "SystemName":
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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"`
|
||||||
|
LinuxDoLevel int `json:"linuxdo_level" gorm:"column:linuxdo_level;type:int;default:0"`
|
||||||
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 +274,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 +321,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
|
||||||
}
|
}
|
||||||
@@ -356,6 +370,18 @@ func IsUserEnabled(userId int) (bool, error) {
|
|||||||
return user.Status == common.UserStatusEnabled, nil
|
return user.Status == common.UserStatusEnabled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsLinuxDoEnabled(userId int) (bool, error) {
|
||||||
|
if userId == 0 {
|
||||||
|
return false, errors.New("user id is empty")
|
||||||
|
}
|
||||||
|
var user User
|
||||||
|
err := DB.Where("id = ?", userId).Select("linuxdo_id, linuxdo_level").Find(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return user.LinuxDoId == "" || user.LinuxDoLevel >= common.LinuxDoMinLevel, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ValidateAccessToken(token string) (user *User) {
|
func ValidateAccessToken(token string) (user *User) {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
4
web/.gitignore
vendored
4
web/.gitignore
vendored
@@ -21,6 +21,4 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.idea
|
.idea/
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
@@ -50,7 +50,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"typescript": "4.4.2"
|
"typescript": "4.4.2",
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import EditUser from './pages/User/EditUser';
|
|||||||
import { getLogo, getSystemName } from './helpers';
|
import { getLogo, getSystemName } 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 Channel from './pages/Channel';
|
import Channel from './pages/Channel';
|
||||||
@@ -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={
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
21
web/src/components/LinuxDoIcon.js
Normal file
21
web/src/components/LinuxDoIcon.js
Normal 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;
|
||||||
67
web/src/components/LinuxDoOAuth.js
Normal file
67
web/src/components/LinuxDoOAuth.js
Normal 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;
|
||||||
@@ -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, showError, showInfo, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||||
import { onGitHubOAuthClicked } from './utils';
|
import { onGitHubOAuthClicked, onLinuxDoOAuthClicked } from './utils';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi-ui';
|
import { Button, Card, Divider, Form, Icon, Layout, Modal } 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"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { API, copy, isRoot, showError, showInfo, showSuccess } from '../helpers';
|
import { API, copy, isRoot, showError, showInfo, 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,
|
Avatar,
|
||||||
Banner,
|
Banner,
|
||||||
@@ -390,13 +390,13 @@ const PersonalSetting = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<Typography.Title heading={6}>个人信息</Typography.Title>
|
<Typography.Title heading={6}>个人信息</Typography.Title>
|
||||||
<div style={{ marginTop: 20 }}>
|
<div style={{marginTop: 20}}>
|
||||||
<Typography.Text strong>邮箱</Typography.Text>
|
<Typography.Text strong>邮箱</Typography.Text>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={userState.user && userState.user.email !== '' ? userState.user.email : '未绑定'}
|
value={userState.user && userState.user.email !== '' ? userState.user.email : '未绑定'}
|
||||||
readonly={true}
|
readonly={true}
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -408,13 +408,13 @@ const PersonalSetting = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{marginTop: 10}}>
|
||||||
<Typography.Text strong>微信</Typography.Text>
|
<Typography.Text strong>微信</Typography.Text>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={userState.user && userState.user.wechat_id !== '' ? '已绑定' : '未绑定'}
|
value={userState.user && userState.user.wechat_id !== '' ? '已绑定' : '未绑定'}
|
||||||
readonly={true}
|
readonly={true}
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -426,21 +426,21 @@ const PersonalSetting = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{marginTop: 10}}>
|
||||||
<Typography.Text strong>GitHub</Typography.Text>
|
<Typography.Text strong>GitHub</Typography.Text>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={userState.user && userState.user.github_id !== '' ? userState.user.github_id : '未绑定'}
|
value={userState.user && userState.user.github_id !== '' ? userState.user.github_id : '未绑定'}
|
||||||
readonly={true}
|
readonly={true}
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onGitHubOAuthClicked(status.github_client_id);
|
onGitHubOAuthClicked(status.github_client_id);
|
||||||
}}
|
}}
|
||||||
disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
|
disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
status.github_oauth ? '绑定' : '未启用'
|
status.github_oauth ? '绑定' : '未启用'
|
||||||
@@ -449,28 +449,51 @@ const PersonalSetting = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{marginTop: 10}}>
|
||||||
<div style={{ marginTop: 10 }}>
|
<Typography.Text strong>LINUX DO</Typography.Text>
|
||||||
<Typography.Text strong>Telegram</Typography.Text>
|
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={userState.user && userState.user.telegram_id !== '' ? userState.user.telegram_id : '未绑定'}
|
value={userState.user && userState.user.linuxdo_id !== '' ? userState.user.linuxdo_id + '(' + userState.user.linuxdo_level + '级)' : '未绑定'}
|
||||||
readonly={true}
|
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}}>
|
||||||
|
<Typography.Text strong>Telegram</Typography.Text>
|
||||||
|
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
value={userState.user && userState.user.telegram_id !== '' ? userState.user.telegram_id : '未绑定'}
|
||||||
|
readonly={true}
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{status.telegram_oauth ?
|
{status.telegram_oauth ?
|
||||||
userState.user.telegram_id !== '' ? <Button disabled={true}>已绑定</Button>
|
userState.user.telegram_id !== '' ? <Button disabled={true}>已绑定</Button>
|
||||||
: <TelegramLoginButton dataAuthUrl="/api/oauth/telegram/bind"
|
: <TelegramLoginButton dataAuthUrl="/api/oauth/telegram/bind"
|
||||||
botName={status.telegram_bot_name} />
|
botName={status.telegram_bot_name}/>
|
||||||
: <Button disabled={true}>未启用</Button>
|
: <Button disabled={true}>未启用</Button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{marginTop: 10}}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
@@ -482,41 +505,41 @@ const PersonalSetting = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{systemToken && (
|
{systemToken && (
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
readOnly
|
||||||
value={systemToken}
|
value={systemToken}
|
||||||
onClick={handleSystemTokenClick}
|
onClick={handleSystemTokenClick}
|
||||||
style={{ marginTop: '10px' }}
|
style={{marginTop: '10px'}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
status.wechat_login && (
|
status.wechat_login && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowWeChatBindModal(true);
|
setShowWeChatBindModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
绑定微信账号
|
绑定微信账号
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Modal
|
<Modal
|
||||||
onCancel={() => setShowWeChatBindModal(false)}
|
onCancel={() => setShowWeChatBindModal(false)}
|
||||||
// onOpen={() => setShowWeChatBindModal(true)}
|
// onOpen={() => setShowWeChatBindModal(true)}
|
||||||
visible={showWeChatBindModal}
|
visible={showWeChatBindModal}
|
||||||
size={'mini'}
|
size={'mini'}
|
||||||
>
|
>
|
||||||
<Image src={status.wechat_qrcode} />
|
<Image src={status.wechat_qrcode}/>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{textAlign: 'center'}}>
|
||||||
<p>
|
<p>
|
||||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder="验证码"
|
placeholder="验证码"
|
||||||
name="wechat_verification_code"
|
name="wechat_verification_code"
|
||||||
value={inputs.wechat_verification_code}
|
value={inputs.wechat_verification_code}
|
||||||
onChange={(v) => handleInputChange('wechat_verification_code', v)}
|
onChange={(v) => handleInputChange('wechat_verification_code', v)}
|
||||||
/>
|
/>
|
||||||
<Button color="" fluid size="large" onClick={bindWeChat}>
|
<Button color="" fluid size="large" onClick={bindWeChat}>
|
||||||
绑定
|
绑定
|
||||||
@@ -525,35 +548,35 @@ const PersonalSetting = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Modal
|
<Modal
|
||||||
onCancel={() => setShowEmailBindModal(false)}
|
onCancel={() => setShowEmailBindModal(false)}
|
||||||
// onOpen={() => setShowEmailBindModal(true)}
|
// onOpen={() => setShowEmailBindModal(true)}
|
||||||
onOk={bindEmail}
|
onOk={bindEmail}
|
||||||
visible={showEmailBindModal}
|
visible={showEmailBindModal}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
centered={true}
|
centered={true}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
>
|
>
|
||||||
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
|
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
|
||||||
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{marginTop: 20, display: 'flex', justifyContent: 'space-between'}}>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
placeholder="输入邮箱地址"
|
placeholder="输入邮箱地址"
|
||||||
onChange={(value) => handleInputChange('email', value)}
|
onChange={(value) => handleInputChange('email', value)}
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
/>
|
/>
|
||||||
<Button onClick={sendVerificationCode}
|
<Button onClick={sendVerificationCode}
|
||||||
disabled={disableButton || loading}>
|
disabled={disableButton || loading}>
|
||||||
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{marginTop: 10}}>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
fluid
|
||||||
placeholder="验证码"
|
placeholder="验证码"
|
||||||
name="email_verification_code"
|
name="email_verification_code"
|
||||||
value={inputs.email_verification_code}
|
value={inputs.email_verification_code}
|
||||||
onChange={(value) => handleInputChange('email_verification_code', value)}
|
onChange={(value) => handleInputChange('email_verification_code', value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{turnstileEnabled ? (
|
{turnstileEnabled ? (
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const SystemSetting = () => {
|
|||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
LinuxDoOAuthEnabled: '',
|
||||||
|
LinuxDoClientId: '',
|
||||||
|
LinuxDoClientSecret: '',
|
||||||
|
LinuxDoMinLevel: 0,
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@@ -82,6 +86,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 +134,9 @@ const SystemSetting = () => {
|
|||||||
name === 'PayAddress' ||
|
name === 'PayAddress' ||
|
||||||
name === 'GitHubClientId' ||
|
name === 'GitHubClientId' ||
|
||||||
name === 'GitHubClientSecret' ||
|
name === 'GitHubClientSecret' ||
|
||||||
|
name === 'LinuxDoClientId' ||
|
||||||
|
name === 'LinuxDoClientSecret' ||
|
||||||
|
name === 'LinuxDoMinLevel' ||
|
||||||
name === 'WeChatServerAddress' ||
|
name === 'WeChatServerAddress' ||
|
||||||
name === 'WeChatServerToken' ||
|
name === 'WeChatServerToken' ||
|
||||||
name === 'WeChatAccountQRCodeImageURL' ||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
@@ -243,6 +251,21 @@ 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);
|
||||||
|
}
|
||||||
|
if (originInputs['LinuxDoMinLevel'] !== inputs.LinuxDoMinLevel) {
|
||||||
|
await updateOption('LinuxDoMinLevel', inputs.LinuxDoMinLevel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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 +435,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 +606,54 @@ 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' rel="noreferrer">
|
||||||
|
点击此处
|
||||||
|
</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.Input
|
||||||
|
label='限制最低信任等级'
|
||||||
|
name='LinuxDoMinLevel'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
max={4}
|
||||||
|
value={inputs.LinuxDoMinLevel}
|
||||||
|
placeholder='输入允许使用的最低 LINUX DO 信任等级'
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -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`;
|
||||||
}
|
}
|
||||||
@@ -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 ? '已启用' : '未启用'}
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ const EditUser = (props) => {
|
|||||||
display_name: '',
|
display_name: '',
|
||||||
password: '',
|
password: '',
|
||||||
github_id: '',
|
github_id: '',
|
||||||
|
linuxdo_id: '',
|
||||||
|
linuxdo_level: 0,
|
||||||
wechat_id: '',
|
wechat_id: '',
|
||||||
|
telegram_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, linuxdo_level, 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 +187,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 + '(' + linuxdo_level + '级)'}
|
||||||
|
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 +207,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}
|
||||||
|
|||||||
10674
web/yarn.lock
Normal file
10674
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user