483 lines
14 KiB
Go
483 lines
14 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// User represents a dashboard user
|
|
type User struct {
|
|
ID int `json:"id"`
|
|
Username string `json:"username"`
|
|
Password string `json:"-"` // Never include in JSON
|
|
Email string `json:"email"`
|
|
Role string `json:"role"` // "admin", "user", "readonly"
|
|
Active bool `json:"active"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
LastLogin *time.Time `json:"last_login,omitempty"`
|
|
}
|
|
|
|
// Session represents a user session
|
|
type Session struct {
|
|
ID string `json:"id"`
|
|
UserID int `json:"user_id"`
|
|
Token string `json:"-"` // Never include in JSON
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
IPAddress string `json:"ip_address"`
|
|
UserAgent string `json:"user_agent"`
|
|
}
|
|
|
|
// APIKey represents an API key for programmatic access
|
|
type APIKey struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Key string `json:"key"`
|
|
KeyHash string `json:"-"` // Never include in JSON
|
|
UserID int `json:"user_id"`
|
|
Permissions string `json:"permissions"` // JSON array of permissions
|
|
Active bool `json:"active"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsed *time.Time `json:"last_used,omitempty"`
|
|
}
|
|
|
|
// AuthManager handles authentication and authorization
|
|
type AuthManager struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewAuthManager creates a new authentication manager
|
|
func NewAuthManager(db *sql.DB) (*AuthManager, error) {
|
|
am := &AuthManager{db: db}
|
|
if err := am.initDatabase(); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize auth database: %w", err)
|
|
}
|
|
return am, nil
|
|
}
|
|
|
|
// initDatabase creates the authentication tables
|
|
func (am *AuthManager) initDatabase() error {
|
|
queries := []string{
|
|
`CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password TEXT NOT NULL,
|
|
email TEXT,
|
|
role TEXT NOT NULL DEFAULT 'user',
|
|
active BOOLEAN DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_login DATETIME
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
token TEXT NOT NULL UNIQUE,
|
|
expires_at DATETIME NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
ip_address TEXT,
|
|
user_agent TEXT,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS api_keys (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
key_value TEXT NOT NULL UNIQUE,
|
|
key_hash TEXT NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
permissions TEXT DEFAULT '[]',
|
|
active BOOLEAN DEFAULT 1,
|
|
expires_at DATETIME,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_used DATETIME,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)`,
|
|
}
|
|
|
|
for _, query := range queries {
|
|
if _, err := am.db.Exec(query); err != nil {
|
|
return fmt.Errorf("failed to execute query: %s, error: %w", query, err)
|
|
}
|
|
}
|
|
|
|
// Create default admin user if no users exist
|
|
return am.createDefaultUser()
|
|
}
|
|
|
|
// createDefaultUser creates the default admin user if no users exist
|
|
func (am *AuthManager) createDefaultUser() error {
|
|
var count int
|
|
err := am.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if count == 0 {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
query := `INSERT INTO users (username, password, email, role, active) VALUES (?, ?, ?, ?, ?)`
|
|
_, err = am.db.Exec(query, "admin", string(hashedPassword), "admin@localhost", "admin", true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Println("Created default admin user (username: admin, password: password)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateUser creates a new user
|
|
func (am *AuthManager) CreateUser(username, password, email, role string) (*User, error) {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := `INSERT INTO users (username, password, email, role, active) VALUES (?, ?, ?, ?, ?)`
|
|
result, err := am.db.Exec(query, username, string(hashedPassword), email, role, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return am.GetUserByID(int(id))
|
|
}
|
|
|
|
// GetUserByID retrieves a user by ID
|
|
func (am *AuthManager) GetUserByID(id int) (*User, error) {
|
|
user := &User{}
|
|
query := `SELECT id, username, password, email, role, active, created_at, updated_at, last_login
|
|
FROM users WHERE id = ?`
|
|
|
|
var lastLogin sql.NullTime
|
|
err := am.db.QueryRow(query, id).Scan(&user.ID, &user.Username, &user.Password, &user.Email,
|
|
&user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt, &lastLogin)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if lastLogin.Valid {
|
|
user.LastLogin = &lastLogin.Time
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetUserByUsername retrieves a user by username
|
|
func (am *AuthManager) GetUserByUsername(username string) (*User, error) {
|
|
user := &User{}
|
|
query := `SELECT id, username, password, email, role, active, created_at, updated_at, last_login
|
|
FROM users WHERE username = ?`
|
|
|
|
var lastLogin sql.NullTime
|
|
err := am.db.QueryRow(query, username).Scan(&user.ID, &user.Username, &user.Password, &user.Email,
|
|
&user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt, &lastLogin)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if lastLogin.Valid {
|
|
user.LastLogin = &lastLogin.Time
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// AuthenticateUser validates username and password
|
|
func (am *AuthManager) AuthenticateUser(username, password string) (*User, error) {
|
|
user, err := am.GetUserByUsername(username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !user.Active {
|
|
return nil, fmt.Errorf("user account is disabled")
|
|
}
|
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid credentials")
|
|
}
|
|
|
|
// Update last login time
|
|
_, err = am.db.Exec("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?", user.ID)
|
|
if err != nil {
|
|
log.Printf("Failed to update last login for user %s: %v", username, err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// CreateSession creates a new session for a user
|
|
func (am *AuthManager) CreateSession(userID int, ipAddress, userAgent string) (*Session, error) {
|
|
sessionID := generateRandomString(32)
|
|
token := generateRandomString(64)
|
|
expiresAt := time.Now().Add(24 * time.Hour) // 24 hour session
|
|
|
|
query := `INSERT INTO sessions (id, user_id, token, expires_at, ip_address, user_agent)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
|
|
_, err := am.db.Exec(query, sessionID, userID, token, expiresAt, ipAddress, userAgent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Session{
|
|
ID: sessionID,
|
|
UserID: userID,
|
|
Token: token,
|
|
ExpiresAt: expiresAt,
|
|
CreatedAt: time.Now(),
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
}, nil
|
|
}
|
|
|
|
// ValidateSession validates a session token
|
|
func (am *AuthManager) ValidateSession(token string) (*User, error) {
|
|
var session Session
|
|
query := `SELECT s.id, s.user_id, s.expires_at, u.id, u.username, u.email, u.role, u.active
|
|
FROM sessions s
|
|
JOIN users u ON s.user_id = u.id
|
|
WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP AND u.active = 1`
|
|
|
|
var user User
|
|
err := am.db.QueryRow(query, token).Scan(&session.ID, &session.UserID, &session.ExpiresAt,
|
|
&user.ID, &user.Username, &user.Email, &user.Role, &user.Active)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// DeleteSession removes a session
|
|
func (am *AuthManager) DeleteSession(token string) error {
|
|
_, err := am.db.Exec("DELETE FROM sessions WHERE token = ?", token)
|
|
return err
|
|
}
|
|
|
|
// CleanupExpiredSessions removes expired sessions
|
|
func (am *AuthManager) CleanupExpiredSessions() error {
|
|
_, err := am.db.Exec("DELETE FROM sessions WHERE expires_at < CURRENT_TIMESTAMP")
|
|
return err
|
|
}
|
|
|
|
// CreateAPIKey creates a new API key
|
|
func (am *AuthManager) CreateAPIKey(name string, userID int, permissions []string, expiresAt *time.Time) (*APIKey, error) {
|
|
key := "hd_" + generateRandomString(40) // Prefix for identification
|
|
keyHash := hashString(key)
|
|
|
|
permissionsJSON := "[]"
|
|
if len(permissions) > 0 {
|
|
// Simple JSON encoding for permissions
|
|
permissionsJSON = fmt.Sprintf(`["%s"]`, permissions[0])
|
|
for i := 1; i < len(permissions); i++ {
|
|
permissionsJSON = permissionsJSON[:len(permissionsJSON)-1] + fmt.Sprintf(`,"%s"]`, permissions[i])
|
|
}
|
|
}
|
|
|
|
query := `INSERT INTO api_keys (name, key_value, key_hash, user_id, permissions, active, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
result, err := am.db.Exec(query, name, key, keyHash, userID, permissionsJSON, true, expiresAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &APIKey{
|
|
ID: int(id),
|
|
Name: name,
|
|
Key: key,
|
|
KeyHash: keyHash,
|
|
UserID: userID,
|
|
Permissions: permissionsJSON,
|
|
Active: true,
|
|
ExpiresAt: expiresAt,
|
|
CreatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// ValidateAPIKey validates an API key and returns the associated user
|
|
func (am *AuthManager) ValidateAPIKey(key string) (*User, error) {
|
|
keyHash := hashString(key)
|
|
|
|
var apiKey APIKey
|
|
var user User
|
|
query := `SELECT a.id, a.user_id, a.active, a.expires_at, u.id, u.username, u.email, u.role, u.active
|
|
FROM api_keys a
|
|
JOIN users u ON a.user_id = u.id
|
|
WHERE a.key_hash = ? AND a.active = 1 AND u.active = 1`
|
|
|
|
var expiresAt sql.NullTime
|
|
err := am.db.QueryRow(query, keyHash).Scan(&apiKey.ID, &apiKey.UserID, &apiKey.Active, &expiresAt,
|
|
&user.ID, &user.Username, &user.Email, &user.Role, &user.Active)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if key is expired
|
|
if expiresAt.Valid && expiresAt.Time.Before(time.Now()) {
|
|
return nil, fmt.Errorf("API key expired")
|
|
}
|
|
|
|
// Update last used timestamp
|
|
_, err = am.db.Exec("UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?", apiKey.ID)
|
|
if err != nil {
|
|
log.Printf("Failed to update API key last used: %v", err)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// GetUsers retrieves all users
|
|
func (am *AuthManager) GetUsers() ([]User, error) {
|
|
query := `SELECT id, username, email, role, active, created_at, updated_at, last_login
|
|
FROM users ORDER BY created_at DESC`
|
|
|
|
rows, err := am.db.Query(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var users []User
|
|
for rows.Next() {
|
|
var user User
|
|
var lastLogin sql.NullTime
|
|
|
|
err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.Role, &user.Active,
|
|
&user.CreatedAt, &user.UpdatedAt, &lastLogin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if lastLogin.Valid {
|
|
user.LastLogin = &lastLogin.Time
|
|
}
|
|
|
|
users = append(users, user)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// GetAPIKeys retrieves API keys for a user
|
|
func (am *AuthManager) GetAPIKeys(userID int) ([]APIKey, error) {
|
|
query := `SELECT id, name, key_value, user_id, permissions, active, expires_at, created_at, last_used
|
|
FROM api_keys WHERE user_id = ? ORDER BY created_at DESC`
|
|
|
|
rows, err := am.db.Query(query, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var keys []APIKey
|
|
for rows.Next() {
|
|
var key APIKey
|
|
var expiresAt, lastUsed sql.NullTime
|
|
|
|
err := rows.Scan(&key.ID, &key.Name, &key.Key, &key.UserID, &key.Permissions,
|
|
&key.Active, &expiresAt, &key.CreatedAt, &lastUsed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if expiresAt.Valid {
|
|
key.ExpiresAt = &expiresAt.Time
|
|
}
|
|
if lastUsed.Valid {
|
|
key.LastUsed = &lastUsed.Time
|
|
}
|
|
|
|
// Mask the key for security (show only first 8 chars)
|
|
if len(key.Key) > 8 {
|
|
key.Key = key.Key[:8] + "..." + key.Key[len(key.Key)-4:]
|
|
}
|
|
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
// UpdateUser updates user information
|
|
func (am *AuthManager) UpdateUser(id int, username, email, role string, active bool) error {
|
|
query := `UPDATE users SET username = ?, email = ?, role = ?, active = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?`
|
|
_, err := am.db.Exec(query, username, email, role, active, id)
|
|
return err
|
|
}
|
|
|
|
// UpdateUserPassword updates a user's password
|
|
func (am *AuthManager) UpdateUserPassword(id int, newPassword string) error {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
query := `UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
|
|
_, err = am.db.Exec(query, string(hashedPassword), id)
|
|
return err
|
|
}
|
|
|
|
// DeleteUser deletes a user (and associated sessions/API keys)
|
|
func (am *AuthManager) DeleteUser(id int) error {
|
|
_, err := am.db.Exec("DELETE FROM users WHERE id = ?", id)
|
|
return err
|
|
}
|
|
|
|
// RevokeAPIKey revokes an API key
|
|
func (am *AuthManager) RevokeAPIKey(id int) error {
|
|
_, err := am.db.Exec("UPDATE api_keys SET active = 0 WHERE id = ?", id)
|
|
return err
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// generateRandomString generates a random string of specified length
|
|
func generateRandomString(length int) string {
|
|
bytes := make([]byte, length/2)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
panic(err)
|
|
}
|
|
return hex.EncodeToString(bytes)
|
|
}
|
|
|
|
// hashString creates a SHA256 hash of a string
|
|
func hashString(s string) string {
|
|
hash := sha256.Sum256([]byte(s))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|