165 lines
3.4 KiB
Go
165 lines
3.4 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type CreateUserParams struct {
|
|
Email string
|
|
PasswordHash string
|
|
Role UserRole
|
|
IsActive bool
|
|
}
|
|
|
|
func (r *UserRepository) HasActiveAdmin(ctx context.Context) (bool, error) {
|
|
var exists int
|
|
if err := r.q.QueryRowContext(
|
|
ctx,
|
|
`SELECT EXISTS(SELECT 1 FROM users WHERE role = ? AND is_active = 1 LIMIT 1)`,
|
|
UserRoleAdmin,
|
|
).Scan(&exists); err != nil {
|
|
return false, fmt.Errorf("query active admin existence: %w", err)
|
|
}
|
|
|
|
return exists == 1, nil
|
|
}
|
|
|
|
func (r *UserRepository) GetByID(ctx context.Context, id int64) (*User, error) {
|
|
user, err := scanUser(r.q.QueryRowContext(
|
|
ctx,
|
|
`SELECT id, email, password_hash, role, is_active, created_at, updated_at, last_login_at
|
|
FROM users
|
|
WHERE id = ?`,
|
|
id,
|
|
))
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
return nil, fmt.Errorf("scan user by id: %w", err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
|
|
user, err := scanUser(r.q.QueryRowContext(
|
|
ctx,
|
|
`SELECT id, email, password_hash, role, is_active, created_at, updated_at, last_login_at
|
|
FROM users
|
|
WHERE email = ? COLLATE NOCASE
|
|
LIMIT 1`,
|
|
strings.TrimSpace(email),
|
|
))
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
return nil, fmt.Errorf("scan user by email: %w", err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (r *UserRepository) Create(ctx context.Context, params CreateUserParams) (*User, error) {
|
|
isActive := 0
|
|
if params.IsActive {
|
|
isActive = 1
|
|
}
|
|
|
|
result, err := r.q.ExecContext(
|
|
ctx,
|
|
`INSERT INTO users (email, password_hash, role, is_active) VALUES (?, ?, ?, ?)`,
|
|
strings.TrimSpace(params.Email),
|
|
params.PasswordHash,
|
|
params.Role,
|
|
isActive,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert user: %w", err)
|
|
}
|
|
|
|
userID, err := result.LastInsertId()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load inserted user id: %w", err)
|
|
}
|
|
|
|
return r.GetByID(ctx, userID)
|
|
}
|
|
|
|
func (r *UserRepository) UpdateLastLoginAt(ctx context.Context, userID int64, loggedInAt time.Time) error {
|
|
result, err := r.q.ExecContext(
|
|
ctx,
|
|
`UPDATE users SET last_login_at = ? WHERE id = ?`,
|
|
formatTimestamp(loggedInAt),
|
|
userID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update user last_login_at: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("read affected user rows: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return ErrNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func scanUser(scanner rowScanner) (*User, error) {
|
|
var (
|
|
user User
|
|
role string
|
|
isActive int
|
|
createdAtRaw string
|
|
updatedAtRaw string
|
|
lastLoginRaw sql.NullString
|
|
)
|
|
|
|
if err := scanner.Scan(
|
|
&user.ID,
|
|
&user.Email,
|
|
&user.PasswordHash,
|
|
&role,
|
|
&isActive,
|
|
&createdAtRaw,
|
|
&updatedAtRaw,
|
|
&lastLoginRaw,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
createdAt, err := parseTimestamp(createdAtRaw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse user created_at: %w", err)
|
|
}
|
|
|
|
updatedAt, err := parseTimestamp(updatedAtRaw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse user updated_at: %w", err)
|
|
}
|
|
|
|
lastLoginAt, err := parseNullableTimestamp(lastLoginRaw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse user last_login_at: %w", err)
|
|
}
|
|
|
|
user.Role = UserRole(role)
|
|
user.IsActive = isActive == 1
|
|
user.CreatedAt = createdAt
|
|
user.UpdatedAt = updatedAt
|
|
user.LastLoginAt = lastLoginAt
|
|
|
|
return &user, nil
|
|
}
|