212 lines
5.1 KiB
Go
212 lines
5.1 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"errors"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"code.capytal.cc/capytal/comicverse/model"
|
|
"code.capytal.cc/loreddev/x/tinyssert"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type User struct {
|
|
baseRepostiory
|
|
}
|
|
|
|
func NewUser(
|
|
ctx context.Context,
|
|
db *sql.DB,
|
|
logger *slog.Logger,
|
|
assert tinyssert.Assertions,
|
|
) (*User, error) {
|
|
assert.NotNil(ctx)
|
|
assert.NotNil(db)
|
|
assert.NotNil(logger)
|
|
|
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b := newBaseRepostiory(ctx, db, logger, assert)
|
|
|
|
return &User{
|
|
baseRepostiory: b,
|
|
}, nil
|
|
}
|
|
|
|
func (repo *User) Create(u model.User) (model.User, error) {
|
|
repo.assert.NotNil(repo.db)
|
|
repo.assert.NotNil(repo.log)
|
|
repo.assert.NotNil(repo.ctx)
|
|
|
|
if err := u.Validate(); err != nil {
|
|
return model.User{}, errors.Join(ErrInvalidInput, err)
|
|
}
|
|
|
|
tx, err := repo.db.BeginTx(repo.ctx, nil)
|
|
if err != nil {
|
|
return model.User{}, errors.Join(ErrDatabaseConn, err)
|
|
}
|
|
|
|
q := `
|
|
INSERT INTO users (id, username, password_hash, created_at, updated_at)
|
|
VALUES (:id, :username, :password_hash, :created_at, :updated_at)
|
|
`
|
|
|
|
log := repo.log.With(
|
|
slog.String("id", u.ID.String()),
|
|
slog.String("username", u.Username),
|
|
slog.String("query", q))
|
|
log.DebugContext(repo.ctx, "Inserting new user")
|
|
|
|
t := time.Now()
|
|
|
|
passwd := base64.URLEncoding.EncodeToString(u.Password)
|
|
|
|
_, err = tx.ExecContext(repo.ctx, q,
|
|
sql.Named("id", u.ID),
|
|
sql.Named("username", u.Username),
|
|
sql.Named("password_hash", passwd),
|
|
sql.Named("created_at", t.Format(dateFormat)),
|
|
sql.Named("updated_at", t.Format(dateFormat)))
|
|
if err != nil {
|
|
log.ErrorContext(repo.ctx, "Failed to create user", slog.String("error", err.Error()))
|
|
return model.User{}, errors.Join(ErrExecuteQuery, err)
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
|
|
return model.User{}, errors.Join(ErrCommitQuery, err)
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func (repo *User) GetByID(id uuid.UUID) (model.User, error) {
|
|
repo.assert.NotNil(repo.db)
|
|
repo.assert.NotNil(repo.log)
|
|
repo.assert.NotNil(repo.ctx)
|
|
|
|
q := `
|
|
SELECT id, username, password_hash, created_at, updated_at FROM users
|
|
WHERE id = :id
|
|
`
|
|
|
|
log := repo.log.With(
|
|
slog.String("id", id.String()),
|
|
slog.String("query", q))
|
|
log.DebugContext(repo.ctx, "Querying user")
|
|
|
|
row := repo.db.QueryRowContext(repo.ctx, q, sql.Named("username", id))
|
|
|
|
user, err := repo.scan(row)
|
|
if err != nil {
|
|
log.ErrorContext(repo.ctx, "Failed to query user", slog.String("error", err.Error()))
|
|
return model.User{}, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (repo *User) GetByUsername(username string) (model.User, error) {
|
|
repo.assert.NotNil(repo.db)
|
|
repo.assert.NotNil(repo.log)
|
|
repo.assert.NotNil(repo.ctx)
|
|
|
|
q := `
|
|
SELECT id, username, password_hash, created_at, updated_at FROM users
|
|
WHERE username = :username
|
|
`
|
|
|
|
log := repo.log.With(
|
|
slog.String("username", username),
|
|
slog.String("query", q))
|
|
log.DebugContext(repo.ctx, "Querying user")
|
|
|
|
row := repo.db.QueryRowContext(repo.ctx, q, sql.Named("username", username))
|
|
|
|
user, err := repo.scan(row)
|
|
if err != nil {
|
|
log.ErrorContext(repo.ctx, "Failed to query user", slog.String("error", err.Error()))
|
|
return model.User{}, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (repo *User) scan(row scan) (model.User, error) {
|
|
var user model.User
|
|
var password_hashStr, createdStr, updatedStr string
|
|
err := row.Scan(&user.ID, &user.Username, &password_hashStr, &createdStr, &updatedStr)
|
|
if err != nil {
|
|
return model.User{}, errors.Join(ErrExecuteQuery, err)
|
|
}
|
|
|
|
passwd, err := base64.URLEncoding.DecodeString(password_hashStr)
|
|
if err != nil {
|
|
return model.User{}, errors.Join(ErrInvalidOutput, err)
|
|
}
|
|
|
|
created, err := time.Parse(dateFormat, createdStr)
|
|
if err != nil {
|
|
return model.User{}, errors.Join(ErrInvalidOutput, err)
|
|
}
|
|
|
|
updated, err := time.Parse(dateFormat, updatedStr)
|
|
if err != nil {
|
|
return model.User{}, errors.Join(ErrInvalidOutput, err)
|
|
}
|
|
|
|
user.Password = passwd
|
|
user.DateCreated = created
|
|
user.DateUpdated = updated
|
|
|
|
if err := user.Validate(); err != nil {
|
|
return model.User{}, errors.Join(ErrInvalidOutput, err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (repo *User) DeleteByID(id uuid.UUID) error {
|
|
repo.assert.NotNil(repo.db)
|
|
repo.assert.NotNil(repo.log)
|
|
repo.assert.NotNil(repo.ctx)
|
|
|
|
tx, err := repo.db.BeginTx(repo.ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
q := `
|
|
DELETE FROM users WHERE id = :id
|
|
`
|
|
|
|
log := repo.log.With(slog.String("id", id.String()), slog.String("query", q))
|
|
log.DebugContext(repo.ctx, "Deleting user")
|
|
|
|
_, err = tx.ExecContext(repo.ctx, q, sql.Named("id", id))
|
|
if err != nil {
|
|
log.ErrorContext(repo.ctx, "Failed to delete user", slog.String("error", err.Error()))
|
|
return errors.Join(ErrExecuteQuery, err)
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
|
|
return errors.Join(ErrCommitQuery, err)
|
|
}
|
|
|
|
return nil
|
|
}
|