Add limited backoffice employee QR login links
This commit is contained in:
@@ -800,6 +800,178 @@ it('caps limited employee permissions to the manager permissions and selected de
|
||||
->not->toContain(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
||||
});
|
||||
|
||||
it('generates reusable QR login links for active scoped employees', function (): void {
|
||||
api_test_covers('POST /limited-backoffice/employees/{employeeId}/login-link', 'happy');
|
||||
|
||||
$department = api_fixtures()->createDepartment(['name' => 'Limited Login Link Department']);
|
||||
$session = limited_backoffice_manager_session([(int)$department['id']]);
|
||||
|
||||
$created = api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Limited QR Employee',
|
||||
'email' => 'limited-qr@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'viewer',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
], $session['headers']);
|
||||
$created
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$employeeId = (int)($created->data()['id'] ?? 0);
|
||||
expect($employeeId)->toBeGreaterThan(0);
|
||||
limited_backoffice_cleanup_created_employee($employeeId);
|
||||
|
||||
$response = api_client()->post(
|
||||
'/limited-backoffice/employees/' . $employeeId . '/login-link',
|
||||
[],
|
||||
$session['headers']
|
||||
);
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$loginPath = (string)($response->data()['login_path'] ?? '');
|
||||
expect($response->data()['employee_id'] ?? null)->toBe($employeeId);
|
||||
expect($loginPath)->toMatch('/^\/login\/qr\?token=[a-f0-9]{64}$/');
|
||||
|
||||
parse_str((string)parse_url($loginPath, PHP_URL_QUERY), $query);
|
||||
$token = (string)($query['token'] ?? '');
|
||||
expect($token)->toMatch('/^[a-f0-9]{64}$/');
|
||||
|
||||
$tokenRow = api_test_runtime()->queryOne(
|
||||
"SELECT `user_id`, `type` FROM `tokens` WHERE `token` = '" .
|
||||
api_test_runtime()->db()->real_escape_string($token) .
|
||||
"' LIMIT 1"
|
||||
);
|
||||
expect($tokenRow)->not->toBeNull();
|
||||
expect((int)($tokenRow['user_id'] ?? 0))->toBe($employeeId);
|
||||
expect($tokenRow['type'] ?? null)->toBe('AUTH_TOKEN');
|
||||
|
||||
$list = api_client()->get('/limited-backoffice/employees', $session['headers']);
|
||||
$list
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
expect($list->body)->not->toContain($token);
|
||||
expect($list->body)->not->toContain('login_path');
|
||||
});
|
||||
|
||||
it('rejects invalid limited backoffice employee QR login link generation', function (): void {
|
||||
api_test_covers('POST /limited-backoffice/employees/{employeeId}/login-link', 'auth');
|
||||
api_test_covers('POST /limited-backoffice/employees/{employeeId}/login-link', 'validation');
|
||||
|
||||
$department = api_fixtures()->createDepartment(['name' => 'Limited Login Link Own']);
|
||||
$otherDepartment = api_fixtures()->createDepartment(['name' => 'Limited Login Link Other']);
|
||||
$session = limited_backoffice_manager_session([(int)$department['id']]);
|
||||
$otherSession = limited_backoffice_manager_session([(int)$otherDepartment['id']]);
|
||||
|
||||
$created = api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Limited Link Target',
|
||||
'email' => 'limited-link-target@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'viewer',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
], $session['headers']);
|
||||
$employeeId = (int)($created->data()['id'] ?? 0);
|
||||
expect($employeeId)->toBeGreaterThan(0);
|
||||
limited_backoffice_cleanup_created_employee($employeeId);
|
||||
|
||||
$withoutManageEmployees = api_fixtures()->createUserSession([
|
||||
limited_backoffice_service::PERMISSION_ACCESS,
|
||||
'department_access_' . (int)$department['id'],
|
||||
]);
|
||||
api_client()->post(
|
||||
'/limited-backoffice/employees/' . $employeeId . '/login-link',
|
||||
[],
|
||||
$withoutManageEmployees['headers']
|
||||
)
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMissingPermissions([limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES]);
|
||||
|
||||
api_client()->post(
|
||||
'/limited-backoffice/employees/' . (int)$session['user']['id'] . '/login-link',
|
||||
[],
|
||||
$session['headers']
|
||||
)
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Managers cannot edit themselves.');
|
||||
|
||||
api_client()->post('/limited-backoffice/employees/999999999/login-link', [], $session['headers'])
|
||||
->assertStatus(404)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Managed employee not found.');
|
||||
|
||||
$otherCreated = api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Limited Other Department Target',
|
||||
'email' => 'limited-other-target@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'viewer',
|
||||
'department_ids' => [(int)$otherDepartment['id']],
|
||||
], $otherSession['headers']);
|
||||
$otherEmployeeId = (int)($otherCreated->data()['id'] ?? 0);
|
||||
expect($otherEmployeeId)->toBeGreaterThan(0);
|
||||
limited_backoffice_cleanup_created_employee($otherEmployeeId);
|
||||
|
||||
api_client()->post(
|
||||
'/limited-backoffice/employees/' . $otherEmployeeId . '/login-link',
|
||||
[],
|
||||
$session['headers']
|
||||
)
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMissingPermissions(['department_access_' . (int)$otherDepartment['id']]);
|
||||
|
||||
api_client()->delete('/limited-backoffice/employees/' . $employeeId, null, $session['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
api_client()->post('/limited-backoffice/employees/' . $employeeId . '/login-link', [], $session['headers'])
|
||||
->assertStatus(409)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Cannot create a login link for an inactive employee.');
|
||||
|
||||
$superuser = api_fixtures()->createUser(['group_id' => 1]);
|
||||
api_test_runtime()->db()->query(
|
||||
'INSERT INTO `limited_backoffice_employees`
|
||||
(`user_id`, `managed_group_id`, `role_key`, `department_ids`, `created_by_user_id`)
|
||||
VALUES (' . (int)$superuser['id'] . ", 1, 'department_admin', '[" . (int)$department['id'] . "]', " . (int)$session['user']['id'] . ')'
|
||||
);
|
||||
api_fixtures()->cleanupDeleteWhere('limited_backoffice_employees', ['user_id' => (int)$superuser['id']]);
|
||||
|
||||
api_client()->post('/limited-backoffice/employees/' . (int)$superuser['id'] . '/login-link', [], $session['headers'])
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Cannot manage superuser accounts.');
|
||||
|
||||
$sharedGroup = api_fixtures()->createGroup();
|
||||
$firstSharedUser = api_fixtures()->createUser(['group_id' => $sharedGroup['id']]);
|
||||
api_fixtures()->createUser(['group_id' => $sharedGroup['id']]);
|
||||
$departmentJson = '[' . (int)$department['id'] . ']';
|
||||
api_test_runtime()->db()->query(
|
||||
'INSERT INTO `limited_backoffice_employees`
|
||||
(`user_id`, `managed_group_id`, `role_key`, `department_ids`, `created_by_user_id`)
|
||||
VALUES (' . (int)$firstSharedUser['id'] . ', ' . (int)$sharedGroup['id'] . ", 'viewer', '" . $departmentJson . "', " . (int)$session['user']['id'] . ')'
|
||||
);
|
||||
api_fixtures()->cleanupDeleteWhere('limited_backoffice_employees', ['user_id' => (int)$firstSharedUser['id']]);
|
||||
|
||||
api_client()->post('/limited-backoffice/employees/' . (int)$firstSharedUser['id'] . '/login-link', [], $session['headers'])
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Cannot manage shared groups.');
|
||||
});
|
||||
|
||||
it('includes limited employees in the regular employee list and protects raw user edits', function (): void {
|
||||
api_test_covers('GET /users', 'limited backoffice employee list');
|
||||
api_test_covers('PUT /users', 'limited backoffice guard');
|
||||
|
||||
Reference in New Issue
Block a user