first commit

This commit is contained in:
2026-07-16 10:13:46 +03:30
commit 423c528b2f
42 changed files with 7298 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
package models
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
)
// ErrUserNotFound is a sentinel error returned by any lookup method below
// when no matching row exists. Callers use errors.Is(err, ErrUserNotFound)
// to distinguish "not found" (an expected, normal outcome - e.g. checking
// if an email is already registered) from a real database failure.
var ErrUserNotFound = errors.New("user not found")
// UserRepository is the ONLY place in the entire application that writes
// SQL queries for the users table. Every handler that needs to read or
// write user data goes through here instead of touching *sql.DB directly.
//
// Why bother with this layer instead of just calling db.Query in handlers?
// - If you ever swap MySQL for Postgres (or add a cache in front), you
// change this one file - no handler code needs to know or care.
// - It gives handlers a small, purpose-built vocabulary (Create,
// FindByEmail, FindByID, SetGoogleID) instead of raw SQL strings
// scattered across the codebase.
type UserRepository struct {
db *sql.DB
}
// NewUserRepository is the constructor. Go doesn't have classes/constructors
// built into the language - "NewXxx returns a *Xxx" is just a naming
// convention the whole ecosystem follows.
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
// Create inserts a new user row. It takes a *User (pointer) specifically so
// it can write the newly generated auto-increment ID back into the
// caller's struct after the insert succeeds - the caller passes in a User
// with ID == 0, and walks away with u.ID populated.
func (r *UserRepository) Create(ctx context.Context, u *User) error {
res, err := r.db.ExecContext(ctx,
"INSERT INTO users (email, password_hash, google_id, created_at) VALUES (?, ?, ?, ?)",
u.Email, u.PasswordHash, u.GoogleID, time.Now(),
)
if err != nil {
return fmt.Errorf("create user: %w", err)
}
id, err := res.LastInsertId()
if err != nil {
return fmt.Errorf("get last insert id: %w", err)
}
u.ID = int(id)
return nil
}
// FindByEmail looks up a user by their email address. This is what the
// login flow uses: the user submits an email, we look up the matching row,
// then compare the submitted password against the stored PasswordHash.
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx,
"SELECT id, email, password_hash, google_id, created_at FROM users WHERE email = ?", email,
).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.GoogleID, &u.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
// sql.ErrNoRows is the driver's own sentinel for "query matched
// zero rows". We translate it into OUR sentinel (ErrUserNotFound)
// so callers never need to know or care that the underlying
// storage is SQL at all.
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("find user by email: %w", err)
}
return &u, nil
}
// FindByID looks up a user by their numeric ID. This is used after a
// session is validated: the session only stores the user's ID, so we look
// the rest of the user up fresh on every authenticated request (see
// middleware.RequireAuth).
func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx,
"SELECT id, email, password_hash, google_id, created_at FROM users WHERE id = ?", id,
).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.GoogleID, &u.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("find user by id: %w", err)
}
return &u, nil
}
// SetGoogleID links a Google account to an existing user row - used the
// first time a user who originally registered with a password logs in via
// "Sign in with Google" using the same email address.
func (r *UserRepository) SetGoogleID(ctx context.Context, userID int, googleID string) error {
_, err := r.db.ExecContext(ctx,
"UPDATE users SET google_id = ? WHERE id = ?", googleID, userID,
)
if err != nil {
return fmt.Errorf("set google id: %w", err)
}
return nil
}