web_session.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package storage // import "miniflux.app/v2/internal/storage"
  4. import (
  5. "database/sql"
  6. "errors"
  7. "fmt"
  8. "time"
  9. "miniflux.app/v2/internal/model"
  10. )
  11. // CreateWebSession persists a new web session built via model.NewWebSession.
  12. func (s *Storage) CreateWebSession(session *model.WebSession) error {
  13. if session == nil {
  14. return errors.New(`store: web session is nil`)
  15. }
  16. stateJSON, err := session.MarshalState()
  17. if err != nil {
  18. return fmt.Errorf(`store: unable to serialize web session state: %v`, err)
  19. }
  20. query := `
  21. INSERT INTO web_sessions (
  22. id,
  23. secret_hash,
  24. user_agent,
  25. ip,
  26. state
  27. )
  28. VALUES ($1, $2, $3, $4, $5)
  29. RETURNING created_at
  30. `
  31. err = s.db.QueryRow(
  32. query,
  33. session.ID,
  34. session.SecretHash,
  35. session.UserAgent,
  36. sql.NullString{String: session.IP, Valid: session.IP != ""},
  37. stateJSON,
  38. ).Scan(&session.CreatedAt)
  39. if err != nil {
  40. return fmt.Errorf(`store: unable to create web session: %v`, err)
  41. }
  42. return nil
  43. }
  44. // WebSessionsByUserID returns web sessions for the given user.
  45. func (s *Storage) WebSessionsByUserID(userID int64) ([]model.WebSession, error) {
  46. query := `
  47. SELECT
  48. id,
  49. secret_hash,
  50. user_id,
  51. created_at,
  52. user_agent,
  53. ip,
  54. state
  55. FROM
  56. web_sessions
  57. WHERE
  58. user_id=$1
  59. ORDER BY
  60. created_at DESC
  61. `
  62. rows, err := s.db.Query(query, userID)
  63. if err != nil {
  64. return nil, fmt.Errorf(`store: unable to fetch web sessions: %v`, err)
  65. }
  66. defer rows.Close()
  67. var sessions []model.WebSession
  68. for rows.Next() {
  69. session, err := scanWebSession(rows)
  70. if err != nil {
  71. return nil, fmt.Errorf(`store: unable to fetch web session row: %v`, err)
  72. }
  73. sessions = append(sessions, *session)
  74. }
  75. if err := rows.Err(); err != nil {
  76. return nil, fmt.Errorf(`store: unable to fetch web sessions: %v`, err)
  77. }
  78. return sessions, nil
  79. }
  80. // WebSessionByID returns the web session identified by id, or nil if not found.
  81. func (s *Storage) WebSessionByID(sessionID string) (*model.WebSession, error) {
  82. if sessionID == "" {
  83. return nil, nil
  84. }
  85. row := s.db.QueryRow(`
  86. SELECT
  87. id,
  88. secret_hash,
  89. user_id,
  90. created_at,
  91. user_agent,
  92. ip,
  93. state
  94. FROM
  95. web_sessions
  96. WHERE
  97. id=$1
  98. `, sessionID)
  99. session, err := scanWebSession(row)
  100. if errors.Is(err, sql.ErrNoRows) {
  101. return nil, nil
  102. }
  103. if err != nil {
  104. return nil, fmt.Errorf(`store: unable to fetch web session: %v`, err)
  105. }
  106. return session, nil
  107. }
  108. // RotateWebSession persists a session whose identity has been rotated via
  109. // (*model.WebSession).Rotate(), updating the row previously keyed by oldID.
  110. func (s *Storage) RotateWebSession(oldID string, session *model.WebSession) error {
  111. if session == nil {
  112. return errors.New(`store: web session is nil`)
  113. }
  114. if oldID == "" || session.ID == "" {
  115. return errors.New(`store: web session ID cannot be empty`)
  116. }
  117. stateJSON, err := session.MarshalState()
  118. if err != nil {
  119. return fmt.Errorf(`store: unable to serialize web session state: %v`, err)
  120. }
  121. err = s.db.QueryRow(`
  122. UPDATE
  123. web_sessions
  124. SET
  125. id=$2,
  126. secret_hash=$3,
  127. user_id=$4,
  128. state=$5,
  129. created_at=now()
  130. WHERE
  131. id=$1
  132. RETURNING created_at
  133. `,
  134. oldID,
  135. session.ID,
  136. session.SecretHash,
  137. session.NullUserID(),
  138. stateJSON,
  139. ).Scan(&session.CreatedAt)
  140. if err != nil {
  141. if errors.Is(err, sql.ErrNoRows) {
  142. return errors.New(`store: nothing has been updated`)
  143. }
  144. return fmt.Errorf(`store: unable to rotate web session: %v`, err)
  145. }
  146. return nil
  147. }
  148. // UpdateWebSession updates the mutable fields of a web session.
  149. func (s *Storage) UpdateWebSession(session *model.WebSession) error {
  150. if session == nil {
  151. return errors.New(`store: web session is nil`)
  152. }
  153. if session.ID == "" {
  154. return errors.New(`store: web session ID cannot be empty`)
  155. }
  156. query := `
  157. UPDATE
  158. web_sessions
  159. SET
  160. user_id=$2,
  161. state=$3
  162. WHERE
  163. id=$1
  164. `
  165. stateJSON, err := session.MarshalState()
  166. if err != nil {
  167. return fmt.Errorf(`store: unable to serialize web session state: %v`, err)
  168. }
  169. result, err := s.db.Exec(
  170. query,
  171. session.ID,
  172. session.NullUserID(),
  173. stateJSON,
  174. )
  175. if err != nil {
  176. return fmt.Errorf(`store: unable to update web session: %v`, err)
  177. }
  178. count, err := result.RowsAffected()
  179. if err != nil {
  180. return fmt.Errorf(`store: unable to update web session: %v`, err)
  181. }
  182. if count != 1 {
  183. return errors.New(`store: nothing has been updated`)
  184. }
  185. return nil
  186. }
  187. // RemoveUserWebSession removes a web session for the given user if present.
  188. func (s *Storage) RemoveUserWebSession(userID int64, sessionID string) error {
  189. if _, err := s.db.Exec(`DELETE FROM web_sessions WHERE user_id=$1 AND id=$2`, userID, sessionID); err != nil {
  190. return fmt.Errorf(`store: unable to remove this web session: %v`, err)
  191. }
  192. return nil
  193. }
  194. // CleanOldWebSessions removes web sessions older than the specified interval (24h minimum).
  195. func (s *Storage) CleanOldWebSessions(interval time.Duration) (int64, error) {
  196. query := `
  197. DELETE FROM
  198. web_sessions
  199. WHERE
  200. created_at < now() - $1::interval
  201. `
  202. days := max(int(interval/(24*time.Hour)), 1)
  203. result, err := s.db.Exec(query, fmt.Sprintf("%d days", days))
  204. if err != nil {
  205. return 0, fmt.Errorf(`store: unable to clean old web sessions: %v`, err)
  206. }
  207. n, _ := result.RowsAffected()
  208. return n, nil
  209. }
  210. // FlushAllSessions removes all sessions from the database.
  211. func (s *Storage) FlushAllSessions() error {
  212. if _, err := s.db.Exec(`DELETE FROM web_sessions`); err != nil {
  213. return fmt.Errorf(`store: unable to delete all web sessions: %v`, err)
  214. }
  215. return nil
  216. }
  217. type webSessionScanner interface {
  218. Scan(dest ...any) error
  219. }
  220. func scanWebSession(scanner webSessionScanner) (*model.WebSession, error) {
  221. var session model.WebSession
  222. var userID sql.NullInt64
  223. var ip sql.NullString
  224. var stateRaw []byte
  225. err := scanner.Scan(
  226. &session.ID,
  227. &session.SecretHash,
  228. &userID,
  229. &session.CreatedAt,
  230. &session.UserAgent,
  231. &ip,
  232. &stateRaw,
  233. )
  234. if err != nil {
  235. return nil, err
  236. }
  237. session.ScanUserID(userID)
  238. if ip.Valid {
  239. session.IP = ip.String
  240. }
  241. if err := session.UnmarshalState(stateRaw); err != nil {
  242. return nil, err
  243. }
  244. return &session, nil
  245. }