190 lines
6.3 KiB
Go
190 lines
6.3 KiB
Go
package user
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"database/sql"
|
||
|
|
"log/slog"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Queriable interface {
|
||
|
|
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||
|
|
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
||
|
|
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||
|
|
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||
|
|
}
|
||
|
|
|
||
|
|
type Repository struct {
|
||
|
|
Db *sql.DB
|
||
|
|
q Queriable
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewRepository(db *sql.DB) (*Repository, error) {
|
||
|
|
return &Repository{Db: db, q: db}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) WithTx(tx *sql.Tx) *Repository {
|
||
|
|
return &Repository{Db: r.Db, q: tx}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) CreateUser(ctx context.Context, login, password string, isActive bool) (*User, error) {
|
||
|
|
query := "INSERT INTO users(login, password, fullname, email, is_admin, created_at, updated_at, activated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?) RETURNING id"
|
||
|
|
|
||
|
|
nowTime := time.Now().UTC()
|
||
|
|
|
||
|
|
var activatedAt *time.Time
|
||
|
|
if isActive {
|
||
|
|
activatedAt = &nowTime
|
||
|
|
}
|
||
|
|
|
||
|
|
user := &User{Login: login, Password: password, FullName: nil, Email: nil, IsAdmin: false, CreatedAt: nowTime, UpdatedAt: nowTime, ActivatedAt: activatedAt}
|
||
|
|
|
||
|
|
err := r.q.QueryRowContext(ctx, query, user.Login, user.Password, user.FullName, user.Email, user.IsAdmin, user.CreatedAt, user.UpdatedAt, user.ActivatedAt).Scan(&user.Id)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("create user", slog.String("op", "repo.CreateUser"), slog.String("login", login), slog.Any("error", err))
|
||
|
|
return nil, ErrInternal
|
||
|
|
}
|
||
|
|
|
||
|
|
return user, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) CountUsers(ctx context.Context, includeInactive bool) (int64, error) {
|
||
|
|
query := "SELECT COUNT(*) FROM users"
|
||
|
|
if !includeInactive {
|
||
|
|
query += " WHERE activated_at IS NOT NULL"
|
||
|
|
}
|
||
|
|
|
||
|
|
var result int64
|
||
|
|
err := r.q.QueryRowContext(ctx, query).Scan(&result)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("count users", slog.String("op", "repo.CountUsers"), slog.Any("error", err))
|
||
|
|
return 0, ErrInternal
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) Users(ctx context.Context, includeInactive bool, limit, offset int64) ([]*User, error) {
|
||
|
|
query := "SELECT id, login, password, fullname, email, is_admin, created_at, updated_at, activated_at FROM users"
|
||
|
|
var args []any
|
||
|
|
|
||
|
|
if !includeInactive {
|
||
|
|
query += " WHERE activated_at IS NOT NULL"
|
||
|
|
}
|
||
|
|
|
||
|
|
if limit > 0 {
|
||
|
|
query += " LIMIT ? OFFSET ?"
|
||
|
|
args = append(args, limit, offset)
|
||
|
|
}
|
||
|
|
|
||
|
|
rows, err := r.Db.QueryContext(ctx, query, args...)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("list users", slog.String("op", "repo.Users"), slog.Any("error", err))
|
||
|
|
return nil, ErrInternal
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
var users []*User
|
||
|
|
for rows.Next() {
|
||
|
|
user := &User{}
|
||
|
|
err := rows.Scan(&user.Id, &user.Login, &user.Password, &user.FullName, &user.Email,
|
||
|
|
&user.IsAdmin, &user.CreatedAt, &user.UpdatedAt, &user.ActivatedAt)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("scan user", slog.String("op", "repo.Users"), slog.Any("error", err))
|
||
|
|
return nil, ErrInternal
|
||
|
|
}
|
||
|
|
users = append(users, user)
|
||
|
|
}
|
||
|
|
|
||
|
|
err = rows.Err()
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("iterate users", slog.String("op", "repo.Users"), slog.Any("error", err))
|
||
|
|
return nil, ErrInternal
|
||
|
|
}
|
||
|
|
|
||
|
|
return users, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) UserById(ctx context.Context, id int64) (*User, error) {
|
||
|
|
query := "SELECT id, login, password, fullname, email, is_admin, created_at, updated_at, activated_at FROM users WHERE id = ?"
|
||
|
|
user := &User{}
|
||
|
|
err := r.q.QueryRowContext(ctx, query, id).Scan(&user.Id, &user.Login, &user.Password, &user.FullName, &user.Email,
|
||
|
|
&user.IsAdmin, &user.CreatedAt, &user.UpdatedAt, &user.ActivatedAt)
|
||
|
|
if err != nil {
|
||
|
|
return nil, ErrNotFound
|
||
|
|
}
|
||
|
|
return user, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) UserByLogin(ctx context.Context, login string) (*User, error) {
|
||
|
|
query := "SELECT id, login, password, fullname, email, is_admin, created_at, updated_at, activated_at FROM users WHERE login = ?"
|
||
|
|
user := &User{}
|
||
|
|
err := r.q.QueryRowContext(ctx, query, login).Scan(&user.Id, &user.Login, &user.Password, &user.FullName, &user.Email,
|
||
|
|
&user.IsAdmin, &user.CreatedAt, &user.UpdatedAt, &user.ActivatedAt)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("select", slog.String("op", "repo.UserByLogin"), slog.String("login", login), slog.Any("error", err))
|
||
|
|
return nil, ErrNotFound
|
||
|
|
}
|
||
|
|
return user, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) UpdateUser(ctx context.Context, user *User) error {
|
||
|
|
updatedAt := time.Now().UTC()
|
||
|
|
query := "UPDATE users SET password = ?, fullname = ?, email = ?, is_admin = ?, updated_at = ?, activated_at = ? WHERE id = ?"
|
||
|
|
_, err := r.q.ExecContext(ctx, query, user.Password, user.FullName, user.Email, user.IsAdmin, updatedAt, user.ActivatedAt, user.Id)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("update user", slog.String("op", "repo.UpdateUser"), slog.Int64("user_id", user.Id), slog.Any("error", err))
|
||
|
|
return ErrNotFound
|
||
|
|
}
|
||
|
|
user.UpdatedAt = updatedAt
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) ActivateUser(ctx context.Context, id int64) error {
|
||
|
|
query := "UPDATE users SET activated_at = ? WHERE id = ?"
|
||
|
|
_, err := r.q.ExecContext(ctx, query, time.Now().UTC(), id)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("activate user", slog.String("op", "repo.ActivateUser"), slog.Int64("user_id", id), slog.Any("error", err))
|
||
|
|
return ErrNotFound
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) DeleteUser(ctx context.Context, id int64) error {
|
||
|
|
query := "DELETE FROM users WHERE id = ?"
|
||
|
|
_, err := r.q.ExecContext(ctx, query, id)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("delete user", slog.String("op", "repo.DeleteUser"), slog.Int64("user_id", id), slog.Any("error", err))
|
||
|
|
return ErrInternal
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) Avatar(ctx context.Context, id int64) ([]byte, error) {
|
||
|
|
query := "SELECT data FROM avatars WHERE id = ?"
|
||
|
|
var data []byte
|
||
|
|
err := r.q.QueryRowContext(ctx, query, id).Scan(&data)
|
||
|
|
if err != nil {
|
||
|
|
return nil, ErrNotFound
|
||
|
|
}
|
||
|
|
return data, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) SetAvatar(ctx context.Context, id int64, data []byte) error {
|
||
|
|
query := "INSERT OR REPLACE INTO avatars (id, data) VALUES (?, ?)"
|
||
|
|
_, err := r.q.ExecContext(ctx, query, id, data)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("set avatar", slog.String("op", "repo.SetAvatar"), slog.Int64("user_id", id), slog.Any("error", err))
|
||
|
|
return ErrNotFound
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Repository) CleanupUsers(ctx context.Context) error {
|
||
|
|
query := "DELETE FROM users WHERE activated_at IS NULL and created_at < ?"
|
||
|
|
_, err := r.q.ExecContext(ctx, query, time.Now().UTC().Add(-5*time.Minute))
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("cleanup users", slog.String("op", "repo.CleanupUsers"), slog.Any("error", err))
|
||
|
|
return ErrInternal
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|