API panel: document the whole REST surface, not half of it
The API section's tables are hand-maintained and carry a comment saying they
mirror the routes in server.go. They had stopped: 60 rows against 141 routes,
so 81 endpoints were reachable and undocumented. Everything added since the
tables were written is in that gap - technical checks, fuel, charging,
maintenance, documents, reminders, attachments, the vehicle-provider and
integration surfaces, and the OCPP socket - along with the per-car
sub-resources (fuel-stats, charging-stats, the provider routes) and
PUT /api/cars/{id}/view. Ten new groups, 60 rows to 123, and nothing listed
that no longer exists.
Checked by extracting every mux.Handle route from server.go and diffing it
against every row in the tables rather than by reading both lists: the seven
attachmentRoutes calls expand to their 21 concrete routes, the three static
panel paths are excluded, and the diff is empty in both directions. That
script is not committed - it is a one-off, and a real guard belongs in the Go
tests where it can see the mux, which is worth doing if these tables drift
again.
The flat list endpoints require ?car={id} - they refuse a cross-car listing
so access can be enforced - so the ones that do now say it. Attachments are
documented once with {records} standing for the seven collections that take a
file, because all seven behave identically and seven copies of three rows
would bury that. EndpointTable grew an optional note line under the header to
say what {records} means.
Two things fixed while in there. PUT rendered uncolored - methodClass has had
no PUT entry since the superadmin group started using it - and the longest
paths, the per-charger OCPP control routes, overran the card and were clipped
by its overflow-hidden rather than scrolling; the table sits in an
overflow-x-auto wrapper now. Thirteen of the eighteen tables scroll at phone
width, four of them before this commit's rows were added.
Group titles are translated in all three languages, plus one new auth label
for the OCPP socket, which authenticates the charger with OCPP Basic auth
rather than a bearer token. The endpoint descriptions stay English:
TRANSLATIONS.md calls them developer reference documentation, and this commit
does not reopen that.
Verified by go build ./... and go vet ./internal/api, and by driving the
panel in a browser against a mock API Server standing in for the real one -
all eighteen tables render, the {records} note and the PUT color show, the
Polish titles resolve, and no table clips at 375px. dist is rebuilt so the
embedded panel matches the source.
Not verified: nothing ran against a live API Server, so the descriptions are
checked against the handlers' code and comments rather than against
responses. The mock only answers /api/identity and /api/status, which is
enough to get past the login gate and reach the section.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a25b31842d
commit
b4e99240c6
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#2563eb" />
|
||||
<title>DriverVault · API Server</title>
|
||||
<script type="module" crossorigin src="/assets/index-B4_v9nDZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D3MeNcl7.css">
|
||||
<script type="module" crossorigin src="/assets/index-D2BCqgpA.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CGuUVjJH.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -78,6 +78,7 @@ const barFills = computed(() =>
|
||||
// registered in internal/api/server.go.
|
||||
const publicApi = [
|
||||
{ method: "GET", path: "/api/health", desc: "Liveness probe (no auth)" },
|
||||
{ method: "GET", path: "/healthz", desc: "The same probe under the conventional container path" },
|
||||
{ method: "GET", path: "/api/status", desc: "Health of PocketBase + Web App" },
|
||||
{ method: "POST", path: "/api/auth/login", desc: "Exchange email + password for a PocketBase token" },
|
||||
{ method: "GET", path: "/api/auth/validate", desc: "Check whether a token is still valid" },
|
||||
@@ -93,30 +94,128 @@ const carsApi = [
|
||||
{ method: "POST", path: "/api/cars", desc: "Create a car" },
|
||||
{ method: "GET", path: "/api/cars/{id}", desc: "Fetch one car" },
|
||||
{ method: "PATCH", path: "/api/cars/{id}", desc: "Update a car" },
|
||||
{ method: "PUT", path: "/api/cars/{id}/view", desc: "Save the car's layout — hidden tabs/fields and their order" },
|
||||
{ method: "DELETE", path: "/api/cars/{id}", desc: "Delete a car (owner only)" },
|
||||
{ method: "GET", path: "/api/cars/{id}/service-records", desc: "A car's service history" },
|
||||
{ method: "GET", path: "/api/cars/{id}/technical-checks", desc: "A car's roadworthiness inspections" },
|
||||
{ method: "GET", path: "/api/cars/{id}/parts", desc: "A car's parts catalog" },
|
||||
{ method: "GET", path: "/api/cars/{id}/fuel-entries", desc: "A car's refuelings" },
|
||||
{ method: "GET", path: "/api/cars/{id}/fuel-stats", desc: "Consumption + cost totals from the fuel log" },
|
||||
{ method: "GET", path: "/api/cars/{id}/charging-sessions", desc: "A car's charging sessions" },
|
||||
{ method: "GET", path: "/api/cars/{id}/charging-stats", desc: "Energy + cost totals from the charging log" },
|
||||
{ method: "GET", path: "/api/cars/{id}/maintenance", desc: "A car's maintenance log" },
|
||||
{ method: "GET", path: "/api/cars/{id}/documents", desc: "A car's documents" },
|
||||
{ method: "GET", path: "/api/cars/{id}/reminders", desc: "A car's reminders" },
|
||||
{ method: "GET", path: "/api/cars/{id}/provider", desc: "Live snapshot from the car's connected service" },
|
||||
{ method: "POST", path: "/api/cars/{id}/provider", desc: "Link the car to a provider vehicle (an empty provider unlinks)" },
|
||||
{ method: "POST", path: "/api/cars/{id}/provider/sync", desc: "Re-apply the provider's data to the car" },
|
||||
{ method: "GET", path: "/api/cars/{id}/shares", desc: "Who a car is shared with (owner)" },
|
||||
{ method: "POST", path: "/api/cars/{id}/shares", desc: "Share a car by email (owner)" },
|
||||
{ method: "DELETE", path: "/api/cars/{id}/shares/{userId}", desc: "Revoke a share (owner)" },
|
||||
];
|
||||
|
||||
const serviceApi = [
|
||||
{ method: "GET", path: "/api/service-records", desc: "List service records" },
|
||||
{ method: "GET", path: "/api/service-records", desc: "List one car's service records (?car={id})" },
|
||||
{ method: "POST", path: "/api/service-records", desc: "Log a service record" },
|
||||
{ method: "GET", path: "/api/service-records/{id}", desc: "Fetch one record" },
|
||||
{ method: "PATCH", path: "/api/service-records/{id}", desc: "Update a record" },
|
||||
{ method: "DELETE", path: "/api/service-records/{id}", desc: "Delete a record" },
|
||||
];
|
||||
|
||||
const technicalApi = [
|
||||
{ method: "GET", path: "/api/technical-checks", desc: "List one car's inspections (?car={id})" },
|
||||
{ method: "POST", path: "/api/technical-checks", desc: "Log an inspection" },
|
||||
{ method: "GET", path: "/api/technical-checks/{id}", desc: "Fetch one inspection" },
|
||||
{ method: "PATCH", path: "/api/technical-checks/{id}", desc: "Update an inspection" },
|
||||
{ method: "DELETE", path: "/api/technical-checks/{id}", desc: "Delete an inspection" },
|
||||
];
|
||||
|
||||
const partsApi = [
|
||||
{ method: "GET", path: "/api/parts", desc: "List parts" },
|
||||
{ method: "GET", path: "/api/parts", desc: "List one car's parts (?car={id})" },
|
||||
{ method: "POST", path: "/api/parts", desc: "Add a part" },
|
||||
{ method: "GET", path: "/api/parts/{id}", desc: "Fetch one part" },
|
||||
{ method: "PATCH", path: "/api/parts/{id}", desc: "Update a part" },
|
||||
{ method: "DELETE", path: "/api/parts/{id}", desc: "Delete a part" },
|
||||
];
|
||||
|
||||
const fuelApi = [
|
||||
{ method: "GET", path: "/api/fuel-entries", desc: "List one car's refuelings (?car={id})" },
|
||||
{ method: "POST", path: "/api/fuel-entries", desc: "Log a refueling" },
|
||||
{ method: "GET", path: "/api/fuel-entries/{id}", desc: "Fetch one refueling" },
|
||||
{ method: "PATCH", path: "/api/fuel-entries/{id}", desc: "Update a refueling" },
|
||||
{ method: "DELETE", path: "/api/fuel-entries/{id}", desc: "Delete a refueling" },
|
||||
];
|
||||
|
||||
const chargingApi = [
|
||||
{ method: "GET", path: "/api/charging-sessions", desc: "List one car's charging sessions (?car={id})" },
|
||||
{ method: "POST", path: "/api/charging-sessions", desc: "Log a charging session" },
|
||||
{ method: "GET", path: "/api/charging-sessions/{id}", desc: "Fetch one session" },
|
||||
{ method: "PATCH", path: "/api/charging-sessions/{id}", desc: "Update a session" },
|
||||
{ method: "DELETE", path: "/api/charging-sessions/{id}", desc: "Delete a session" },
|
||||
];
|
||||
|
||||
const maintenanceApi = [
|
||||
{ method: "GET", path: "/api/maintenance", desc: "List one car's maintenance jobs (?car={id})" },
|
||||
{ method: "POST", path: "/api/maintenance", desc: "Log a maintenance job" },
|
||||
{ method: "GET", path: "/api/maintenance/{id}", desc: "Fetch one job" },
|
||||
{ method: "PATCH", path: "/api/maintenance/{id}", desc: "Update a job" },
|
||||
{ method: "DELETE", path: "/api/maintenance/{id}", desc: "Delete a job" },
|
||||
];
|
||||
|
||||
const documentsApi = [
|
||||
{ method: "GET", path: "/api/car-documents", desc: "List one car's documents (?car={id})" },
|
||||
{ method: "POST", path: "/api/car-documents", desc: "Add a document" },
|
||||
{ method: "GET", path: "/api/car-documents/{id}", desc: "Fetch one document" },
|
||||
{ method: "PATCH", path: "/api/car-documents/{id}", desc: "Update a document" },
|
||||
{ method: "DELETE", path: "/api/car-documents/{id}", desc: "Delete a document" },
|
||||
];
|
||||
|
||||
const remindersApi = [
|
||||
{ method: "GET", path: "/api/reminders", desc: "List one car's reminders (?car={id})" },
|
||||
{ method: "POST", path: "/api/reminders", desc: "Create a reminder" },
|
||||
{ method: "GET", path: "/api/reminders/{id}", desc: "Fetch one reminder" },
|
||||
{ method: "PATCH", path: "/api/reminders/{id}", desc: "Update a reminder" },
|
||||
{ method: "DELETE", path: "/api/reminders/{id}", desc: "Delete a reminder" },
|
||||
{ method: "POST", path: "/api/reminders/{id}/complete", desc: "Mark done — a recurring reminder rolls forward from now" },
|
||||
];
|
||||
|
||||
// One optional file per record, on identical terms for every collection that
|
||||
// takes one, so the three routes are documented once with {records} standing
|
||||
// for any of them (see the note under the table).
|
||||
const attachmentsApi = [
|
||||
{ method: "POST", path: "/api/{records}/{id}/file", desc: "Attach a file, replacing any previous one (PDF or image, max 10 MB)" },
|
||||
{ method: "GET", path: "/api/{records}/{id}/file", desc: "Download the attachment (car access re-checked on every request)" },
|
||||
{ method: "DELETE", path: "/api/{records}/{id}/file", desc: "Detach and delete the file" },
|
||||
];
|
||||
|
||||
const attachmentsNote =
|
||||
"{records} is any of service-records, technical-checks, maintenance, fuel-entries, charging-sessions, car-documents, parts.";
|
||||
|
||||
const providersApi = [
|
||||
{ method: "GET", path: "/api/vehicle-providers", desc: "Registered providers and whether the caller can use each one" },
|
||||
{ method: "GET", path: "/api/vehicle-providers/{provider}/vehicles", desc: "Vehicles on the caller's provider account, as importable cars" },
|
||||
{ method: "POST", path: "/api/vehicle-providers/{provider}/import", desc: "Create a car from a provider vehicle (409 if already imported)" },
|
||||
];
|
||||
|
||||
const integrationsApi = [
|
||||
{ method: "GET", path: "/api/integrations/toyota", desc: "Resolved Toyota Connected settings (secrets masked)" },
|
||||
{ method: "PUT", path: "/api/integrations/toyota", desc: "Save the caller's own layer (user or org scope)" },
|
||||
{ method: "POST", path: "/api/integrations/toyota/health", desc: "Live probe with the resolved credentials" },
|
||||
{ method: "GET", path: "/api/integrations/toyota/vehicles", desc: "Raw MyToyota vehicle payload" },
|
||||
{ method: "GET", path: "/api/integrations/anker-solix", desc: "Resolved Anker Solix settings (secrets masked)" },
|
||||
{ method: "PUT", path: "/api/integrations/anker-solix", desc: "Save the caller's own layer (user or org scope)" },
|
||||
{ method: "POST", path: "/api/integrations/anker-solix/health", desc: "Live probe with the resolved credentials" },
|
||||
{ method: "GET", path: "/api/integrations/anker-solix/chargers", desc: "EV chargers on the linked Anker account" },
|
||||
{ method: "GET", path: "/api/integrations/anker-solix/chargers/{sn}/control", desc: "Control mode, token state and live CSMS session for one charger" },
|
||||
{ method: "POST", path: "/api/integrations/anker-solix/chargers/{sn}/control/token", desc: "(Re)issue the charger's control token" },
|
||||
{ method: "DELETE", path: "/api/integrations/anker-solix/chargers/{sn}/control/token", desc: "Revoke the control token" },
|
||||
{ method: "POST", path: "/api/integrations/anker-solix/chargers/{sn}/{action}", desc: "One OCPP command: start, stop, limit, clear-limit, availability, reset, unlock, trigger, config" },
|
||||
];
|
||||
|
||||
const ocppApi = [
|
||||
{ method: "GET", path: "/ocpp/{serial}", desc: "WebSocket the charger dials out to; OCPP Basic auth with the serial + its control token" },
|
||||
];
|
||||
|
||||
const accountApi = [
|
||||
{ method: "GET", path: "/api/me", desc: "Current user profile" },
|
||||
{ method: "PATCH", path: "/api/me", desc: "Update profile" },
|
||||
@@ -159,6 +258,7 @@ const superadminApi = [
|
||||
{ method: "DELETE", path: "/api/admin/plugins/{name}", desc: "Remove an external plugin" },
|
||||
{ method: "POST", path: "/api/admin/plugins/{name}/health", desc: "Run a health check now" },
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -259,7 +359,22 @@ const superadminApi = [
|
||||
<EndpointTable :title="t('api.groupIdentity')" :auth="t('api.authBearer')" :endpoints="identityApi" />
|
||||
<EndpointTable :title="t('api.groupCars')" :auth="t('api.authBearer')" :endpoints="carsApi" />
|
||||
<EndpointTable :title="t('api.groupService')" :auth="t('api.authBearer')" :endpoints="serviceApi" />
|
||||
<EndpointTable :title="t('api.groupTechnical')" :auth="t('api.authBearer')" :endpoints="technicalApi" />
|
||||
<EndpointTable :title="t('api.groupParts')" :auth="t('api.authBearer')" :endpoints="partsApi" />
|
||||
<EndpointTable :title="t('api.groupFuel')" :auth="t('api.authBearer')" :endpoints="fuelApi" />
|
||||
<EndpointTable :title="t('api.groupCharging')" :auth="t('api.authBearer')" :endpoints="chargingApi" />
|
||||
<EndpointTable :title="t('api.groupMaintenance')" :auth="t('api.authBearer')" :endpoints="maintenanceApi" />
|
||||
<EndpointTable :title="t('api.groupDocuments')" :auth="t('api.authBearer')" :endpoints="documentsApi" />
|
||||
<EndpointTable :title="t('api.groupReminders')" :auth="t('api.authBearer')" :endpoints="remindersApi" />
|
||||
<EndpointTable
|
||||
:title="t('api.groupAttachments')"
|
||||
:auth="t('api.authBearer')"
|
||||
:endpoints="attachmentsApi"
|
||||
:note="attachmentsNote"
|
||||
/>
|
||||
<EndpointTable :title="t('api.groupProviders')" :auth="t('api.authBearer')" :endpoints="providersApi" />
|
||||
<EndpointTable :title="t('api.groupIntegrations')" :auth="t('api.authBearer')" :endpoints="integrationsApi" />
|
||||
<EndpointTable :title="t('api.groupOcpp')" :auth="t('api.authCharger')" :endpoints="ocppApi" />
|
||||
<EndpointTable :title="t('api.groupAccount')" :auth="t('api.authBearer')" :endpoints="accountApi" />
|
||||
<EndpointTable :title="t('api.groupManagement')" :auth="t('api.authManager')" :endpoints="managementApi" />
|
||||
<EndpointTable :title="t('api.groupSuperadmin')" :auth="t('api.authSuperadmin')" :endpoints="superadminApi" />
|
||||
|
||||
@@ -5,11 +5,13 @@ defineProps({
|
||||
title: String,
|
||||
auth: String,
|
||||
endpoints: Array, // [{ method, path, desc }]
|
||||
note: String, // optional line under the header, e.g. what a {placeholder} stands for
|
||||
});
|
||||
|
||||
const methodClass = {
|
||||
GET: "text-success",
|
||||
POST: "text-brandtext",
|
||||
PUT: "text-info",
|
||||
PATCH: "text-warning",
|
||||
DELETE: "text-danger",
|
||||
};
|
||||
@@ -17,30 +19,37 @@ const methodClass = {
|
||||
|
||||
<template>
|
||||
<div class="dh-card overflow-hidden">
|
||||
<div class="flex items-center justify-between border-b border-subtle px-5 py-4">
|
||||
<div class="text-base font-bold tracking-[-0.02em] text-strong">{{ title }}</div>
|
||||
<span class="eyebrow">{{ auth }}</span>
|
||||
<div class="border-b border-subtle px-5 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-base font-bold tracking-[-0.02em] text-strong">{{ title }}</div>
|
||||
<span class="eyebrow">{{ auth }}</span>
|
||||
</div>
|
||||
<p v-if="note" class="mt-1.5 text-xs text-muted">{{ note }}</p>
|
||||
</div>
|
||||
<!-- The longest paths (the per-charger OCPP control routes) outrun a narrow
|
||||
window, so the table scrolls inside the card rather than being clipped. -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr class="[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium">
|
||||
<th>{{ t("api.colEndpoint") }}</th>
|
||||
<th>{{ t("api.colDescription") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="e in endpoints"
|
||||
:key="e.method + e.path"
|
||||
class="transition-colors hover:bg-sunken"
|
||||
>
|
||||
<td class="data border-t border-subtle px-5 py-2.5 text-xs whitespace-nowrap">
|
||||
<span class="font-semibold" :class="methodClass[e.method]">{{ e.method }}</span>
|
||||
<span class="text-strong"> {{ e.path }}</span>
|
||||
</td>
|
||||
<td class="border-t border-subtle px-5 py-2.5 text-body">{{ e.desc }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr class="[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium">
|
||||
<th>{{ t("api.colEndpoint") }}</th>
|
||||
<th>{{ t("api.colDescription") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="e in endpoints"
|
||||
:key="e.method + e.path"
|
||||
class="transition-colors hover:bg-sunken"
|
||||
>
|
||||
<td class="data border-t border-subtle px-5 py-2.5 text-xs whitespace-nowrap">
|
||||
<span class="font-semibold" :class="methodClass[e.method]">{{ e.method }}</span>
|
||||
<span class="text-strong"> {{ e.path }}</span>
|
||||
</td>
|
||||
<td class="border-t border-subtle px-5 py-2.5 text-body">{{ e.desc }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -167,11 +167,22 @@
|
||||
"authBearer": "Bearer-token",
|
||||
"authManager": "Admin / superadmin",
|
||||
"authSuperadmin": "Superadmin",
|
||||
"authCharger": "Laderens loginoplysninger",
|
||||
"groupPublic": "Offentlig",
|
||||
"groupIdentity": "Identitet",
|
||||
"groupCars": "Biler",
|
||||
"groupService": "Serviceposter",
|
||||
"groupTechnical": "Syn",
|
||||
"groupParts": "Reservedele",
|
||||
"groupFuel": "Brændstof",
|
||||
"groupCharging": "Opladning",
|
||||
"groupMaintenance": "Værksted",
|
||||
"groupDocuments": "Dokumenter",
|
||||
"groupReminders": "Påmindelser",
|
||||
"groupAttachments": "Vedhæftede filer",
|
||||
"groupProviders": "Køretøjsudbydere",
|
||||
"groupIntegrations": "Integrationer",
|
||||
"groupOcpp": "OCPP",
|
||||
"groupAccount": "Konto",
|
||||
"groupManagement": "Administration",
|
||||
"groupSuperadmin": "Superadmin"
|
||||
|
||||
@@ -167,11 +167,22 @@
|
||||
"authBearer": "Bearer token",
|
||||
"authManager": "Admin / superadmin",
|
||||
"authSuperadmin": "Superadmin",
|
||||
"authCharger": "Charger credentials",
|
||||
"groupPublic": "Public",
|
||||
"groupIdentity": "Identity",
|
||||
"groupCars": "Cars",
|
||||
"groupService": "Service records",
|
||||
"groupTechnical": "Technical checks",
|
||||
"groupParts": "Parts",
|
||||
"groupFuel": "Fuel",
|
||||
"groupCharging": "Charging",
|
||||
"groupMaintenance": "Maintenance",
|
||||
"groupDocuments": "Documents",
|
||||
"groupReminders": "Reminders",
|
||||
"groupAttachments": "Attachments",
|
||||
"groupProviders": "Vehicle providers",
|
||||
"groupIntegrations": "Integrations",
|
||||
"groupOcpp": "OCPP",
|
||||
"groupAccount": "Account",
|
||||
"groupManagement": "Management",
|
||||
"groupSuperadmin": "Superadmin"
|
||||
|
||||
@@ -167,11 +167,22 @@
|
||||
"authBearer": "Token Bearer",
|
||||
"authManager": "Administrator / superadministrator",
|
||||
"authSuperadmin": "Superadministrator",
|
||||
"authCharger": "Dane logowania ładowarki",
|
||||
"groupPublic": "Publiczne",
|
||||
"groupIdentity": "Tożsamość",
|
||||
"groupCars": "Samochody",
|
||||
"groupService": "Wpisy serwisowe",
|
||||
"groupTechnical": "Przeglądy techniczne",
|
||||
"groupParts": "Części",
|
||||
"groupFuel": "Paliwo",
|
||||
"groupCharging": "Ładowanie",
|
||||
"groupMaintenance": "Naprawy",
|
||||
"groupDocuments": "Dokumenty",
|
||||
"groupReminders": "Przypomnienia",
|
||||
"groupAttachments": "Załączniki",
|
||||
"groupProviders": "Dostawcy pojazdów",
|
||||
"groupIntegrations": "Integracje",
|
||||
"groupOcpp": "OCPP",
|
||||
"groupAccount": "Konto",
|
||||
"groupManagement": "Zarządzanie",
|
||||
"groupSuperadmin": "Superadministrator"
|
||||
|
||||
Reference in New Issue
Block a user