feat(core): harden sessions settlement and credit activity
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

This commit is contained in:
wchino
2026-07-22 20:53:43 +08:00
parent 35f6e46958
commit 15bd997c4e
48 changed files with 1920 additions and 93 deletions

View File

@@ -2,8 +2,8 @@
use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use App\Support\SitePlatformRole;
use Illuminate\Support\Str;
use App\Support\SitePlatformRole;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -92,6 +92,89 @@ test('admin login returns bearer token when captcha passes validation', function
->assertJsonPath('data.scope', 'admin');
});
test('later admin login replaces the previous browser session', function () {
$admin = AdminUser::query()->create([
'username' => 'single_session_admin',
'name' => '单会话管理员',
'email' => null,
'password' => 'secret-strong',
'status' => 0,
]);
grantSuperAdminRole($admin);
$login = function () {
$captchaKey = (string) Str::uuid();
Cache::put(
'admin_captcha:'.$captchaKey,
hash_hmac('sha256', 'xwz2', (string) config('app.key')),
now()->addSeconds(120),
);
return $this->postJson('/api/v1/admin/auth/login', [
'account' => 'single_session_admin',
'password' => 'secret-strong',
'captcha_key' => $captchaKey,
'captcha_code' => 'xwz2',
])->assertOk();
};
$firstToken = (string) $login()->json('data.token');
$this->withHeader('Authorization', 'Bearer '.$firstToken)
->getJson('/api/v1/admin/ping')
->assertOk();
$secondToken = (string) $login()->json('data.token');
app('auth')->forgetGuards();
$this->withHeader('Authorization', 'Bearer '.$firstToken)
->getJson('/api/v1/admin/ping')
->assertUnauthorized()
->assertJsonPath('code', ErrorCode::AdminSessionReplaced->value);
app('auth')->forgetGuards();
$this->withHeader('Authorization', 'Bearer '.$secondToken)
->getJson('/api/v1/admin/ping')
->assertOk()
->assertJsonPath('code', ErrorCode::Success->value);
expect($admin->fresh()->admin_session_version)->toBe(2)
->and($admin->tokens()->where('name', 'admin-api')->count())->toBe(2);
});
test('admin logout revokes browser sessions without deleting programmatic tokens', function () {
$admin = AdminUser::query()->create([
'username' => 'logout_admin',
'name' => '退出管理员',
'email' => null,
'password' => 'secret-strong',
'status' => 0,
'admin_session_version' => 1,
]);
$browserToken = $admin->createToken(
'admin-api',
['admin-session:1'],
now()->addDay(),
)->plainTextToken;
$admin->createToken('test', ['*'], now()->addDay());
$this->withHeader('Authorization', 'Bearer '.$browserToken)
->postJson('/api/v1/admin/auth/logout')
->assertOk()
->assertJsonPath('code', ErrorCode::Success->value)
->assertJsonPath('data.logged_out', true);
expect($admin->tokens()->where('name', 'admin-api')->count())->toBe(0)
->and($admin->tokens()->where('name', 'test')->count())->toBe(1);
app('auth')->forgetGuards();
$this->withHeader('Authorization', 'Bearer '.$browserToken)
->getJson('/api/v1/admin/ping')
->assertUnauthorized()
->assertJsonPath('code', ErrorCode::AdminUnauthenticated->value);
});
test('agent operator auth me omits platform-only navigation', function (): void {
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);