diff --git a/internal/handler/auth.go b/internal/handler/auth.go new file mode 100644 index 0000000..6e695dc --- /dev/null +++ b/internal/handler/auth.go @@ -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"}) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..10123a3 --- /dev/null +++ b/internal/middleware/auth.go @@ -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() + } +}