feat: Add JWT authentication and API key security
- Add users table for dashboard authentication (email, password_hash, role) - Add api_keys table for agent authentication (key_hash, prefix, name) - Implement JWT auth with 24h expiration - Implement API key auth with SHA256 hashing - Add auth endpoints: POST /api/v1/auth/login, POST /api/v1/auth/register - Add API key endpoints: GET/POST/DELETE /api/v1/api-keys - Protect all /api/v1/* routes (except /health and /auth/*) - Create default admin user (admin@ophion.local) - First registered user automatically becomes admin - Use bcrypt for password hashing (cost 12) - Use SHA256 for API key hashing - Add security headers middleware
This commit is contained in:
@@ -8,9 +8,12 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bigtux/ophion/internal/auth"
|
||||
"github.com/bigtux/ophion/internal/security"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
@@ -24,8 +27,9 @@ import (
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
type Server struct {
|
||||
app *fiber.App
|
||||
db *sql.DB
|
||||
app *fiber.App
|
||||
db *sql.DB
|
||||
authHandler *auth.AuthHandler
|
||||
}
|
||||
|
||||
type Metric struct {
|
||||
@@ -102,9 +106,17 @@ func main() {
|
||||
} else {
|
||||
log.Println("✓ Connected to PostgreSQL")
|
||||
initSchema(db)
|
||||
// Create default admin user
|
||||
if err := auth.CreateDefaultAdmin(db); err != nil {
|
||||
log.Printf("⚠ Failed to create default admin: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
server := &Server{db: db}
|
||||
// Initialize auth handler
|
||||
jwtSecret := getEnv("JWT_SECRET", "ophion-super-secret-key-change-in-production")
|
||||
authHandler := auth.NewAuthHandler(db, jwtSecret)
|
||||
|
||||
server := &Server{db: db, authHandler: authHandler}
|
||||
|
||||
// Create Fiber app
|
||||
app := fiber.New(fiber.Config{
|
||||
@@ -215,12 +227,34 @@ func initSchema(db *sql.DB) {
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Users table for JWT auth
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- API Keys table for agent auth
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
prefix VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_used TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_metrics_service_name ON metrics(service, name);
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
|
||||
CREATE INDEX IF NOT EXISTS idx_spans_trace_id ON spans(trace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_spans_service ON spans(service);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
|
||||
`
|
||||
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
@@ -231,36 +265,147 @@ func initSchema(db *sql.DB) {
|
||||
}
|
||||
|
||||
func (s *Server) setupRoutes() {
|
||||
// Health check
|
||||
// Health check (public)
|
||||
s.app.Get("/health", s.healthCheck)
|
||||
|
||||
// Security headers
|
||||
s.app.Use(security.SecurityHeaders())
|
||||
|
||||
// API v1
|
||||
api := s.app.Group("/api/v1")
|
||||
|
||||
// Ingest endpoints (for agents)
|
||||
api.Post("/metrics", s.ingestMetrics)
|
||||
api.Post("/logs", s.ingestLogs)
|
||||
api.Post("/traces", s.ingestTraces)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔓 PUBLIC ROUTES (no auth required)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// Auth endpoints
|
||||
authGroup := api.Group("/auth")
|
||||
authGroup.Post("/login", s.authHandler.Login)
|
||||
authGroup.Post("/register", s.authHandler.Register)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔐 PROTECTED ROUTES (auth required)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// Ingest endpoints (for agents - API key or JWT)
|
||||
ingest := api.Group("/ingest", s.authMiddleware())
|
||||
ingest.Post("/metrics", s.ingestMetrics)
|
||||
ingest.Post("/logs", s.ingestLogs)
|
||||
ingest.Post("/traces", s.ingestTraces)
|
||||
|
||||
// Legacy ingest routes (also protected, for backwards compat)
|
||||
api.Post("/metrics", s.authMiddleware(), s.ingestMetrics)
|
||||
api.Post("/logs", s.authMiddleware(), s.ingestLogs)
|
||||
api.Post("/traces", s.authMiddleware(), s.ingestTraces)
|
||||
|
||||
// Protected routes
|
||||
protected := api.Group("", s.authMiddleware())
|
||||
|
||||
// Query endpoints (for dashboard)
|
||||
api.Get("/metrics", s.queryMetrics)
|
||||
api.Get("/metrics/names", s.getMetricNames)
|
||||
api.Get("/logs", s.queryLogs)
|
||||
api.Get("/traces", s.queryTraces)
|
||||
api.Get("/traces/:traceId", s.getTrace)
|
||||
api.Get("/services", s.getServices)
|
||||
protected.Get("/metrics", s.queryMetrics)
|
||||
protected.Get("/metrics/names", s.getMetricNames)
|
||||
protected.Get("/logs", s.queryLogs)
|
||||
protected.Get("/traces", s.queryTraces)
|
||||
protected.Get("/traces/:traceId", s.getTrace)
|
||||
protected.Get("/services", s.getServices)
|
||||
|
||||
// Agents
|
||||
api.Get("/agents", s.getAgents)
|
||||
api.Post("/agents/register", s.registerAgent)
|
||||
protected.Get("/agents", s.getAgents)
|
||||
protected.Post("/agents/register", s.registerAgent)
|
||||
|
||||
// Alerts
|
||||
api.Get("/alerts", s.getAlerts)
|
||||
api.Post("/alerts", s.createAlert)
|
||||
api.Put("/alerts/:id/resolve", s.resolveAlert)
|
||||
protected.Get("/alerts", s.getAlerts)
|
||||
protected.Post("/alerts", s.createAlert)
|
||||
protected.Put("/alerts/:id/resolve", s.resolveAlert)
|
||||
|
||||
// Dashboard
|
||||
api.Get("/dashboard/overview", s.getDashboardOverview)
|
||||
protected.Get("/dashboard/overview", s.getDashboardOverview)
|
||||
|
||||
// User info
|
||||
protected.Get("/me", s.authHandler.Me)
|
||||
|
||||
// API Keys management (JWT only)
|
||||
protected.Post("/api-keys", s.authHandler.CreateAPIKey)
|
||||
protected.Get("/api-keys", s.authHandler.ListAPIKeys)
|
||||
protected.Delete("/api-keys/:id", s.authHandler.DeleteAPIKey)
|
||||
}
|
||||
|
||||
// authMiddleware creates authentication middleware that accepts both JWT and API keys
|
||||
func (s *Server) authMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
"message": "Missing Authorization header",
|
||||
})
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Check if it's an API key (starts with ophion_)
|
||||
if strings.HasPrefix(token, security.APIKeyPrefix) {
|
||||
return s.authenticateAPIKey(c, token)
|
||||
}
|
||||
|
||||
// Otherwise treat as JWT
|
||||
return s.authenticateJWT(c, token)
|
||||
}
|
||||
}
|
||||
|
||||
// authenticateAPIKey validates API key authentication
|
||||
func (s *Server) authenticateAPIKey(c *fiber.Ctx, apiKey string) error {
|
||||
if !security.ValidateAPIKeyFormat(apiKey) {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
"message": "Invalid API key format",
|
||||
})
|
||||
}
|
||||
|
||||
keyHash := security.HashAPIKey(apiKey)
|
||||
|
||||
var keyID, name, userID string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, name, user_id FROM api_keys WHERE key_hash = $1
|
||||
`, keyHash).Scan(&keyID, &name, &userID)
|
||||
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
"message": "Invalid API key",
|
||||
})
|
||||
}
|
||||
|
||||
// Update last_used (async)
|
||||
go s.db.Exec(`UPDATE api_keys SET last_used = NOW() WHERE id = $1`, keyID)
|
||||
|
||||
// Set context
|
||||
c.Locals("auth_type", "api_key")
|
||||
c.Locals("api_key_id", keyID)
|
||||
c.Locals("user_id", userID)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// authenticateJWT validates JWT token authentication
|
||||
func (s *Server) authenticateJWT(c *fiber.Ctx, token string) error {
|
||||
authService := s.authHandler.GetAuthService()
|
||||
claims, err := authService.ValidateAccessToken(token)
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Set context
|
||||
c.Locals("auth_type", "jwt")
|
||||
c.Locals("user_id", claims.UserID)
|
||||
c.Locals("email", claims.Email)
|
||||
c.Locals("role", claims.Role)
|
||||
c.Locals("token_id", claims.TokenID)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (s *Server) healthCheck(c *fiber.Ctx) error {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InsightsEngine motor de geração de insights
|
||||
|
||||
457
internal/auth/handlers.go
Normal file
457
internal/auth/handlers.go
Normal file
@@ -0,0 +1,457 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/bigtux/ophion/internal/security"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔐 AUTH HANDLERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
type AuthHandler struct {
|
||||
db *sql.DB
|
||||
authService *AuthService
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"-"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID string `json:"id"`
|
||||
KeyHash string `json:"-"`
|
||||
Prefix string `json:"prefix"`
|
||||
Name string `json:"name"`
|
||||
UserID string `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed *time.Time `json:"last_used,omitempty"`
|
||||
}
|
||||
|
||||
// NewAuthHandler cria novo handler de autenticação
|
||||
func NewAuthHandler(db *sql.DB, jwtSecret string) *AuthHandler {
|
||||
config := AuthConfig{
|
||||
JWTSecret: []byte(jwtSecret),
|
||||
JWTExpiration: 24 * time.Hour,
|
||||
RefreshExpiration: 7 * 24 * time.Hour,
|
||||
Issuer: "ophion",
|
||||
}
|
||||
|
||||
// Implementação simples do APIKeyStore
|
||||
store := &DBAPIKeyStore{db: db}
|
||||
|
||||
authService := NewAuthService(config, nil, store)
|
||||
|
||||
return &AuthHandler{
|
||||
db: db,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthService retorna o serviço de autenticação
|
||||
func (h *AuthHandler) GetAuthService() *AuthService {
|
||||
return h.authService
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Login
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
} `json:"user"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *fiber.Ctx) error {
|
||||
var req LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if req.Email == "" || req.Password == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Email and password are required"})
|
||||
}
|
||||
|
||||
// Sanitize email
|
||||
email, err := security.SanitizeEmail(req.Email)
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid email format"})
|
||||
}
|
||||
|
||||
// Check brute force protection
|
||||
allowed, remaining, _ := h.authService.CheckLoginAllowed(email)
|
||||
if !allowed {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Too many failed attempts",
|
||||
"message": "Account temporarily locked. Try again in " + remaining.String(),
|
||||
})
|
||||
}
|
||||
|
||||
// Find user
|
||||
var user User
|
||||
err = h.db.QueryRow(`
|
||||
SELECT id, email, password_hash, role, created_at
|
||||
FROM users WHERE email = $1
|
||||
`, email).Scan(&user.ID, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
h.authService.RecordLoginAttempt(email, false)
|
||||
return c.Status(401).JSON(fiber.Map{"error": "Invalid credentials"})
|
||||
}
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Database error"})
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !security.VerifyPassword(req.Password, user.PasswordHash) {
|
||||
blocked, remaining := h.authService.RecordLoginAttempt(email, false)
|
||||
if blocked {
|
||||
return c.Status(429).JSON(fiber.Map{
|
||||
"error": "Too many failed attempts",
|
||||
"message": "Account temporarily locked",
|
||||
})
|
||||
}
|
||||
return c.Status(401).JSON(fiber.Map{
|
||||
"error": "Invalid credentials",
|
||||
"remaining_attempts": remaining,
|
||||
})
|
||||
}
|
||||
|
||||
// Successful login
|
||||
h.authService.RecordLoginAttempt(email, true)
|
||||
|
||||
// Generate token
|
||||
token, _, err := h.authService.GenerateTokenPair(user.ID, "", user.Email, user.Role)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to generate token"})
|
||||
}
|
||||
|
||||
resp := LoginResponse{
|
||||
Token: token,
|
||||
ExpiresIn: 86400, // 24 hours
|
||||
}
|
||||
resp.User.ID = user.ID
|
||||
resp.User.Email = user.Email
|
||||
resp.User.Role = user.Role
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Register
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(c *fiber.Ctx) error {
|
||||
var req RegisterRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if req.Email == "" || req.Password == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Email and password are required"})
|
||||
}
|
||||
|
||||
// Sanitize email
|
||||
email, err := security.SanitizeEmail(req.Email)
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid email format"})
|
||||
}
|
||||
|
||||
// Check password (relaxed policy for simplicity)
|
||||
if len(req.Password) < 6 {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Password must be at least 6 characters"})
|
||||
}
|
||||
|
||||
// Check if any users exist (first user becomes admin)
|
||||
var userCount int
|
||||
h.db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&userCount)
|
||||
|
||||
role := "user"
|
||||
if userCount == 0 {
|
||||
role = "admin"
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
var existingID string
|
||||
err = h.db.QueryRow(`SELECT id FROM users WHERE email = $1`, email).Scan(&existingID)
|
||||
if err != sql.ErrNoRows {
|
||||
return c.Status(409).JSON(fiber.Map{"error": "Email already registered"})
|
||||
}
|
||||
|
||||
// Hash password
|
||||
passwordHash, err := security.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to hash password"})
|
||||
}
|
||||
|
||||
// Create user
|
||||
userID := uuid.New().String()
|
||||
_, err = h.db.Exec(`
|
||||
INSERT INTO users (id, email, password_hash, role, created_at)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
`, userID, email, passwordHash, role)
|
||||
if err != nil {
|
||||
log.Printf("Error creating user: %v", err)
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to create user"})
|
||||
}
|
||||
|
||||
// Generate token
|
||||
token, _, err := h.authService.GenerateTokenPair(userID, "", email, role)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to generate token"})
|
||||
}
|
||||
|
||||
return c.Status(201).JSON(fiber.Map{
|
||||
"message": "User registered successfully",
|
||||
"token": token,
|
||||
"user": fiber.Map{
|
||||
"id": userID,
|
||||
"email": email,
|
||||
"role": role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// API Keys
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
type CreateAPIKeyRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) CreateAPIKey(c *fiber.Ctx) error {
|
||||
var req CreateAPIKeyRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Name is required"})
|
||||
}
|
||||
|
||||
userID := GetUserID(c)
|
||||
if userID == "" {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
|
||||
}
|
||||
|
||||
// Generate API key
|
||||
key, keyHash, prefix := security.GenerateAPIKey()
|
||||
|
||||
keyID := uuid.New().String()
|
||||
_, err := h.db.Exec(`
|
||||
INSERT INTO api_keys (id, key_hash, prefix, name, user_id, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
`, keyID, keyHash, prefix, req.Name, userID)
|
||||
if err != nil {
|
||||
log.Printf("Error creating API key: %v", err)
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to create API key"})
|
||||
}
|
||||
|
||||
return c.Status(201).JSON(fiber.Map{
|
||||
"message": "API key created successfully",
|
||||
"key": key, // Only shown once!
|
||||
"id": keyID,
|
||||
"name": req.Name,
|
||||
"prefix": prefix,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ListAPIKeys(c *fiber.Ctx) error {
|
||||
userID := GetUserID(c)
|
||||
role := GetRole(c)
|
||||
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
|
||||
// Admin can see all keys
|
||||
if role == "admin" {
|
||||
rows, err = h.db.Query(`
|
||||
SELECT id, prefix, name, user_id, created_at, last_used
|
||||
FROM api_keys ORDER BY created_at DESC
|
||||
`)
|
||||
} else {
|
||||
rows, err = h.db.Query(`
|
||||
SELECT id, prefix, name, user_id, created_at, last_used
|
||||
FROM api_keys WHERE user_id = $1 ORDER BY created_at DESC
|
||||
`, userID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Database error"})
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var keys []fiber.Map
|
||||
for rows.Next() {
|
||||
var k APIKey
|
||||
if err := rows.Scan(&k.ID, &k.Prefix, &k.Name, &k.UserID, &k.CreatedAt, &k.LastUsed); err == nil {
|
||||
keys = append(keys, fiber.Map{
|
||||
"id": k.ID,
|
||||
"prefix": k.Prefix,
|
||||
"name": k.Name,
|
||||
"user_id": k.UserID,
|
||||
"created_at": k.CreatedAt,
|
||||
"last_used": k.LastUsed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if keys == nil {
|
||||
keys = []fiber.Map{}
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"api_keys": keys})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) DeleteAPIKey(c *fiber.Ctx) error {
|
||||
keyID := c.Params("id")
|
||||
userID := GetUserID(c)
|
||||
role := GetRole(c)
|
||||
|
||||
var ownerID string
|
||||
err := h.db.QueryRow(`SELECT user_id FROM api_keys WHERE id = $1`, keyID).Scan(&ownerID)
|
||||
if err == sql.ErrNoRows {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "API key not found"})
|
||||
}
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Database error"})
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if role != "admin" && ownerID != userID {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Permission denied"})
|
||||
}
|
||||
|
||||
_, err = h.db.Exec(`DELETE FROM api_keys WHERE id = $1`, keyID)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to delete API key"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "API key deleted"})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Me (current user)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *AuthHandler) Me(c *fiber.Ctx) error {
|
||||
userID := GetUserID(c)
|
||||
email := c.Locals("email")
|
||||
role := GetRole(c)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"id": userID,
|
||||
"email": email,
|
||||
"role": role,
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🗄️ DB API KEY STORE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
type DBAPIKeyStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (s *DBAPIKeyStore) ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error) {
|
||||
var info APIKeyInfo
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT id, name FROM api_keys WHERE key_hash = $1
|
||||
`, keyHash).Scan(&info.ID, &info.Name)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.Scopes = []string{"*"} // Full access for now
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (s *DBAPIKeyStore) UpdateLastUsed(ctx context.Context, keyID string) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
UPDATE api_keys SET last_used = NOW() WHERE id = $1
|
||||
`, keyID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔧 ADMIN USER SETUP
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// CreateDefaultAdmin cria usuário admin padrão se não existir
|
||||
func CreateDefaultAdmin(db *sql.DB) error {
|
||||
// Check if any users exist
|
||||
var count int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count); err != nil {
|
||||
// Table might not exist yet
|
||||
return nil
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return nil // Admin already exists
|
||||
}
|
||||
|
||||
adminEmail := "admin@ophion.local"
|
||||
adminPassword := os.Getenv("ADMIN_PASSWORD")
|
||||
if adminPassword == "" {
|
||||
adminPassword = "ophion123"
|
||||
}
|
||||
|
||||
passwordHash, err := security.HashPassword(adminPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userID := uuid.New().String()
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO users (id, email, password_hash, role, created_at)
|
||||
VALUES ($1, $2, $3, 'admin', NOW())
|
||||
ON CONFLICT (email) DO NOTHING
|
||||
`, userID, adminEmail, passwordHash)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("✓ Default admin user created: %s", adminEmail)
|
||||
return nil
|
||||
}
|
||||
|
||||
// HashAPIKey creates SHA256 hash of API key
|
||||
func HashAPIKey(key string) string {
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
Reference in New Issue
Block a user