This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authsqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gamertan.com/web/auth"
|
||||
"gamertan.com/web/organizations"
|
||||
)
|
||||
|
||||
var _ auth.UserDirectoryRepository = (*Store)(nil)
|
||||
var _ organizations.DirectoryRepository = (*Store)(nil)
|
||||
|
||||
// UserDirectory is an administrative read; the adapter cannot infer application
|
||||
// authorization. Search covers ID, username, email and display name. SQLite LIKE
|
||||
// folds ASCII case; non-ASCII display-name text matches with its original case.
|
||||
func (store *Store) UserDirectory(ctx context.Context, query auth.UserDirectoryQuery) (auth.UserDirectoryPage, error) {
|
||||
pattern, limit, valid := directoryQuery(query.Search, query.AfterID, query.Limit)
|
||||
if !valid {
|
||||
return auth.UserDirectoryPage{}, auth.ErrDirectoryQuery
|
||||
}
|
||||
rows, err := store.db.QueryContext(ctx, `SELECT id,username,email,display_name,status,password_change_required,registration_pending,created_at,updated_at
|
||||
FROM gwf_users WHERE id>? AND (?='' OR id=? OR username_normalized LIKE ? ESCAPE '\' OR email_normalized LIKE ? ESCAPE '\' OR display_name LIKE ? ESCAPE '\')
|
||||
ORDER BY id LIMIT ?`, query.AfterID, strings.TrimSpace(query.Search), strings.TrimSpace(query.Search), strings.ToLower(pattern), strings.ToLower(pattern), pattern, limit+1)
|
||||
if err != nil {
|
||||
return auth.UserDirectoryPage{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
page := auth.UserDirectoryPage{Users: make([]auth.User, 0, limit)}
|
||||
for rows.Next() {
|
||||
user, err := scanPasskeyUser(rows)
|
||||
if err != nil {
|
||||
return auth.UserDirectoryPage{}, err
|
||||
}
|
||||
page.Users = append(page.Users, user)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return auth.UserDirectoryPage{}, err
|
||||
}
|
||||
if len(page.Users) > limit {
|
||||
page.Users = page.Users[:limit]
|
||||
page.NextID = page.Users[limit-1].ID
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// OrganizationDirectory reads all personal/business and active/archived records.
|
||||
// It does not join membership, grant access, or choose a merchant. Search covers
|
||||
// exact ID and literal slug/name text using SQLite's ASCII case folding.
|
||||
func (store *Store) OrganizationDirectory(ctx context.Context, query organizations.DirectoryQuery) (organizations.DirectoryPage, error) {
|
||||
pattern, limit, valid := directoryQuery(query.Search, query.AfterID, query.Limit)
|
||||
if !valid {
|
||||
return organizations.DirectoryPage{}, organizations.ErrDirectoryQuery
|
||||
}
|
||||
rows, err := store.db.QueryContext(ctx, `SELECT id,slug,name,status,personal,revision,created_at,updated_at
|
||||
FROM gwf_organizations WHERE id>? AND (?='' OR id=? OR slug LIKE ? ESCAPE '\' OR name LIKE ? ESCAPE '\')
|
||||
ORDER BY id LIMIT ?`, query.AfterID, strings.TrimSpace(query.Search), strings.TrimSpace(query.Search), pattern, pattern, limit+1)
|
||||
if err != nil {
|
||||
return organizations.DirectoryPage{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
page := organizations.DirectoryPage{Organizations: make([]organizations.Organization, 0, limit)}
|
||||
for rows.Next() {
|
||||
var value organizations.Organization
|
||||
var created, updated int64
|
||||
if err = rows.Scan(&value.ID, &value.Slug, &value.Name, &value.Status, &value.Personal, &value.Revision, &created, &updated); err != nil {
|
||||
return organizations.DirectoryPage{}, err
|
||||
}
|
||||
value.CreatedAt, value.UpdatedAt = time.Unix(created, 0).UTC(), time.Unix(updated, 0).UTC()
|
||||
page.Organizations = append(page.Organizations, value)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return organizations.DirectoryPage{}, err
|
||||
}
|
||||
if len(page.Organizations) > limit {
|
||||
page.Organizations = page.Organizations[:limit]
|
||||
page.NextID = page.Organizations[limit-1].ID
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func directoryQuery(search, after string, limit int) (string, int, bool) {
|
||||
if !text(search, 128, true) || after != "" && !opaqueID(after) || limit < 0 || limit > 200 {
|
||||
return "", 0, false
|
||||
}
|
||||
if limit == 0 {
|
||||
limit = 50
|
||||
}
|
||||
// Wildcards and the escape character are literal user text, never operators.
|
||||
pattern := "%" + strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(strings.TrimSpace(search)) + "%"
|
||||
return pattern, limit, true
|
||||
}
|
||||
Reference in New Issue
Block a user