- Installed and configured SCST with 7 handlers - Installed and configured mhVTL with 2 Quantum libraries and 8 LTO-8 drives - Implemented all VTL API endpoints (8/9 working) - Fixed NULL device_path handling in drives endpoint - Added comprehensive error handling and validation - Implemented async tape load/unload operations - Created SCST installation guide for Ubuntu 24.04 - Created mhVTL installation and configuration guide - Added VTL testing guide and automated test scripts - All core API tests passing (89% success rate) Infrastructure status: - PostgreSQL: Configured with proper permissions - SCST: Active with kernel module loaded - mhVTL: 2 libraries (Quantum Scalar i500, Scalar i40) - mhVTL: 8 drives (all Quantum ULTRIUM-HH8 LTO-8) - Calypso API: 8/9 VTL endpoints functional Documentation added: - src/srs-technical-spec-documents/scst-installation.md - src/srs-technical-spec-documents/mhvtl-installation.md - VTL-TESTING-GUIDE.md - scripts/test-vtl.sh Co-Authored-By: Warp <agent@warp.dev>
263 lines
6.8 KiB
Go
263 lines
6.8 KiB
Go
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/config"
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
"github.com/atlasos/calypso/internal/iam"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// Handler handles authentication requests
|
|
type Handler struct {
|
|
db *database.DB
|
|
config *config.Config
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewHandler creates a new auth handler
|
|
func NewHandler(db *database.DB, cfg *config.Config, log *logger.Logger) *Handler {
|
|
return &Handler{
|
|
db: db,
|
|
config: cfg,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// LoginRequest represents a login request
|
|
type LoginRequest struct {
|
|
Username string `json:"username" binding:"required"`
|
|
Password string `json:"password" binding:"required"`
|
|
}
|
|
|
|
// LoginResponse represents a login response
|
|
type LoginResponse struct {
|
|
Token string `json:"token"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
User UserInfo `json:"user"`
|
|
}
|
|
|
|
// UserInfo represents user information in auth responses
|
|
type UserInfo struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
FullName string `json:"full_name"`
|
|
Roles []string `json:"roles"`
|
|
}
|
|
|
|
// Login handles user login
|
|
func (h *Handler) Login(c *gin.Context) {
|
|
var req LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
|
|
// Get user from database
|
|
user, err := iam.GetUserByUsername(h.db, req.Username)
|
|
if err != nil {
|
|
h.logger.Warn("Login attempt failed", "username", req.Username, "error", "user not found")
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
|
return
|
|
}
|
|
|
|
// Check if user is active
|
|
if !user.IsActive {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled"})
|
|
return
|
|
}
|
|
|
|
// Verify password
|
|
if !h.verifyPassword(req.Password, user.PasswordHash) {
|
|
h.logger.Warn("Login attempt failed", "username", req.Username, "error", "invalid password")
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
|
return
|
|
}
|
|
|
|
// Generate JWT token
|
|
token, expiresAt, err := h.generateToken(user)
|
|
if err != nil {
|
|
h.logger.Error("Failed to generate token", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
|
|
return
|
|
}
|
|
|
|
// Create session
|
|
if err := h.createSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent"), expiresAt); err != nil {
|
|
h.logger.Error("Failed to create session", "error", err)
|
|
// Continue anyway, token is still valid
|
|
}
|
|
|
|
// Update last login
|
|
if err := h.updateLastLogin(user.ID); err != nil {
|
|
h.logger.Warn("Failed to update last login", "error", err)
|
|
}
|
|
|
|
// Get user roles
|
|
roles, err := iam.GetUserRoles(h.db, user.ID)
|
|
if err != nil {
|
|
h.logger.Warn("Failed to get user roles", "error", err)
|
|
roles = []string{}
|
|
}
|
|
|
|
h.logger.Info("User logged in successfully", "username", req.Username, "user_id", user.ID)
|
|
|
|
c.JSON(http.StatusOK, LoginResponse{
|
|
Token: token,
|
|
ExpiresAt: expiresAt,
|
|
User: UserInfo{
|
|
ID: user.ID,
|
|
Username: user.Username,
|
|
Email: user.Email,
|
|
FullName: user.FullName,
|
|
Roles: roles,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Logout handles user logout
|
|
func (h *Handler) Logout(c *gin.Context) {
|
|
// Extract token
|
|
authHeader := c.GetHeader("Authorization")
|
|
if authHeader != "" {
|
|
parts := strings.SplitN(authHeader, " ", 2)
|
|
if len(parts) == 2 && parts[0] == "Bearer" {
|
|
// Invalidate session (token hash would be stored)
|
|
// For now, just return success
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"})
|
|
}
|
|
|
|
// Me returns current user information
|
|
func (h *Handler) Me(c *gin.Context) {
|
|
user, exists := c.Get("user")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
|
|
return
|
|
}
|
|
|
|
authUser, ok := user.(*iam.User)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user context"})
|
|
return
|
|
}
|
|
|
|
roles, err := iam.GetUserRoles(h.db, authUser.ID)
|
|
if err != nil {
|
|
h.logger.Warn("Failed to get user roles", "error", err)
|
|
roles = []string{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, UserInfo{
|
|
ID: authUser.ID,
|
|
Username: authUser.Username,
|
|
Email: authUser.Email,
|
|
FullName: authUser.FullName,
|
|
Roles: roles,
|
|
})
|
|
}
|
|
|
|
// ValidateToken validates a JWT token and returns the user
|
|
func (h *Handler) ValidateToken(tokenString string) (*iam.User, error) {
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, jwt.ErrSignatureInvalid
|
|
}
|
|
return []byte(h.config.Auth.JWTSecret), nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !token.Valid {
|
|
return nil, jwt.ErrSignatureInvalid
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, jwt.ErrInvalidKey
|
|
}
|
|
|
|
userID, ok := claims["user_id"].(string)
|
|
if !ok {
|
|
return nil, jwt.ErrInvalidKey
|
|
}
|
|
|
|
// Get user from database
|
|
user, err := iam.GetUserByID(h.db, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !user.IsActive {
|
|
return nil, jwt.ErrInvalidKey
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// verifyPassword verifies a password against an Argon2id hash
|
|
func (h *Handler) verifyPassword(password, hash string) bool {
|
|
// TODO: Implement proper Argon2id verification
|
|
// For now, this is a stub
|
|
// In production, use golang.org/x/crypto/argon2 and compare hashes
|
|
return true
|
|
}
|
|
|
|
// generateToken generates a JWT token for a user
|
|
func (h *Handler) generateToken(user *iam.User) (string, time.Time, error) {
|
|
expiresAt := time.Now().Add(h.config.Auth.TokenLifetime)
|
|
|
|
claims := jwt.MapClaims{
|
|
"user_id": user.ID,
|
|
"username": user.Username,
|
|
"exp": expiresAt.Unix(),
|
|
"iat": time.Now().Unix(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenString, err := token.SignedString([]byte(h.config.Auth.JWTSecret))
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
|
|
return tokenString, expiresAt, nil
|
|
}
|
|
|
|
// createSession creates a session record in the database
|
|
func (h *Handler) createSession(userID, token, ipAddress, userAgent string, expiresAt time.Time) error {
|
|
// Hash the token for storage
|
|
tokenHash := hashToken(token)
|
|
|
|
query := `
|
|
INSERT INTO sessions (user_id, token_hash, ip_address, user_agent, expires_at)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
`
|
|
_, err := h.db.Exec(query, userID, tokenHash, ipAddress, userAgent, expiresAt)
|
|
return err
|
|
}
|
|
|
|
// updateLastLogin updates the user's last login timestamp
|
|
func (h *Handler) updateLastLogin(userID string) error {
|
|
query := `UPDATE users SET last_login_at = NOW() WHERE id = $1`
|
|
_, err := h.db.Exec(query, userID)
|
|
return err
|
|
}
|
|
|
|
// hashToken creates a simple hash of the token for storage
|
|
func hashToken(token string) string {
|
|
// TODO: Use proper cryptographic hash (SHA-256)
|
|
// For now, return a placeholder
|
|
return token[:32] + "..."
|
|
}
|
|
|