package database import ( "database/sql" "errors" "time" "git.noroutes.net/NoRoutes/vibetunes/internal/idgen" ) type Session struct { ID string UserID string CreatedAt time.Time ExpiresAt time.Time UserAgent string IPAddress string } func CreateSession(db *sql.DB, userID string, ttl time.Duration, userAgent, ip string) (*Session, error) { id := idgen.New("se") expires := time.Now().Add(ttl).UTC() _, err := db.Exec(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address) VALUES (?, ?, ?, ?, ?)`, id, userID, expires.Format(timeLayout), userAgent, ip) if err != nil { return nil, err } return GetSession(db, id) } const timeLayout = "2006-01-02T15:04:05.000Z" func GetSession(db *sql.DB, id string) (*Session, error) { var s Session var createdAt, expiresAt string err := db.QueryRow(`SELECT id, user_id, created_at, expires_at, user_agent, ip_address FROM sessions WHERE id = ?`, id). Scan(&s.ID, &s.UserID, &createdAt, &expiresAt, &s.UserAgent, &s.IPAddress) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } s.CreatedAt, _ = parseTime(createdAt) s.ExpiresAt, _ = parseTime(expiresAt) return &s, nil } func DeleteSession(db *sql.DB, id string) error { _, err := db.Exec(`DELETE FROM sessions WHERE id = ?`, id) return err } // DeleteUserSessions revokes every session for a user (e.g. on password // change or "log out everywhere"). func DeleteUserSessions(db *sql.DB, userID string) error { _, err := db.Exec(`DELETE FROM sessions WHERE user_id = ?`, userID) return err } func ListUserSessions(db *sql.DB, userID string) ([]*Session, error) { rows, err := db.Query(`SELECT id, user_id, created_at, expires_at, user_agent, ip_address FROM sessions WHERE user_id = ? ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var out []*Session for rows.Next() { var s Session var createdAt, expiresAt string if err := rows.Scan(&s.ID, &s.UserID, &createdAt, &expiresAt, &s.UserAgent, &s.IPAddress); err != nil { return nil, err } s.CreatedAt, _ = parseTime(createdAt) s.ExpiresAt, _ = parseTime(expiresAt) out = append(out, &s) } return out, rows.Err() } // DeleteExpiredSessions purges sessions past their expiry, keeping the // table small over a long-running server's lifetime. func DeleteExpiredSessions(db *sql.DB) error { _, err := db.Exec(`DELETE FROM sessions WHERE expires_at < strftime('%Y-%m-%dT%H:%M:%fZ','now')`) return err }