mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
refactor(server/db): delegate users, sessions, profile to dbgen (D2)
Convert the auth_queries.go user + session reads/writes and profile_queries.go to the sqlc-generated layer, adding shared userFromGen/sessionFromGen mappers (db/mappers.go) for the int64/*string -> int/bool/string domain-model narrowing. Delegated: GetUserByID, GetUserByUsername, UpdateUserStatus, UpdateUserTOTPSecret, ResetAllUserStatuses, BanUser, UnbanUser, ListMembers, CreateSession (EvictOldestSessions + InsertSession), GetSessionByTokenHash, GetSessionWithBanStatus, DeleteSession, DeleteOtherSessions, DeleteExpiredSessions, TouchSession, UpdateUserProfile, UpdateUserPassword, ListUserSessions, DeleteSessionByID (query changed to :execresult so the RowsAffected ErrNotFound check is preserved). Behavior and public signatures unchanged. Verified: go build ./...; go test ./db ./service ./auth; sqlc-verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
This commit is contained in:
+81
-132
@@ -7,6 +7,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/owncord/server/db/dbgen"
|
||||
)
|
||||
|
||||
// ─── User Operations ──────────────────────────────────────────────────────────
|
||||
@@ -119,53 +121,34 @@ func (d *DB) CreateUserWithInvite(username, passwordHash string, roleID int, inv
|
||||
// GetUserByUsername returns the user with the given username (case-insensitive),
|
||||
// or nil if not found.
|
||||
func (d *DB) GetUserByUsername(username string) (*User, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT id, username, password, avatar, role_id, totp_secret, status,
|
||||
created_at, last_seen, banned, ban_reason, ban_expires
|
||||
FROM users WHERE username = ? COLLATE NOCASE`,
|
||||
username,
|
||||
)
|
||||
return scanUser(row)
|
||||
}
|
||||
|
||||
// GetUserByID returns the user with the given ID, or nil if not found.
|
||||
func (d *DB) GetUserByID(id int64) (*User, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT id, username, password, avatar, role_id, totp_secret, status,
|
||||
created_at, last_seen, banned, ban_reason, ban_expires
|
||||
FROM users WHERE id = ?`,
|
||||
id,
|
||||
)
|
||||
return scanUser(row)
|
||||
}
|
||||
|
||||
// scanUser reads a User from a *sql.Row, returning nil (not an error) when the
|
||||
// row is not found.
|
||||
func scanUser(row *sql.Row) (*User, error) {
|
||||
u := &User{}
|
||||
var banned int
|
||||
err := row.Scan(
|
||||
&u.ID, &u.Username, &u.PasswordHash, &u.Avatar, &u.RoleID,
|
||||
&u.TOTPSecret, &u.Status, &u.CreatedAt, &u.LastSeen,
|
||||
&banned, &u.BanReason, &u.BanExpires,
|
||||
)
|
||||
u, err := d.q.GetUserByUsername(dbCtx(), username)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanUser: %w", err)
|
||||
return nil, fmt.Errorf("GetUserByUsername: %w", err)
|
||||
}
|
||||
u.Banned = banned != 0
|
||||
return u, nil
|
||||
return userFromGen(u), nil
|
||||
}
|
||||
|
||||
// GetUserByID returns the user with the given ID, or nil if not found.
|
||||
func (d *DB) GetUserByID(id int64) (*User, error) {
|
||||
u, err := d.q.GetUserByID(dbCtx(), id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserByID: %w", err)
|
||||
}
|
||||
return userFromGen(u), nil
|
||||
}
|
||||
|
||||
// UpdateUserStatus sets the status column for the given user ID.
|
||||
func (d *DB) UpdateUserStatus(id int64, status string) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE users SET status = ?, last_seen = datetime('now') WHERE id = ?`,
|
||||
status, id,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateUserStatus(dbCtx(), dbgen.UpdateUserStatusParams{
|
||||
Status: status,
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateUserStatus: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -173,8 +156,10 @@ func (d *DB) UpdateUserStatus(id int64, status string) error {
|
||||
|
||||
// UpdateUserTOTPSecret sets or clears the TOTP secret for a user.
|
||||
func (d *DB) UpdateUserTOTPSecret(id int64, secret *string) error {
|
||||
_, err := d.sqlDB.Exec(`UPDATE users SET totp_secret = ? WHERE id = ?`, secret, id)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateUserTOTPSecret(dbCtx(), dbgen.UpdateUserTOTPSecretParams{
|
||||
TotpSecret: secret,
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateUserTOTPSecret: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -183,8 +168,7 @@ func (d *DB) UpdateUserTOTPSecret(id int64, secret *string) error {
|
||||
// ResetAllUserStatuses sets all users to "offline". Called on server startup
|
||||
// to clear stale statuses from a previous run or crash.
|
||||
func (d *DB) ResetAllUserStatuses() error {
|
||||
_, err := d.sqlDB.Exec(`UPDATE users SET status = 'offline' WHERE status != 'offline'`)
|
||||
if err != nil {
|
||||
if err := d.q.ResetAllUserStatuses(dbCtx()); err != nil {
|
||||
return fmt.Errorf("ResetAllUserStatuses: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -198,11 +182,12 @@ func (d *DB) BanUser(id int64, reason string, expires *time.Time) error {
|
||||
s := expires.UTC().Format("2006-01-02T15:04:05Z")
|
||||
expiresStr = &s
|
||||
}
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE users SET banned = 1, ban_reason = ?, ban_expires = ? WHERE id = ?`,
|
||||
reason, expiresStr, id,
|
||||
)
|
||||
if err != nil {
|
||||
reasonCopy := reason
|
||||
if err := d.q.BanUser(dbCtx(), dbgen.BanUserParams{
|
||||
BanReason: &reasonCopy,
|
||||
BanExpires: expiresStr,
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("BanUser: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -210,11 +195,7 @@ func (d *DB) BanUser(id int64, reason string, expires *time.Time) error {
|
||||
|
||||
// UnbanUser removes the ban from a user.
|
||||
func (d *DB) UnbanUser(id int64) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE users SET banned = 0, ban_reason = NULL, ban_expires = NULL WHERE id = ?`,
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UnbanUser(dbCtx(), id); err != nil {
|
||||
return fmt.Errorf("UnbanUser: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -233,21 +214,20 @@ const maxSessionsPerUser = 25
|
||||
// the limit is reached.
|
||||
func (d *DB) CreateSession(userID int64, tokenHash, device, ip string) (int64, error) {
|
||||
// Evict oldest sessions if at or above the cap.
|
||||
_, _ = d.sqlDB.Exec(
|
||||
`DELETE FROM sessions WHERE id IN (
|
||||
SELECT id FROM sessions WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT -1 OFFSET ?
|
||||
)`,
|
||||
userID, maxSessionsPerUser-1,
|
||||
)
|
||||
_ = d.q.EvictOldestSessions(dbCtx(), dbgen.EvictOldestSessionsParams{
|
||||
UserID: userID,
|
||||
Offset: maxSessionsPerUser - 1,
|
||||
})
|
||||
|
||||
expiresAt := time.Now().Add(sessionTTL).UTC().Format("2006-01-02T15:04:05Z")
|
||||
res, err := d.sqlDB.Exec(
|
||||
`INSERT INTO sessions (user_id, token, device, ip_address, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
userID, tokenHash, device, ip, expiresAt,
|
||||
)
|
||||
deviceCopy, ipCopy := device, ip
|
||||
res, err := d.q.InsertSession(dbCtx(), dbgen.InsertSessionParams{
|
||||
UserID: userID,
|
||||
Token: tokenHash,
|
||||
Device: &deviceCopy,
|
||||
IpAddress: &ipCopy,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CreateSession: %w", err)
|
||||
}
|
||||
@@ -257,23 +237,15 @@ func (d *DB) CreateSession(userID int64, tokenHash, device, ip string) (int64, e
|
||||
// GetSessionByTokenHash retrieves a session by its hashed token, or nil if
|
||||
// not found.
|
||||
func (d *DB) GetSessionByTokenHash(tokenHash string) (*Session, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT id, user_id, token, device, ip_address, created_at, last_used, expires_at
|
||||
FROM sessions WHERE token = ?`,
|
||||
tokenHash,
|
||||
)
|
||||
s := &Session{}
|
||||
err := row.Scan(
|
||||
&s.ID, &s.UserID, &s.TokenHash, &s.Device, &s.IP,
|
||||
&s.CreatedAt, &s.LastUsed, &s.ExpiresAt,
|
||||
)
|
||||
s, err := d.q.GetSessionByTokenHash(dbCtx(), tokenHash)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetSessionByTokenHash: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
sess := sessionFromGen(s)
|
||||
return &sess, nil
|
||||
}
|
||||
|
||||
// SessionWithBanStatus combines session data with user ban fields
|
||||
@@ -288,36 +260,33 @@ type SessionWithBanStatus struct {
|
||||
// GetSessionWithBanStatus returns the session joined with the user's ban
|
||||
// status in a single query. Returns nil, nil when not found.
|
||||
func (d *DB) GetSessionWithBanStatus(tokenHash string) (*SessionWithBanStatus, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT s.id, s.user_id, s.token, s.device, s.ip_address,
|
||||
s.created_at, s.last_used, s.expires_at,
|
||||
u.banned, u.ban_reason, u.ban_expires
|
||||
FROM sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.token = ?`,
|
||||
tokenHash,
|
||||
)
|
||||
r := &SessionWithBanStatus{}
|
||||
var banned int
|
||||
err := row.Scan(
|
||||
&r.ID, &r.UserID, &r.TokenHash, &r.Device, &r.IP,
|
||||
&r.CreatedAt, &r.LastUsed, &r.ExpiresAt,
|
||||
&banned, &r.BanReason, &r.BanExpires,
|
||||
)
|
||||
row, err := d.q.GetSessionWithBanStatus(dbCtx(), tokenHash)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetSessionWithBanStatus: %w", err)
|
||||
}
|
||||
r.Banned = banned != 0
|
||||
return r, nil
|
||||
return &SessionWithBanStatus{
|
||||
Session: Session{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
TokenHash: row.Token,
|
||||
Device: derefString(row.Device),
|
||||
IP: derefString(row.IpAddress),
|
||||
CreatedAt: row.CreatedAt,
|
||||
LastUsed: row.LastUsed,
|
||||
ExpiresAt: row.ExpiresAt,
|
||||
},
|
||||
Banned: row.Banned != 0,
|
||||
BanReason: row.BanReason,
|
||||
BanExpires: row.BanExpires,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteSession removes the session with the given token hash.
|
||||
func (d *DB) DeleteSession(tokenHash string) error {
|
||||
_, err := d.sqlDB.Exec(`DELETE FROM sessions WHERE token = ?`, tokenHash)
|
||||
if err != nil {
|
||||
if err := d.q.DeleteSessionByToken(dbCtx(), tokenHash); err != nil {
|
||||
return fmt.Errorf("DeleteSession: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -327,10 +296,10 @@ func (d *DB) DeleteSession(tokenHash string) error {
|
||||
// with keepSessionID. Used after password change or 2FA state change to
|
||||
// invalidate all other sessions (BUG-108).
|
||||
func (d *DB) DeleteOtherSessions(userID, keepSessionID int64) (int64, error) {
|
||||
result, err := d.sqlDB.Exec(
|
||||
`DELETE FROM sessions WHERE user_id = ? AND id != ?`,
|
||||
userID, keepSessionID,
|
||||
)
|
||||
result, err := d.q.DeleteOtherSessions(dbCtx(), dbgen.DeleteOtherSessionsParams{
|
||||
UserID: userID,
|
||||
ID: keepSessionID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("DeleteOtherSessions: %w", err)
|
||||
}
|
||||
@@ -341,10 +310,7 @@ func (d *DB) DeleteOtherSessions(userID, keepSessionID int64) (int64, error) {
|
||||
// DeleteExpiredSessions removes all sessions whose expires_at is in the past.
|
||||
// Compares using strftime to handle both ISO-8601 and SQLite datetime formats.
|
||||
func (d *DB) DeleteExpiredSessions() error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`DELETE FROM sessions WHERE strftime('%s', expires_at) < strftime('%s', 'now')`,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.DeleteExpiredSessions(dbCtx()); err != nil {
|
||||
return fmt.Errorf("DeleteExpiredSessions: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -352,11 +318,7 @@ func (d *DB) DeleteExpiredSessions() error {
|
||||
|
||||
// TouchSession updates last_used for the session with the given token hash.
|
||||
func (d *DB) TouchSession(tokenHash string) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE sessions SET last_used = datetime('now') WHERE token = ?`,
|
||||
tokenHash,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.TouchSession(dbCtx(), tokenHash); err != nil {
|
||||
return fmt.Errorf("TouchSession: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -471,32 +433,19 @@ type MemberSummary struct {
|
||||
// ListMembers returns non-banned users as lightweight summaries.
|
||||
// M-12: Limited to 1000 rows to prevent unbounded result sets on large servers.
|
||||
func (d *DB) ListMembers() ([]MemberSummary, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT u.id, u.username, u.avatar, u.status, LOWER(r.name)
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.banned = 0
|
||||
ORDER BY u.username ASC
|
||||
LIMIT 1000`,
|
||||
)
|
||||
rows, err := d.q.ListMembers(dbCtx())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListMembers: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var members []MemberSummary
|
||||
for rows.Next() {
|
||||
var m MemberSummary
|
||||
if err := rows.Scan(&m.ID, &m.Username, &m.Avatar, &m.Status, &m.Role); err != nil {
|
||||
return nil, fmt.Errorf("ListMembers scan: %w", err)
|
||||
}
|
||||
members = append(members, m)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("ListMembers rows: %w", rows.Err())
|
||||
}
|
||||
if members == nil {
|
||||
members = []MemberSummary{}
|
||||
members := make([]MemberSummary, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
members = append(members, MemberSummary{
|
||||
ID: r.ID,
|
||||
Username: r.Username,
|
||||
Avatar: r.Avatar,
|
||||
Status: r.Status,
|
||||
Role: r.Lower,
|
||||
})
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ type Querier interface {
|
||||
DeleteLockout(ctx context.Context, key string) error
|
||||
DeleteOrphanedAttachments(ctx context.Context, uploadedAt string) ([]string, error)
|
||||
DeleteOtherSessions(ctx context.Context, arg DeleteOtherSessionsParams) (sql.Result, error)
|
||||
DeleteSessionByID(ctx context.Context, arg DeleteSessionByIDParams) error
|
||||
DeleteSessionByID(ctx context.Context, arg DeleteSessionByIDParams) (sql.Result, error)
|
||||
DeleteSessionByToken(ctx context.Context, token string) error
|
||||
DisablePlugin(ctx context.Context, id int64) error
|
||||
EditMessageContent(ctx context.Context, arg EditMessageContentParams) error
|
||||
|
||||
@@ -32,7 +32,7 @@ func (q *Queries) DeleteOtherSessions(ctx context.Context, arg DeleteOtherSessio
|
||||
return q.db.ExecContext(ctx, deleteOtherSessions, arg.UserID, arg.ID)
|
||||
}
|
||||
|
||||
const deleteSessionByID = `-- name: DeleteSessionByID :exec
|
||||
const deleteSessionByID = `-- name: DeleteSessionByID :execresult
|
||||
DELETE FROM sessions WHERE id = ? AND user_id = ?
|
||||
`
|
||||
|
||||
@@ -41,9 +41,8 @@ type DeleteSessionByIDParams struct {
|
||||
UserID int64 `json:"userId"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteSessionByID(ctx context.Context, arg DeleteSessionByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteSessionByID, arg.ID, arg.UserID)
|
||||
return err
|
||||
func (q *Queries) DeleteSessionByID(ctx context.Context, arg DeleteSessionByIDParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, deleteSessionByID, arg.ID, arg.UserID)
|
||||
}
|
||||
|
||||
const deleteSessionByToken = `-- name: DeleteSessionByToken :exec
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package db
|
||||
|
||||
import "github.com/owncord/server/db/dbgen"
|
||||
|
||||
// This file holds the conversions from sqlc-generated row/model types
|
||||
// (db/dbgen) to the domain model types this package exposes. sqlc emits int64
|
||||
// for integer columns and *string for nullable text; the domain models use
|
||||
// narrower Go types (int, bool, non-pointer string) for ergonomics, so each
|
||||
// delegating read maps through one of these helpers. See docs/plans/sqlc-adoption.md.
|
||||
|
||||
// derefString returns the pointed-to string, or "" when the pointer is nil.
|
||||
// Used for columns the schema allows to be NULL but the domain model exposes
|
||||
// as a plain string (device, ip_address).
|
||||
func derefString(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
// userFromGen maps a generated user row to the domain User model.
|
||||
func userFromGen(u dbgen.User) *User {
|
||||
return &User{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
PasswordHash: u.Password,
|
||||
Avatar: u.Avatar,
|
||||
RoleID: u.RoleID,
|
||||
TOTPSecret: u.TotpSecret,
|
||||
Status: u.Status,
|
||||
CreatedAt: u.CreatedAt,
|
||||
LastSeen: u.LastSeen,
|
||||
Banned: u.Banned != 0,
|
||||
BanReason: u.BanReason,
|
||||
BanExpires: u.BanExpires,
|
||||
}
|
||||
}
|
||||
|
||||
// sessionFromGen maps a generated session row to the domain Session model.
|
||||
func sessionFromGen(s dbgen.Session) Session {
|
||||
return Session{
|
||||
ID: s.ID,
|
||||
UserID: s.UserID,
|
||||
TokenHash: s.Token,
|
||||
Device: derefString(s.Device),
|
||||
IP: derefString(s.IpAddress),
|
||||
CreatedAt: s.CreatedAt,
|
||||
LastUsed: s.LastUsed,
|
||||
ExpiresAt: s.ExpiresAt,
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,19 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/owncord/server/db/dbgen"
|
||||
)
|
||||
|
||||
// UpdateUserProfile updates the username and avatar for the given user.
|
||||
// Returns ErrNotFound if the user does not exist. Returns an error wrapping
|
||||
// a UNIQUE constraint violation if the username is already taken.
|
||||
func (d *DB) UpdateUserProfile(userID int64, username string, avatar *string) error {
|
||||
result, err := d.sqlDB.Exec(
|
||||
`UPDATE users SET username = ?, avatar = ? WHERE id = ?`,
|
||||
username, avatar, userID,
|
||||
)
|
||||
result, err := d.q.UpdateUserProfile(dbCtx(), dbgen.UpdateUserProfileParams{
|
||||
Username: username,
|
||||
Avatar: avatar,
|
||||
ID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateUserProfile: %w", err)
|
||||
}
|
||||
@@ -27,11 +30,10 @@ func (d *DB) UpdateUserProfile(userID int64, username string, avatar *string) er
|
||||
|
||||
// UpdateUserPassword sets a new password hash for the given user.
|
||||
func (d *DB) UpdateUserPassword(userID int64, newPasswordHash string) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE users SET password = ? WHERE id = ?`,
|
||||
newPasswordHash, userID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateUserPassword(dbCtx(), dbgen.UpdateUserPasswordParams{
|
||||
Password: newPasswordHash,
|
||||
ID: userID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateUserPassword: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -40,34 +42,13 @@ func (d *DB) UpdateUserPassword(userID int64, newPasswordHash string) error {
|
||||
// ListUserSessions returns all sessions for the given user in a single query.
|
||||
// Results are ordered by created_at descending (newest first).
|
||||
func (d *DB) ListUserSessions(userID int64) ([]Session, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT id, user_id, token, device, ip_address, created_at, last_used, expires_at
|
||||
FROM sessions
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC`,
|
||||
userID,
|
||||
)
|
||||
rows, err := d.q.ListUserSessions(dbCtx(), userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListUserSessions: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var sessions []Session
|
||||
for rows.Next() {
|
||||
var s Session
|
||||
if err := rows.Scan(
|
||||
&s.ID, &s.UserID, &s.TokenHash, &s.Device, &s.IP,
|
||||
&s.CreatedAt, &s.LastUsed, &s.ExpiresAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("ListUserSessions scan: %w", err)
|
||||
}
|
||||
sessions = append(sessions, s)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("ListUserSessions rows: %w", rows.Err())
|
||||
}
|
||||
if sessions == nil {
|
||||
sessions = []Session{}
|
||||
sessions := make([]Session, 0, len(rows))
|
||||
for _, s := range rows {
|
||||
sessions = append(sessions, sessionFromGen(s))
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
@@ -76,10 +57,10 @@ func (d *DB) ListUserSessions(userID int64) ([]Session, error) {
|
||||
// the specified user. Returns ErrNotFound if the session does not exist or
|
||||
// does not belong to the user.
|
||||
func (d *DB) DeleteSessionByID(sessionID, userID int64) error {
|
||||
result, err := d.sqlDB.Exec(
|
||||
`DELETE FROM sessions WHERE id = ? AND user_id = ?`,
|
||||
sessionID, userID,
|
||||
)
|
||||
result, err := d.q.DeleteSessionByID(dbCtx(), dbgen.DeleteSessionByIDParams{
|
||||
ID: sessionID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("DeleteSessionByID: %w", err)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ WHERE s.token = ?;
|
||||
-- name: DeleteSessionByToken :exec
|
||||
DELETE FROM sessions WHERE token = ?;
|
||||
|
||||
-- name: DeleteSessionByID :exec
|
||||
-- name: DeleteSessionByID :execresult
|
||||
DELETE FROM sessions WHERE id = ? AND user_id = ?;
|
||||
|
||||
-- name: DeleteOtherSessions :execresult
|
||||
|
||||
Reference in New Issue
Block a user