187 lines
4.9 KiB
Go
187 lines
4.9 KiB
Go
// FILE: internal/storage/user.go
|
|
package storage
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
// CreateUser creates user with transaction isolation to prevent race conditions
|
|
func (s *Store) CreateUser(record UserRecord) error {
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Check uniqueness within transaction
|
|
exists, err := s.userExists(tx, record.Username, record.Email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
return fmt.Errorf("username or email already exists")
|
|
}
|
|
|
|
// Insert user
|
|
query := `INSERT INTO users (
|
|
user_id, username, email, password_hash, created_at
|
|
) VALUES (?, ?, ?, ?, ?)`
|
|
|
|
_, err = tx.Exec(query,
|
|
record.UserID, record.Username, record.Email,
|
|
record.PasswordHash, record.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// userExists verifies username/email uniqueness within a transaction
|
|
func (s *Store) userExists(tx *sql.Tx, username, email string) (bool, error) {
|
|
var count int
|
|
query := `SELECT COUNT(*) FROM users WHERE username = ? COLLATE NOCASE`
|
|
args := []interface{}{username}
|
|
|
|
if email != "" {
|
|
query = `SELECT COUNT(*) FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE`
|
|
args = append(args, email)
|
|
}
|
|
|
|
err := tx.QueryRow(query, args...).Scan(&count)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// UpdateUserPassword updates user password hash
|
|
func (s *Store) UpdateUserPassword(userID string, passwordHash string) error {
|
|
query := `UPDATE users SET password_hash = ? WHERE user_id = ?`
|
|
_, err := s.db.Exec(query, passwordHash, userID)
|
|
return err
|
|
}
|
|
|
|
// UpdateUserEmail updates user email
|
|
func (s *Store) UpdateUserEmail(userID string, email string) error {
|
|
query := `UPDATE users SET email = ? WHERE user_id = ?`
|
|
_, err := s.db.Exec(query, email, userID)
|
|
return err
|
|
}
|
|
|
|
// UpdateUserUsername updates username
|
|
func (s *Store) UpdateUserUsername(userID string, username string) error {
|
|
query := `UPDATE users SET username = ? WHERE user_id = ?`
|
|
_, err := s.db.Exec(query, username, userID)
|
|
return err
|
|
}
|
|
|
|
// GetAllUsers retrieves all users
|
|
func (s *Store) GetAllUsers() ([]UserRecord, error) {
|
|
query := `SELECT user_id, username, email, password_hash, created_at, last_login_at
|
|
FROM users ORDER BY created_at DESC`
|
|
|
|
rows, err := s.db.Query(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var users []UserRecord
|
|
for rows.Next() {
|
|
var user UserRecord
|
|
err := rows.Scan(
|
|
&user.UserID, &user.Username, &user.Email,
|
|
&user.PasswordHash, &user.CreatedAt, &user.LastLoginAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
|
|
return users, rows.Err()
|
|
}
|
|
|
|
// UpdateUserLastLoginSync updates user last login time
|
|
func (s *Store) UpdateUserLastLoginSync(userID string, loginTime time.Time) error {
|
|
query := `UPDATE users SET last_login_at = ? WHERE user_id = ?`
|
|
|
|
_, err := s.db.Exec(query, loginTime, userID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update last login for user %s: %w", userID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUserByUsername retrieves user by username with case-insensitive matching
|
|
func (s *Store) GetUserByUsername(username string) (*UserRecord, error) {
|
|
var user UserRecord
|
|
query := `SELECT user_id, username, email, password_hash, created_at, last_login_at
|
|
FROM users WHERE username = ? COLLATE NOCASE`
|
|
|
|
err := s.db.QueryRow(query, username).Scan(
|
|
&user.UserID, &user.Username, &user.Email,
|
|
&user.PasswordHash, &user.CreatedAt, &user.LastLoginAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// GetUserByEmail retrieves user by email with case-insensitive matching
|
|
func (s *Store) GetUserByEmail(email string) (*UserRecord, error) {
|
|
var user UserRecord
|
|
query := `SELECT user_id, username, email, password_hash, created_at, last_login_at
|
|
FROM users WHERE email = ? COLLATE NOCASE`
|
|
|
|
err := s.db.QueryRow(query, email).Scan(
|
|
&user.UserID, &user.Username, &user.Email,
|
|
&user.PasswordHash, &user.CreatedAt, &user.LastLoginAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// GetUserByID retrieves user by unique user ID
|
|
func (s *Store) GetUserByID(userID string) (*UserRecord, error) {
|
|
var user UserRecord
|
|
query := `SELECT user_id, username, email, password_hash, created_at, last_login_at
|
|
FROM users WHERE user_id = ?`
|
|
|
|
err := s.db.QueryRow(query, userID).Scan(
|
|
&user.UserID, &user.Username, &user.Email,
|
|
&user.PasswordHash, &user.CreatedAt, &user.LastLoginAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// DeleteUser removes a user from the database
|
|
func (s *Store) DeleteUser(userID string) error {
|
|
if !s.healthStatus.Load() {
|
|
return nil
|
|
}
|
|
|
|
select {
|
|
case s.writeChan <- func(tx *sql.Tx) error {
|
|
query := `DELETE FROM users WHERE user_id = ?`
|
|
_, err := tx.Exec(query, userID)
|
|
return err
|
|
}:
|
|
return nil
|
|
default:
|
|
log.Printf("Storage write queue full, dropping user deletion")
|
|
return nil
|
|
}
|
|
} |