feat: Implement user management functionality with impersonation support

This commit is contained in:
2026-02-09 17:22:52 +01:00
parent 63268e35dc
commit 304c87f933
12 changed files with 857 additions and 5 deletions

View File

@@ -13,7 +13,36 @@
"lmtime": 0,
"modified": true
},
"autoload": {},
"autoload": {
"Domain": {
"Tasks": {
"class.WorkTimeRepository.php": {
"type": "-",
"size": 3269,
"lmtime": 0,
"modified": false
},
"MailToTaskImporter.php": {
"type": "-",
"size": 24511,
"lmtime": 1770587036920,
"modified": false
},
"TaskAttachmentRepository.php": {
"type": "-",
"size": 8411,
"lmtime": 0,
"modified": false
},
"WorkTimeRepository.php": {
"type": "-",
"size": 3269,
"lmtime": 0,
"modified": false
}
}
}
},
"ceidg.php": {
"type": "-",
"size": 3950,
@@ -30,8 +59,8 @@
},
"config.php": {
"type": "-",
"size": 729,
"lmtime": 1770583610342,
"size": 1249,
"lmtime": 1770587027872,
"modified": false
},
"cron.php": {

462
CODE_INDEX.md Normal file
View File

@@ -0,0 +1,462 @@
# CODE INDEX
Generated: 2026-02-09 17:02:20
Scope: root `*.php`, `autoload/**/*.php`, `tests/**/*.php`.
Excluded: `libraries/**`, `templates/**`.
## Summary
- Files with declarations: 41
- Classes/interfaces/traits: 33
- Functions/methods: 333
## Declarations By File
### `ajax.php`
- Function: `__autoload_my_classes()` (line 3)
### `api.php`
- Function: `__autoload_my_classes()` (line 4)
### `autoload/class.Cache.php`
- Class: `Cache` (line 2)
- Function: `store()` (line 4)
- Function: `get_file_name()` (line 9)
- Function: `fetch()` (line 20)
### `autoload/class.Chunk.php`
- Class: `Chunk` (line 2)
- Function: `__construct()` (line 62)
- Function: `__destruct()` (line 108)
- Function: `read()` (line 122)
### `autoload/class.Cron.php`
- Class: `Cron` (line 3)
- Function: `recursive_tasks()` (line 5)
- Function: `import_tasks_from_email()` (line 121)
- Function: `tasks_emails()` (line 132)
### `autoload/class.DbModel.php`
- Class: `DbModel` (line 2)
- Function: `__construct()` (line 8)
- Function: `__get()` (line 20)
- Function: `__set()` (line 26)
- Function: `save()` (line 31)
- Function: `delete()` (line 52)
### `autoload/class.Excel.php`
- Class: `Excel` (line 10)
- Function: `filename()` (line 21)
- Function: `__construct()` (line 33)
- Function: `headers()` (line 45)
- Function: `send_to_file()` (line 53)
- Function: `send()` (line 59)
- Function: `bofMarker()` (line 70)
- Function: `eofMarker()` (line 79)
- Function: `left()` (line 88)
- Function: `right()` (line 101)
- Function: `up()` (line 111)
- Function: `down()` (line 124)
- Function: `top()` (line 133)
- Function: `home()` (line 141)
- Function: `number()` (line 151)
- Function: `label()` (line 162)
### `autoload/class.Html.php`
- Class: `Html` (line 3)
- Function: `form_text()` (line 5)
- Function: `input_switch()` (line 12)
- Function: `select()` (line 19)
- Function: `textarea()` (line 26)
- Function: `input_icon()` (line 39)
- Function: `input()` (line 52)
- Function: `button()` (line 65)
- Function: `panel()` (line 78)
### `autoload/class.S.php`
- Class: `S` (line 2)
- Function: `array_unique_multi()` (line 4)
- Function: `number_display()` (line 16)
- Function: `prepar_request()` (line 21)
- Function: `seo()` (line 33)
- Function: `no_pl_excel()` (line 48)
- Function: `noPL()` (line 63)
- Function: `alert()` (line 100)
- Function: `hash()` (line 105)
- Function: `sort_array_of_array()` (line 121)
- Function: `json_to_array()` (line 130)
- Function: `get_session()` (line 149)
- Function: `del_session()` (line 154)
- Function: `set_session()` (line 158)
- Function: `get()` (line 163)
- Function: `pre()` (line 184)
- Function: `email_check()` (line 200)
- Function: `send_email()` (line 205)
### `autoload/class.Tpl.php`
- Class: `Tpl` (line 2)
- Function: `__construct()` (line 7)
- Function: `view()` (line 13)
- Function: `secureHTML()` (line 21)
- Function: `render()` (line 31)
- Function: `__set()` (line 64)
- Function: `__get()` (line 69)
### `autoload/Controllers/TasksController.php`
- Class: `TasksController` (line 4)
- Function: `workTime()` (line 8)
- Function: `workTimeViewModel()` (line 20)
- Function: `resolveTaskStatusForForm()` (line 28)
- Function: `resolveTaskStatusForSave()` (line 38)
- Function: `taskChangeStatus()` (line 48)
- Function: `shouldStopTimerOnStatus()` (line 74)
- Function: `shouldSendStatusChangeEmail()` (line 79)
- Function: `sendEmailTaskChangeStatus()` (line 84)
### `autoload/controls/class.BackendSites.php`
- Class: `BackendSites` (line 3)
- Function: `topic_delete()` (line 5)
- Function: `topic_accept()` (line 21)
- Function: `topic_unaccept()` (line 31)
- Function: `topic_save()` (line 41)
- Function: `topic_edit()` (line 58)
- Function: `topics()` (line 65)
- Function: `collective_topics()` (line 70)
- Function: `collective_topic_edit()` (line 75)
- Function: `collective_topic_save()` (line 82)
### `autoload/controls/class.Crm.php`
- Class: `Crm` (line 14)
- Function: `client_delete()` (line 17)
- Function: `client_save()` (line 32)
- Function: `client_edit()` (line 51)
- Function: `main_view()` (line 63)
### `autoload/controls/class.Cron.php`
- Class: `Cron` (line 3)
- Function: `main_view()` (line 5)
### `autoload/controls/class.Finances.php`
- Class: `Finances` (line 3)
- Function: `category_delete()` (line 5)
- Function: `operation_save()` (line 18)
- Function: `operation_delete()` (line 39)
- Function: `operation_edit()` (line 54)
- Function: `category_save()` (line 71)
- Function: `category_edit()` (line 90)
- Function: `operations_list()` (line 103)
- Function: `main_view()` (line 137)
### `autoload/controls/class.Projects.php`
- Class: `Projects` (line 3)
- Function: `project_save()` (line 5)
- Function: `project_edit()` (line 25)
- Function: `main_view()` (line 42)
- Function: `task_order_save()` (line 51)
- Function: `action_mark_as_done()` (line 63)
- Function: `action_edit()` (line 77)
- Function: `task_update()` (line 91)
- Function: `task_text_update()` (line 98)
- Function: `task_text_new()` (line 104)
- Function: `project_delete()` (line 110)
- Function: `task_change_status()` (line 126)
- Function: `ajax_user_tasks()` (line 137)
- Function: `task_delete()` (line 177)
- Function: `open_task_details()` (line 193)
- Function: `task_details()` (line 201)
- Function: `project_default()` (line 226)
- Function: `tasks()` (line 235)
### `autoload/controls/class.Site.php`
- Class: `Site` (line 3)
- Function: `route()` (line 5)
### `autoload/controls/class.Tasks.php`
- Class: `Tasks` (line 3)
- Function: `task_change_dates()` (line 5)
- Function: `task_delete()` (line 17)
- Function: `main_view_by_ajax()` (line 34)
- Function: `main_view()` (line 96)
- Function: `action_change_status()` (line 183)
- Function: `comment_delete()` (line 201)
- Function: `comment_save()` (line 218)
- Function: `action_delete()` (line 240)
- Function: `action_save()` (line 259)
- Function: `tasks_order_save()` (line 282)
- Function: `send_email_task_change_status()` (line 292)
- Function: `task_change_project()` (line 309)
- Function: `task_change_client()` (line 320)
- Function: `task_change_priority()` (line 334)
- Function: `task_change_status()` (line 348)
- Function: `task_end()` (line 353)
- Function: `task_start()` (line 364)
- Function: `task_edit()` (line 375)
- Function: `task_save()` (line 398)
- Function: `task_popup()` (line 423)
- Function: `task_attachment_upload()` (line 449)
- Function: `normalize_uploads_array()` (line 496)
- Function: `task_attachment_delete()` (line 519)
- Function: `task_attachment_rename()` (line 539)
- Function: `filtr_save_form()` (line 559)
- Function: `filtr_save()` (line 571)
- Function: `filtr_update()` (line 589)
- Function: `work_time()` (line 609)
- Function: `change_task_work_date_start()` (line 614)
- Function: `change_task_work_date_end()` (line 619)
- Function: `work_delete()` (line 624)
- Function: `filtr_set_default()` (line 633)
- Function: `filtr_get()` (line 650)
### `autoload/controls/class.Users.php`
- Class: `Users` (line 4)
- Function: `permissions()` (line 7)
- Function: `logout()` (line 46)
- Function: `settings_save()` (line 57)
- Function: `settings()` (line 71)
- Function: `login()` (line 85)
- Function: `login_form()` (line 117)
### `autoload/controls/class.Wiki.php`
- Class: `Wiki` (line 4)
- Function: `category_delete()` (line 7)
- Function: `category_save()` (line 20)
- Function: `category_edit()` (line 34)
- Function: `category_preview()` (line 47)
- Function: `main_view()` (line 59)
### `autoload/Domain/Tasks/MailToTaskImporter.php`
- Class: `MailToTaskImporter` (line 4)
- Function: `__construct()` (line 14)
- Function: `importFromImap()` (line 27)
- Function: `buildMailbox()` (line 197)
- Function: `resolveClientIdBySenderDomain()` (line 214)
- Function: `parseEmailsField()` (line 241)
- Function: `extractDomainFromEmail()` (line 254)
- Function: `parseReceivedDate()` (line 265)
- Function: `extractSender()` (line 274)
- Function: `decodeHeaderValue()` (line 289)
- Function: `messageKey()` (line 306)
- Function: `isMessageFinalized()` (line 318)
- Function: `getImportStatus()` (line 324)
- Function: `saveImportLog()` (line 329)
- Function: `ensureImportTable()` (line 346)
- Function: `extractMessageContent()` (line 365)
- Function: `flattenParts()` (line 410)
- Function: `parseSinglePart()` (line 441)
- Function: `partParams()` (line 485)
- Function: `decodePartBody()` (line 510)
- Function: `mimeType()` (line 521)
- Function: `htmlToText()` (line 540)
- Function: `cleanBodyText()` (line 557)
- Function: `prepareImportedTaskText()` (line 601)
- Function: `shouldImportAttachment()` (line 622)
- Function: `extractReferencedCidValues()` (line 647)
- Function: `normalizeContentId()` (line 668)
- Function: `parseWithAI()` (line 678)
### `autoload/Domain/Tasks/TaskAttachmentRepository.php`
- Class: `TaskAttachmentRepository` (line 4)
- Function: `__construct()` (line 11)
- Function: `listByTaskId()` (line 25)
- Function: `upload()` (line 46)
- Function: `uploadFromContent()` (line 76)
- Function: `rename()` (line 100)
- Function: `delete()` (line 112)
- Function: `purgeByTaskId()` (line 137)
- Function: `effectiveTitle()` (line 159)
- Function: `sanitizeFileName()` (line 165)
- Function: `ensureStorage()` (line 173)
- Function: `ensureTable()` (line 185)
- Function: `storeMeta()` (line 209)
- Function: `buildPublicUrl()` (line 230)
- Function: `formatSize()` (line 235)
- Function: `resolveFilePath()` (line 247)
### `autoload/Domain/Tasks/WorkTimeRepository.php`
- Class: `WorkTimeRepository` (line 4)
- Function: `__construct()` (line 9)
- Function: `getClientsWithUnsettledTasks()` (line 20)
- Function: `buildClientTasksByMonth()` (line 44)
- Function: `getClientTaskRows()` (line 74)
- Function: `getUnsettledTaskStatuses()` (line 91)
- Function: `getTaskTotalTimeByMonth()` (line 96)
### `autoload/factory/class.BackendSites.php`
- Class: `BackendSites` (line 3)
- Function: `topic_delete()` (line 6)
- Function: `topic_unaccept()` (line 12)
- Function: `topic_accept()` (line 18)
- Function: `topic_save()` (line 24)
- Function: `topic()` (line 57)
- Function: `collective_topic()` (line 63)
- Function: `collective_topic_save()` (line 69)
### `autoload/factory/class.Crm.php`
- Class: `Crm` (line 14)
- Function: `settings()` (line 19)
- Function: `get_client_name()` (line 31)
- Function: `get_client_list()` (line 36)
- Function: `client_delete()` (line 41)
- Function: `client_details()` (line 47)
- Function: `client_save()` (line 52)
### `autoload/factory/class.Cron.php`
- Class: `Cron` (line 3)
- Function: `remove_points_history()` (line 5)
- Function: `update_points()` (line 18)
- Function: `send_push()` (line 31)
- Function: `send_emails()` (line 291)
### `autoload/factory/class.Finances.php`
- Class: `Finances` (line 3)
- Function: `first_operation_date()` (line 5)
- Function: `get_operation_tags()` (line 11)
- Function: `client_name()` (line 25)
- Function: `clients_list_by_dates()` (line 31)
- Function: `clients_list()` (line 37)
- Function: `category_delete()` (line 43)
- Function: `default_group()` (line 48)
- Function: `groups_list()` (line 54)
- Function: `operation_delete()` (line 60)
- Function: `tags_json()` (line 66)
- Function: `tags_list()` (line 72)
- Function: `operations_list()` (line 91)
- Function: `operation_details()` (line 105)
- Function: `operation_save()` (line 115)
- Function: `category_details()` (line 171)
- Function: `category_save()` (line 177)
- Function: `wallet_expenses_this_month()` (line 203)
- Function: `wallet_income_this_month()` (line 214)
- Function: `wallet_summary_this_month()` (line 225)
- Function: `wallet_summary()` (line 236)
- Function: `operations()` (line 244)
- Function: `categories()` (line 273)
### `autoload/factory/class.Projects.php`
- Class: `Projects` (line 3)
- Function: `projects_list()` (line 5)
- Function: `count_open_subtasks()` (line 12)
- Function: `task_text_new()` (line 18)
- Function: `task_text_update()` (line 29)
- Function: `task_total_time()` (line 40)
- Function: `send_email_task_change_status()` (line 63)
- Function: `task_order_save()` (line 90)
- Function: `action_mark_as_done()` (line 104)
- Function: `action_name()` (line 112)
- Function: `send_email_notification()` (line 118)
- Function: `get_task_name()` (line 127)
- Function: `set_project_as_default()` (line 133)
- Function: `task_update()` (line 139)
- Function: `project_delete()` (line 151)
- Function: `project_name()` (line 157)
- Function: `open_task()` (line 163)
- Function: `task_change_status()` (line 176)
- Function: `task_delete()` (line 247)
- Function: `project_save()` (line 257)
- Function: `project_user_id()` (line 329)
- Function: `task_user_id()` (line 335)
- Function: `project_details()` (line 341)
- Function: `tasks_without_project()` (line 351)
- Function: `get_project_name()` (line 365)
- Function: `user_projects()` (line 371)
- Function: `get_unassigned_tasks()` (line 402)
- Function: `get_closed_tasks()` (line 431)
- Function: `get_toreview_tasks()` (line 476)
- Function: `get_inprogress_tasks()` (line 509)
- Function: `user_tasks()` (line 555)
### `autoload/factory/class.Tasks.php`
- Class: `Tasks` (line 4)
- Function: `filtr_details()` (line 11)
- Function: `get_priorities()` (line 17)
- Function: `task_change_dates()` (line 22)
- Function: `parent_tasks()` (line 37)
- Function: `get_tasks_gantt()` (line 59)
- Function: `work_delete()` (line 117)
- Function: `change_task_work_date_end()` (line 122)
- Function: `change_task_work_date_start()` (line 128)
- Function: `task_works()` (line 134)
- Function: `get_statuses()` (line 140)
- Function: `clear_task_opened()` (line 150)
- Function: `set_task_opened_by_user()` (line 156)
- Function: `is_taks_is_opened_by_user()` (line 169)
- Function: `get_filtrs()` (line 175)
- Function: `filtr_update()` (line 180)
- Function: `filtr_save()` (line 190)
- Function: `action_change_status()` (line 208)
- Function: `comment_delete()` (line 213)
- Function: `comment_save()` (line 218)
- Function: `action_delete()` (line 233)
- Function: `action_save()` (line 239)
- Function: `get_tasks()` (line 248)
- Function: `get_open_task_id()` (line 295)
- Function: `task_start()` (line 301)
- Function: `task_end()` (line 340)
- Function: `is_work_duration_too_short()` (line 368)
- Function: `task_details()` (line 379)
- Function: `task_total_time()` (line 394)
- Function: `is_task_open()` (line 417)
- Function: `work_time_clients()` (line 427)
- Function: `task_save()` (line 434)
- Function: `task_delete()` (line 532)
- Function: `task_first_id()` (line 540)
- Function: `task_delete_all()` (line 549)
- Function: `task_delete_from_db()` (line 567)
- Function: `filtr_set_default()` (line 582)
- Function: `get_default_filtr()` (line 590)
### `autoload/factory/class.Users.php`
- Class: `Users` (line 3)
- Function: `user_details()` (line 5)
- Function: `get_default_project()` (line 18)
- Function: `get_user_email()` (line 24)
- Function: `user_name()` (line 30)
- Function: `users_list()` (line 39)
- Function: `settings_save()` (line 62)
- Function: `login()` (line 73)
### `autoload/factory/class.Wiki.php`
- Class: `Wiki` (line 4)
- Function: `category_delete()` (line 6)
- Function: `category_save()` (line 11)
- Function: `category_details()` (line 45)
- Function: `category_users()` (line 52)
- Function: `get_categories()` (line 58)
### `autoload/view/class.Cron.php`
- Class: `Cron` (line 4)
- Function: `main_view()` (line 6)
### `autoload/view/class.Projects.php`
- Class: `Projects` (line 3)
### `autoload/view/class.Site.php`
- Class: `Site` (line 3)
- Function: `show()` (line 5)
### `autoload/view/class.Users.php`
- Class: `Users` (line 3)
- Function: `points_history()` (line 5)
- Function: `settings()` (line 12)
### `ceidg.php`
- Function: `__autoload_my_classes()` (line 3)
- Function: `memory_get_process_usage()` (line 114)
### `cron.php`
- Function: `__autoload_my_classes()` (line 4)
### `index.php`
- Function: `__autoload_my_classes()` (line 3)
### `tests/Controllers/TasksControllerTest.php`
- Function: `assert_same()` (line 7)
- Function: `run_tasks_controller_tests()` (line 13)
### `tests/Domain/Tasks/TaskAttachmentRepositoryTest.php`
- Function: `run_task_attachment_repository_tests()` (line 7)
### `tests/Domain/Tasks/WorkTimeRepositoryTest.php`
- Function: `assert_true()` (line 7)
- Function: `run_work_time_repository_tests()` (line 13)

View File

@@ -0,0 +1,133 @@
<?php
namespace Controllers;
class UsersController
{
private const ADMIN_USER_ID = 1;
private const IMPERSONATOR_SESSION_KEY = 'impersonator_user';
public static function mainView()
{
global $user;
if ( !$user )
return \controls\Users::login_form();
$impersonator_user = self::getImpersonatorUser();
if ( !self::canManageUsers( $user, $impersonator_user ) )
self::forbiddenRedirect();
$users_repository = new \Domain\Users\UserRepository();
return \Tpl::view( 'users/main-view', self::buildMainViewModel(
$user,
$impersonator_user,
$users_repository -> all()
) );
}
public static function loginAs()
{
global $user;
if ( !$user )
return \controls\Users::login_form();
$impersonator_user = self::getImpersonatorUser();
if ( !self::canManageUsers( $user, $impersonator_user ) )
self::forbiddenRedirect();
$target_user_id = (int)\S::get( 'user_id' );
$users_repository = new \Domain\Users\UserRepository();
$target_user = $users_repository -> byId( $target_user_id );
if ( !$target_user )
{
\S::alert( 'Nie znaleziono wskazanego uzytkownika.' );
header( 'Location: /users/main_view/' );
exit;
}
$new_session_state = self::impersonationStateAfterLoginAs( $user, $target_user, $impersonator_user );
\S::set_session( 'user', $new_session_state['user'] );
\S::set_session( self::IMPERSONATOR_SESSION_KEY, $new_session_state['impersonator_user'] );
\S::alert( 'Zalogowano jako: ' . $target_user['name'] . ' ' . $target_user['surname'] . '.' );
header( 'Location: /' );
exit;
}
public static function switchBackToAdmin()
{
$impersonator_user = self::getImpersonatorUser();
if ( !$impersonator_user or !isset( $impersonator_user['id'] ) or (int)$impersonator_user['id'] !== self::ADMIN_USER_ID )
{
\S::alert( 'Brak aktywnej sesji podszywania.' );
header( 'Location: /' );
exit;
}
\S::set_session( 'user', $impersonator_user );
\S::del_session( self::IMPERSONATOR_SESSION_KEY );
\S::alert( 'Powrot do konta administratora.' );
header( 'Location: /users/main_view/' );
exit;
}
public static function canManageUsers( $current_user, $impersonator_user = null )
{
if ( !is_array( $current_user ) )
return false;
if ( isset( $current_user['id'] ) and (int)$current_user['id'] === self::ADMIN_USER_ID )
return true;
if ( is_array( $impersonator_user ) and isset( $impersonator_user['id'] ) and (int)$impersonator_user['id'] === self::ADMIN_USER_ID )
return true;
return false;
}
public static function buildMainViewModel( $current_user, $impersonator_user, array $users )
{
return [
'current_user' => $current_user,
'impersonator_user' => $impersonator_user,
'users' => $users,
'can_switch_back' => is_array( $impersonator_user ) and isset( $impersonator_user['id'] ) and (int)$impersonator_user['id'] === self::ADMIN_USER_ID
];
}
public static function impersonationStateAfterLoginAs( $current_user, $target_user, $existing_impersonator_user = null )
{
$impersonator_user = $existing_impersonator_user;
if ( !is_array( $impersonator_user ) )
$impersonator_user = ( is_array( $current_user ) and isset( $current_user['id'] ) and (int)$current_user['id'] === self::ADMIN_USER_ID ) ? $current_user : null;
return [
'user' => $target_user,
'impersonator_user' => $impersonator_user
];
}
private static function getImpersonatorUser()
{
$session_value = \S::get_session( self::IMPERSONATOR_SESSION_KEY );
if ( is_array( $session_value ) )
return $session_value;
return null;
}
private static function forbiddenRedirect()
{
\S::alert( 'Brak uprawnien do zarzadzania uzytkownikami.' );
header( 'Location: /' );
exit;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Domain\Users;
class UserRepository
{
private $mdb;
public function __construct( $mdb = null )
{
if ( $mdb )
$this -> mdb = $mdb;
else if ( isset( $GLOBALS['mdb'] ) )
$this -> mdb = $GLOBALS['mdb'];
else
$this -> mdb = null;
}
public function all()
{
if ( !$this -> mdb )
return [];
return $this -> mdb -> select( 'users', '*', [ 'ORDER' => [ 'id' => 'ASC' ] ] );
}
public function byId( $user_id )
{
if ( !$this -> mdb )
return false;
return $this -> mdb -> get( 'users', '*', [ 'id' => (int)$user_id ] );
}
}

View File

@@ -119,4 +119,28 @@ class Users
return \Tpl::view( 'users/login-form' );
}
}
/**
* @deprecated Use \Controllers\UsersController::mainView() instead.
*/
public static function main_view()
{
return \Controllers\UsersController::mainView();
}
/**
* @deprecated Use \Controllers\UsersController::loginAs() instead.
*/
public static function login_as()
{
return \Controllers\UsersController::loginAs();
}
/**
* @deprecated Use \Controllers\UsersController::switchBackToAdmin() instead.
*/
public static function back_to_admin()
{
return \Controllers\UsersController::switchBackToAdmin();
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace factory;
class Users
{

View File

@@ -39,6 +39,13 @@
<i class="fa fa-user"></i> <span><?= $this -> user[ 'email' ];?></span>
</div>
<ul>
<? $impersonator_user = \S::get_session( 'impersonator_user' );?>
<? if ( is_array( $impersonator_user ) and isset( $impersonator_user['id'] ) and (int)$impersonator_user['id'] === 1 ):?>
<li>
<a href="/users/back_to_admin/">Powrot do admina</a>
</li>
<li id="divider"></li>
<? endif;?>
<li>
<a href="/users/settings/">Ustawienia</a>
</li>
@@ -51,6 +58,7 @@
</div>
<div class="main-menu">
<ul>
<? $can_manage_users = (int)$this -> user['id'] === 1;?>
<? if ( \controls\Users::permissions( $this -> user[ 'id' ], 'projects' ) ):?>
<li>
<a href="/tasks/main_view/">Zadania</a>
@@ -81,6 +89,11 @@
<a href="/wiki/main_view/">Wiki</a>
</li>
<? endif;?>
<? if ( $can_manage_users ):?>
<li>
<a href="/users/main_view/">U&#380;ytkownicy</a>
</li>
<? endif;?>
<? if ( \controls\Users::permissions( $this -> user[ 'id' ], 'zaplecze' ) ):?>
<li>
<a href="#">Zaplecze</a>

View File

@@ -45,6 +45,13 @@
<i class="fa fa-user"></i> <span><?= $this -> user[ 'email' ];?></span>
</div>
<ul>
<? $impersonator_user = \S::get_session( 'impersonator_user' );?>
<? if ( is_array( $impersonator_user ) and isset( $impersonator_user['id'] ) and (int)$impersonator_user['id'] === 1 ):?>
<li>
<a href="/users/back_to_admin/">Powrot do admina</a>
</li>
<li id="divider"></li>
<? endif;?>
<li>
<a href="/users/settings/">Ustawienia</a>
</li>
@@ -57,6 +64,7 @@
</div>
<div class="main-menu">
<ul>
<? $can_manage_users = (int)$this -> user['id'] === 1;?>
<? if ( \controls\Users::permissions( $this -> user[ 'id' ], 'tasks' ) ):?>
<li>
<a href="/tasks/main_view/">Zadania</a>
@@ -87,6 +95,11 @@
<a href="/wiki/main_view/">Wiki</a>
</li>
<? endif;?>
<? if ( $can_manage_users ):?>
<li>
<a href="/users/main_view/">U&#380;ytkownicy</a>
</li>
<? endif;?>
<? if ( \controls\Users::permissions( $this -> user[ 'id' ], 'zaplecze' ) ):?>
<li>
<a href="/backend_sites/collective_topics/">Zaplecze - tematy zbiorcze</a>

View File

@@ -0,0 +1,55 @@
<div class="form_container full">
<div class="block-header">
<h2>Zarz&#261;dzanie <strong>u&#380;ytkownikami</strong></h2>
</div>
<div class="action_menu">
<? if ( $this -> can_switch_back ):?>
<a href="/users/back_to_admin/" class="btn btn-warning" title="Powr&#243;t do konta administratora">
<i class="fa fa-undo"></i> Powrot do admina
</a>
<? endif;?>
</div>
<div class="content">
<table class="table">
<thead>
<tr>
<th style="width: 60px;">ID</th>
<th>Imi&#281; i nazwisko</th>
<th>Email</th>
<th style="width: 240px;">Akcje</th>
</tr>
</thead>
<tbody>
<? if ( is_array( $this -> users ) and count( $this -> users ) ): foreach ( $this -> users as $user_tmp ):?>
<? $is_current = (int)$this -> current_user['id'] === (int)$user_tmp['id'];?>
<tr>
<td class="center"><?= (int)$user_tmp['id'];?></td>
<td class="left">
<?= htmlspecialchars( trim( $user_tmp['name'] . ' ' . $user_tmp['surname'] ) );?>
<? if ( (int)$user_tmp['id'] === 1 ):?>
<span class="label label-info" style="margin-left: 8px;">ADMIN</span>
<? endif;?>
</td>
<td class="left"><?= htmlspecialchars( $user_tmp['email'] );?></td>
<td class="center">
<? if ( $is_current ):?>
<span class="btn btn-default btn_small disabled">Aktywna sesja</span>
<? else:?>
<a href="/users/login_as/user_id=<?= (int)$user_tmp['id'];?>" class="btn btn-primary btn_small">
<i class="fa fa-sign-in"></i>
Zaloguj jako
</a>
<? endif;?>
</td>
</tr>
<? endforeach; else:?>
<tr>
<td colspan="4" class="center">Brak u&#380;ytkownik&#243;w.</td>
</tr>
<? endif;?>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../../autoload/Controllers/UsersController.php';
use Controllers\UsersController;
function assert_users_controller_same( $expected, $actual, $message )
{
if ( $expected !== $actual )
throw new Exception( $message );
}
function run_users_controller_tests()
{
$admin_user = [ 'id' => 1, 'name' => 'Admin', 'surname' => 'One' ];
$regular_user = [ 'id' => 3, 'name' => 'Jan', 'surname' => 'Kowalski' ];
$target_user = [ 'id' => 5, 'name' => 'Anna', 'surname' => 'Nowak' ];
assert_users_controller_same( true, UsersController::canManageUsers( $admin_user, null ), 'Expected admin to manage users.' );
assert_users_controller_same( false, UsersController::canManageUsers( $regular_user, null ), 'Expected regular user to not manage users.' );
assert_users_controller_same( true, UsersController::canManageUsers( $regular_user, $admin_user ), 'Expected impersonated admin session to manage users.' );
$state = UsersController::impersonationStateAfterLoginAs( $admin_user, $target_user, null );
assert_users_controller_same( 5, (int)$state['user']['id'], 'Expected impersonation to switch current user to target user.' );
assert_users_controller_same( 1, (int)$state['impersonator_user']['id'], 'Expected impersonator to be preserved as admin user.' );
$state_with_existing = UsersController::impersonationStateAfterLoginAs( $regular_user, $target_user, $admin_user );
assert_users_controller_same( 1, (int)$state_with_existing['impersonator_user']['id'], 'Expected existing impersonator to stay unchanged.' );
$view_model = UsersController::buildMainViewModel( $target_user, $admin_user, [ $admin_user, $regular_user, $target_user ] );
assert_users_controller_same( true, $view_model['can_switch_back'], 'Expected can_switch_back to be true when impersonator is admin.' );
}

View File

@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/../../../autoload/Domain/Users/UserRepository.php';
use Domain\Users\UserRepository;
class FakeUsersMdb
{
public $last_select = [];
public $last_get = [];
private $rows = [];
public function __construct( array $rows )
{
$this -> rows = $rows;
}
public function select( $table, $columns, $where )
{
$this -> last_select = [ 'table' => $table, 'columns' => $columns, 'where' => $where ];
return $this -> rows;
}
public function get( $table, $columns, $where )
{
$this -> last_get = [ 'table' => $table, 'columns' => $columns, 'where' => $where ];
foreach ( $this -> rows as $row )
{
if ( (int)$row['id'] === (int)$where['id'] )
return $row;
}
return false;
}
}
function run_user_repository_tests()
{
$rows = [
[ 'id' => 1, 'email' => 'admin@example.com', 'name' => 'Admin', 'surname' => 'User' ],
[ 'id' => 3, 'email' => 'user@example.com', 'name' => 'Normal', 'surname' => 'User' ]
];
$fake_mdb = new FakeUsersMdb( $rows );
$repository = new UserRepository( $fake_mdb );
$all = $repository -> all();
assert_true( count( $all ) === 2, 'Expected all() to return all users.' );
assert_true( $fake_mdb -> last_select['table'] === 'users', 'Expected all() to query users table.' );
$user = $repository -> byId( 3 );
assert_true( (int)$user['id'] === 3, 'Expected byId() to return matching user.' );
assert_true( (int)$fake_mdb -> last_get['where']['id'] === 3, 'Expected byId() to query requested id.' );
}

View File

@@ -2,12 +2,16 @@
require_once __DIR__ . '/Domain/Tasks/WorkTimeRepositoryTest.php';
require_once __DIR__ . '/Domain/Tasks/TaskAttachmentRepositoryTest.php';
require_once __DIR__ . '/Domain/Users/UserRepositoryTest.php';
require_once __DIR__ . '/Controllers/TasksControllerTest.php';
require_once __DIR__ . '/Controllers/UsersControllerTest.php';
$tests = [
'run_work_time_repository_tests',
'run_task_attachment_repository_tests',
'run_tasks_controller_tests'
'run_user_repository_tests',
'run_tasks_controller_tests',
'run_users_controller_tests'
];
$failed = 0;