feat(users): user repository

This commit is contained in:
Guz
2025-05-30 18:03:56 -03:00
parent acda6dbd24
commit dbf30a9908
2 changed files with 179 additions and 0 deletions

13
model/user.go Normal file
View File

@@ -0,0 +1,13 @@
package model
import (
"time"
)
type User struct {
Username string `json:"username"` // Must be unique
Password []byte `json:"password"`
DateCreated time.Time `json:"date_created"`
DateUpdated time.Time `json:"date_updated"`
}

166
repository/users.go Normal file
View File

@@ -0,0 +1,166 @@
package repository
import (
"context"
"database/sql"
"encoding/base64"
"errors"
"log/slog"
"time"
"forge.capytal.company/capytalcode/project-comicverse/model"
"forge.capytal.company/loreddev/x/tinyssert"
)
type UserRepository struct {
db *sql.DB
ctx context.Context
log *slog.Logger
assert tinyssert.Assertions
}
func NewUserRepository(
db *sql.DB,
ctx context.Context,
logger *slog.Logger,
assert tinyssert.Assertions,
) (*UserRepository, error) {
assert.NotNil(db)
assert.NotNil(logger)
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL PRIMARY KEY,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
)`)
if err != nil {
return nil, err
}
return &UserRepository{
db: db,
log: logger,
assert: assert,
}, nil
}
func (r *UserRepository) Create(u model.User) (model.User, error) {
tx, err := r.db.BeginTx(r.ctx, nil)
if err != nil {
return model.User{}, err
}
q := `
INSERT INTO users (username, password_hash, created_at, updated_at)
VALUES (:username, :password_hash, :created_at, :updated_at)
`
log := r.log.With(slog.String("username", u.Username), slog.String("query", q))
log.DebugContext(r.ctx, "Inserting new user")
t := time.Now()
passwd := base64.URLEncoding.EncodeToString(u.Password)
_, err = tx.ExecContext(r.ctx, q,
sql.Named("username", u.Username),
sql.Named("password_hash", passwd),
sql.Named("created_at", t.Format(repositoryDateFormat)),
sql.Named("updated_at", t.Format(repositoryDateFormat)))
if err != nil {
log.ErrorContext(r.ctx, "Failed to create user", slog.String("error", err.Error()))
return model.User{}, nil
}
if err := tx.Commit(); err != nil {
log.ErrorContext(r.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
return model.User{}, err
}
return u, nil
}
func (r *UserRepository) GetByUsername(username string) (model.User, error) {
tx, err := r.db.BeginTx(r.ctx, nil)
if err != nil {
return model.User{}, err
}
q := `
SELECT FROM users (username, password_hash, created_at, updated_at)
WHERE username = :username
`
log := r.log.With(slog.String("username", username), slog.String("query", q))
log.DebugContext(r.ctx, "Querying user")
row := tx.QueryRowContext(r.ctx, q, sql.Named("username", username))
var password_hash, dateCreated, dateUpdated string
if err = row.Scan(&username, &password_hash, &dateCreated, &dateUpdated); err != nil {
return model.User{}, err
}
if err := tx.Commit(); err != nil {
log.ErrorContext(r.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
return model.User{}, err
}
passwd, err := base64.URLEncoding.DecodeString(password_hash)
if err != nil {
return model.User{}, err
}
c, err := time.Parse(repositoryDateFormat, dateCreated)
if err != nil {
return model.User{}, errors.Join(ErrInvalidData, err)
}
u, err := time.Parse(repositoryDateFormat, dateUpdated)
if err != nil {
return model.User{}, errors.Join(ErrInvalidData, err)
}
return model.User{
Username: username,
Password: passwd,
DateCreated: c,
DateUpdated: u,
}, nil
}
func (r *UserRepository) Delete(u model.User) error {
tx, err := r.db.BeginTx(r.ctx, nil)
if err != nil {
return err
}
q := `
DELETE FROM users WHERE username = :username
`
log := r.log.With(slog.String("username", u.Username), slog.String("query", q))
log.DebugContext(r.ctx, "Deleting user")
_, err = tx.ExecContext(r.ctx, q, sql.Named("username", u.Username))
if err != nil {
log.ErrorContext(r.ctx, "Failed to delete user", slog.String("error", err.Error()))
return err
}
if err := tx.Commit(); err != nil {
log.ErrorContext(r.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
return err
}
return nil
}
var (
ErrNotFound = sql.ErrNoRows
ErrInvalidData = errors.New("model was saved with invalid data")
repositoryDateFormat = time.RFC3339
)