| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
- // SPDX-License-Identifier: Apache-2.0
- package storage // import "miniflux.app/v2/internal/storage"
- import (
- "database/sql"
- "errors"
- "fmt"
- "time"
- "miniflux.app/v2/internal/model"
- )
- // CreateWebSession persists a new web session built via model.NewWebSession.
- func (s *Storage) CreateWebSession(session *model.WebSession) error {
- if session == nil {
- return errors.New(`store: web session is nil`)
- }
- stateJSON, err := session.MarshalState()
- if err != nil {
- return fmt.Errorf(`store: unable to serialize web session state: %v`, err)
- }
- query := `
- INSERT INTO web_sessions (
- id,
- secret_hash,
- user_agent,
- ip,
- state
- )
- VALUES ($1, $2, $3, $4, $5)
- RETURNING created_at
- `
- err = s.db.QueryRow(
- query,
- session.ID,
- session.SecretHash,
- session.UserAgent,
- sql.NullString{String: session.IP, Valid: session.IP != ""},
- stateJSON,
- ).Scan(&session.CreatedAt)
- if err != nil {
- return fmt.Errorf(`store: unable to create web session: %v`, err)
- }
- return nil
- }
- // WebSessionsByUserID returns web sessions for the given user.
- func (s *Storage) WebSessionsByUserID(userID int64) ([]model.WebSession, error) {
- query := `
- SELECT
- id,
- secret_hash,
- user_id,
- created_at,
- user_agent,
- ip,
- state
- FROM
- web_sessions
- WHERE
- user_id=$1
- ORDER BY
- created_at DESC
- `
- rows, err := s.db.Query(query, userID)
- if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch web sessions: %v`, err)
- }
- defer rows.Close()
- var sessions []model.WebSession
- for rows.Next() {
- session, err := scanWebSession(rows)
- if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch web session row: %v`, err)
- }
- sessions = append(sessions, *session)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf(`store: unable to fetch web sessions: %v`, err)
- }
- return sessions, nil
- }
- // WebSessionByID returns the web session identified by id, or nil if not found.
- func (s *Storage) WebSessionByID(sessionID string) (*model.WebSession, error) {
- if sessionID == "" {
- return nil, nil
- }
- row := s.db.QueryRow(`
- SELECT
- id,
- secret_hash,
- user_id,
- created_at,
- user_agent,
- ip,
- state
- FROM
- web_sessions
- WHERE
- id=$1
- `, sessionID)
- session, err := scanWebSession(row)
- if errors.Is(err, sql.ErrNoRows) {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch web session: %v`, err)
- }
- return session, nil
- }
- // RotateWebSession persists a session whose identity has been rotated via
- // (*model.WebSession).Rotate(), updating the row previously keyed by oldID.
- func (s *Storage) RotateWebSession(oldID string, session *model.WebSession) error {
- if session == nil {
- return errors.New(`store: web session is nil`)
- }
- if oldID == "" || session.ID == "" {
- return errors.New(`store: web session ID cannot be empty`)
- }
- stateJSON, err := session.MarshalState()
- if err != nil {
- return fmt.Errorf(`store: unable to serialize web session state: %v`, err)
- }
- err = s.db.QueryRow(`
- UPDATE
- web_sessions
- SET
- id=$2,
- secret_hash=$3,
- user_id=$4,
- state=$5,
- created_at=now()
- WHERE
- id=$1
- RETURNING created_at
- `,
- oldID,
- session.ID,
- session.SecretHash,
- session.NullUserID(),
- stateJSON,
- ).Scan(&session.CreatedAt)
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return errors.New(`store: nothing has been updated`)
- }
- return fmt.Errorf(`store: unable to rotate web session: %v`, err)
- }
- return nil
- }
- // UpdateWebSession updates the mutable fields of a web session.
- func (s *Storage) UpdateWebSession(session *model.WebSession) error {
- if session == nil {
- return errors.New(`store: web session is nil`)
- }
- if session.ID == "" {
- return errors.New(`store: web session ID cannot be empty`)
- }
- query := `
- UPDATE
- web_sessions
- SET
- user_id=$2,
- state=$3
- WHERE
- id=$1
- `
- stateJSON, err := session.MarshalState()
- if err != nil {
- return fmt.Errorf(`store: unable to serialize web session state: %v`, err)
- }
- result, err := s.db.Exec(
- query,
- session.ID,
- session.NullUserID(),
- stateJSON,
- )
- if err != nil {
- return fmt.Errorf(`store: unable to update web session: %v`, err)
- }
- count, err := result.RowsAffected()
- if err != nil {
- return fmt.Errorf(`store: unable to update web session: %v`, err)
- }
- if count != 1 {
- return errors.New(`store: nothing has been updated`)
- }
- return nil
- }
- // RemoveUserWebSession removes a web session for the given user if present.
- func (s *Storage) RemoveUserWebSession(userID int64, sessionID string) error {
- if _, err := s.db.Exec(`DELETE FROM web_sessions WHERE user_id=$1 AND id=$2`, userID, sessionID); err != nil {
- return fmt.Errorf(`store: unable to remove this web session: %v`, err)
- }
- return nil
- }
- // CleanOldWebSessions removes web sessions older than the specified interval (24h minimum).
- func (s *Storage) CleanOldWebSessions(interval time.Duration) (int64, error) {
- query := `
- DELETE FROM
- web_sessions
- WHERE
- created_at < now() - $1::interval
- `
- days := max(int(interval/(24*time.Hour)), 1)
- result, err := s.db.Exec(query, fmt.Sprintf("%d days", days))
- if err != nil {
- return 0, fmt.Errorf(`store: unable to clean old web sessions: %v`, err)
- }
- n, _ := result.RowsAffected()
- return n, nil
- }
- // FlushAllSessions removes all sessions from the database.
- func (s *Storage) FlushAllSessions() error {
- if _, err := s.db.Exec(`DELETE FROM web_sessions`); err != nil {
- return fmt.Errorf(`store: unable to delete all web sessions: %v`, err)
- }
- return nil
- }
- type webSessionScanner interface {
- Scan(dest ...any) error
- }
- func scanWebSession(scanner webSessionScanner) (*model.WebSession, error) {
- var session model.WebSession
- var userID sql.NullInt64
- var ip sql.NullString
- var stateRaw []byte
- err := scanner.Scan(
- &session.ID,
- &session.SecretHash,
- &userID,
- &session.CreatedAt,
- &session.UserAgent,
- &ip,
- &stateRaw,
- )
- if err != nil {
- return nil, err
- }
- session.ScanUserID(userID)
- if ip.Valid {
- session.IP = ip.String
- }
- if err := session.UnmarshalState(stateRaw); err != nil {
- return nil, err
- }
- return &session, nil
- }
|