feat: refactor super admin to use is_super_admin flag and enhance site deletion logic
- Changed super admin detection from role-based to `is_super_admin` flag in AdminUser model
- Added `requireDefaultAdminSiteId()` method to throw validation error when no integration site exists
- Enhanced site deletion to migrate platform role bindings to fallback site and auto-delete site-specific admin accounts
- Made agent line code optional with auto-generation fallback using `{site_code}-agent-{counter}` format
This commit is contained in:
@@ -23,6 +23,22 @@ final class AdminAgentNodeAccess
|
||||
?? AdminSite::query()->orderBy('id')->value('id'));
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin)
|
||||
if ($requestedSiteId !== null && $requestedSiteId > 0) {
|
||||
if (in_array($requestedSiteId, $accessibleSiteIds, true)) {
|
||||
return $requestedSiteId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return first accessible site if no specific site requested
|
||||
return $accessibleSiteIds[0] ?? null;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return null;
|
||||
|
||||
@@ -32,6 +32,14 @@ final class AdminAgentScope
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can see all nodes in the site
|
||||
return in_array((int) $node->admin_site_id, $accessibleSiteIds, true);
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return false;
|
||||
@@ -90,6 +98,14 @@ final class AdminAgentScope
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can edit all nodes in the site
|
||||
return in_array((int) $node->admin_site_id, $accessibleSiteIds, true);
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return false;
|
||||
@@ -115,6 +131,17 @@ final class AdminAgentScope
|
||||
return $query;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can see all nodes in the site
|
||||
if (in_array($adminSiteId, $accessibleSiteIds, true)) {
|
||||
return $query;
|
||||
}
|
||||
return $query->whereRaw('0 = 1');
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null || (int) $actor->admin_site_id !== $adminSiteId) {
|
||||
return $query->whereRaw('0 = 1');
|
||||
|
||||
@@ -38,6 +38,7 @@ final class AdminAuthProfile
|
||||
* can_create_child_agent: bool,
|
||||
* can_create_player: bool
|
||||
* },
|
||||
* site: ?array{id: int, code: string, name: string},
|
||||
* is_super_admin: bool,
|
||||
* operational_permissions: list<string>,
|
||||
* delegation_ceiling: list<string>,
|
||||
@@ -58,6 +59,7 @@ final class AdminAuthProfile
|
||||
'permissions' => $permissionSlugs,
|
||||
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs, $fresh),
|
||||
'agent' => $agent,
|
||||
'site' => self::siteContext($fresh),
|
||||
'is_super_admin' => $fresh->isSuperAdmin(),
|
||||
'operational_permissions' => $permissionSlugs,
|
||||
'delegation_ceiling' => AgentDelegationAuthorization::delegationLegacySlugsForAdminUser($fresh),
|
||||
@@ -71,19 +73,32 @@ final class AdminAuthProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* id: int,
|
||||
* admin_site_id: int,
|
||||
* admin_site_name: string,
|
||||
* site_code: string,
|
||||
* path: string,
|
||||
* code: string,
|
||||
* name: string,
|
||||
* depth: int,
|
||||
* can_create_child_agent: bool,
|
||||
* can_create_player: bool
|
||||
* }|null
|
||||
* @return array{id: int, code: string, name: string}|null
|
||||
*/
|
||||
private static function siteContext(AdminUser $admin): ?array
|
||||
{
|
||||
if ($admin->isSuperAdmin() || $admin->primaryAgentNode() !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! SitePlatformRole::userHasSiteAdminRole($admin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sites = AdminUserSiteBindingPresenter::accessibleSitesFor($admin);
|
||||
if ($sites === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$site = $sites[0];
|
||||
|
||||
return [
|
||||
'id' => (int) $site['id'],
|
||||
'code' => (string) $site['code'],
|
||||
'name' => (string) $site['name'],
|
||||
];
|
||||
}
|
||||
|
||||
private static function agentContext(AdminUser $admin): ?array
|
||||
{
|
||||
if ($admin->isSuperAdmin()) {
|
||||
|
||||
@@ -462,7 +462,7 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.settlement-reports.summary', 'module_code' => 'settlement', 'name' => '代理结算报表摘要', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-reports/summary', 'route_name' => 'api.v1.admin.settlement-reports.summary', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['settlement.agent.view', 'settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
|
||||
['code' => 'admin.settlement-reports.show', 'module_code' => 'settlement', 'name' => '信用占成盘报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-reports', 'route_name' => 'api.v1.admin.settlement-reports.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['settlement.agent.view', 'settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
|
||||
|
||||
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view']],
|
||||
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.report.view', 'prd.dashboard.view']],
|
||||
['code' => 'admin.play-types.patch', 'module_code' => 'config', 'name' => '玩法类型切换', 'http_method' => 'PATCH', 'uri_pattern' => '/api/v1/admin/play-types/{play_code}', 'route_name' => 'api.v1.admin.play-types.patch', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']],
|
||||
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||
['code' => 'admin.config.play-versions.show', 'module_code' => 'config', 'name' => '玩法版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||
@@ -494,6 +494,7 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.integration-sites.store', 'module_code' => 'integration', 'name' => '创建接入站点', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites', 'route_name' => 'api.v1.admin.integration-sites.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.show', 'module_code' => 'integration', 'name' => '接入站点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.update', 'module_code' => 'integration', 'name' => '更新接入站点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.destroy', 'module_code' => 'integration', 'name' => '删除接入站点', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.rotate-secrets', 'module_code' => 'integration', 'name' => '重置接入密钥', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/rotate-secrets', 'route_name' => 'api.v1.admin.integration-sites.rotate-secrets', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.connectivity-test', 'module_code' => 'integration', 'name' => '接入站点联通检测', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/connectivity-test', 'route_name' => 'api.v1.admin.integration-sites.connectivity-test', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.export', 'module_code' => 'integration', 'name' => '导出接入参数表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/export', 'route_name' => 'api.v1.admin.integration-sites.export', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
|
||||
@@ -29,6 +29,7 @@ final class AdminIntegrationSitePresenter
|
||||
'wallet_api_key_masked' => is_string($site->wallet_api_key_encrypted) && $site->wallet_api_key_encrypted !== ''
|
||||
? '••••••••'
|
||||
: null,
|
||||
'is_default' => (bool) $site->is_default,
|
||||
'updated_at' => $site->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
@@ -38,16 +39,16 @@ final class AdminIntegrationSitePresenter
|
||||
*/
|
||||
public static function detail(AdminSite $site): array
|
||||
{
|
||||
return array_merge(self::listItem($site), [
|
||||
return [
|
||||
...self::listItem($site),
|
||||
'wallet_debit_path' => (string) $site->wallet_debit_path,
|
||||
'wallet_credit_path' => (string) $site->wallet_credit_path,
|
||||
'wallet_balance_path' => (string) $site->wallet_balance_path,
|
||||
'iframe_allowed_origins' => $site->iframe_allowed_origins ?? [],
|
||||
'lottery_h5_base_url' => $site->lottery_h5_base_url,
|
||||
'notes' => $site->notes,
|
||||
'is_default' => (bool) $site->is_default,
|
||||
'created_at' => $site->created_at?->toIso8601String(),
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,10 +11,12 @@ final class PlatformSystemRoles
|
||||
|
||||
public const SLUG_AGENT = 'agent';
|
||||
|
||||
public const SLUG_SITE_ADMIN = SitePlatformRole::SLUG;
|
||||
|
||||
/** @return list<string> */
|
||||
public static function fixedSlugs(): array
|
||||
{
|
||||
return [self::SLUG_SUPER_ADMIN, self::SLUG_AGENT];
|
||||
return [self::SLUG_SUPER_ADMIN, self::SLUG_SITE_ADMIN, self::SLUG_AGENT];
|
||||
}
|
||||
|
||||
public static function isFixedSlug(string $slug): bool
|
||||
@@ -49,6 +51,7 @@ final class PlatformSystemRoles
|
||||
public static function ensureAll(): void
|
||||
{
|
||||
self::ensureSuperAdminRole();
|
||||
SiteAdminDefaultRolePermissions::ensurePlatformSiteAdminRole();
|
||||
AgentDefaultRolePermissions::ensurePlatformAgentRole();
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Support/SiteAdminDefaultRolePermissions.php
Normal file
64
app/Support/SiteAdminDefaultRolePermissions.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
|
||||
/**
|
||||
* 平台「站点管理员」系统角色(slug=site_admin)的默认 prd.* 模板。
|
||||
* 接入站点创建时自动绑定;权限可在「平台角色管理」调整。
|
||||
*/
|
||||
final class SiteAdminDefaultRolePermissions
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const TEMPLATE_SLUGS = [
|
||||
'prd.dashboard.view',
|
||||
'prd.agent.view',
|
||||
'prd.agent.manage',
|
||||
'prd.agent.role.view',
|
||||
'prd.agent.role.manage',
|
||||
'prd.agent.user.view',
|
||||
'prd.agent.user.manage',
|
||||
'prd.agent.profile.manage',
|
||||
'prd.users.manage',
|
||||
'prd.tickets.view',
|
||||
'prd.report.view',
|
||||
'prd.settlement.agent.view',
|
||||
'prd.settlement.agent.manage',
|
||||
'prd.integration.view',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function templateSlugs(): array
|
||||
{
|
||||
return self::TEMPLATE_SLUGS;
|
||||
}
|
||||
|
||||
public static function ensurePlatformSiteAdminRole(): AdminRole
|
||||
{
|
||||
$role = AdminRole::query()->updateOrCreate(
|
||||
[
|
||||
'slug' => SitePlatformRole::SLUG,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
],
|
||||
[
|
||||
'code' => SitePlatformRole::SLUG,
|
||||
'name' => '站点管理员',
|
||||
'description' => '接入站点后台默认权限(代理/玩家/结算运营 + 站点仪表盘)',
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 40,
|
||||
'owner_agent_id' => null,
|
||||
'delegated_from_role_id' => null,
|
||||
],
|
||||
);
|
||||
|
||||
$role->syncLegacyPermissionSlugs(
|
||||
AdminPermissionInheritance::expand(self::TEMPLATE_SLUGS),
|
||||
);
|
||||
|
||||
return $role->fresh() ?? $role;
|
||||
}
|
||||
}
|
||||
54
app/Support/SitePlatformRole.php
Normal file
54
app/Support/SitePlatformRole.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** 接入站点后台账号统一使用平台系统角色 slug=site_admin。 */
|
||||
final class SitePlatformRole
|
||||
{
|
||||
public const SLUG = 'site_admin';
|
||||
|
||||
public static function resolve(): AdminRole
|
||||
{
|
||||
return SiteAdminDefaultRolePermissions::ensurePlatformSiteAdminRole();
|
||||
}
|
||||
|
||||
public static function id(): int
|
||||
{
|
||||
return (int) self::resolve()->id;
|
||||
}
|
||||
|
||||
public static function idOrFail(): int
|
||||
{
|
||||
$id = (int) (AdminRole::query()
|
||||
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||
->where('slug', self::SLUG)
|
||||
->where('status', 1)
|
||||
->value('id') ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'role' => ['platform_site_admin_role_missing: run php artisan lottery:admin-auth-sync'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public static function userHasSiteAdminRole(AdminUser $user): bool
|
||||
{
|
||||
if ($user->isSuperAdmin() || $user->hasPrimaryAgentBinding()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DB::table('admin_user_site_roles as usr')
|
||||
->join('admin_roles as r', 'r.id', '=', 'usr.role_id')
|
||||
->where('usr.admin_user_id', $user->id)
|
||||
->where('r.slug', self::SLUG)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
63
app/Support/SuperAdminAccount.php
Normal file
63
app/Support/SuperAdminAccount.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** 平台唯一超级管理员账号(不绑定站点)。 */
|
||||
final class SuperAdminAccount
|
||||
{
|
||||
public static function assign(AdminUser $user): AdminUser
|
||||
{
|
||||
return DB::transaction(function () use ($user): AdminUser {
|
||||
DB::table('admin_users')
|
||||
->where('id', '!=', $user->id)
|
||||
->update(['is_super_admin' => false]);
|
||||
|
||||
$user->forceFill(['is_super_admin' => true])->save();
|
||||
|
||||
self::removeLegacySiteRoleBinding((int) $user->id);
|
||||
|
||||
return $user->fresh() ?? $user;
|
||||
});
|
||||
}
|
||||
|
||||
public static function revoke(AdminUser $user): AdminUser
|
||||
{
|
||||
$user->forceFill(['is_super_admin' => false])->save();
|
||||
|
||||
return $user->fresh() ?? $user;
|
||||
}
|
||||
|
||||
public static function count(): int
|
||||
{
|
||||
return (int) AdminUser::query()->where('is_super_admin', true)->count();
|
||||
}
|
||||
|
||||
public static function assertNotSiteRoleAssignment(array $roleSlugs): void
|
||||
{
|
||||
if (in_array(AdminUser::ROLE_SUPER_ADMIN, $roleSlugs, true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'role_slugs' => [__('admin.super_admin_not_site_role')],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private static function removeLegacySiteRoleBinding(int $userId): void
|
||||
{
|
||||
$superRoleId = DB::table('admin_roles')
|
||||
->where('slug', AdminUser::ROLE_SUPER_ADMIN)
|
||||
->value('id');
|
||||
|
||||
if ($superRoleId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('admin_user_site_roles')
|
||||
->where('admin_user_id', $userId)
|
||||
->where('role_id', $superRoleId)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user