151 lines
4.0 KiB
Go
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
|
|
}
|
|
|
|
|