1. CORS whitelist (was '*'): only buhapp.mygoodservice.ru, t.me, web.telegram.org 2. Rate limit on /auth/login (10/min/IP) and /auth/register (5/hour/IP) 3. TGHandler: removed unused JWTSecret, added WebhookSecret, real secret check 4. Login: constant-time bcrypt on user enumeration (dummy hash) 5. TgUsername saved in users (was lost) 6. User.IsVerified=true for Telegram users 7. Register: 409 instead of 500 on duplicate email 8. Bumped version to 0.4.0
158 lines
5.3 KiB
Go
158 lines
5.3 KiB
Go
package users
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/jackc/pgx/v5"
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
)
|
||
|
||
type User struct {
|
||
ID uuid.UUID `json:"id"`
|
||
Email string `json:"email,omitempty"`
|
||
Phone string `json:"phone,omitempty"`
|
||
TgID *int64 `json:"telegram_id,omitempty"`
|
||
TgUsername string `json:"tg_username,omitempty"`
|
||
Name string `json:"name"`
|
||
Birthdate *time.Time `json:"birthdate,omitempty"`
|
||
Gender string `json:"gender"`
|
||
City string `json:"city"`
|
||
Bio string `json:"bio"`
|
||
PhotoURL string `json:"photo_url,omitempty"`
|
||
IsVerified bool `json:"is_verified"`
|
||
IsBlocked bool `json:"is_blocked"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
||
RatingAvg float64 `json:"rating_avg,omitempty"`
|
||
RatingCount int `json:"rating_count,omitempty"`
|
||
}
|
||
|
||
type Repo struct {
|
||
pool *pgxpool.Pool
|
||
}
|
||
|
||
func NewRepo(pool *pgxpool.Pool) *Repo {
|
||
return &Repo{pool: pool}
|
||
}
|
||
|
||
func (r *Repo) CreateWithPassword(ctx context.Context, u *User, passwordHash string) error {
|
||
return r.pool.QueryRow(ctx, `
|
||
INSERT INTO users (email, phone, password_hash, name, birthdate, gender, city)
|
||
VALUES (NULLIF($1, ''), NULLIF($2, ''), $3, $4, $5, NULLIF($6, ''), NULLIF($7, ''))
|
||
RETURNING id, created_at, updated_at`,
|
||
u.Email, u.Phone, passwordHash, u.Name, u.Birthdate, u.Gender, u.City,
|
||
).Scan(&u.ID, &u.CreatedAt, &u.UpdatedAt)
|
||
}
|
||
|
||
func (r *Repo) UpdateRaw(ctx context.Context, sql string, args ...interface{}) (int64, error) {
|
||
tag, err := r.pool.Exec(ctx, sql, args...)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return tag.RowsAffected(), nil
|
||
}
|
||
|
||
// CreateWithTelegramID — создать пользователя с привязкой к Telegram
|
||
func (r *Repo) CreateWithTelegramID(ctx context.Context, u *User, tgID int64) error {
|
||
return r.pool.QueryRow(ctx, `
|
||
INSERT INTO users (id, telegram_id, tg_username, name, password_hash, is_verified)
|
||
VALUES ($1, $2, NULLIF($3, ''), $4, '', $5)
|
||
ON CONFLICT (telegram_id) DO UPDATE
|
||
SET name = EXCLUDED.name,
|
||
tg_username = EXCLUDED.tg_username,
|
||
updated_at = NOW()
|
||
RETURNING id, created_at, updated_at`,
|
||
u.ID, tgID, u.TgUsername, u.Name, u.IsVerified,
|
||
).Scan(&u.ID, &u.CreatedAt, &u.UpdatedAt)
|
||
}
|
||
|
||
func (r *Repo) GetByTelegramID(ctx context.Context, tgID int64) (*User, error) {
|
||
u := &User{}
|
||
var email, phone, gender, city, bio, photo *string
|
||
var birth *time.Time
|
||
var telegramID *int64
|
||
err := r.pool.QueryRow(ctx, `
|
||
SELECT id, email, phone, telegram_id, name, birthdate, gender, city, bio, photo_url,
|
||
is_verified, is_blocked, created_at, updated_at, last_seen_at
|
||
FROM users WHERE telegram_id=$1`, tgID,
|
||
).Scan(&u.ID, &email, &phone, &telegramID, &u.Name, &birth, &gender, &city, &bio, &photo,
|
||
&u.IsVerified, &u.IsBlocked, &u.CreatedAt, &u.UpdatedAt, &u.LastSeenAt)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if email != nil { u.Email = *email }
|
||
if phone != nil { u.Phone = *phone }
|
||
if telegramID != nil { u.TgID = telegramID }
|
||
if gender != nil { u.Gender = *gender }
|
||
if city != nil { u.City = *city }
|
||
if bio != nil { u.Bio = *bio }
|
||
if photo != nil { u.PhotoURL = *photo }
|
||
if birth != nil { u.Birthdate = birth }
|
||
return u, nil
|
||
}
|
||
|
||
func (r *Repo) TouchLastSeen(ctx context.Context, id uuid.UUID) error {
|
||
_, err := r.pool.Exec(ctx, `UPDATE users SET last_seen_at=NOW() WHERE id=$1`, id)
|
||
return err
|
||
}
|
||
|
||
func (r *Repo) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
|
||
u := &User{}
|
||
var email, phone, gender, city, bio, photo *string
|
||
var birth *time.Time
|
||
err := r.pool.QueryRow(ctx, `
|
||
SELECT id, email, phone, name, birthdate, gender, city, bio, photo_url,
|
||
is_verified, is_blocked, created_at, updated_at, last_seen_at
|
||
FROM users WHERE id=$1`, id,
|
||
).Scan(&u.ID, &email, &phone, &u.Name, &birth, &gender, &city, &bio, &photo,
|
||
&u.IsVerified, &u.IsBlocked, &u.CreatedAt, &u.UpdatedAt, &u.LastSeenAt)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, nil
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if email != nil { u.Email = *email }
|
||
if phone != nil { u.Phone = *phone }
|
||
if gender != nil { u.Gender = *gender }
|
||
if city != nil { u.City = *city }
|
||
if bio != nil { u.Bio = *bio }
|
||
if photo != nil { u.PhotoURL = *photo }
|
||
if birth != nil { u.Birthdate = birth }
|
||
return u, nil
|
||
}
|
||
|
||
func (r *Repo) GetByEmail(ctx context.Context, email string) (*User, string, error) {
|
||
u := &User{}
|
||
var hash string
|
||
var em *string
|
||
var birth *time.Time
|
||
var gender, city, bio, photo *string
|
||
err := r.pool.QueryRow(ctx, `
|
||
SELECT id, email, password_hash, name, birthdate, gender, city, bio, photo_url,
|
||
is_verified, is_blocked, created_at, updated_at, last_seen_at
|
||
FROM users WHERE email=$1`, email,
|
||
).Scan(&u.ID, &em, &hash, &u.Name, &birth, &gender, &city, &bio, &photo,
|
||
&u.IsVerified, &u.IsBlocked, &u.CreatedAt, &u.UpdatedAt, &u.LastSeenAt)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, "", nil
|
||
}
|
||
if err != nil {
|
||
return nil, "", err
|
||
}
|
||
if em != nil { u.Email = *em }
|
||
if gender != nil { u.Gender = *gender }
|
||
if city != nil { u.City = *city }
|
||
if bio != nil { u.Bio = *bio }
|
||
if photo != nil { u.PhotoURL = *photo }
|
||
if birth != nil { u.Birthdate = birth }
|
||
return u, hash, nil
|
||
}
|