package repository import ( "context" "errors" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // AuthRepo persists account credentials and sessions. type AuthRepo struct { Pool *pgxpool.Pool } // AccountRow is a registered user snapshot. type AccountRow struct { ID uuid.UUID Phone string PasswordHash string Nickname string Status string } // GetByPhone loads a registered user by phone. func (r *AuthRepo) GetByPhone(ctx context.Context, phone string) (*AccountRow, error) { row := &AccountRow{} var nick *string err := r.Pool.QueryRow(ctx, ` SELECT id, phone, password_hash, COALESCE(nickname,''), status FROM users WHERE phone=$1 AND deleted_at IS NULL`, phone, ).Scan(&row.ID, &row.Phone, &row.PasswordHash, &nick, &row.Status) if errors.Is(err, pgx.ErrNoRows) { return nil, err } if err != nil { return nil, err } if nick != nil { row.Nickname = *nick } return row, nil } // GetAccount loads account fields for a user id. func (r *AuthRepo) GetAccount(ctx context.Context, userID uuid.UUID) (*AccountRow, error) { row := &AccountRow{} var phone, hash, nick *string err := r.Pool.QueryRow(ctx, ` SELECT id, phone, password_hash, nickname, status FROM users WHERE id=$1 AND deleted_at IS NULL`, userID, ).Scan(&row.ID, &phone, &hash, &nick, &row.Status) if err != nil { return nil, err } if phone != nil { row.Phone = *phone } if hash != nil { row.PasswordHash = *hash } if nick != nil { row.Nickname = *nick } return row, nil } // RegisterOnUser upgrades an anonymous user with phone credentials. func (r *AuthRepo) RegisterOnUser(ctx context.Context, userID uuid.UUID, phone, hash, nickname string) error { tag, err := r.Pool.Exec(ctx, ` UPDATE users SET phone=$2, password_hash=$3, nickname=NULLIF($4,''), updated_at=now() WHERE id=$1 AND deleted_at IS NULL AND phone IS NULL`, userID, phone, hash, nickname, ) if err != nil { return err } if tag.RowsAffected() == 0 { return errString("register conflict") } return nil } // CreateUserWithPhone inserts a new registered user. func (r *AuthRepo) CreateUserWithPhone(ctx context.Context, phone, hash, nickname string) (uuid.UUID, error) { var id uuid.UUID err := r.Pool.QueryRow(ctx, ` INSERT INTO users(phone, password_hash, nickname) VALUES ($1,$2,NULLIF($3,'')) RETURNING id`, phone, hash, nickname, ).Scan(&id) return id, err } // TouchPassword updates stored password hash (open-login record). func (r *AuthRepo) TouchPassword(ctx context.Context, userID uuid.UUID, hash string) error { _, err := r.Pool.Exec(ctx, ` UPDATE users SET password_hash=$2, updated_at=now() WHERE id=$1 AND deleted_at IS NULL`, userID, hash) return err } // BindDevice sets device_identities.user_id to account. func (r *AuthRepo) BindDevice(ctx context.Context, deviceKey string, userID uuid.UUID) error { _, err := r.Pool.Exec(ctx, ` INSERT INTO device_identities(device_key, user_id) VALUES ($1,$2) ON CONFLICT (device_key) DO UPDATE SET user_id=$2, updated_at=now(), deleted_at=NULL`, deviceKey, userID, ) return err } // CreateSession inserts a session token. func (r *AuthRepo) CreateSession(ctx context.Context, userID uuid.UUID, token string, expires time.Time) error { _, err := r.Pool.Exec(ctx, ` INSERT INTO user_sessions(user_id, token, expires_at) VALUES ($1,$2,$3)`, userID, token, expires, ) return err } // UserIDByToken resolves a live session. func (r *AuthRepo) UserIDByToken(ctx context.Context, token string) (uuid.UUID, error) { var id uuid.UUID err := r.Pool.QueryRow(ctx, ` SELECT user_id FROM user_sessions WHERE token=$1 AND revoked_at IS NULL AND expires_at > now()`, token, ).Scan(&id) return id, err } // RevokeSession marks token revoked. func (r *AuthRepo) RevokeSession(ctx context.Context, token string) error { _, err := r.Pool.Exec(ctx, ` UPDATE user_sessions SET revoked_at=now() WHERE token=$1 AND revoked_at IS NULL`, token) return err } // IsRegistered reports whether user has phone. func (r *AuthRepo) IsRegistered(ctx context.Context, userID uuid.UUID) (bool, error) { var ok bool err := r.Pool.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM users WHERE id=$1 AND phone IS NOT NULL AND deleted_at IS NULL )`, userID).Scan(&ok) return ok, err }