mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
232 lines
6.7 KiB
Go
232 lines
6.7 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"clicd/internal/config"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type LoginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type LoginResponse struct {
|
|
Token string `json:"token"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message,omitempty"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
func jsonResponse(w http.ResponseWriter, status int, resp APIResponse) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func tokenFromRequest(r *http.Request) string {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if strings.HasPrefix(authHeader, "Bearer ") {
|
|
return strings.TrimPrefix(authHeader, "Bearer ")
|
|
}
|
|
|
|
cookie, err := r.Cookie("clicd_token")
|
|
if err == nil {
|
|
return cookie.Value
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func isValidToken(tokenString string) bool {
|
|
_, ok := claimsFromToken(tokenString)
|
|
return ok
|
|
}
|
|
|
|
func claimsFromToken(tokenString string) (jwt.MapClaims, bool) {
|
|
if tokenString == "" {
|
|
return nil, false
|
|
}
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, jwt.ErrSignatureInvalid
|
|
}
|
|
return []byte(config.AppConfig.JWTSecret), nil
|
|
})
|
|
if err != nil || !token.Valid {
|
|
return nil, false
|
|
}
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
// For sub-user tokens, check token_version against stored version (password rotation invalidation)
|
|
if subUser, _ := claims["sub_user"].(string); subUser != "" {
|
|
tokenVersionFloat, hasVersion := claims["token_version"].(float64)
|
|
tokenVersion := int(tokenVersionFloat)
|
|
for i := range config.AppConfig.SubUsers {
|
|
if config.AppConfig.SubUsers[i].Username == subUser {
|
|
stored := config.AppConfig.SubUsers[i].TokenVersion
|
|
// If stored version > 0, require token_version to match exactly.
|
|
// This also rejects legacy tokens that lack token_version entirely.
|
|
if stored > 0 && (!hasVersion || tokenVersion != stored) {
|
|
return nil, false
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return claims, ok
|
|
}
|
|
|
|
func claimsFromRequest(r *http.Request) (jwt.MapClaims, bool) {
|
|
return claimsFromToken(tokenFromRequest(r))
|
|
}
|
|
|
|
func isSubUserRequest(r *http.Request) bool {
|
|
claims, ok := claimsFromRequest(r)
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, ok = claims["sub_user"]
|
|
return ok
|
|
}
|
|
|
|
func isAuthenticatedRequest(r *http.Request) bool {
|
|
return isValidToken(tokenFromRequest(r))
|
|
}
|
|
|
|
// HandleLogin processes login requests
|
|
func HandleLogin(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
|
return
|
|
}
|
|
|
|
var req LoginRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
ip := clientIP(r)
|
|
ua := r.Header.Get("User-Agent")
|
|
|
|
if req.Username != config.AppConfig.AdminUser {
|
|
RecordLoginLog(req.Username, ip, ua, false)
|
|
jsonResponse(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Invalid credentials"})
|
|
return
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(config.AppConfig.AdminPassHash), []byte(req.Password)); err != nil {
|
|
RecordLoginLog(req.Username, ip, ua, false)
|
|
jsonResponse(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Invalid credentials"})
|
|
return
|
|
}
|
|
|
|
RecordLoginLog(req.Username, ip, ua, true)
|
|
|
|
// Generate JWT token
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"username": req.Username,
|
|
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
})
|
|
|
|
tokenString, err := token.SignedString([]byte(config.AppConfig.JWTSecret))
|
|
if err != nil {
|
|
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to generate token"})
|
|
return
|
|
}
|
|
|
|
jsonResponse(w, http.StatusOK, APIResponse{
|
|
Success: true,
|
|
Data: LoginResponse{
|
|
Token: tokenString,
|
|
Username: req.Username,
|
|
},
|
|
})
|
|
}
|
|
|
|
// HandleChangePassword processes password change requests
|
|
func HandleChangePassword(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
OldPassword string `json:"old_password"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
if len(req.NewPassword) < 8 {
|
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "New password must be at least 8 characters"})
|
|
return
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(config.AppConfig.AdminPassHash), []byte(req.OldPassword)); err != nil {
|
|
jsonResponse(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Current password is incorrect"})
|
|
return
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to hash password"})
|
|
return
|
|
}
|
|
|
|
config.AppConfig.AdminPassHash = string(hash)
|
|
if err := config.SaveConfig(); err != nil {
|
|
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to save configuration"})
|
|
return
|
|
}
|
|
|
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Password changed successfully"})
|
|
}
|
|
|
|
// HandleCheckAuth checks if the user is authenticated
|
|
func HandleCheckAuth(w http.ResponseWriter, r *http.Request) {
|
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Authenticated"})
|
|
}
|
|
|
|
// AuthMiddleware extracts JWT from cookies or Authorization header
|
|
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
tokenString := tokenFromRequest(r)
|
|
if !isValidToken(tokenString) && !isValidApiKeyRequest(r) {
|
|
jsonResponse(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Authentication required"})
|
|
return
|
|
}
|
|
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
// AdminMiddleware requires a valid administrator token and rejects sub-user tokens.
|
|
func AdminMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
return AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
|
if isSubUserRequest(r) {
|
|
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Administrator permission required"})
|
|
return
|
|
}
|
|
next(w, r)
|
|
})
|
|
}
|