<?php
namespace App\AI\Contexts\Entity; class User extends \App\AI\Contexts\Entity\EntityAbstract { public function getDeveloperMessages(): array { $developerMessages = []; $developerMessages[] = "Never show internal IDs such as draft_id, tool call IDs or storage paths to the user unless explicitly requested.
            Use natural references like \"this draft\" or \"the user draft\"."; $developerMessages[] = "Add users instructions:
            - username, firstName and lastName are required fields.
            - If you only have the e-mail available, try to extract the firstName and lastName from it.
        "; $developerMessages[] = "User login blocking workflow:
            - Never call block_user_login from the first user request.
            - First call preview_user_login_block using user_id and login_blocked_until (a future date-time in ISO local format, e.g. YYYY-MM-DD HH:MM:SS).
            - Show the returned review_text and confirmation_phrase.
            - Only call block_user_login when the user confirms with a phrase containing CONFIRM and the same login_blocked_until was agreed.
            - If can_block=false, explain why and do not ask for confirmation."; $developerMessages[] = "User login unblocking workflow:
            - Never call unblock_user_login from the first user request.
            - First call preview_user_login_unblock using user_id.
            - Show the returned review_text and confirmation_phrase.
            - Only call unblock_user_login when the user provides a confirmation phrase that contains CONFIRM.
            - If can_unblock=false, explain why and do not ask for confirmation."; return $developerMessages; } protected $entityFields = [ 'username', 'firstName', 'lastName', 'title', 'email', 'address', 'telephone', 'birthday', 'PNC', 'comments', ]; protected $mandatoryFields = ['username', 'firstName', 'lastName', 'email']; protected function checkPermission_create_user_draft() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function checkPermission_update_user_draft() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function checkPermission_preview_user() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function checkPermission_submit_user() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function checkPermission_preview_user_login_block() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function checkPermission_block_user_login() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function checkPermission_preview_user_login_unblock() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function checkPermission_unblock_user_login() : bool { return \App\UserAccess\CurrentUserAccessFast::getInstance()->hasAccessToSection('Modules/Admin/Users'); } protected function generateFunctionObject_create_user_draft(): \App\AI\Api\ChatGpt\Responses\ToolFunction { $funcObj = new \App\AI\Api\ChatGpt\Responses\ToolFunction( "create_user_draft", "Create a new application user draft. This does NOT commit the user. It only stores a draft and returns what is missing. Office, department, and language default to the current operator; password must be set separately in Users administration.", false ); $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $this->populateFunctionInsertUpdateParameters($paramsObj); $paramsObj->setRequired(['username', 'firstName', 'lastName', 'email']); return $funcObj; } protected function generateFunctionObject_update_user_draft(): \App\AI\Api\ChatGpt\Responses\ToolFunction { $funcObj = new \App\AI\Api\ChatGpt\Responses\ToolFunction( "update_user_draft", "Update an existing user draft with new or corrected fields. Does NOT commit the user.", false ); $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $draftIdObj = new \App\AI\Api\ChatGpt\Responses\Property(); $draftIdObj->type = "integer"; $draftIdObj->description = "The draft ID returned when the draft was created."; $paramsObj->addProperty("draft_id", $draftIdObj); $patchObj = new \App\AI\Api\ChatGpt\Responses\Property(); $patchObj->type = "object"; $patchObj->description = "An object with the fields to update in the draft."; $paramsObj->addProperty("patch", $patchObj); $this->populateFunctionInsertUpdateParameters($patchObj); $paramsObj->setRequired(['draft_id', 'patch']); return $funcObj; } protected function generateFunctionObject_preview_user(): \App\AI\Api\ChatGpt\Responses\ToolFunction { $funcObj = new \App\AI\Api\ChatGpt\Responses\ToolFunction( "preview_user", "Return a human-friendly preview of the user draft for review. Does NOT commit.", false ); $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $draftIdObj = new \App\AI\Api\ChatGpt\Responses\Property(); $draftIdObj->type = "integer"; $paramsObj->addProperty("draft_id", $draftIdObj); $paramsObj->setRequired(['draft_id']); return $funcObj; } protected function generateFunctionObject_submit_user(): \App\AI\Api\ChatGpt\Responses\ToolFunction { $funcObj = new \App\AI\Api\ChatGpt\Responses\ToolFunction( "submit_user", "Commit the user draft into the system. Only call after the user explicitly confirms. Login password is not set here.", false ); $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $draftIdObj = new \App\AI\Api\ChatGpt\Responses\Property(); $draftIdObj->type = "integer"; $paramsObj->addProperty("draft_id", $draftIdObj); $confirmObj = new \App\AI\Api\ChatGpt\Responses\Property(); $confirmObj->type = "string"; $confirmObj->description = "Must be the exact user confirmation phrase: 'CONFIRM'."; $paramsObj->addProperty("user_confirmation_text", $confirmObj); $paramsObj->setRequired(['draft_id', 'user_confirmation_text']); return $funcObj; } protected function generateFunctionObject_preview_user_login_block(\App\AI\Api\ChatGpt\Responses\ToolFunction $funcObj) : \App\AI\Api\ChatGpt\Responses\ToolFunction { $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $userObj = new \App\AI\Api\ChatGpt\Responses\Property(); $userObj->type = "integer"; $userObj->description = "The unique ID of the user whose login should be blocked."; $paramsObj->addProperty("user_id", $userObj); $untilObj = new \App\AI\Api\ChatGpt\Responses\Property(); $untilObj->type = "string"; $untilObj->format = "date-time"; $untilObj->description = "UTC/local datetime until which login is blocked (ISO-style: YYYY-MM-DD HH:MM:SS). Must be in the future."; $paramsObj->addProperty("login_blocked_until", $untilObj); $paramsObj->required = ["user_id", "login_blocked_until"]; return $funcObj; } protected function generateFunctionObject_block_user_login(\App\AI\Api\ChatGpt\Responses\ToolFunction $funcObj) : \App\AI\Api\ChatGpt\Responses\ToolFunction { $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $userObj = new \App\AI\Api\ChatGpt\Responses\Property(); $userObj->type = "integer"; $userObj->description = "The unique ID of the user whose login should be blocked."; $paramsObj->addProperty("user_id", $userObj); $untilObj = new \App\AI\Api\ChatGpt\Responses\Property(); $untilObj->type = "string"; $untilObj->format = "date-time"; $untilObj->description = "Same datetime as agreed in preview_user_login_block (stored as cantLoginUntil)."; $paramsObj->addProperty("login_blocked_until", $untilObj); $confirmObj = new \App\AI\Api\ChatGpt\Responses\Property(); $confirmObj->type = "string"; $confirmObj->description = "User confirmation; must contain CONFIRM (e.g. confirmation phrase from preview)."; $paramsObj->addProperty("user_confirmation_text", $confirmObj); $paramsObj->required = ["user_id", "login_blocked_until", "user_confirmation_text"]; return $funcObj; } protected function generateFunctionObject_preview_user_login_unblock(\App\AI\Api\ChatGpt\Responses\ToolFunction $funcObj) : \App\AI\Api\ChatGpt\Responses\ToolFunction { $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $userObj = new \App\AI\Api\ChatGpt\Responses\Property(); $userObj->type = "integer"; $userObj->description = "The unique ID of the user whose login block should be cleared."; $paramsObj->addProperty("user_id", $userObj); $paramsObj->required = ["user_id"]; return $funcObj; } protected function generateFunctionObject_unblock_user_login(\App\AI\Api\ChatGpt\Responses\ToolFunction $funcObj) : \App\AI\Api\ChatGpt\Responses\ToolFunction { $paramsObj = new \App\AI\Api\ChatGpt\Responses\Parameters(); $funcObj->setParameters($paramsObj); $userObj = new \App\AI\Api\ChatGpt\Responses\Property(); $userObj->type = "integer"; $userObj->description = "The unique ID of the user whose login block should be cleared."; $paramsObj->addProperty("user_id", $userObj); $confirmObj = new \App\AI\Api\ChatGpt\Responses\Property(); $confirmObj->type = "string"; $confirmObj->description = "User confirmation; must contain CONFIRM."; $paramsObj->addProperty("user_confirmation_text", $confirmObj); $paramsObj->required = ["user_id", "user_confirmation_text"]; return $funcObj; } protected function populateFunctionInsertUpdateParameters( \App\AI\Api\ChatGpt\Responses\Parameters|\App\AI\Api\ChatGpt\Responses\Property $paramsObj ) : \App\AI\Api\ChatGpt\Responses\Parameters|\App\AI\Api\ChatGpt\Responses\Property { $usernameObj = new \App\AI\Api\ChatGpt\Responses\Property(); $usernameObj->type = "string"; $usernameObj->description = "Login username (unique)."; $paramsObj->addProperty("username", $usernameObj); $firstObj = new \App\AI\Api\ChatGpt\Responses\Property(); $firstObj->type = "string"; $firstObj->description = "First name."; $paramsObj->addProperty("firstName", $firstObj); $lastObj = new \App\AI\Api\ChatGpt\Responses\Property(); $lastObj->type = "string"; $lastObj->description = "Last name."; $paramsObj->addProperty("lastName", $lastObj); $titleObj = new \App\AI\Api\ChatGpt\Responses\Property(); $titleObj->type = "string"; $titleObj->description = "Salutation: mr, mrs, miss, or mstr (mapped to system person title codes). Optional."; $titleObj->enum = ['mr', 'mrs', 'miss', 'mstr']; $paramsObj->addProperty("title", $titleObj); $emailObj = new \App\AI\Api\ChatGpt\Responses\Property(); $emailObj->type = "string"; $emailObj->format = "email"; $emailObj->description = "Email address (unique). Required for non–web-service users."; $paramsObj->addProperty("email", $emailObj); $addrObj = new \App\AI\Api\ChatGpt\Responses\Property(); $addrObj->type = "string"; $addrObj->description = "Postal address. Optional."; $paramsObj->addProperty("address", $addrObj); $telObj = new \App\AI\Api\ChatGpt\Responses\Property(); $telObj->type = "string"; $telObj->description = "Telephone. Optional."; $paramsObj->addProperty("telephone", $telObj); $birthObj = new \App\AI\Api\ChatGpt\Responses\Property(); $birthObj->type = "string"; $birthObj->format = "date"; $birthObj->description = "Birthday as ISO date (YYYY-MM-DD). Optional."; $paramsObj->addProperty("birthday", $birthObj); $pncObj = new \App\AI\Api\ChatGpt\Responses\Property(); $pncObj->type = "string"; $pncObj->description = "Personal numeric code / national ID (unique if set). Optional."; $paramsObj->addProperty("PNC", $pncObj); $commentsObj = new \App\AI\Api\ChatGpt\Responses\Property(); $commentsObj->type = "string"; $commentsObj->description = "Comments. Optional."; $paramsObj->addProperty("comments", $commentsObj); return $paramsObj; } protected function executeFunction_create_user_draft(string $arguments) : array { $args = json_decode($arguments, 1); return $this->getCreateEntityDraft($args); } protected function executeFunction_update_user_draft(string $arguments) : array { $args = json_decode($arguments, 1); return $this->getUpdateEntityDraft($args); } protected function executeFunction_preview_user(string $arguments) : array { $args = json_decode($arguments, 1); return $this->getPreviewEntityDraft($args); } protected function executeFunction_submit_user(string $arguments) : array { $args = json_decode($arguments, 1); $draft = $this->getPreviewEntityDraft($args); $result = []; if ($args['user_confirmation_text']) { try { \DB::startTransaction(); $userEnt = $this->createUserEntityFromDraft($draft['fields']); \DB::commitTransaction(); $draft['fields']['id'] = $userEnt->getId(); $result['draft_id'] = $args['draft_id']; $result['idUser'] = $userEnt->getId(); $result['message'] = 'User committed successfully. Set password and access in Users administration if needed.'; } catch (\App\Entity\Exception $ex) { \DB::rollbackTransaction(); $result['draft_id'] = $args['draft_id']; $result['error'] = $ex->getMessageBulk(); } } else { $result['draft_id'] = $args['draft_id'] ?? null; $result['error'] = 'User was not committed, confirmation not received.'; } return $result; } protected function parseLoginBlockedUntilDatetime(?string $value): ?\Engine\DateTime { $trimmed = trim((string) $value); if ($trimmed === '') { return null; } $dt = \Engine\DateTime::createFromIsoDateOrDatetime($trimmed); if ($dt->isValid() && !$dt->isEmpty()) { return $dt; } $fallback = \Engine\DateTime::create($trimmed); if ($fallback->isValid() && !$fallback->isEmpty()) { return $fallback; } return null; } protected function executeFunction_preview_user_login_block(string $arguments) : array { $args = json_decode($arguments, 1); $results = []; $userEnt = \Entity\User::select($args['user_id']); if (!$userEnt->recordExists()) { $results['error'] = 'User not found'; return $results; } if ($userEnt->getId() === $this->idUser) { $results['can_block'] = false; $results['error'] = 'You cannot block your own login.'; return $results; } $dt = $this->parseLoginBlockedUntilDatetime($args['login_blocked_until'] ?? ''); if ($dt === null) { $results['can_block'] = false; $results['error'] = 'Invalid or missing login_blocked_until datetime.'; return $results; } $untilTs = (int) $dt->format('U'); if ($untilTs <= TIMESTAMP) { $results['can_block'] = false; $results['error'] = 'login_blocked_until must be in the future.'; return $results; } if ($userEnt->cantLoginUntil > TIMESTAMP) { $results['can_block'] = false; $results['error'] = sprintf( 'User login is already blocked until %s.', date(DATETIME_FORMAT, $userEnt->cantLoginUntil) ); return $results; } $entity = \Entity\User::update($args['user_id']); $msgs = new \App\MessagesCollector(); $results['can_block'] = $this->determineOperationAvailability($entity, $msgs); if ($msgs->hasErrorOrWarning()) { $results['error'] = $msgs->getMessageBulk(); return $results; } $returnFields = ['id', 'username', 'firstName', 'lastName', 'cantLoginUntil']; foreach ($returnFields as $field) { $results['user'][$field] = $userEnt->$field; } $untilFormatted = $dt->formatDatetimeIso(); $displayName = trim($userEnt->firstName . ' ' . $userEnt->lastName); if ($displayName === '') { $displayName = $userEnt->username; } $results['review_text'] = sprintf( 'You are about to block login for user %s (%s, ID %s) until %s.', $displayName, $userEnt->username, $userEnt->getId(), $untilFormatted ); $results['confirmation_phrase'] = sprintf('CONFIRM BLOCK USER LOGIN %s', $userEnt->getId()); return $results; } protected function executeFunction_block_user_login(string $arguments) : array { $args = json_decode($arguments, 1); $results = []; if ((int) ($args['user_id'] ?? 0) === $this->idUser) { $results['error'] = 'You cannot block your own login.'; return $results; } if (!$args['user_confirmation_text']) { $results['user_id'] = $args['user_id']; $results['error'] = 'Login was not blocked.'; return $results; } $dt = $this->parseLoginBlockedUntilDatetime($args['login_blocked_until'] ?? ''); if ($dt === null || (int) $dt->format('U') <= TIMESTAMP) { $results['error'] = 'Invalid login_blocked_until.'; return $results; } try { \DB::startTransaction(); $userUpd = \Entity\User::update($args['user_id']); $userUpd->cantLoginUntil = $dt->formatTime(); $userUpd->save(); \DB::commitTransaction(); $results['message'] = sprintf( 'Successfully blocked login for user ID %s until %s.', $args['user_id'], $dt->formatDatetimeIso() ); } catch (\App\Entity\Exception $ex) { \DB::rollbackTransaction(); $results['message'] = $ex->getMessagesCollector()->getMessageBulk(); } return $results; } protected function executeFunction_preview_user_login_unblock(string $arguments) : array { $args = json_decode($arguments, 1); $results = []; $userEnt = \Entity\User::select($args['user_id']); if (!$userEnt->recordExists()) { $results['error'] = 'User not found'; return $results; } $currentlyBlocked = $userEnt->cantLoginUntil > TIMESTAMP; $returnFields = ['id', 'username', 'firstName', 'lastName', 'cantLoginUntil']; foreach ($returnFields as $field) { $results['user'][$field] = $userEnt->$field; } if (!$currentlyBlocked) { $results['can_unblock'] = false; $results['error'] = 'User login is not blocked.'; return $results; } $entity = \Entity\User::update($args['user_id']); $msgs = new \App\MessagesCollector(); $results['can_unblock'] = $this->determineOperationAvailability($entity, $msgs); if ($msgs->hasErrorOrWarning()) { $results['error'] = $msgs->getMessageBulk(); return $results; } $blockedUntilTxt = date(DATETIME_FORMAT, $userEnt->cantLoginUntil); $displayName = trim($userEnt->firstName . ' ' . $userEnt->lastName); if ($displayName === '') { $displayName = $userEnt->username; } $results['review_text'] = sprintf( 'You are about to clear login block for user %s (%s, ID %s). Current block ends %s.', $displayName, $userEnt->username, $userEnt->getId(), $blockedUntilTxt ); $results['confirmation_phrase'] = sprintf('CONFIRM UNBLOCK USER LOGIN %s', $userEnt->getId()); return $results; } protected function executeFunction_unblock_user_login(string $arguments) : array { $args = json_decode($arguments, 1); $results = []; if (!$args['user_confirmation_text']) { $results['user_id'] = $args['user_id']; $results['error'] = 'Login block was not cleared.'; return $results; } try { \DB::startTransaction(); $userUpd = \Entity\User::update($args['user_id']); $userUpd->cantLoginUntil = \Engine\DateTime::create('')->formatTime(); $userUpd->save(); \DB::commitTransaction(); $results['message'] = sprintf('Successfully cleared login block for user ID %s.', $args['user_id']); } catch (\App\Entity\Exception $ex) { \DB::rollbackTransaction(); $results['message'] = $ex->getMessagesCollector()->getMessageBulk(); } return $results; } protected function normalizePersonTitleForUser(?string $title): ?string { if ($title === null || $title === '') { return null; } $CFG = \Engine\Loader_Cfg::getInstance()->getGlobal(); $allowed = $CFG['generalData']['personTitles']; $token = strtolower(trim($title)); $aliases = [ 'mr' => 'm', 'mrs' => 'f', 'miss' => 's', 'mstr' => 't', ]; $code = $aliases[$token] ?? null; if ($code !== null && in_array($code, $allowed, true)) { return $code; } if (strlen($token) === 1 && in_array($token, $allowed, true)) { return $token; } if (in_array($title, $allowed, true)) { return $title; } return null; } protected function createUserEntityFromDraft(array $fields): \Entity\User { $entityObj = \Entity\User::insert(); $entityObj->username = $fields['username']; $entityObj->webserviceAuth = false; $entityObj->idOffice = $this->idUser; $entityObj->idDepartment = $this->idDepartment; $entityObj->language = LANG; $titleCode = $this->normalizePersonTitleForUser($fields['title'] ?? null); if ($titleCode !== null) { $entityObj->title = $titleCode; } $entityObj->firstName = $fields['firstName']; $entityObj->lastName = $fields['lastName']; $entityObj->comments = (string)($fields['comments'] ?? ''); $entityObj->PNC = (string)($fields['PNC'] ?? ''); $entityObj->birthday = !empty($fields['birthday']) ? $fields['birthday'] : null; $entityObj->address = (string)($fields['address'] ?? ''); $entityObj->telephone = (string)($fields['telephone'] ?? ''); $entityObj->email = $fields['email']; $entityObj->save(); $defaultUserAccess = \App\UserAccess\ReferenceUserAccess::getInstance(); $accessOptionsList = []; foreach ($defaultUserAccess->getAllOptionsList() as $option) { if ($option['type'] === 'checkbox') { if ($option['value']) { $accessOptionsList[$option['id']] = $option['value']; } } elseif ($option['type'] === 'select') { $accessOptionsList[$option['id']] = $option['value']; } } $userEnt = \Entity\User::update($entityObj->getId()); $userEnt->accessSectionsList = []; $userEnt->accessOptionsList = $accessOptionsList; $userEnt->save(); return \Entity\User::select($entityObj->getId()); } protected function developerMessage_submit_user(string $arguments, array $response) : string { if (!empty($response['idUser'])) { return "User created with ID: ".$response['idUser'].". Allow the user to open `Users administration` to set password and review access: " . \Engine\Mvc\Dispatcher::getInstance()->getAppUrl('Modules/Admin/Users', 'index'); } return ''; } } 