users, sessions, profiles).gnexus-auth (auth.gnexus.space) как единый источник идентичности.gnexus-auth.gnexus-auth для актуализации ролей, прав, статусов.gnexus/auth-client и HTTP-клиентПакет: gnexus/auth-client (репозиторий git.gnexus.space/root/gnexus-auth-client-php).
Подключение: через Composer repositories.type = vcs:
"repositories": [{
"type": "vcs",
"url": "https://git.gnexus.space/root/gnexus-auth-client-php.git"
}],
"require": {
"gnexus/auth-client": "^0.1"
}
PSR-реализации: пакет требует только интерфейсы PSR-18/17/7 (psr/http-client, psr/http-factory, psr/http-message). Реализацию пишем сами — thin cURL-адаптер:
CurlHttpClient — PSR-18 ClientInterfacePsr7Factory — PSR-17 RequestFactoryInterface + StreamFactoryInterfacePsr7Request, Psr7Response, Psr7Stream — PSR-7 DTOserver/SHServ/Integrations/GAuth/Http/.Решение: PHP $_SESSION + опциональная таблица shserv_sessions для долгосрочного хранения refresh-токена.
Обоснование:
$_SESSION.shserv_sessions (cookie → session_id → refresh_token).gnexus-auth и чистим локальную сессию.Решение: синхронизировать с gnexus-auth через webhooks group.user_added / group.user_removed.
Обоснование:
gnexus-auth.Решение: guest — явная client role в gnexus-auth, аутентифицированный пользователь.
Обоснование:
Authorization: Bearer.guest — это реальная роль, назначаемая в gnexus-auth (как client role, не system_role).guest с минимальным набором прав.user или admin.shserv_users — локальное отображение пользователейCREATE TABLE shserv_users (
id INT AUTO_INCREMENT PRIMARY KEY,
gauth_user_id VARCHAR(64) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
avatar_url VARCHAR(500),
system_role VARCHAR(32) NOT NULL DEFAULT 'user', -- superadmin | admin | user
status VARCHAR(32) NOT NULL DEFAULT 'active', -- active | blocked | archived
max_role VARCHAR(32) NULL, -- роль, назначенная в gnexus-auth
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_gauth (gauth_user_id),
INDEX idx_email (email)
);
shserv_roles — локальные роли (mirror + override)CREATE TABLE shserv_roles (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(32) NOT NULL UNIQUE, -- superadmin | admin | user | guest
name VARCHAR(64) NOT NULL,
is_system TINYINT(1) NOT NULL DEFAULT 0, -- system role из gnexus-auth
default_permissions JSON, -- массив permission slugs
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
shserv_permissions — реестр правCREATE TABLE shserv_permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(64) NOT NULL UNIQUE, -- devices.scan | devices.control | scripts.edit | areas.manage | admin.users | admin.roles
name VARCHAR(128) NOT NULL,
description TEXT,
default_for_roles JSON, -- {"superadmin": true, "admin": true, "user": false, "guest": false}
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
shserv_user_permissions — персональные overridesCREATE TABLE shserv_user_permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
permission_slug VARCHAR(64) NOT NULL,
granted TINYINT(1) NOT NULL DEFAULT 1, -- 1 = разрешено, 0 = явно запрещено
set_by_user_id INT NULL, -- кто установил override
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_perm (user_id, permission_slug),
FOREIGN KEY (user_id) REFERENCES shserv_users(id) ON DELETE CASCADE
);
shserv_groups — группы (реплика из gnexus-auth)CREATE TABLE shserv_groups (
id INT AUTO_INCREMENT PRIMARY KEY,
gauth_group_id VARCHAR(64) NOT NULL UNIQUE,
slug VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
shserv_group_members — membershipCREATE TABLE shserv_group_members (
id INT AUTO_INCREMENT PRIMARY KEY,
group_id INT NOT NULL,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_membership (group_id, user_id),
FOREIGN KEY (group_id) REFERENCES shserv_groups(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES shserv_users(id) ON DELETE CASCADE
);
shserv_group_permissions — права группCREATE TABLE shserv_group_permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
group_id INT NOT NULL,
permission_slug VARCHAR(64) NOT NULL,
granted TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_group_perm (group_id, permission_slug),
FOREIGN KEY (group_id) REFERENCES shserv_groups(id) ON DELETE CASCADE
);
shserv_sessions — локальные сессии (для refresh-токенов)CREATE TABLE shserv_sessions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
session_token VARCHAR(128) NOT NULL UNIQUE,
refresh_token VARCHAR(255),
access_token VARCHAR(255),
expires_at TIMESTAMP NULL,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_token (session_token),
FOREIGN KEY (user_id) REFERENCES shserv_users(id) ON DELETE CASCADE
);
server/SHServ/Integrations/GAuth/)Пакет gnexus/auth-client уже предоставляет: GAuthClient, GAuthConfig, HttpTokenEndpoint, HttpRuntimeUserProvider, HmacWebhookVerifier, JsonWebhookParser, PkceGenerator, весь DTO и exception набор. Мы реализуем только то, что пакет оставляет на усмотрение consuming-приложения:
Integrations/GAuth/
Http/
CurlHttpClient.php -- PSR-18 ClientInterface (thin cURL)
Psr7Factory.php -- PSR-17 RequestFactory + StreamFactory
Psr7Request.php -- PSR-7 RequestInterface
Psr7Response.php -- PSR-7 ResponseInterface
Psr7Stream.php -- PSR-7 StreamInterface
Store/
SessionStateStore.php -- StateStoreInterface (на $_SESSION)
SessionPkceStore.php -- PkceStoreInterface (на $_SESSION)
DbTokenStore.php -- TokenStoreInterface (на shserv_sessions)
Webhook/
WebhookRouter.php -- route event.type → handler
Handlers/
UserHandler.php -- user.* events
RoleHandler.php -- client.roles_changed
PermissionHandler.php -- client.permissions_changed
GroupHandler.php -- group.user_added / removed
SessionHandler.php -- auth.global_logout / session.revoked
AuthService.php -- high-level wrapper: login URL, callback, logout, me
UserResolver.php -- маппинг AuthenticatedUser → shserv_users
PermissionResolver.php -- resolve effective permissions
| Роль | Описание | Источник | Тип |
|---|---|---|---|
superadmin |
Полный доступ | AuthenticatedUser->systemRole |
system_role |
admin |
Администрирование SHServ | AuthenticatedUser->systemRole |
system_role |
user |
Обычный пользователь | AuthenticatedUser->systemRole |
system_role |
guest |
Аутентифицированный с минимальными правами | ClientAccess->roleIds[] |
client_role |
Модель:
systemRole (superadmin/admin/user) определяет административный уровень.clientAccessList[].roleIds[] — клиентские роли в контексте SHServ (где clientId === GAUTH_CLIENT_ID).guest — client role, назначается по умолчанию новым пользователям.Пакет gnexus/auth-client возвращает permissionIds[] в ClientAccess (вместе с roleIds[]). Эти ID мапятся на локальные shserv_permissions.slug.
Starter set прав SHServ:
devices.view -- просмотр списка устройств devices.scan -- сканирование сети devices.control -- управление устройствами devices.setup -- добавление новых устройств devices.edit -- редактирование устройств devices.delete -- удаление устройств areas.view -- просмотр зон areas.manage -- управление зонами scripts.view -- просмотр скриптов scripts.edit -- редактирование скриптов scripts.run -- запуск скриптов firmware.view -- просмотр прошивок firmware.upload -- загрузка прошивок admin.users -- управление пользователями admin.roles -- управление ролями/правами admin.audit -- просмотр аудита settings.edit -- изменение настроек системы
effective_permissions(user) =
union(
permissions_from_role(user.system_role), -- дефолт по роли
permissions_from_groups(user.groups), -- права групп
user_overrides(user.id) -- персональные overrides
)
minus explicitly_denied(user.id)
superadmin всегда имеет все права (fast path).admin / user / guest — через таблицы.granted = 0 явно запрещает право, даже если оно есть у роли/группы.Админ/суперадмин через API POST /api/v1/admin/users/{id}/permissions может:
granted = 1) — даёт право, которого нет у роли.granted = 0) — урезает право, имеющееся у роли.GET /auth/login → редирект на gnexus-auth /oauth/authorize GET /auth/callback → обмен code на tokens, создание/обновление local user, редирект в приложение POST /auth/logout → отзыв refresh token, очистка сессии GET /auth/me → текущий пользователь + его effective permissions
POST /webhooks/gnexus-auth
GAuthClient::verifyWebhook().GAuthClient::parseWebhook().WebhookHandler по event.type.admin.users / admin.roles)GET /api/v1/admin/users
GET /api/v1/admin/users/{id}
PATCH /api/v1/admin/users/{id}/role
POST /api/v1/admin/users/{id}/permissions -- {permission_slug, granted}
DELETE /api/v1/admin/users/{id}/permissions/{slug}
GET /api/v1/admin/roles
GET /api/v1/admin/permissions
GET /api/v1/admin/groups
Все существующие /api/v1/* endpoints (кроме устройственных /about и /events/new) требуют авторизации.
Для Fury Router (нет middleware) — внедряем проверку в базовый контроллер:
AuthControllerTrait с методом require_auth() и require_permission($slug).client.js — добавить Authorization: Bearer {access_token} header ко всем запросам./auth/login.POST /auth/logout, затем очистка local storage.src/stores/auth.js)export const useAuthStore = defineStore('auth', () => {
const user = ref(null);
const permissions = ref([]);
const isAuthenticated = computed(() => !!user.value);
const hasPermission = (slug) => permissions.value.includes(slug);
// ...
});
window.location = '/auth/login'./#/).GET /auth/me → получает user + permissions.hasPermission().AppShell.user.updated user.blocked user.unblocked user.archived user.restored user.deleted client.roles_changed client.permissions_changed client.access_granted client.access_revoked client.access_denied group.user_added group.user_removed auth.global_logout session.revoked
| Событие | Действие |
|---|---|
user.updated |
Обновить display_name, avatar_url, email в shserv_users |
user.blocked |
status = 'blocked', очистить сессии |
user.unblocked |
status = 'active' |
user.deleted |
Удалить из shserv_users (cascade cleanup) |
client.roles_changed |
Обновить system_role / client roles |
client.permissions_changed |
Обновить shserv_user_permissions |
group.user_added |
INSERT shserv_group_members |
group.user_removed |
DELETE shserv_group_members |
auth.global_logout |
Удалить все shserv_sessions для user |
session.revoked |
Удалить конкретную сессию из shserv_sessions |
CurlHttpClient + Psr7Factory + DTO).gnexus/auth-client через Composer vcs репозиторий (git.gnexus.space/root/gnexus-auth-client-php).GAuthConfig и AuthService wrapper.Client в gnexus-auth (client_id, secret, redirect_uri, webhook).GET /auth/login — GAuthClient::buildAuthorizationRequest(..., scopes: ['openid','email','profile','roles','permissions']) → редирект.GET /auth/callback — exchangeAuthorizationCode(), fetchUser().
AuthenticatedUser содержит: userId, email, systemRole, status, profile, clientAccessList (с roleIds[], permissionIds[]).shserv_users, маппинг gauth_user_id.POST /auth/logout — revokeToken() + local cleanup.GET /auth/me — текущий пользователь + effective permissions.SessionStateStore + SessionPkceStore на PHP $_SESSION.DbTokenStore для refresh-токена (связь session_token ↔ refresh_token).shserv_roles, shserv_permissions.PermissionResolver — алгоритм union/minus.AuthControllerTrait — require_auth(), require_permission().POST /webhooks/gnexus-auth endpoint.HmacWebhookVerifier::verify(rawBody, headers, secret) (из пакета).JsonWebhookParser::parse(rawBody) → WebhookEvent (из пакета).WebhookRouter → event-specific handlers (UserHandler, RoleHandler, GroupHandler, SessionHandler).src/stores/auth.js) с user, permissions, isAuthenticated, hasPermission().client.js / http.js.window.location = '/auth/login' (backend сделает redirect в gnexus-auth).gnexus-auth runtime API или expiresAt прошёл — backend-endpoint POST /auth/refresh обновляет access token.hasPermission().AppShell.webclient_legacy (если ещё используется в продакшене).Example_AuthController.php, старые auth routes.users → _legacy_users, sessions → _legacy_sessions, profiles → _legacy_profiles (на случай аудита).SHServ/Entities/User.php, Session.php, Profile.php (или перенести в _legacy/).docs/server-api.md и docs/architecture.md.Client credentials — хранить в .env:
GAUTH_BASE_URL=https://auth.gnexus.space GAUTH_CLIENT_ID=shserv GAUTH_CLIENT_SECRET=... GAUTH_WEBHOOK_SECRET=...
Пакет gnexus/auth-client читает их через GAuthConfig (передаются явно в конструктор).
Redirect URI — регистрировать в gnexus-auth Client:
https://smarthome.gnexus.space/auth/callbackhttp://smart-home-serv.local/auth/callbackhttp://localhost/auth/callback (если gnexus-auth разрешает local/dev origins).Token refresh — lazy refresh:
$_SESSION + expiresAt.expiresAt прошёл — frontend вызывает POST /auth/refresh.DbTokenStore, делает GAuthClient::refreshToken(), обновляет сессию.Миграция старых пользователей — legacy таблицы (users, sessions, profiles) архивировать (RENAME TABLE users TO _legacy_users), не удалять.
Аудит в SHServ — админ-действия (смена роли, override permission) логировать в shserv_audit:
CREATE TABLE shserv_audit (
id INT AUTO_INCREMENT PRIMARY KEY,
actor_user_id INT NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(32),
target_id INT,
old_value JSON,
new_value JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Тестирование пакета — пакет gnexus/auth-client имеет unit tests (tests/Unit/). Перед интеграцией рекомендуется прогнать их:
cd packages/auth-client && php vendor/bin/phpunit