feat: implement JWT authentication middleware and handlers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
109
internal/handler/auth.go
Normal file
109
internal/handler/auth.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gitm/internal/database"
|
||||||
|
"gitm/internal/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleLogin(c *gin.Context) {
|
||||||
|
var req LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Password required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashedPassword, err := database.GetSetting("admin_password")
|
||||||
|
if err != nil || hashedPassword == "" {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Server not initialized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(req.Password)); err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token, err := middleware.GenerateToken("admin")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, LoginResponse{Token: token})
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleInit(c *gin.Context) {
|
||||||
|
var req LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Password required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashedPassword, _ := database.GetSetting("admin_password")
|
||||||
|
if hashedPassword != "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Already initialized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
database.SetSetting("admin_password", string(hash))
|
||||||
|
database.SetSetting("listen_addr", ":9000")
|
||||||
|
database.SetSetting("max_concurrent", "3")
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Initialized successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGetSettings(c *gin.Context) {
|
||||||
|
listenAddr, _ := database.GetSetting("listen_addr")
|
||||||
|
if listenAddr == "" {
|
||||||
|
listenAddr = ":9000"
|
||||||
|
}
|
||||||
|
reposDir, _ := database.GetSetting("repos_dir")
|
||||||
|
maxConcurrent, _ := database.GetSetting("max_concurrent")
|
||||||
|
maxCon := 3
|
||||||
|
if maxConcurrent != "" {
|
||||||
|
fmt.Sscanf(maxConcurrent, "%d", &maxCon)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"admin_password": "********",
|
||||||
|
"listen_addr": listenAddr,
|
||||||
|
"repos_dir": reposDir,
|
||||||
|
"max_concurrent": maxCon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleUpdateSettings(c *gin.Context) {
|
||||||
|
var req map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pwd, ok := req["admin_password"].(string); ok && pwd != "" && pwd != "********" {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
database.SetSetting("admin_password", string(hash))
|
||||||
|
}
|
||||||
|
if addr, ok := req["listen_addr"].(string); ok && addr != "" {
|
||||||
|
database.SetSetting("listen_addr", addr)
|
||||||
|
}
|
||||||
|
if dir, ok := req["repos_dir"].(string); ok && dir != "" {
|
||||||
|
database.SetSetting("repos_dir", dir)
|
||||||
|
}
|
||||||
|
if max, ok := req["max_concurrent"].(float64); ok {
|
||||||
|
database.SetSetting("max_concurrent", fmt.Sprintf("%d", int(max)))
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Settings updated"})
|
||||||
|
}
|
||||||
71
internal/middleware/auth.go
Normal file
71
internal/middleware/auth.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
var jwtSecret = []byte("gitm-default-secret")
|
||||||
|
|
||||||
|
func SetJWTSecret(secret string) {
|
||||||
|
jwtSecret = []byte(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateToken(userID string) (string, error) {
|
||||||
|
claims := Claims{
|
||||||
|
UserID: userID,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(jwtSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateToken(tokenString string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return jwtSecret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
return nil, jwt.ErrSignatureInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claims, err := ValidateToken(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("user_id", claims.UserID)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user