Files
Nip-05-api/internal/user/repo.go
2026-04-29 02:35:00 +00:00

151 lines
4.0 KiB
Go

package user
import (
"context"
"database/sql"
"errors"
"time"
"github.com/noderunners/nip05api/internal/db"
)
type Repo struct{ db *db.DB }
func NewRepo(d *db.DB) *Repo { return &Repo{db: d} }
func parseTime(s sql.NullString) *time.Time {
if !s.Valid || s.String == "" {
return nil
}
t, err := time.Parse(time.RFC3339, s.String)
if err != nil {
t, err = time.Parse("2006-01-02 15:04:05", s.String)
if err != nil {
return nil
}
}
return &t
}
func mustParseTime(s string) time.Time {
t, err := time.Parse(time.RFC3339, s)
if err == nil {
return t
}
t, err = time.Parse("2006-01-02 15:04:05", s)
if err == nil {
return t
}
return time.Time{}
}
func formatTime(t *time.Time) any {
if t == nil {
return nil
}
return t.UTC().Format(time.RFC3339)
}
func scanUser(row interface {
Scan(dest ...any) error
}) (*User, error) {
var u User
var sub string
var expiresAt, lastSynced, reminderSent, deactivatedAt, createdAt sql.NullString
if err := row.Scan(
&u.ID, &u.Pubkey, &u.Username, &sub,
&expiresAt, &u.IsActive, &u.ManualUsername,
&lastSynced, &reminderSent, &deactivatedAt, &createdAt,
); err != nil {
return nil, err
}
u.SubscriptionType = SubscriptionType(sub)
u.ExpiresAt = parseTime(expiresAt)
u.LastSyncedAt = parseTime(lastSynced)
u.ExpiringReminderSentAt = parseTime(reminderSent)
u.DeactivatedAt = parseTime(deactivatedAt)
if t := parseTime(createdAt); t != nil {
u.CreatedAt = *t
}
return &u, nil
}
const userCols = `id, pubkey, username, subscription_type, expires_at, is_active, manual_username, last_synced_at, expiring_reminder_sent_at, deactivated_at, created_at`
func (r *Repo) GetByPubkey(ctx context.Context, pubkey string) (*User, error) {
row := r.db.QueryRowContext(ctx, `SELECT `+userCols+` FROM users WHERE pubkey = ?`, pubkey)
u, err := scanUser(row)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return u, err
}
func (r *Repo) GetByUsername(ctx context.Context, username string) (*User, error) {
row := r.db.QueryRowContext(ctx, `SELECT `+userCols+` FROM users WHERE username = ? COLLATE NOCASE`, username)
u, err := scanUser(row)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return u, err
}
func (r *Repo) Insert(ctx context.Context, u *User) error {
now := time.Now().UTC()
res, err := r.db.ExecContext(ctx, `INSERT INTO users
(pubkey, username, subscription_type, expires_at, is_active, manual_username, created_at)
VALUES (?, ?, ?, ?, 1, ?, ?)`,
u.Pubkey, u.Username, string(u.SubscriptionType),
formatTime(u.ExpiresAt), u.ManualUsername,
now.Format(time.RFC3339))
if err != nil {
return err
}
id, err := res.LastInsertId()
if err != nil {
return err
}
u.ID = id
u.CreatedAt = now
return nil
}
func (r *Repo) Update(ctx context.Context, u *User) error {
_, err := r.db.ExecContext(ctx, `UPDATE users SET
username = ?,
subscription_type = ?,
expires_at = ?,
is_active = ?,
manual_username = ?,
last_synced_at = ?,
expiring_reminder_sent_at = ?,
deactivated_at = ?
WHERE pubkey = ?`,
u.Username, string(u.SubscriptionType),
formatTime(u.ExpiresAt), u.IsActive, u.ManualUsername,
formatTime(u.LastSyncedAt), formatTime(u.ExpiringReminderSentAt),
formatTime(u.DeactivatedAt), u.Pubkey)
return err
}
func (r *Repo) Delete(ctx context.Context, pubkey string) error {
_, err := r.db.ExecContext(ctx, `DELETE FROM users WHERE pubkey = ?`, pubkey)
return err
}
// SetActiveExpiry sets a user's expires_at to an absolute value and reactivates
// them. Idempotent: applying the same input twice produces the same end state.
func (r *Repo) SetActiveExpiry(ctx context.Context, pubkey string, sub SubscriptionType, expiresAt *time.Time) error {
_, err := r.db.ExecContext(ctx, `UPDATE users SET
is_active = 1,
deactivated_at = NULL,
expiring_reminder_sent_at = NULL,
subscription_type = ?,
expires_at = ?
WHERE pubkey = ?`,
string(sub), formatTime(expiresAt), pubkey)
return err
}