Перейти к содержанию

Database — низкоуровневый CRUD

Database-секция админ-панели — это прямой доступ к произвольным таблицам Supabase через bridge. По сути SQL-консоль с UI, но ограниченная whitelisted-таблицами для безопасности.

Что внутри

flowchart LR
  UI[UI таблица] --> List[adminCatalogList
SELECT * FROM <table>] UI --> Update[adminCatalogUpdate
UPDATE <table> SET ... WHERE id] UI --> Delete[adminCatalogDelete
UPDATE <table> SET is_deleted=true] UI --> Find[adminFindByHash
SELECT WHERE sha256=$1] UI --> Wipe[adminWipeAll
DELETE FROM <table> — only on staging]

Whitelist таблиц (заданный в AppBridge):

HunterGraphics.Shell/Bridge/AppBridge.cs (упрощено)
private static readonly HashSet<string> _allowedAdminTables = new(StringComparer.Ordinal)
{
    "redux_items",
    "redux_versions",
    "gunpacks",
    "gunpack_guns",
    "armor_packs",
    "armor_pieces",
    "dlc_imports",
    "library_components",
    "gta_versions",
    "user_builds",
    "popular_choices",
    "pro_players",
    "admin_notes",
};

private void EnsureAllowedTable(string table)
{
    if (!_allowedAdminTables.Contains(table))
        throw new InvalidOperationException($"Table '{table}' not in admin whitelist");
}

Это первая линия защиты — UI может попросить «обнови row в users таблице», но bridge откажет. users и auth.* админка трогать не может (это уровень Supabase auth, отдельный flow).

Handlers

HunterGraphics.Shell/Bridge/WebViewBridgeHost.cs
["adminCatalogList"] = async payload =>
{
    var table = payload?.GetProperty("table").GetString()!;
    var search = payload?.TryGetProperty("search", out var s) == true ? s.GetString() : null;
    return await _bridge.AdminCatalogListAsync(table, search);
},

["adminCatalogUpdate"] = async payload =>
{
    var table = payload?.GetProperty("table").GetString()!;
    var id = payload?.GetProperty("id").GetString()!;
    var patch = payload?.GetProperty("patch") ?? throw new ArgumentException("patch required");
    return await _bridge.AdminCatalogUpdateAsync(table, id, patch);
},

["adminCatalogDelete"] = async payload =>
{
    var table = payload?.GetProperty("table").GetString()!;
    var id = payload?.GetProperty("id").GetString()!;
    return await _bridge.AdminCatalogDeleteAsync(table, id);
},

AdminCatalogUpdateAsync принимает arbitrary JSON patch и шлёт в Supabase как update().eq('id', ...). Это даёт админу полную свободу менять любую field — пять минут можно обновить description одного мода или массово переименовать author'а через скрипт.

Soft-delete всегда

AdminCatalogDeleteAsync никогда не делает DELETE FROM. Всегда UPDATE ... SET is_deleted = true. Причины:

  • Восстановление. Админ случайно удалил redux — можно вернуть руками через is_deleted = false.
  • Юзеры с ссылками. Если row удалена hard, у юзера в install-state.json остаётся reduxId который дереферится в null — UI крашится. С soft-delete мы можем показать «Мод удалён, но вот что у вас стоит» вместо ошибки.
  • R2-файлы. Real DELETE требовал бы также чистить R2 — отдельная сложная задача, см. r2-layout.

Hard-delete делается отдельной утилитой r2_gc.ps1 раз в месяц вручную.

adminFindByHash

Полезная фича для дебага:

public async Task<List<FindByHashRow>> AdminFindByHashAsync(string sha)
{
    // Ищем по нескольким таблицам и колонкам
    var rows = new List<FindByHashRow>();
    rows.AddRange(await _supa.QueryAsync("redux_versions",   $"sha256 = '{sha}'"));
    rows.AddRange(await _supa.QueryAsync("gunpacks",         $"rpf_sha256 = '{sha}'"));
    rows.AddRange(await _supa.QueryAsync("dlc_imports",      $"sha256 = '{sha}'"));
    return rows;
}

Use case: юзер прислал лог [inject] dirty file abc123def456... — админ кидает этот SHA в Find, видит «это из мода Y версии Z», понимает что юзер ставил другой мод до нашего.

adminWipeAll

public async Task<int> AdminWipeAllAsync(string table)
{
    if (!_environment.IsStaging)
        throw new InvalidOperationException("adminWipeAll allowed only on staging env");

    EnsureAllowedTable(table);
    return await _supa.DeleteAllAsync(table);
}

На production environment гард IsStaging блокирует. Stage- и dev-инстансы Supabase можно clean'уть одной кнопкой при тестах.

SQL editor

В UI это табличный инспектор + edit-modal — не raw SQL. Это намеренно: raw SQL даёт админу слишком много власти (можно случайно сломать БД). Если нужны сложные запросы — открываем Supabase Studio напрямую через браузер (см. infra/supabase-schema).

Дальше: GTA Versions →