Files
OpenFlare/internal/apps/oauth/routers.go
T
2026-06-08 20:34:27 +08:00

197 lines
5.9 KiB
Go

/*
Copyright 2025 linux.do
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oauth
import (
"fmt"
"net/http"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/linux-do/credit/internal/db"
"github.com/linux-do/credit/internal/model"
"github.com/linux-do/credit/internal/service"
"github.com/linux-do/credit/internal/util"
"github.com/shopspring/decimal"
)
// GetLoginURL godoc
// @Tags oauth
// @Produce json
// @Success 200 {object} util.ResponseAny
// @Router /api/v1/oauth/login [get]
func GetLoginURL(c *gin.Context) {
ctx := c.Request.Context()
// 生成 state
state := uuid.NewString()
cmd := db.Redis.Set(ctx, db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), state, OAuthStateCacheKeyExpiration)
if cmd.Err() != nil {
c.JSON(http.StatusInternalServerError, util.Err(cmd.Err().Error()))
return
}
// 构造登录 URL
var authURL string
if oidcVerifier != nil {
// OIDC 模式:state 同时用作 nonce
authURL = oauthConf.AuthCodeURL(state, oidc.Nonce(state))
} else {
// 纯 OAuth2 模式
authURL = oauthConf.AuthCodeURL(state)
}
c.JSON(http.StatusOK, util.OK(authURL))
}
type CallbackRequest struct {
State string `json:"state"`
Code string `json:"code"`
}
// Callback godoc
// @Tags oauth
// @Param request body CallbackRequest true "request body"
// @Produce json
// @Success 200 {object} util.ResponseAny
// @Router /api/v1/oauth/callback [post]
func Callback(c *gin.Context) {
// 解析请求
var req CallbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
return
}
ctx := c.Request.Context()
// 验证 state
cmd := db.Redis.Get(ctx, db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, req.State)))
if cmd.Val() != req.State {
c.JSON(http.StatusBadRequest, util.Err(InvalidState))
return
}
db.Redis.Del(ctx, db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, req.State)))
// 执行 OAuth/OIDC 认证
user, err := doOAuth(ctx, req.Code, req.State)
if err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
session := sessions.Default(c)
session.Set(UserIDKey, user.ID)
session.Set(UserNameKey, user.Username)
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
LogForAudit(ctx, user, c)
c.JSON(http.StatusOK, util.OKNil())
}
type BasicUserInfo struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
TrustLevel model.TrustLevel `json:"trust_level"`
AvatarUrl string `json:"avatar_url"`
TotalReceive decimal.Decimal `json:"total_receive"`
TotalPayment decimal.Decimal `json:"total_payment"`
TotalTransfer decimal.Decimal `json:"total_transfer"`
TotalCommunity decimal.Decimal `json:"total_community"`
CommunityBalance decimal.Decimal `json:"community_balance"`
AvailableBalance decimal.Decimal `json:"available_balance"`
PendingBalance decimal.Decimal `json:"pending_balance"`
PayScore int64 `json:"pay_score"`
IsPayKey bool `json:"is_pay_key"`
IsAdmin bool `json:"is_admin"`
RemainQuota decimal.Decimal `json:"remain_quota"`
PayLevel model.PayLevel `json:"pay_level"`
DailyLimit *int64 `json:"daily_limit"`
}
// UserInfo godoc
// @Tags oauth
// @Produce json
// @Success 200 {object} util.ResponseAny
// @Router /api/v1/oauth/user-info [get]
func UserInfo(c *gin.Context) {
user, _ := util.GetFromContext[*model.User](c, UserObjKey)
var payConfig model.UserPayConfig
if err := payConfig.GetByPayScore(db.DB(c.Request.Context()), user.PayScore); err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
// 计算剩余额度(-1 表示无限额)
remainQuota := decimal.NewFromInt(-1)
if payConfig.DailyLimit != nil && *payConfig.DailyLimit > 0 {
todayUsed, err := service.GetTodayUsedAmount(db.DB(c.Request.Context()), user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
remainQuota = decimal.NewFromInt(*payConfig.DailyLimit).Sub(todayUsed)
}
c.JSON(
http.StatusOK,
util.OK(BasicUserInfo{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
TrustLevel: user.TrustLevel,
AvatarUrl: user.AvatarUrl,
TotalReceive: user.TotalReceive,
TotalPayment: user.TotalPayment,
TotalTransfer: user.TotalTransfer,
TotalCommunity: user.TotalCommunity,
CommunityBalance: user.CommunityBalance,
AvailableBalance: user.AvailableBalance,
PendingBalance: user.PendingBalance,
PayScore: user.PayScore,
IsPayKey: user.PayKey != "",
IsAdmin: user.IsAdmin,
RemainQuota: remainQuota,
PayLevel: payConfig.Level,
DailyLimit: payConfig.DailyLimit,
}),
)
}
// Logout godoc
// @Tags oauth
// @Produce json
// @Success 200 {object} util.ResponseAny
// @Router /api/v1/oauth/logout [get]
func Logout(c *gin.Context) {
session := sessions.Default(c)
session.Options(util.GetSessionOptions(-1))
session.Clear()
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
c.JSON(http.StatusOK, util.OKNil())
}