first commit
This commit is contained in:
150
internal/user/repo.go
Normal file
150
internal/user/repo.go
Normal file
@@ -0,0 +1,150 @@
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user