
Features: - Complete project management system with Epic/Story/Task hierarchy - Vue.js 3 + Element Plus frontend with kanban board - Go backend with Gin framework and GORM - OAuth2 integration with Gitea - Docker containerization with MySQL - RESTful API for project, task, and user management - JWT authentication and authorization - Responsive web interface with dashboard
159 lines
4.3 KiB
Go
159 lines
4.3 KiB
Go
package handlers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"time"
|
||
|
||
"giteapm/internal/middleware"
|
||
"giteapm/internal/models"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"golang.org/x/oauth2"
|
||
)
|
||
|
||
type GiteaUser struct {
|
||
ID int `json:"id"`
|
||
Login string `json:"login"`
|
||
FullName string `json:"full_name"`
|
||
Email string `json:"email"`
|
||
AvatarURL string `json:"avatar_url"`
|
||
}
|
||
|
||
func (h *Handlers) GiteaLogin(c *gin.Context) {
|
||
config := &oauth2.Config{
|
||
ClientID: h.Cfg.Gitea.ClientID,
|
||
ClientSecret: h.Cfg.Gitea.ClientSecret,
|
||
RedirectURL: h.Cfg.Gitea.RedirectURL,
|
||
Scopes: []string{"read:user"},
|
||
Endpoint: oauth2.Endpoint{
|
||
AuthURL: h.Cfg.Gitea.BaseURL + "/login/oauth/authorize",
|
||
TokenURL: h.Cfg.Gitea.BaseURL + "/login/oauth/access_token",
|
||
},
|
||
}
|
||
|
||
state := "random-state-string"
|
||
url := config.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||
|
||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||
"auth_url": url,
|
||
}))
|
||
}
|
||
|
||
func (h *Handlers) GiteaCallback(c *gin.Context) {
|
||
// 记录所有查询参数用于调试
|
||
fmt.Printf("OAuth回调收到的所有参数: %v\n", c.Request.URL.Query())
|
||
|
||
code := c.Query("code")
|
||
state := c.Query("state")
|
||
errorParam := c.Query("error")
|
||
|
||
fmt.Printf("OAuth回调 - code: %s, state: %s, error: %s\n", code, state, errorParam)
|
||
|
||
if errorParam != "" {
|
||
c.JSON(http.StatusBadRequest, ErrorResponse(400, fmt.Sprintf("OAuth授权失败: %s", errorParam)))
|
||
return
|
||
}
|
||
|
||
if code == "" {
|
||
c.JSON(http.StatusBadRequest, ErrorResponse(400, "缺少授权码"))
|
||
return
|
||
}
|
||
|
||
config := &oauth2.Config{
|
||
ClientID: h.Cfg.Gitea.ClientID,
|
||
ClientSecret: h.Cfg.Gitea.ClientSecret,
|
||
RedirectURL: h.Cfg.Gitea.RedirectURL,
|
||
Scopes: []string{"read:user"},
|
||
Endpoint: oauth2.Endpoint{
|
||
AuthURL: h.Cfg.Gitea.BaseURL + "/login/oauth/authorize",
|
||
TokenURL: h.Cfg.Gitea.BaseURL + "/login/oauth/access_token",
|
||
},
|
||
}
|
||
|
||
token, err := config.Exchange(c.Request.Context(), code)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, ErrorResponse(400, "授权码交换失败"))
|
||
return
|
||
}
|
||
|
||
client := config.Client(c.Request.Context(), token)
|
||
resp, err := client.Get(h.Cfg.Gitea.BaseURL + "/api/v1/user")
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, ErrorResponse(500, "获取用户信息失败"))
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, ErrorResponse(500, "读取用户信息失败"))
|
||
return
|
||
}
|
||
|
||
var giteaUser GiteaUser
|
||
if err := json.Unmarshal(body, &giteaUser); err != nil {
|
||
c.JSON(http.StatusInternalServerError, ErrorResponse(500, "解析用户信息失败"))
|
||
return
|
||
}
|
||
|
||
user, err := models.GetUserByGiteaID(uint(giteaUser.ID))
|
||
if err != nil {
|
||
user = &models.User{
|
||
GiteaUserID: func() *uint { id := uint(giteaUser.ID); return &id }(),
|
||
Username: giteaUser.Login,
|
||
Email: giteaUser.Email,
|
||
FullName: giteaUser.FullName,
|
||
AvatarURL: giteaUser.AvatarURL,
|
||
Role: "developer",
|
||
}
|
||
if err := models.CreateUser(user); err != nil {
|
||
c.JSON(http.StatusInternalServerError, ErrorResponse(500, "创建用户失败"))
|
||
return
|
||
}
|
||
} else {
|
||
user.Username = giteaUser.Login
|
||
user.Email = giteaUser.Email
|
||
user.FullName = giteaUser.FullName
|
||
user.AvatarURL = giteaUser.AvatarURL
|
||
if err := models.UpdateUser(user); err != nil {
|
||
c.JSON(http.StatusInternalServerError, ErrorResponse(500, "更新用户失败"))
|
||
return
|
||
}
|
||
}
|
||
|
||
jwtToken, err := h.generateJWT(user)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, ErrorResponse(500, "生成JWT失败"))
|
||
return
|
||
}
|
||
|
||
// 重定向到前端页面,并通过URL参数传递token
|
||
redirectURL := fmt.Sprintf("/?token=%s", jwtToken)
|
||
c.Redirect(http.StatusFound, redirectURL)
|
||
}
|
||
|
||
func (h *Handlers) Logout(c *gin.Context) {
|
||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||
"message": "登出成功",
|
||
}))
|
||
}
|
||
|
||
func (h *Handlers) generateJWT(user *models.User) (string, error) {
|
||
claims := &middleware.Claims{
|
||
UserID: user.ID,
|
||
Username: user.Username,
|
||
Role: user.Role,
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(h.Cfg.JWT.ExpireHour) * time.Hour)),
|
||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||
Subject: fmt.Sprintf("%d", user.ID),
|
||
},
|
||
}
|
||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
return token.SignedString([]byte(h.Cfg.JWT.Secret))
|
||
} |