5d92820591
Timezone is now sent from the client on each request that needs it, instead of storing it in the database. This simplifies the model and allows timezone to be dynamic (useful for traveling users). Changes: - Remove timezone field from User entity - Remove timezone from user registration - GET /habits/today now requires ?timezone= query param - Add migration to drop timezone column from database - Update related tests
187 lines
4.1 KiB
Go
187 lines
4.1 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"apocapoc-api/internal/domain/entities"
|
|
"apocapoc-api/internal/shared/errors"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type UserRepository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewUserRepository(db *sql.DB) *UserRepository {
|
|
return &UserRepository{db: db}
|
|
}
|
|
|
|
func (r *UserRepository) Create(ctx context.Context, user *entities.User) error {
|
|
user.ID = uuid.New().String()
|
|
|
|
query := `
|
|
INSERT INTO users (id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
|
|
_, err := r.db.ExecContext(ctx, query,
|
|
user.ID,
|
|
user.Email,
|
|
user.PasswordHash,
|
|
user.EmailVerified,
|
|
user.EmailVerificationToken,
|
|
user.EmailVerificationExpiry,
|
|
user.CreatedAt,
|
|
user.UpdatedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
if isUniqueConstraintError(err) {
|
|
return errors.ErrAlreadyExists
|
|
}
|
|
return fmt.Errorf("failed to create user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *UserRepository) FindByID(ctx context.Context, id string) (*entities.User, error) {
|
|
query := `
|
|
SELECT id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at
|
|
FROM users
|
|
WHERE id = ?
|
|
`
|
|
|
|
var user entities.User
|
|
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
|
&user.ID,
|
|
&user.Email,
|
|
&user.PasswordHash,
|
|
&user.EmailVerified,
|
|
&user.EmailVerificationToken,
|
|
&user.EmailVerificationExpiry,
|
|
&user.CreatedAt,
|
|
&user.UpdatedAt,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find user: %w", err)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
|
query := `
|
|
SELECT id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at
|
|
FROM users
|
|
WHERE email = ?
|
|
`
|
|
|
|
var user entities.User
|
|
err := r.db.QueryRowContext(ctx, query, email).Scan(
|
|
&user.ID,
|
|
&user.Email,
|
|
&user.PasswordHash,
|
|
&user.EmailVerified,
|
|
&user.EmailVerificationToken,
|
|
&user.EmailVerificationExpiry,
|
|
&user.CreatedAt,
|
|
&user.UpdatedAt,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find user: %w", err)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *UserRepository) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) {
|
|
query := `
|
|
SELECT id, email, password_hash, email_verified, email_verification_token, email_verification_expiry, created_at, updated_at
|
|
FROM users
|
|
WHERE email_verification_token = ?
|
|
`
|
|
|
|
var user entities.User
|
|
err := r.db.QueryRowContext(ctx, query, token).Scan(
|
|
&user.ID,
|
|
&user.Email,
|
|
&user.PasswordHash,
|
|
&user.EmailVerified,
|
|
&user.EmailVerificationToken,
|
|
&user.EmailVerificationExpiry,
|
|
&user.CreatedAt,
|
|
&user.UpdatedAt,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find user by verification token: %w", err)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *UserRepository) Update(ctx context.Context, user *entities.User) error {
|
|
query := `
|
|
UPDATE users
|
|
SET email = ?, password_hash = ?, email_verified = ?, email_verification_token = ?, email_verification_expiry = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`
|
|
|
|
result, err := r.db.ExecContext(ctx, query,
|
|
user.Email,
|
|
user.PasswordHash,
|
|
user.EmailVerified,
|
|
user.EmailVerificationToken,
|
|
user.EmailVerificationExpiry,
|
|
user.UpdatedAt,
|
|
user.ID,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update user: %w", err)
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return errors.ErrNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
|
query := `DELETE FROM users WHERE id = ?`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete user: %w", err)
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return errors.ErrNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isUniqueConstraintError(err error) bool {
|
|
return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed")
|
|
}
|