feat: enhance agent management and validation logic

- Updated AGENTS.md to clarify agent account restrictions and permissions.
- Implemented checks in AgentNodeAdminUserStoreController and AgentNodeRoleStoreController to restrict admin user and role creation to the agent's own node.
- Enhanced validation in AdminPlayerStoreController and AdminPlayerUpdateController to enforce credit limit and rebate rate rules based on player funding mode.
- Refactored various request classes to utilize shared admin account field rules for consistency.
- Improved error handling in services related to credit allocation and rebate limits to ensure proper validation and messaging.
This commit is contained in:
2026-06-14 21:13:27 +08:00
parent 395e1c7400
commit 5b6d4cb74d
56 changed files with 1558 additions and 222 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin;
use App\Http\Requests\Admin\Concerns\AdminAccountFieldRules;
use App\Http\Requests\Admin\Concerns\AgentProfileFieldRules;
use App\Http\Requests\ApiFormRequest;
use App\Models\AdminSite;
@@ -11,6 +12,7 @@ use Illuminate\Validation\Validator;
final class AdminAgentLineStoreRequest extends ApiFormRequest
{
use AdminAccountFieldRules;
use AgentProfileFieldRules;
public function authorize(): bool
@@ -20,6 +22,7 @@ final class AdminAgentLineStoreRequest extends ApiFormRequest
protected function prepareForValidation(): void
{
$this->normalizeAdminAccountFields();
$this->prepareAgentProfileFieldsForValidation();
if ($this->has('site_code')) {
@@ -42,8 +45,8 @@ final class AdminAgentLineStoreRequest extends ApiFormRequest
'site_code' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9][a-z0-9_-]*$/', Rule::exists('admin_sites', 'code')],
'code' => ['sometimes', 'nullable', 'string', 'max:64', 'regex:/^[a-z0-9][a-z0-9_-]*$/', Rule::unique('agent_nodes', 'code')],
'name' => ['required', 'string', 'max:128'],
'username' => ['required', 'string', 'max:64', Rule::unique('admin_users', 'username')],
'password' => ['required', 'string', 'min:8', 'max:128'],
'username' => [...$this->adminOperatorUsernameRules(), Rule::unique('admin_users', 'username')],
'password' => $this->adminLoginPasswordRules(),
'email' => ['nullable', 'string', 'email', 'max:255', Rule::unique('admin_users', 'email')],
'status' => ['sometimes', 'integer', 'in:0,1'],
...$this->agentProfileFieldRules(),

View File

@@ -11,9 +11,32 @@ final class AdminAgentProfileUpdateRequest extends ApiFormRequest
public function authorize(): bool
{
$admin = $this->user();
if (! $admin instanceof \App\Models\AdminUser) {
return false;
}
$agentNode = $this->route('agent_node');
if (! $agentNode instanceof \App\Models\AgentNode) {
return false;
}
if (! \App\Support\AdminAgentScope::nodeVisibleTo($admin, $agentNode)) {
return false;
}
if (! \App\Support\AdminAgentScope::nodeProfileEditableBy($admin, $agentNode)) {
return false;
}
return true;
}
protected function failedAuthorization(): void
{
abort(403, 'admin.permission_denied');
}
protected function prepareForValidation(): void
{
$this->prepareAgentProfileFieldsForValidation();

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin;
use App\Http\Requests\Admin\Concerns\AdminAccountFieldRules;
use App\Http\Requests\ApiFormRequest;
/**
@@ -11,19 +12,26 @@ use App\Http\Requests\ApiFormRequest;
*/
final class AdminLoginRequest extends ApiFormRequest
{
use AdminAccountFieldRules;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->normalizeAdminAccountFields();
}
/**
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return [
'account' => ['required', 'string', 'min:2', 'max:64', 'regex:/^[a-zA-Z0-9._-]+$/u'],
'password' => ['required', 'string', 'max:256'],
'account' => $this->adminLoginAccountRules(),
'password' => $this->adminLoginPasswordRules(),
'captcha_key' => ['required', 'string', 'uuid'],
'captcha_code' => ['required', 'string', 'max:32'],
];

View File

@@ -3,6 +3,7 @@
namespace App\Http\Requests\Admin;
use Illuminate\Validation\Rule;
use App\Http\Requests\Admin\Concerns\AdminAccountFieldRules;
use App\Http\Requests\ApiFormRequest;
/**
@@ -12,6 +13,8 @@ use App\Http\Requests\ApiFormRequest;
*/
final class AdminPlayerStoreRequest extends ApiFormRequest
{
use AdminAccountFieldRules;
public function authorize(): bool
{
return true;
@@ -22,13 +25,13 @@ final class AdminPlayerStoreRequest extends ApiFormRequest
return [
'site_code' => ['required', 'string', 'max:64'],
'site_player_id' => ['nullable', 'string', 'max:128'],
'username' => ['nullable', 'string', 'max:128'],
'password' => ['nullable', 'string', 'min:6', 'max:128'],
'username' => $this->nativePlayerUsernameRules(),
'password' => $this->nativePlayerPasswordRules(),
'nickname' => ['nullable', 'string', 'max:128'],
'default_currency' => ['sometimes', 'string', 'max:16', Rule::exists('currencies', 'code')],
'status' => ['sometimes', 'integer', 'in:0,1,2'],
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
'credit_limit' => ['sometimes', 'integer', 'min:0'],
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1', 'exists:agent_nodes,id'],
'credit_limit' => ['sometimes', 'integer', 'min:0', 'max:999999999'],
'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
];
@@ -36,6 +39,8 @@ final class AdminPlayerStoreRequest extends ApiFormRequest
protected function prepareForValidation(): void
{
$this->normalizeAdminAccountFields();
if (! $this->has('default_currency')) {
return;
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Requests\Admin;
use Illuminate\Validation\Rule;
use App\Http\Requests\Admin\Concerns\AdminAccountFieldRules;
use App\Http\Requests\ApiFormRequest;
/**
@@ -12,6 +13,8 @@ use App\Http\Requests\ApiFormRequest;
*/
final class AdminPlayerUpdateRequest extends ApiFormRequest
{
use AdminAccountFieldRules;
public function authorize(): bool
{
return true;
@@ -20,11 +23,11 @@ final class AdminPlayerUpdateRequest extends ApiFormRequest
public function rules(): array
{
return [
'username' => ['sometimes', 'string', 'max:128'],
'username' => $this->nativePlayerUsernameRules(required: false),
'nickname' => ['sometimes', 'nullable', 'string', 'max:128'],
'default_currency' => ['sometimes', 'string', 'max:16', Rule::exists('currencies', 'code')],
'status' => ['sometimes', 'integer', Rule::in([0, 1, 2])],
'credit_limit' => ['sometimes', 'integer', 'min:0'],
'credit_limit' => ['sometimes', 'integer', 'min:0', 'max:999999999'],
'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'rebate_profiles' => ['sometimes', 'array'],
@@ -39,6 +42,8 @@ final class AdminPlayerUpdateRequest extends ApiFormRequest
protected function prepareForValidation(): void
{
$this->normalizeAdminAccountFields();
if (! $this->has('default_currency')) {
return;
}

View File

@@ -2,24 +2,32 @@
namespace App\Http\Requests\Admin;
use App\Http\Requests\Admin\Concerns\AdminAccountFieldRules;
use App\Http\Requests\ApiFormRequest;
use Illuminate\Validation\Rule;
final class AgentAdminUserStoreRequest extends ApiFormRequest
{
use AdminAccountFieldRules;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->normalizeAdminAccountFields();
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'username' => ['required', 'string', 'max:64', Rule::unique('admin_users', 'username')],
'username' => [...$this->adminOperatorUsernameRules(), Rule::unique('admin_users', 'username')],
'nickname' => ['required', 'string', 'max:128'],
'email' => ['nullable', 'email', 'max:255', Rule::unique('admin_users', 'email')],
'password' => ['required', 'string', 'min:8', 'max:128'],
'password' => $this->adminLoginPasswordRules(),
'status' => ['sometimes', 'integer', 'in:0,1'],
'role_ids' => ['sometimes', 'array'],
'role_ids.*' => ['integer', 'exists:admin_roles,id'],

View File

@@ -2,12 +2,14 @@
namespace App\Http\Requests\Admin;
use App\Http\Requests\Admin\Concerns\AdminAccountFieldRules;
use App\Http\Requests\Admin\Concerns\AgentProfileFieldRules;
use App\Http\Requests\ApiFormRequest;
use Illuminate\Validation\Rule;
final class AgentNodeStoreRequest extends ApiFormRequest
{
use AdminAccountFieldRules;
use AgentProfileFieldRules;
public function authorize(): bool
{
@@ -16,6 +18,7 @@ final class AgentNodeStoreRequest extends ApiFormRequest
protected function prepareForValidation(): void
{
$this->normalizeAdminAccountFields();
$this->prepareAgentProfileFieldsForValidation();
}
@@ -26,7 +29,7 @@ final class AgentNodeStoreRequest extends ApiFormRequest
'parent_id' => ['required', 'integer', 'exists:agent_nodes,id'],
'code' => ['sometimes', 'nullable', 'string', 'max:64', 'regex:/^[a-zA-Z0-9_-]+$/'],
'name' => ['required', 'string', 'max:128'],
'username' => ['sometimes', 'nullable', 'string', 'max:64', Rule::unique('admin_users', 'username')],
'username' => ['sometimes', 'nullable', ...$this->adminOperatorUsernameRules(required: false), Rule::unique('admin_users', 'username')],
'email' => ['nullable', 'email', 'max:255', Rule::unique('admin_users', 'email')],
'password' => ['required', 'string', 'min:8', 'max:128'],
'status' => ['sometimes', 'integer', 'in:0,1'],

View File

@@ -2,23 +2,31 @@
namespace App\Http\Requests\Admin;
use App\Http\Requests\Admin\Concerns\AdminAccountFieldRules;
use App\Http\Requests\ApiFormRequest;
final class AgentNodeUpdateRequest extends ApiFormRequest
{
use AdminAccountFieldRules;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->normalizeAdminAccountFields();
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:128'],
'username' => ['sometimes', 'string', 'max:64'],
'username' => $this->adminOperatorUsernameRules(required: false),
'email' => ['sometimes', 'nullable', 'email', 'max:255'],
'password' => ['sometimes', 'nullable', 'string', 'min:8', 'max:128'],
'password' => $this->adminLoginPasswordRules(required: false),
'status' => ['sometimes', 'integer', 'in:0,1'],
];
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Http\Requests\Admin\Concerns;
trait AdminAccountFieldRules
{
/** @return array<int, mixed> */
protected function adminLoginAccountRules(bool $required = true): array
{
$rules = ['string', 'min:2', 'max:64', 'regex:/^[a-zA-Z0-9._-]+$/u'];
return $required ? array_merge(['required'], $rules) : array_merge(['sometimes'], $rules);
}
/** @return array<int, mixed> */
protected function adminLoginPasswordRules(bool $required = true, int $min = 8, int $max = 256): array
{
$rules = ['string', 'min:'.$min, 'max:'.$max];
if ($required) {
return array_merge(['required'], $rules);
}
return array_merge(['sometimes', 'nullable'], $rules);
}
/** @return array<int, mixed> */
protected function adminOperatorUsernameRules(bool $required = true): array
{
$rules = ['string', 'min:2', 'max:64', 'regex:/^[a-zA-Z0-9._-]+$/u'];
return $required ? array_merge(['required'], $rules) : array_merge(['sometimes'], $rules);
}
/** @return array<int, mixed> */
protected function nativePlayerUsernameRules(bool $required = false): array
{
$rules = ['string', 'min:2', 'max:64', 'regex:/^[a-zA-Z0-9._-]+$/u'];
return $required ? array_merge(['required'], $rules) : array_merge(['nullable'], $rules);
}
/** @return array<int, mixed> */
protected function nativePlayerPasswordRules(bool $required = false): array
{
$rules = ['string', 'min:6', 'max:128'];
return $required ? array_merge(['required'], $rules) : array_merge(['nullable'], $rules);
}
protected function normalizeAdminAccountFields(): void
{
if ($this->has('account')) {
$this->merge(['account' => strtolower(trim((string) $this->input('account')))]);
}
if ($this->has('username')) {
$this->merge(['username' => strtolower(trim((string) $this->input('username')))]);
}
}
}

View File

@@ -10,7 +10,7 @@ trait AgentProfileFieldRules
return [
'total_share_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'relative_share_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'credit_limit' => ['sometimes', 'integer', 'min:0'],
'credit_limit' => ['sometimes', 'integer', 'min:0', 'max:999999999'],
'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'can_grant_extra_rebate' => ['sometimes', 'boolean'],