| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767 |
- // 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"
- "fmt"
- "log/slog"
- "runtime"
- "strings"
- "miniflux.app/v2/internal/crypto"
- "miniflux.app/v2/internal/model"
- "github.com/lib/pq"
- "golang.org/x/crypto/bcrypt"
- )
- // CountUsers returns the total number of users.
- func (s *Storage) CountUsers() (int, error) {
- var result int
- err := s.db.QueryRow(`SELECT count(*) FROM users`).Scan(&result)
- if err != nil {
- return 0, fmt.Errorf("storage: unable to count users: %w", err)
- }
- return result, nil
- }
- // SetLastLogin updates the last login date of a user.
- func (s *Storage) SetLastLogin(userID int64) error {
- query := `UPDATE users SET last_login_at=now() WHERE id=$1`
- _, err := s.db.Exec(query, userID)
- if err != nil {
- return fmt.Errorf(`store: unable to update last login date: %v`, err)
- }
- return nil
- }
- // UserExists checks if a user exists by using the given username.
- func (s *Storage) UserExists(username string) bool {
- var result bool
- s.db.QueryRow(`SELECT true FROM users WHERE username=LOWER($1) LIMIT 1`, username).Scan(&result)
- return result
- }
- // AnotherUserExists checks if another user exists with the given username.
- func (s *Storage) AnotherUserExists(userID int64, username string) bool {
- var result bool
- s.db.QueryRow(`SELECT true FROM users WHERE id != $1 AND username=LOWER($2) LIMIT 1`, userID, username).Scan(&result)
- return result
- }
- // CreateUser creates a new user.
- func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*model.User, error) {
- var hashedPassword string
- if userCreationRequest.Password != "" {
- var err error
- hashedPassword, err = crypto.HashPassword(userCreationRequest.Password)
- if err != nil {
- return nil, err
- }
- }
- query := `
- INSERT INTO users
- (username, password, is_admin, google_id, openid_connect_id)
- VALUES
- (LOWER($1), $2, $3, $4, $5)
- RETURNING
- id,
- username,
- is_admin,
- language,
- theme,
- timezone,
- entry_direction,
- entries_per_page,
- keyboard_shortcuts,
- show_reading_time,
- entry_swipe,
- gesture_nav,
- stylesheet,
- custom_js,
- external_font_hosts,
- google_id,
- openid_connect_id,
- display_mode,
- entry_order,
- default_reading_speed,
- cjk_reading_speed,
- default_home_page,
- categories_sorting_order,
- mark_read_on_view,
- media_playback_rate,
- block_filter_entry_rules,
- keep_filter_entry_rules,
- always_open_external_links,
- open_external_links_in_new_tab
- `
- tx, err := s.db.Begin()
- if err != nil {
- return nil, fmt.Errorf(`store: unable to start transaction: %v`, err)
- }
- var user model.User
- err = tx.QueryRow(
- query,
- userCreationRequest.Username,
- hashedPassword,
- userCreationRequest.IsAdmin,
- userCreationRequest.GoogleID,
- userCreationRequest.OpenIDConnectID,
- ).Scan(
- &user.ID,
- &user.Username,
- &user.IsAdmin,
- &user.Language,
- &user.Theme,
- &user.Timezone,
- &user.EntryDirection,
- &user.EntriesPerPage,
- &user.KeyboardShortcuts,
- &user.ShowReadingTime,
- &user.EntrySwipe,
- &user.GestureNav,
- &user.Stylesheet,
- &user.CustomJS,
- &user.ExternalFontHosts,
- &user.GoogleID,
- &user.OpenIDConnectID,
- &user.DisplayMode,
- &user.EntryOrder,
- &user.DefaultReadingSpeed,
- &user.CJKReadingSpeed,
- &user.DefaultHomePage,
- &user.CategoriesSortingOrder,
- &user.MarkReadOnView,
- &user.MediaPlaybackRate,
- &user.BlockFilterEntryRules,
- &user.KeepFilterEntryRules,
- &user.AlwaysOpenExternalLinks,
- &user.OpenExternalLinksInNewTab,
- )
- if err != nil {
- tx.Rollback()
- return nil, fmt.Errorf(`store: unable to create user: %v`, err)
- }
- _, err = tx.Exec(`INSERT INTO categories (user_id, title) VALUES ($1, $2)`, user.ID, "All")
- if err != nil {
- tx.Rollback()
- return nil, fmt.Errorf(`store: unable to create user default category: %v`, err)
- }
- _, err = tx.Exec(`INSERT INTO integrations (user_id) VALUES ($1)`, user.ID)
- if err != nil {
- tx.Rollback()
- return nil, fmt.Errorf(`store: unable to create integration row: %v`, err)
- }
- if err := tx.Commit(); err != nil {
- return nil, fmt.Errorf(`store: unable to commit transaction: %v`, err)
- }
- return &user, nil
- }
- // UpdateUser updates a user.
- func (s *Storage) UpdateUser(user *model.User) error {
- user.ExternalFontHosts = strings.TrimSpace(user.ExternalFontHosts)
- if user.Password != "" {
- hashedPassword, err := crypto.HashPassword(user.Password)
- if err != nil {
- return err
- }
- query := `
- UPDATE users SET
- username=LOWER($1),
- password=$2,
- is_admin=$3,
- theme=$4,
- language=$5,
- timezone=$6,
- entry_direction=$7,
- entries_per_page=$8,
- keyboard_shortcuts=$9,
- show_reading_time=$10,
- entry_swipe=$11,
- gesture_nav=$12,
- stylesheet=$13,
- custom_js=$14,
- external_font_hosts=$15,
- google_id=$16,
- openid_connect_id=$17,
- display_mode=$18,
- entry_order=$19,
- default_reading_speed=$20,
- cjk_reading_speed=$21,
- default_home_page=$22,
- categories_sorting_order=$23,
- mark_read_on_view=$24,
- mark_read_on_media_player_completion=$25,
- media_playback_rate=$26,
- block_filter_entry_rules=$27,
- keep_filter_entry_rules=$28,
- always_open_external_links=$29,
- open_external_links_in_new_tab=$30
- WHERE
- id=$31
- `
- _, err = s.db.Exec(
- query,
- user.Username,
- hashedPassword,
- user.IsAdmin,
- user.Theme,
- user.Language,
- user.Timezone,
- user.EntryDirection,
- user.EntriesPerPage,
- user.KeyboardShortcuts,
- user.ShowReadingTime,
- user.EntrySwipe,
- user.GestureNav,
- user.Stylesheet,
- user.CustomJS,
- user.ExternalFontHosts,
- user.GoogleID,
- user.OpenIDConnectID,
- user.DisplayMode,
- user.EntryOrder,
- user.DefaultReadingSpeed,
- user.CJKReadingSpeed,
- user.DefaultHomePage,
- user.CategoriesSortingOrder,
- user.MarkReadOnView,
- user.MarkReadOnMediaPlayerCompletion,
- user.MediaPlaybackRate,
- user.BlockFilterEntryRules,
- user.KeepFilterEntryRules,
- user.AlwaysOpenExternalLinks,
- user.OpenExternalLinksInNewTab,
- user.ID,
- )
- if err != nil {
- return fmt.Errorf(`store: unable to update user: %v`, err)
- }
- } else {
- query := `
- UPDATE users SET
- username=LOWER($1),
- is_admin=$2,
- theme=$3,
- language=$4,
- timezone=$5,
- entry_direction=$6,
- entries_per_page=$7,
- keyboard_shortcuts=$8,
- show_reading_time=$9,
- entry_swipe=$10,
- gesture_nav=$11,
- stylesheet=$12,
- custom_js=$13,
- external_font_hosts=$14,
- google_id=$15,
- openid_connect_id=$16,
- display_mode=$17,
- entry_order=$18,
- default_reading_speed=$19,
- cjk_reading_speed=$20,
- default_home_page=$21,
- categories_sorting_order=$22,
- mark_read_on_view=$23,
- mark_read_on_media_player_completion=$24,
- media_playback_rate=$25,
- block_filter_entry_rules=$26,
- keep_filter_entry_rules=$27,
- always_open_external_links=$28,
- open_external_links_in_new_tab=$29
- WHERE
- id=$30
- `
- _, err := s.db.Exec(
- query,
- user.Username,
- user.IsAdmin,
- user.Theme,
- user.Language,
- user.Timezone,
- user.EntryDirection,
- user.EntriesPerPage,
- user.KeyboardShortcuts,
- user.ShowReadingTime,
- user.EntrySwipe,
- user.GestureNav,
- user.Stylesheet,
- user.CustomJS,
- user.ExternalFontHosts,
- user.GoogleID,
- user.OpenIDConnectID,
- user.DisplayMode,
- user.EntryOrder,
- user.DefaultReadingSpeed,
- user.CJKReadingSpeed,
- user.DefaultHomePage,
- user.CategoriesSortingOrder,
- user.MarkReadOnView,
- user.MarkReadOnMediaPlayerCompletion,
- user.MediaPlaybackRate,
- user.BlockFilterEntryRules,
- user.KeepFilterEntryRules,
- user.AlwaysOpenExternalLinks,
- user.OpenExternalLinksInNewTab,
- user.ID,
- )
- if err != nil {
- return fmt.Errorf(`store: unable to update user: %v`, err)
- }
- }
- return nil
- }
- // UserLanguage returns the language of the given user.
- func (s *Storage) UserLanguage(userID int64) (language string) {
- err := s.db.QueryRow(`SELECT language FROM users WHERE id = $1`, userID).Scan(&language)
- if err != nil {
- return "en_US"
- }
- return language
- }
- // UserByID finds a user by the ID.
- func (s *Storage) UserByID(userID int64) (*model.User, error) {
- query := `
- SELECT
- id,
- username,
- is_admin,
- theme,
- language,
- timezone,
- entry_direction,
- entries_per_page,
- keyboard_shortcuts,
- show_reading_time,
- entry_swipe,
- gesture_nav,
- last_login_at,
- stylesheet,
- custom_js,
- external_font_hosts,
- google_id,
- openid_connect_id,
- display_mode,
- entry_order,
- default_reading_speed,
- cjk_reading_speed,
- default_home_page,
- categories_sorting_order,
- mark_read_on_view,
- mark_read_on_media_player_completion,
- media_playback_rate,
- block_filter_entry_rules,
- keep_filter_entry_rules,
- always_open_external_links,
- open_external_links_in_new_tab
- FROM
- users
- WHERE
- id = $1
- `
- return s.fetchUser(query, userID)
- }
- // UserByUsername finds a user by the username.
- func (s *Storage) UserByUsername(username string) (*model.User, error) {
- query := `
- SELECT
- id,
- username,
- is_admin,
- theme,
- language,
- timezone,
- entry_direction,
- entries_per_page,
- keyboard_shortcuts,
- show_reading_time,
- entry_swipe,
- gesture_nav,
- last_login_at,
- stylesheet,
- custom_js,
- external_font_hosts,
- google_id,
- openid_connect_id,
- display_mode,
- entry_order,
- default_reading_speed,
- cjk_reading_speed,
- default_home_page,
- categories_sorting_order,
- mark_read_on_view,
- mark_read_on_media_player_completion,
- media_playback_rate,
- block_filter_entry_rules,
- keep_filter_entry_rules,
- always_open_external_links,
- open_external_links_in_new_tab
- FROM
- users
- WHERE
- username=LOWER($1)
- `
- return s.fetchUser(query, username)
- }
- // UserByField finds a user by a field value.
- func (s *Storage) UserByField(field, value string) (*model.User, error) {
- query := `
- SELECT
- id,
- username,
- is_admin,
- theme,
- language,
- timezone,
- entry_direction,
- entries_per_page,
- keyboard_shortcuts,
- show_reading_time,
- entry_swipe,
- gesture_nav,
- last_login_at,
- stylesheet,
- custom_js,
- external_font_hosts,
- google_id,
- openid_connect_id,
- display_mode,
- entry_order,
- default_reading_speed,
- cjk_reading_speed,
- default_home_page,
- categories_sorting_order,
- mark_read_on_view,
- mark_read_on_media_player_completion,
- media_playback_rate,
- block_filter_entry_rules,
- keep_filter_entry_rules,
- always_open_external_links,
- open_external_links_in_new_tab
- FROM
- users
- WHERE
- %s=$1
- `
- return s.fetchUser(fmt.Sprintf(query, pq.QuoteIdentifier(field)), value)
- }
- // AnotherUserWithFieldExists returns true if a user has the value set for the given field.
- func (s *Storage) AnotherUserWithFieldExists(userID int64, field, value string) bool {
- var result bool
- query := `SELECT true FROM users WHERE id <> $1 AND ` + pq.QuoteIdentifier(field) + `=$2 LIMIT 1`
- s.db.QueryRow(query, userID, value).Scan(&result)
- return result
- }
- // UserByAPIKey returns a User from an API Key.
- func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
- query := `
- SELECT
- u.id,
- u.username,
- u.is_admin,
- u.theme,
- u.language,
- u.timezone,
- u.entry_direction,
- u.entries_per_page,
- u.keyboard_shortcuts,
- u.show_reading_time,
- u.entry_swipe,
- u.gesture_nav,
- u.last_login_at,
- u.stylesheet,
- u.custom_js,
- u.external_font_hosts,
- u.google_id,
- u.openid_connect_id,
- u.display_mode,
- u.entry_order,
- u.default_reading_speed,
- u.cjk_reading_speed,
- u.default_home_page,
- u.categories_sorting_order,
- u.mark_read_on_view,
- u.mark_read_on_media_player_completion,
- media_playback_rate,
- u.block_filter_entry_rules,
- u.keep_filter_entry_rules,
- u.always_open_external_links,
- u.open_external_links_in_new_tab
- FROM
- users u
- LEFT JOIN
- api_keys ON api_keys.user_id=u.id
- WHERE
- api_keys.token = $1
- `
- return s.fetchUser(query, token)
- }
- func (s *Storage) fetchUser(query string, args ...any) (*model.User, error) {
- var user model.User
- err := s.db.QueryRow(query, args...).Scan(
- &user.ID,
- &user.Username,
- &user.IsAdmin,
- &user.Theme,
- &user.Language,
- &user.Timezone,
- &user.EntryDirection,
- &user.EntriesPerPage,
- &user.KeyboardShortcuts,
- &user.ShowReadingTime,
- &user.EntrySwipe,
- &user.GestureNav,
- &user.LastLoginAt,
- &user.Stylesheet,
- &user.CustomJS,
- &user.ExternalFontHosts,
- &user.GoogleID,
- &user.OpenIDConnectID,
- &user.DisplayMode,
- &user.EntryOrder,
- &user.DefaultReadingSpeed,
- &user.CJKReadingSpeed,
- &user.DefaultHomePage,
- &user.CategoriesSortingOrder,
- &user.MarkReadOnView,
- &user.MarkReadOnMediaPlayerCompletion,
- &user.MediaPlaybackRate,
- &user.BlockFilterEntryRules,
- &user.KeepFilterEntryRules,
- &user.AlwaysOpenExternalLinks,
- &user.OpenExternalLinksInNewTab,
- )
- if err == sql.ErrNoRows {
- return nil, nil
- } else if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch user: %v`, err)
- }
- return &user, nil
- }
- // RemoveUser deletes a user.
- func (s *Storage) RemoveUser(userID int64) error {
- tx, err := s.db.Begin()
- if err != nil {
- return fmt.Errorf(`store: unable to start transaction: %v`, err)
- }
- if _, err := tx.Exec(`DELETE FROM users WHERE id=$1`, userID); err != nil {
- tx.Rollback()
- return fmt.Errorf(`store: unable to remove user #%d: %v`, userID, err)
- }
- if _, err := tx.Exec(`DELETE FROM integrations WHERE user_id=$1`, userID); err != nil {
- tx.Rollback()
- return fmt.Errorf(`store: unable to remove integration settings for user #%d: %v`, userID, err)
- }
- if err := tx.Commit(); err != nil {
- return fmt.Errorf(`store: unable to commit transaction: %v`, err)
- }
- return nil
- }
- // RemoveUserAsync deletes user data without locking the database.
- func (s *Storage) RemoveUserAsync(userID int64) {
- go func() {
- if err := s.deleteUserFeeds(userID); err != nil {
- slog.Error("Unable to delete user feeds",
- slog.Int64("user_id", userID),
- slog.Any("error", err),
- )
- return
- }
- s.db.Exec(`DELETE FROM users WHERE id=$1`, userID)
- s.db.Exec(`DELETE FROM integrations WHERE user_id=$1`, userID)
- slog.Debug("User deleted",
- slog.Int64("user_id", userID),
- slog.Int("goroutines", runtime.NumGoroutine()),
- )
- }()
- }
- func (s *Storage) deleteUserFeeds(userID int64) error {
- rows, err := s.db.Query(`SELECT id FROM feeds WHERE user_id=$1`, userID)
- if err != nil {
- return fmt.Errorf(`store: unable to get user feeds: %v`, err)
- }
- defer rows.Close()
- for rows.Next() {
- var feedID int64
- rows.Scan(&feedID)
- slog.Debug("Deleting feed",
- slog.Int64("user_id", userID),
- slog.Int64("feed_id", feedID),
- slog.Int("goroutines", runtime.NumGoroutine()),
- )
- if err := s.RemoveFeed(userID, feedID); err != nil {
- return err
- }
- }
- return nil
- }
- // Users returns all users.
- func (s *Storage) Users() (model.Users, error) {
- query := `
- SELECT
- id,
- username,
- is_admin,
- theme,
- language,
- timezone,
- entry_direction,
- entries_per_page,
- keyboard_shortcuts,
- show_reading_time,
- entry_swipe,
- gesture_nav,
- last_login_at,
- stylesheet,
- custom_js,
- external_font_hosts,
- google_id,
- openid_connect_id,
- display_mode,
- entry_order,
- default_reading_speed,
- cjk_reading_speed,
- default_home_page,
- categories_sorting_order,
- mark_read_on_view,
- mark_read_on_media_player_completion,
- media_playback_rate,
- block_filter_entry_rules,
- keep_filter_entry_rules,
- always_open_external_links,
- open_external_links_in_new_tab
- FROM
- users
- ORDER BY username ASC
- `
- rows, err := s.db.Query(query)
- if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch users: %v`, err)
- }
- defer rows.Close()
- var users model.Users
- for rows.Next() {
- var user model.User
- err := rows.Scan(
- &user.ID,
- &user.Username,
- &user.IsAdmin,
- &user.Theme,
- &user.Language,
- &user.Timezone,
- &user.EntryDirection,
- &user.EntriesPerPage,
- &user.KeyboardShortcuts,
- &user.ShowReadingTime,
- &user.EntrySwipe,
- &user.GestureNav,
- &user.LastLoginAt,
- &user.Stylesheet,
- &user.CustomJS,
- &user.ExternalFontHosts,
- &user.GoogleID,
- &user.OpenIDConnectID,
- &user.DisplayMode,
- &user.EntryOrder,
- &user.DefaultReadingSpeed,
- &user.CJKReadingSpeed,
- &user.DefaultHomePage,
- &user.CategoriesSortingOrder,
- &user.MarkReadOnView,
- &user.MarkReadOnMediaPlayerCompletion,
- &user.MediaPlaybackRate,
- &user.BlockFilterEntryRules,
- &user.KeepFilterEntryRules,
- &user.AlwaysOpenExternalLinks,
- &user.OpenExternalLinksInNewTab,
- )
- if err != nil {
- return nil, fmt.Errorf(`store: unable to fetch users row: %v`, err)
- }
- users = append(users, &user)
- }
- return users, nil
- }
- // CheckPassword validate the hashed password.
- func (s *Storage) CheckPassword(username, password string) error {
- var hash string
- username = strings.ToLower(username)
- err := s.db.QueryRow("SELECT password FROM users WHERE username=$1", username).Scan(&hash)
- if err == sql.ErrNoRows {
- return fmt.Errorf(`store: unable to find this user: %s`, username)
- } else if err != nil {
- return fmt.Errorf(`store: unable to fetch user: %v`, err)
- }
- if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil {
- return fmt.Errorf(`store: invalid password for "%s" (%v)`, username, err)
- }
- return nil
- }
- // HasPassword returns true if the given user has a password defined.
- func (s *Storage) HasPassword(userID int64) (bool, error) {
- var result bool
- query := `SELECT true FROM users WHERE id=$1 AND password <> '' LIMIT 1`
- err := s.db.QueryRow(query, userID).Scan(&result)
- if err == sql.ErrNoRows {
- return false, nil
- } else if err != nil {
- return false, fmt.Errorf(`store: unable to execute query: %v`, err)
- }
- if result {
- return true, nil
- }
- return false, nil
- }
|