Initial commit: Gitea Project Management System
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
This commit is contained in:
159
internal/api/handlers/auth.go
Normal file
159
internal/api/handlers/auth.go
Normal file
@@ -0,0 +1,159 @@
|
||||
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))
|
||||
}
|
Reference in New Issue
Block a user