diff --git a/api.raml b/api.raml index 94508a64..b3af7fba 100644 --- a/api.raml +++ b/api.raml @@ -460,6 +460,25 @@ types: description: A server-side error occurred. body: type: Error + /update-last-login: + put: + description: Update the last login timestamp of a user based on their employee ID. + responses: + 200: + description: Successfully updated the last login time. + body: + properties: + employee_id: string + last_login_utc: string + example: | + { + "employee_id": "12345", + "last_login_utc": "2024-09-18T12:34:56Z" + } + 404: + description: Not found. The user with the specified `employeeId` was not found. + 500: + description: Server error. Failed to update the last login time. /mfa: get: description: Get a list of configured MFA devices for user diff --git a/application/common/models/Authentication.php b/application/common/models/Authentication.php index 7b3ee98f..3e03fb01 100644 --- a/application/common/models/Authentication.php +++ b/application/common/models/Authentication.php @@ -2,8 +2,6 @@ namespace common\models; -use common\components\Emailer; -use common\helpers\MySqlDateTime; use yii\web\HttpException; /** @@ -91,7 +89,7 @@ protected function authenticateByInvite($invite) /** * Run User validation rules. If all rules pass, $this->authenticatedUser will be a * clone of the User, and the User record in the database will be updated with new - * login and reminder dates. + * reminder dates. * * @param User $user */ @@ -100,15 +98,13 @@ protected function validateUser(User $user) if ($user->validate()) { $this->authenticatedUser = clone $user; - $user->last_login_utc = MySqlDateTime::now(); - $user->updateProfileReviewDates(); $user->checkAndProcessHIBP(); if (!$user->save()) { \Yii::error([ - 'action' => 'save last_login_utc and nag dates for user after authentication', + 'action' => 'save nag dates for user after authentication', 'status' => 'error', 'message' => $user->getFirstErrors(), ]); diff --git a/application/common/models/User.php b/application/common/models/User.php index 1ad64f07..aa806d84 100644 --- a/application/common/models/User.php +++ b/application/common/models/User.php @@ -4,7 +4,6 @@ use Br33f\Ga4\MeasurementProtocol\Dto\Event\BaseEvent; use Closure; -use common\components\Emailer; use common\components\HIBP; use common\components\Sheets; use common\helpers\MySqlDateTime; @@ -13,7 +12,6 @@ use Ramsey\Uuid\Uuid; use Sil\EmailService\Client\EmailServiceClientException; use Sil\PhpArrayDotNotation\DotNotation; -use TheIconic\Tracking\GoogleAnalytics\Analytics; use Yii; use yii\behaviors\AttributeBehavior; use yii\data\ActiveDataProvider; @@ -803,6 +801,13 @@ public function save($runValidation = true, $attributeNames = null) return parent::save($runValidation, $attributeNames); } + public function updateLastLogin() + { + $this->last_login_utc = MySqlDateTime::now(); + + return $this->save(false, ['last_login_utc']); + } + /** * @param bool $isNewUser * @param array $changedAttributes diff --git a/application/features/authentication.feature b/application/features/authentication.feature index 7b1ac5b7..376e727b 100644 --- a/application/features/authentication.feature +++ b/application/features/authentication.feature @@ -51,8 +51,7 @@ Feature: Authentication | active | yes | | locked | no | And a record still exists with an employee_id of "123" -# TODO: check that last_login_utc was updated and current -# And none of the data has changed +# TODO: check that none of the data has changed Scenario: Attempt to authenticate an unknown user Given I provide the following valid data: diff --git a/application/features/bootstrap/FeatureContext.php b/application/features/bootstrap/FeatureContext.php index b4beece0..e9ad002e 100644 --- a/application/features/bootstrap/FeatureContext.php +++ b/application/features/bootstrap/FeatureContext.php @@ -452,9 +452,9 @@ public function shouldBeStoredAsNowUTC($property) $minAcceptable = $expectedNow - self::ACCEPTABLE_DELTA_IN_SECONDS; $maxAcceptable = $expectedNow + self::ACCEPTABLE_DELTA_IN_SECONDS; - $storedNow = strtotime($this->userFromDb->$property); + $storedNow = $this->userFromDb->$property ? strtotime($this->userFromDb->$property) : 0; - Assert::range($storedNow, $minAcceptable, $maxAcceptable); + Assert::range($storedNow, $minAcceptable, $maxAcceptable, "Stored time $storedNow is not within acceptable range of $minAcceptable to $maxAcceptable"); } /** @@ -490,8 +490,8 @@ public function theOnlyPropertyToChangeShouldBe($property) $previous = $this->userFromDbBefore->$name; $current = $this->userFromDb->$name; - $name === $property ? Assert::notEq($current, $previous) - : Assert::eq($current, $previous); + $name === $property ? Assert::notEq($current, $previous, "$property is equal. Previous: $previous, Current: $current") + : Assert::eq($current, $previous, "$property is not equal. Previous: $previous, Current: $current"); } } diff --git a/application/features/user.feature b/application/features/user.feature index fd84d02e..8db1293f 100644 --- a/application/features/user.feature +++ b/application/features/user.feature @@ -135,6 +135,16 @@ Feature: User | hide | yes | | groups | mensa,hackers | + Scenario: update the last_login_utc of an existing user + Given the requester is authorized + And I add a user with an employee_id of "123" + And a record exists with an employee_id of "123" + When I request "/user/123/update-last-login" be updated + Then the response status code should be "200" + And a record exists with an employee_id of "123" + And last_login_utc should be stored as now UTC + And the only property to change should be last_login_utc + Scenario: Deactivate an existing user Given a record does not exist with an employee_id of "123" And the requester is authorized @@ -771,4 +781,4 @@ Feature: User Examples: | active | isOrIsNot | | no | is NOT | - | yes | is | \ No newline at end of file + | yes | is | diff --git a/application/frontend/config/main.php b/application/frontend/config/main.php index 13818255..b10e8bc1 100644 --- a/application/frontend/config/main.php +++ b/application/frontend/config/main.php @@ -45,12 +45,13 @@ /* * User routes */ - 'GET user' => 'user/index', - 'GET user/' => 'user/view', - 'POST user' => 'user/create', - 'PUT user/' => 'user/update', - 'PUT user//password' => 'user/update-password', - 'PUT user//password/assess' => 'user/assess-password', + 'GET user' => 'user/index', + 'GET user/' => 'user/view', + 'POST user' => 'user/create', + 'PUT user/' => 'user/update', + 'PUT user//update-last-login' => 'user/update-last-login', + 'PUT user//password' => 'user/update-password', + 'PUT user//password/assess' => 'user/assess-password', /* * Authentication routes diff --git a/application/frontend/controllers/UserController.php b/application/frontend/controllers/UserController.php index 696a4808..7dd4e5a9 100644 --- a/application/frontend/controllers/UserController.php +++ b/application/frontend/controllers/UserController.php @@ -75,6 +75,24 @@ public function actionUpdate(string $employeeId) return $user; } + public function actionUpdateLastLogin(string $employeeId) + { + $user = User::findOne(condition: ['employee_id' => $employeeId]); + + if ($user === null) { + Yii::$app->response->statusCode = 404; + + return null; + } + + if (!$user->updateLastLogin()) { + Yii::$app->response->statusCode = 500; + return null; + } + + return ['employee_id' => $user->employee_id, 'last_login_utc' => $user->last_login_utc]; + } + public function actionUpdatePassword(string $employeeId) { $user = User::findOne(['employee_id' => $employeeId]);