Files
honeydany/app/dashboard/auth.go
T

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[:])
}