package sqlstore import ( "context" "database/sql" "errors" "fmt" "strings" "github.com/dariuszjaniak/warp/server/internal/storage" ) // Users is the shared SQL implementation of storage.UserStore. // // Unlike the settings store, whose four statements each driver writes in its // own style, the user store has around thirty. Keeping two copies of them // differing only in `?` versus `$1` would be a standing invitation to fix a // bug in one and not the other, so the SQL is written once with `?` and the // driver supplies a Rebind function. The statements themselves use only // syntax both engines agree on: ON CONFLICT ... DO UPDATE with `excluded`, // LIMIT/OFFSET, and comparisons against bound values rather than boolean // literals. type Users struct { db *sql.DB rebind func(string) string isUnique func(error) bool } // NewUsers builds a user store. rebind translates the `?` placeholders into // the driver's own style, and isUnique recognises its unique-constraint // error; nil means "no translation" and "never", respectively. func NewUsers(db *sql.DB, rebind func(string) string, isUnique func(error) bool) *Users { if rebind == nil { rebind = func(query string) string { return query } } if isUnique == nil { isUnique = func(error) bool { return false } } return &Users{db: db, rebind: rebind, isUnique: isUnique} } // defaultAccountPageSize bounds an admin user listing that asks for no limit, // so a forgotten ?limit= cannot stream the whole user table. const defaultAccountPageSize = 100 // maxAccountPageSize caps what a caller may ask for. const maxAccountPageSize = 1000 // q rebinds a statement for this driver. func (u *Users) q(query string) string { return u.rebind(query) } // tx runs fn in a transaction, rolling back on any error. The rollback after // a successful commit is a no-op, which is why it can be deferred blindly. func (u *Users) tx(ctx context.Context, fn func(*sql.Tx) error) error { tx, err := u.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin transaction: %w", err) } defer func() { _ = tx.Rollback() }() if err := fn(tx); err != nil { return err } if err := tx.Commit(); err != nil { return fmt.Errorf("commit transaction: %w", err) } return nil } const insertAccountSQL = ` INSERT INTO user_accounts (localpart, password_hash, display_name, admin, created_at_ms) VALUES (?, ?, ?, ?, ?)` // CreateAccount implements storage.UserStore. // // Taken localparts are detected by inserting and letting the primary key // complain, rather than by checking first: the check-then-insert version has // a race that two simultaneous registrations of the same name would walk // straight into. func (u *Users) CreateAccount(ctx context.Context, acc storage.NewAccount) (storage.Account, error) { _, err := u.db.ExecContext(ctx, u.q(insertAccountSQL), acc.Localpart, acc.PasswordHash, acc.DisplayName, acc.Admin, acc.CreatedAtMS) if err != nil { if u.isUnique(err) { return storage.Account{}, fmt.Errorf("create account %q: %w", acc.Localpart, storage.ErrUserExists) } return storage.Account{}, fmt.Errorf("create account %q: %w", acc.Localpart, err) } return storage.Account{ Localpart: acc.Localpart, DisplayName: acc.DisplayName, Admin: acc.Admin, CreatedAtMS: acc.CreatedAtMS, }, nil } const selectAccountSQL = ` SELECT localpart, display_name, avatar_url, admin, deactivated, created_at_ms FROM user_accounts WHERE localpart = ?` // Account implements storage.UserStore. func (u *Users) Account(ctx context.Context, localpart string) (storage.Account, error) { var acc storage.Account err := u.db.QueryRowContext(ctx, u.q(selectAccountSQL), localpart).Scan( &acc.Localpart, &acc.DisplayName, &acc.AvatarURL, &acc.Admin, &acc.Deactivated, &acc.CreatedAtMS) switch { case errors.Is(err, sql.ErrNoRows): return storage.Account{}, fmt.Errorf("account %q: %w", localpart, storage.ErrNoSuchUser) case err != nil: return storage.Account{}, fmt.Errorf("account %q: %w", localpart, err) } return acc, nil } const selectPasswordHashSQL = `SELECT password_hash FROM user_accounts WHERE localpart = ?` // PasswordHash implements storage.UserStore. func (u *Users) PasswordHash(ctx context.Context, localpart string) (string, error) { var hash string err := u.db.QueryRowContext(ctx, u.q(selectPasswordHashSQL), localpart).Scan(&hash) switch { case errors.Is(err, sql.ErrNoRows): return "", fmt.Errorf("password for %q: %w", localpart, storage.ErrNoSuchUser) case err != nil: return "", fmt.Errorf("password for %q: %w", localpart, err) } return hash, nil } // SetPasswordHash implements storage.UserStore. func (u *Users) SetPasswordHash(ctx context.Context, localpart, hash string) error { return u.updateAccount(ctx, localpart, `UPDATE user_accounts SET password_hash = ? WHERE localpart = ?`, hash) } // SetAdmin implements storage.UserStore. func (u *Users) SetAdmin(ctx context.Context, localpart string, admin bool) error { return u.updateAccount(ctx, localpart, `UPDATE user_accounts SET admin = ? WHERE localpart = ?`, admin) } // SetDisplayName implements storage.UserStore. func (u *Users) SetDisplayName(ctx context.Context, localpart, displayName string) error { return u.updateAccount(ctx, localpart, `UPDATE user_accounts SET display_name = ? WHERE localpart = ?`, displayName) } // SetAvatarURL implements storage.UserStore. func (u *Users) SetAvatarURL(ctx context.Context, localpart, avatarURL string) error { return u.updateAccount(ctx, localpart, `UPDATE user_accounts SET avatar_url = ? WHERE localpart = ?`, avatarURL) } // updateAccount runs a single-column update and turns "nothing changed" into // ErrNoSuchUser, so callers do not have to look the account up first. func (u *Users) updateAccount(ctx context.Context, localpart, query string, value any) error { res, err := u.db.ExecContext(ctx, u.q(query), value, localpart) if err != nil { return fmt.Errorf("update account %q: %w", localpart, err) } affected, err := res.RowsAffected() if err != nil { return fmt.Errorf("update account %q: %w", localpart, err) } if affected == 0 { return fmt.Errorf("update account %q: %w", localpart, storage.ErrNoSuchUser) } return nil } // likeEscape is the escape character the search patterns use. // // It is deliberately not a backslash: a backslash in a SQL string literal // means different things depending on PostgreSQL's standard_conforming_ // strings setting, and "!" means exactly one thing everywhere. const likeEscape = '!' const ( countAccountsSQL = ` SELECT COUNT(*) FROM user_accounts WHERE LOWER(localpart) LIKE ? ESCAPE '!' OR LOWER(display_name) LIKE ? ESCAPE '!'` listAccountsSQL = ` SELECT localpart, display_name, avatar_url, admin, deactivated, created_at_ms FROM user_accounts WHERE LOWER(localpart) LIKE ? ESCAPE '!' OR LOWER(display_name) LIKE ? ESCAPE '!' ORDER BY localpart LIMIT ? OFFSET ?` ) // ListAccounts implements storage.UserStore. // // The search is a LOWER(...) LIKE rather than ILIKE, which is PostgreSQL // only, and the pattern is escaped so that searching for "_" does not // quietly match every single-character name. func (u *Users) ListAccounts(ctx context.Context, filter storage.AccountFilter) ([]storage.Account, int, error) { pattern := likeContains(filter.Search) limit := filter.Limit switch { case limit <= 0: limit = defaultAccountPageSize case limit > maxAccountPageSize: limit = maxAccountPageSize } offset := max(filter.Offset, 0) var total int if err := u.db.QueryRowContext(ctx, u.q(countAccountsSQL), pattern, pattern).Scan(&total); err != nil { return nil, 0, fmt.Errorf("count accounts: %w", err) } rows, err := u.db.QueryContext(ctx, u.q(listAccountsSQL), pattern, pattern, limit, offset) if err != nil { return nil, 0, fmt.Errorf("list accounts: %w", err) } defer rows.Close() accounts := []storage.Account{} for rows.Next() { var acc storage.Account if err := rows.Scan(&acc.Localpart, &acc.DisplayName, &acc.AvatarURL, &acc.Admin, &acc.Deactivated, &acc.CreatedAtMS); err != nil { return nil, 0, fmt.Errorf("scan account: %w", err) } accounts = append(accounts, acc) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("list accounts: %w", err) } return accounts, total, nil } // likeContains turns a search term into a LIKE pattern matching it // anywhere, escaping the two wildcards and the escape character itself. The // statements using it declare ESCAPE explicitly, because SQLite has no // default escape character at all. func likeContains(search string) string { search = strings.ToLower(strings.TrimSpace(search)) var b strings.Builder b.Grow(len(search) + 2) b.WriteByte('%') for _, r := range search { if r == '%' || r == '_' || r == likeEscape { b.WriteByte(likeEscape) } b.WriteRune(r) } b.WriteByte('%') return b.String() } const statsSQL = ` SELECT (SELECT COUNT(*) FROM user_accounts), (SELECT COUNT(*) FROM user_accounts WHERE admin = ?), (SELECT COUNT(*) FROM user_accounts WHERE deactivated = ?), (SELECT COUNT(*) FROM user_devices)` // Stats implements storage.UserStore. func (u *Users) Stats(ctx context.Context) (storage.Stats, error) { var s storage.Stats // The flags are compared against bound values rather than TRUE, which // SQLite only learned in 3.23 and which PostgreSQL spells differently // from an integer. err := u.db.QueryRowContext(ctx, u.q(statsSQL), true, true). Scan(&s.Users, &s.Admins, &s.Deactivated, &s.Devices) if err != nil { return storage.Stats{}, fmt.Errorf("user stats: %w", err) } return s, nil } const ( selectDeviceForUpdateSQL = ` SELECT display_name, created_at_ms FROM user_devices WHERE localpart = ? AND device_id = ?` insertDeviceSQL = ` INSERT INTO user_devices (localpart, device_id, display_name, created_at_ms, last_seen_ms, last_seen_ip, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?)` touchDeviceSQL = ` UPDATE user_devices SET last_seen_ms = ?, last_seen_ip = ?, user_agent = ? WHERE localpart = ? AND device_id = ?` deleteDeviceTokensSQL = `DELETE FROM user_access_tokens WHERE localpart = ? AND device_id = ?` deleteDeviceRefreshTokensSQL = `DELETE FROM user_refresh_tokens WHERE localpart = ? AND device_id = ?` insertAccessTokenSQL = ` INSERT INTO user_access_tokens (token_hash, localpart, device_id, created_at_ms, expires_at_ms) VALUES (?, ?, ?, ?, ?)` insertRefreshTokenSQL = ` INSERT INTO user_refresh_tokens (token_hash, localpart, device_id, created_at_ms, expires_at_ms) VALUES (?, ?, ?, ?, ?)` accountExistsSQL = `SELECT 1 FROM user_accounts WHERE localpart = ?` ) // CreateSession implements storage.UserStore. // // Logging in on a device ID that already exists keeps the device — and its // display name, because initial_device_display_name only applies to a device // being created — but revokes the tokens it held, which is what the spec // requires. func (u *Users) CreateSession(ctx context.Context, s storage.NewSession) (storage.Device, error) { device := storage.Device{ Localpart: s.Localpart, ID: s.DeviceID, DisplayName: s.DisplayName, CreatedAtMS: s.CreatedAtMS, LastSeenMS: s.CreatedAtMS, LastSeenIP: s.LastSeenIP, UserAgent: s.UserAgent, } err := u.tx(ctx, func(tx *sql.Tx) error { var exists int switch err := tx.QueryRowContext(ctx, u.q(accountExistsSQL), s.Localpart).Scan(&exists); { case errors.Is(err, sql.ErrNoRows): return fmt.Errorf("create session for %q: %w", s.Localpart, storage.ErrNoSuchUser) case err != nil: return fmt.Errorf("create session for %q: %w", s.Localpart, err) } var existingName string var existingCreated int64 err := tx.QueryRowContext(ctx, u.q(selectDeviceForUpdateSQL), s.Localpart, s.DeviceID). Scan(&existingName, &existingCreated) switch { case errors.Is(err, sql.ErrNoRows): if _, err := tx.ExecContext(ctx, u.q(insertDeviceSQL), s.Localpart, s.DeviceID, s.DisplayName, s.CreatedAtMS, s.CreatedAtMS, s.LastSeenIP, s.UserAgent); err != nil { return fmt.Errorf("create device %q: %w", s.DeviceID, err) } case err != nil: return fmt.Errorf("read device %q: %w", s.DeviceID, err) default: device.DisplayName = existingName device.CreatedAtMS = existingCreated if _, err := tx.ExecContext(ctx, u.q(touchDeviceSQL), s.CreatedAtMS, s.LastSeenIP, s.UserAgent, s.Localpart, s.DeviceID); err != nil { return fmt.Errorf("update device %q: %w", s.DeviceID, err) } for _, query := range []string{deleteDeviceTokensSQL, deleteDeviceRefreshTokensSQL} { if _, err := tx.ExecContext(ctx, u.q(query), s.Localpart, s.DeviceID); err != nil { return fmt.Errorf("revoke tokens of device %q: %w", s.DeviceID, err) } } } if _, err := tx.ExecContext(ctx, u.q(insertAccessTokenSQL), s.AccessTokenHash, s.Localpart, s.DeviceID, s.CreatedAtMS, s.AccessTokenExpiresAtMS); err != nil { return fmt.Errorf("store access token: %w", err) } if s.RefreshTokenHash != "" { if _, err := tx.ExecContext(ctx, u.q(insertRefreshTokenSQL), s.RefreshTokenHash, s.Localpart, s.DeviceID, s.CreatedAtMS, s.RefreshTokenExpiresAtMS); err != nil { return fmt.Errorf("store refresh token: %w", err) } } return nil }) if err != nil { return storage.Device{}, err } return device, nil } const ( selectRefreshTokenSQL = ` SELECT localpart, device_id, expires_at_ms FROM user_refresh_tokens WHERE token_hash = ?` deleteRefreshTokenSQL = `DELETE FROM user_refresh_tokens WHERE token_hash = ?` selectAccountFlagsSQL = `SELECT admin, deactivated FROM user_accounts WHERE localpart = ?` ) // RefreshSession implements storage.UserStore. // // The spent refresh token and the access token it had minted both go away // immediately. The spec allows keeping the old access token alive for a grace // period; refusing it at once is the stricter reading and means a stolen // token stops working the moment the real client refreshes. func (u *Users) RefreshSession(ctx context.Context, r storage.RefreshedSession) (storage.TokenInfo, error) { var info storage.TokenInfo err := u.tx(ctx, func(tx *sql.Tx) error { var expiresAt int64 err := tx.QueryRowContext(ctx, u.q(selectRefreshTokenSQL), r.RefreshTokenHash). Scan(&info.Localpart, &info.DeviceID, &expiresAt) switch { case errors.Is(err, sql.ErrNoRows): return fmt.Errorf("refresh token: %w", storage.ErrNoSuchToken) case err != nil: return fmt.Errorf("refresh token: %w", err) } if expiresAt != 0 && expiresAt <= r.RefreshedAtMS { // Clean the dead token up on the way past; nothing else sweeps. if _, err := tx.ExecContext(ctx, u.q(deleteRefreshTokenSQL), r.RefreshTokenHash); err != nil { return fmt.Errorf("delete expired refresh token: %w", err) } return fmt.Errorf("refresh token has expired: %w", storage.ErrNoSuchToken) } if _, err := tx.ExecContext(ctx, u.q(deleteRefreshTokenSQL), r.RefreshTokenHash); err != nil { return fmt.Errorf("spend refresh token: %w", err) } if _, err := tx.ExecContext(ctx, u.q(deleteDeviceTokensSQL), info.Localpart, info.DeviceID); err != nil { return fmt.Errorf("revoke previous access token: %w", err) } if _, err := tx.ExecContext(ctx, u.q(insertAccessTokenSQL), r.NewAccessTokenHash, info.Localpart, info.DeviceID, r.RefreshedAtMS, r.AccessTokenExpiresAtMS); err != nil { return fmt.Errorf("store access token: %w", err) } if r.NewRefreshTokenHash != "" { if _, err := tx.ExecContext(ctx, u.q(insertRefreshTokenSQL), r.NewRefreshTokenHash, info.Localpart, info.DeviceID, r.RefreshedAtMS, r.RefreshTokenExpiresAtMS); err != nil { return fmt.Errorf("store refresh token: %w", err) } } if err := tx.QueryRowContext(ctx, u.q(selectAccountFlagsSQL), info.Localpart). Scan(&info.Admin, &info.Deactivated); err != nil { return fmt.Errorf("read account %q: %w", info.Localpart, err) } info.ExpiresAtMS = r.AccessTokenExpiresAtMS return nil }) if err != nil { return storage.TokenInfo{}, err } return info, nil } const selectTokenInfoSQL = ` SELECT t.localpart, t.device_id, t.expires_at_ms, a.admin, a.deactivated FROM user_access_tokens t JOIN user_accounts a ON a.localpart = t.localpart WHERE t.token_hash = ?` // TokenInfo implements storage.UserStore. func (u *Users) TokenInfo(ctx context.Context, tokenHash string) (storage.TokenInfo, error) { var info storage.TokenInfo err := u.db.QueryRowContext(ctx, u.q(selectTokenInfoSQL), tokenHash).Scan( &info.Localpart, &info.DeviceID, &info.ExpiresAtMS, &info.Admin, &info.Deactivated) switch { case errors.Is(err, sql.ErrNoRows): return storage.TokenInfo{}, storage.ErrNoSuchToken case err != nil: return storage.TokenInfo{}, fmt.Errorf("look up access token: %w", err) } return info, nil } // TouchDevice implements storage.UserStore. A device that has gone away // between authenticating and recording the visit is not an error: the // request is being answered either way. func (u *Users) TouchDevice(ctx context.Context, localpart, deviceID, ip, userAgent string, seenAtMS int64) error { if _, err := u.db.ExecContext(ctx, u.q(touchDeviceSQL), seenAtMS, ip, userAgent, localpart, deviceID); err != nil { return fmt.Errorf("touch device %q: %w", deviceID, err) } return nil } const ( selectDeviceSQL = ` SELECT localpart, device_id, display_name, created_at_ms, last_seen_ms, last_seen_ip, user_agent FROM user_devices WHERE localpart = ? AND device_id = ?` selectDevicesSQL = ` SELECT localpart, device_id, display_name, created_at_ms, last_seen_ms, last_seen_ip, user_agent FROM user_devices WHERE localpart = ? ORDER BY device_id` updateDeviceDisplayNameSQL = ` UPDATE user_devices SET display_name = ? WHERE localpart = ? AND device_id = ?` ) // Device implements storage.UserStore. func (u *Users) Device(ctx context.Context, localpart, deviceID string) (storage.Device, error) { var d storage.Device err := u.db.QueryRowContext(ctx, u.q(selectDeviceSQL), localpart, deviceID).Scan( &d.Localpart, &d.ID, &d.DisplayName, &d.CreatedAtMS, &d.LastSeenMS, &d.LastSeenIP, &d.UserAgent) switch { case errors.Is(err, sql.ErrNoRows): return storage.Device{}, fmt.Errorf("device %q: %w", deviceID, storage.ErrNoSuchDevice) case err != nil: return storage.Device{}, fmt.Errorf("device %q: %w", deviceID, err) } return d, nil } // Devices implements storage.UserStore. func (u *Users) Devices(ctx context.Context, localpart string) ([]storage.Device, error) { rows, err := u.db.QueryContext(ctx, u.q(selectDevicesSQL), localpart) if err != nil { return nil, fmt.Errorf("list devices of %q: %w", localpart, err) } defer rows.Close() devices := []storage.Device{} for rows.Next() { var d storage.Device if err := rows.Scan(&d.Localpart, &d.ID, &d.DisplayName, &d.CreatedAtMS, &d.LastSeenMS, &d.LastSeenIP, &d.UserAgent); err != nil { return nil, fmt.Errorf("scan device: %w", err) } devices = append(devices, d) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("list devices of %q: %w", localpart, err) } return devices, nil } // SetDeviceDisplayName implements storage.UserStore. func (u *Users) SetDeviceDisplayName(ctx context.Context, localpart, deviceID, displayName string) error { res, err := u.db.ExecContext(ctx, u.q(updateDeviceDisplayNameSQL), displayName, localpart, deviceID) if err != nil { return fmt.Errorf("rename device %q: %w", deviceID, err) } affected, err := res.RowsAffected() if err != nil { return fmt.Errorf("rename device %q: %w", deviceID, err) } if affected == 0 { return fmt.Errorf("rename device %q: %w", deviceID, storage.ErrNoSuchDevice) } return nil } // DeleteDevices implements storage.UserStore. // // Tokens are deleted explicitly rather than left to ON DELETE CASCADE: SQLite // only enforces foreign keys when the connection asks it to, so relying on // the cascade would make revocation depend on a DSN pragma. func (u *Users) DeleteDevices(ctx context.Context, localpart string, deviceIDs []string) (int, error) { if len(deviceIDs) == 0 { return 0, nil } args := make([]any, 0, len(deviceIDs)+1) args = append(args, localpart) for _, id := range deviceIDs { args = append(args, id) } in := "(" + strings.TrimSuffix(strings.Repeat("?,", len(deviceIDs)), ",") + ")" var deleted int err := u.tx(ctx, func(tx *sql.Tx) error { for _, table := range []string{"user_access_tokens", "user_refresh_tokens"} { query := "DELETE FROM " + table + " WHERE localpart = ? AND device_id IN " + in if _, err := tx.ExecContext(ctx, u.q(query), args...); err != nil { return fmt.Errorf("revoke tokens: %w", err) } } res, err := tx.ExecContext(ctx, u.q("DELETE FROM user_devices WHERE localpart = ? AND device_id IN "+in), args...) if err != nil { return fmt.Errorf("delete devices: %w", err) } affected, err := res.RowsAffected() if err != nil { return fmt.Errorf("delete devices: %w", err) } deleted = int(affected) return nil }) return deleted, err } // DeleteAllDevices implements storage.UserStore. func (u *Users) DeleteAllDevices(ctx context.Context, localpart string) (int, error) { var deleted int err := u.tx(ctx, func(tx *sql.Tx) error { for _, table := range []string{"user_access_tokens", "user_refresh_tokens"} { if _, err := tx.ExecContext(ctx, u.q("DELETE FROM "+table+" WHERE localpart = ?"), localpart); err != nil { return fmt.Errorf("revoke tokens of %q: %w", localpart, err) } } res, err := tx.ExecContext(ctx, u.q("DELETE FROM user_devices WHERE localpart = ?"), localpart) if err != nil { return fmt.Errorf("delete devices of %q: %w", localpart, err) } affected, err := res.RowsAffected() if err != nil { return fmt.Errorf("delete devices of %q: %w", localpart, err) } deleted = int(affected) return nil }) return deleted, err } const ( selectAccountDataSQL = ` SELECT content FROM user_account_data WHERE localpart = ? AND room_id = ? AND data_type = ?` upsertAccountDataSQL = ` INSERT INTO user_account_data (localpart, room_id, data_type, content, stream_pos) VALUES (?, ?, ?, ?, ?) ON CONFLICT (localpart, room_id, data_type) DO UPDATE SET content = excluded.content, stream_pos = excluded.stream_pos` ) // AccountData implements storage.UserStore. func (u *Users) AccountData(ctx context.Context, localpart, roomID, dataType string) ([]byte, error) { var content string err := u.db.QueryRowContext(ctx, u.q(selectAccountDataSQL), localpart, roomID, dataType).Scan(&content) switch { case errors.Is(err, sql.ErrNoRows): return nil, fmt.Errorf("account data %q: %w", dataType, storage.ErrNoSuchAccountData) case err != nil: return nil, fmt.Errorf("account data %q: %w", dataType, err) } return []byte(content), nil } // SetAccountData implements storage.UserStore. The content is stored as text // rather than a JSON column type: the two engines disagree on what JSON // normalisation does, and account data has to come back out byte for byte. // // The write also stamps the row with a position in the account data stream, // taken inside the same transaction as the write itself. Account data is // one of the streams /sync reports, and a position assigned separately // would either be visible before the content it describes or after it — in // the second case a client would be told the stream had moved and then find // the old value. func (u *Users) SetAccountData(ctx context.Context, localpart, roomID, dataType string, content []byte) error { err := u.tx(ctx, func(tx *sql.Tx) error { pos, err := nextSequence(ctx, tx, u.q, syncSequences, seqAccountData) if err != nil { return err } if _, err := tx.ExecContext(ctx, u.q(upsertAccountDataSQL), localpart, roomID, dataType, string(content), pos); err != nil { return err } return nil }) if err != nil { return fmt.Errorf("set account data %q: %w", dataType, err) } return nil } // Compile-time proof that the shared implementation satisfies the interface. var _ storage.UserStore = (*Users)(nil)