96 lines
3.9 KiB
Go
96 lines
3.9 KiB
Go
// 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
|
|
}
|