fix(auth): wiring Auth/Sessions, нормализация email, GetUserByID для /me, 409 на дубль, timing-guard логина
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -57,6 +58,41 @@ func TestGetUserByEmail_FindsRegisteredUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegisterUser_DuplicateEmailReturnsErrEmailTaken verifies the fix for
|
||||
// the duplicate-registration gap: a second RegisterUser call for an
|
||||
// already-taken email must fail with the ErrEmailTaken sentinel (mapped from
|
||||
// the UNIQUE constraint violation on users.email), not a generic pgx error.
|
||||
func TestRegisterUser_DuplicateEmailReturnsErrEmailTaken(t *testing.T) {
|
||||
s, ctx := newStore(t)
|
||||
|
||||
if _, _, err := s.RegisterUser(ctx, "dup@example.com", "argon2-hash"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, _, err := s.RegisterUser(ctx, "dup@example.com", "argon2-hash"); !errors.Is(err, ErrEmailTaken) {
|
||||
t.Fatalf("expected ErrEmailTaken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetUserByID_ReturnsUser verifies the fix for the /me gap: GetUserByID
|
||||
// returns the same user created by RegisterUser, including their real email.
|
||||
func TestGetUserByID_ReturnsUser(t *testing.T) {
|
||||
s, ctx := newStore(t)
|
||||
|
||||
u, _, err := s.RegisterUser(ctx, "gina@example.com", "argon2-hash")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := s.GetUserByID(ctx, u.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.ID != u.ID || got.Email != "gina@example.com" {
|
||||
t.Fatalf("unexpected user: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSessionLifecycle_CreateGetDelete verifies CreateSession + GetSessionUser
|
||||
// round-trips to the owning user ID, an expired session is excluded from
|
||||
// GetSessionUser, and DeleteSession removes the session.
|
||||
|
||||
@@ -48,3 +48,19 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, email, created_at, password_hash FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.CreatedAt,
|
||||
&i.PasswordHash,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -3,3 +3,6 @@ INSERT INTO users (id, email, password_hash) VALUES ($1, $2, $3) RETURNING *;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT * FROM users WHERE email = $1;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users WHERE id = $1;
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"github.com/vasyakrg/dns-autoresolver/internal/provider"
|
||||
@@ -14,6 +15,11 @@ import (
|
||||
"github.com/vasyakrg/dns-autoresolver/internal/store/dto"
|
||||
)
|
||||
|
||||
// ErrEmailTaken is returned by RegisterUser when the email is already
|
||||
// registered — a UNIQUE constraint violation (pgerrcode 23505) on
|
||||
// users.email.
|
||||
var ErrEmailTaken = errors.New("store: email already registered")
|
||||
|
||||
// Account/Template/Domain are provider-neutral domain structs returned by the
|
||||
// thin wrappers below, so callers (internal/api) never need to import
|
||||
// internal/store/db directly.
|
||||
@@ -279,6 +285,17 @@ func (s *Store) GetUserByEmail(ctx context.Context, email string) (User, error)
|
||||
return toUser(u), nil
|
||||
}
|
||||
|
||||
// GetUserByID looks up a user by primary key — used by handleMe (Task 3
|
||||
// hardening) to return the authenticated caller's real email instead of
|
||||
// leaving it blank.
|
||||
func (s *Store) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
|
||||
u, err := s.q.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
return toUser(u), nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateProjectForUser(ctx context.Context, userID uuid.UUID, name string) (Project, error) {
|
||||
p, err := s.q.CreateProject(ctx, db.CreateProjectParams{ID: uuid.New(), UserID: userID, Name: name})
|
||||
if err != nil {
|
||||
@@ -337,6 +354,10 @@ func (s *Store) RegisterUser(ctx context.Context, email, passwordHash string) (U
|
||||
uid := uuid.New()
|
||||
dbu, err := q.CreateUser(ctx, db.CreateUserParams{ID: uid, Email: email, PasswordHash: ptr(passwordHash)})
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return User{}, Project{}, ErrEmailTaken
|
||||
}
|
||||
return User{}, Project{}, err
|
||||
}
|
||||
dbp, err := q.CreateProject(ctx, db.CreateProjectParams{ID: uuid.New(), UserID: uid, Name: "default"})
|
||||
|
||||
Reference in New Issue
Block a user