360 lines
11 KiB
PHP
360 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Controllers\View;
|
||
|
||
use CodeIgniter\HTTP\ResponseInterface;
|
||
use CodeIgniter\RESTful\ResourceController;
|
||
use App\Models\UserModel;
|
||
use App\Models\AuthorizedUserModel;
|
||
use CodeIgniter\I18n\Time;
|
||
|
||
class AuthorizedUsersController extends ResourceController
|
||
{
|
||
private const TOKEN_TTL_HOURS = 24;
|
||
|
||
protected $userModel;
|
||
protected $authorizedUserModel;
|
||
|
||
public function __construct(private \CodeIgniter\HTTP\IncomingRequest $request)
|
||
{
|
||
$this->userModel = new UserModel();
|
||
$this->authorizedUserModel = new AuthorizedUserModel();
|
||
}
|
||
|
||
private function requireLogin()
|
||
{
|
||
if (!session()->get('is_logged_in')) {
|
||
return $this->failUnauthorized('Authentication required.');
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function requireOwnership(array $authorizedUser)
|
||
{
|
||
$userId = (int) session()->get('user_id');
|
||
if ($userId <= 0 || (int) ($authorizedUser['user_id'] ?? 0) !== $userId) {
|
||
return $this->failForbidden('You do not have access to this resource.');
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function hashToken(string $token): string
|
||
{
|
||
return hash('sha256', $token);
|
||
}
|
||
/**
|
||
* Return a list of authorized users for the logged-in main user.
|
||
*
|
||
* @return ResponseInterface
|
||
*/
|
||
public function index()
|
||
{
|
||
if ($resp = $this->requireLogin()) {
|
||
return $resp;
|
||
}
|
||
|
||
$userId = session()->get('user_id');
|
||
$authorizedUsers = $this->authorizedUserModel->where('user_id', $userId)->findAll();
|
||
|
||
return $this->respond($authorizedUsers);
|
||
}
|
||
|
||
/**
|
||
* Show a specific authorized user by ID.
|
||
*
|
||
* @param int|string|null $id
|
||
* @return ResponseInterface
|
||
*/
|
||
public function show($id = null)
|
||
{
|
||
if ($resp = $this->requireLogin()) {
|
||
return $resp;
|
||
}
|
||
|
||
$authorizedUser = $this->authorizedUserModel->find($id);
|
||
|
||
if (!$authorizedUser) {
|
||
return $this->failNotFound('Authorized user not found.');
|
||
}
|
||
|
||
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||
return $resp;
|
||
}
|
||
|
||
return $this->respond($authorizedUser);
|
||
}
|
||
|
||
/**
|
||
* Create a new authorized user (add by email).
|
||
*
|
||
* @return ResponseInterface
|
||
*/
|
||
public function create()
|
||
{
|
||
if ($resp = $this->requireLogin()) {
|
||
return $resp;
|
||
}
|
||
|
||
$email = strtolower($this->request->getPost('email'));
|
||
|
||
// Validate email
|
||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||
return $this->failValidationErrors('Invalid email address.');
|
||
}
|
||
|
||
$user = $this->userModel->where('email', $email)->first();
|
||
|
||
if (!$user) {
|
||
return $this->respondCreated(['message' => 'Authorized user added. A confirmation email has been sent.']);
|
||
}
|
||
|
||
// Generate a token for confirmation
|
||
helper('text');
|
||
$token = bin2hex(random_bytes(48));
|
||
$tokenHash = $this->hashToken($token);
|
||
|
||
// Add entry to the authorized_users table
|
||
$this->authorizedUserModel->insert([
|
||
'user_id' => session()->get('user_id'), // Main user ID
|
||
'authorized_user_id' => $user['id'],
|
||
'email' => $email,
|
||
'token' => $tokenHash,
|
||
'status' => 'Pending'
|
||
]);
|
||
|
||
// Send confirmation email to the authorized user
|
||
$this->sendAuthorizedUserConfirmationEmail($email, $token);
|
||
|
||
return $this->respondCreated(['message' => 'Authorized user added. A confirmation email has been sent.']);
|
||
}
|
||
|
||
/**
|
||
* Update an existing authorized user.
|
||
*
|
||
* @param int|string|null $id
|
||
* @return ResponseInterface
|
||
*/
|
||
public function update($id = null)
|
||
{
|
||
if ($resp = $this->requireLogin()) {
|
||
return $resp;
|
||
}
|
||
|
||
// Fetch the authorized user
|
||
$authorizedUser = $this->authorizedUserModel->find($id);
|
||
|
||
if (!$authorizedUser) {
|
||
return $this->failNotFound('Authorized user not found.');
|
||
}
|
||
|
||
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||
return $resp;
|
||
}
|
||
|
||
// Update the authorized user’s information (e.g., email)
|
||
$email = strtolower($this->request->getPost('email'));
|
||
if ($email && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||
$authorizedUser['email'] = $email;
|
||
}
|
||
|
||
$this->authorizedUserModel->save($authorizedUser);
|
||
|
||
return $this->respondUpdated(['message' => 'Authorized user information updated.']);
|
||
}
|
||
|
||
/**
|
||
* Delete an authorized user.
|
||
*
|
||
* @param int|string|null $id
|
||
* @return ResponseInterface
|
||
*/
|
||
public function delete($id = null)
|
||
{
|
||
if ($resp = $this->requireLogin()) {
|
||
return $resp;
|
||
}
|
||
|
||
$authorizedUser = $this->authorizedUserModel->find($id);
|
||
|
||
if (!$authorizedUser) {
|
||
return $this->failNotFound('Authorized user not found.');
|
||
}
|
||
|
||
if ($resp = $this->requireOwnership($authorizedUser)) {
|
||
return $resp;
|
||
}
|
||
|
||
// Delete the authorized user record
|
||
$this->authorizedUserModel->delete($id);
|
||
|
||
return $this->respondDeleted(['message' => 'Authorized user deleted successfully.']);
|
||
}
|
||
|
||
/**
|
||
* Confirms the authorized user's token and allows them to set a password.
|
||
*
|
||
* @return ResponseInterface
|
||
*/
|
||
public function confirm()
|
||
{
|
||
$token = $this->request->getGet('token');
|
||
|
||
if (!$token) {
|
||
return $this->fail('Invalid confirmation link.');
|
||
}
|
||
|
||
$tokenHash = $this->hashToken($token);
|
||
$authorizedUser = $this->authorizedUserModel
|
||
->groupStart()
|
||
->where('token', $tokenHash)
|
||
->orWhere('token', $token)
|
||
->groupEnd()
|
||
->where('created_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString())
|
||
->first();
|
||
|
||
if (!$authorizedUser) {
|
||
return $this->fail('Invalid or expired confirmation link.');
|
||
}
|
||
|
||
// Mark the authorized user as active and rotate token for password setup
|
||
$nextToken = bin2hex(random_bytes(48));
|
||
$nextTokenHash = $this->hashToken($nextToken);
|
||
$this->authorizedUserModel->update($authorizedUser['id'], [
|
||
'status' => 'Active',
|
||
'token' => $nextTokenHash,
|
||
]);
|
||
|
||
return redirect()->to('/set_authorized_user_password/' . $authorizedUser['authorized_user_id'] . '?token=' . $nextToken);
|
||
}
|
||
|
||
/**
|
||
* Displays the form for the authorized user to set their password.
|
||
*
|
||
* @param int $authorizedUserId
|
||
* @return ResponseInterface
|
||
*/
|
||
public function setPassword($authorizedUserId)
|
||
{
|
||
$token = (string) $this->request->getGet('token');
|
||
if ($token === '') {
|
||
return $this->fail('Invalid confirmation link.');
|
||
}
|
||
|
||
$tokenHash = $this->hashToken($token);
|
||
$authorizedUser = $this->authorizedUserModel
|
||
->groupStart()
|
||
->where('token', $tokenHash)
|
||
->orWhere('token', $token)
|
||
->groupEnd()
|
||
->where('authorized_user_id', $authorizedUserId)
|
||
->where('status', 'Active')
|
||
->where('updated_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString())
|
||
->first();
|
||
|
||
if (!$authorizedUser) {
|
||
return $this->fail('Invalid or expired confirmation link.');
|
||
}
|
||
|
||
$user = $this->userModel->find($authorizedUserId);
|
||
|
||
if (!$user) {
|
||
return $this->failNotFound('User not found.');
|
||
}
|
||
|
||
return view('user/set_authorized_user_password', [
|
||
'userId' => $authorizedUserId,
|
||
'token' => $token,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Processes the password for the authorized user.
|
||
*
|
||
* @return ResponseInterface
|
||
*/
|
||
public function savePassword($authorizedUserId = null)
|
||
{
|
||
$validation = \Config\Services::validation();
|
||
$validation->setRules([
|
||
'password' => [
|
||
'label' => 'Password',
|
||
'rules' => 'required|min_length[8]|regex_match[/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\\-=\\+*#$%&!?])[A-Za-z\\d@\\-=\\+*#$%&!?]{8,}$/]',
|
||
],
|
||
'password_confirm' => 'required|matches[password]',
|
||
'user_id' => 'required|integer',
|
||
'token' => 'required',
|
||
]);
|
||
|
||
if (!$this->validate($validation->getRules())) {
|
||
return $this->failValidationErrors($validation->getErrors());
|
||
}
|
||
|
||
$userId = (int) $this->request->getPost('user_id');
|
||
$token = (string) $this->request->getPost('token');
|
||
$authorizedUserId = $authorizedUserId !== null ? (int) $authorizedUserId : $userId;
|
||
|
||
if ($userId <= 0 || $authorizedUserId <= 0 || $userId !== $authorizedUserId) {
|
||
return $this->fail('Invalid request.');
|
||
}
|
||
|
||
$tokenHash = $this->hashToken($token);
|
||
$authorizedUser = $this->authorizedUserModel
|
||
->groupStart()
|
||
->where('token', $tokenHash)
|
||
->orWhere('token', $token)
|
||
->groupEnd()
|
||
->where('authorized_user_id', $authorizedUserId)
|
||
->where('status', 'Active')
|
||
->where('updated_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString())
|
||
->first();
|
||
|
||
if (!$authorizedUser) {
|
||
return $this->fail('Invalid or expired confirmation link.');
|
||
}
|
||
|
||
$user = $this->userModel->find($authorizedUserId);
|
||
if (!$user) {
|
||
return $this->failNotFound('User not found.');
|
||
}
|
||
|
||
$password = (string) $this->request->getPost('password');
|
||
$hashedPassword = pbkdf2_hash($password);
|
||
|
||
$this->userModel->update($authorizedUserId, ['password' => $hashedPassword]);
|
||
$this->authorizedUserModel->update($authorizedUser['id'], ['token' => null]);
|
||
|
||
return $this->respond(['message' => 'Password has been successfully set.']);
|
||
}
|
||
/**
|
||
* Sends a confirmation email to the authorized user.
|
||
*
|
||
* @param string $email
|
||
* @param string $token
|
||
*/
|
||
private function sendAuthorizedUserConfirmationEmail($email, $token)
|
||
{
|
||
// Generate the confirmation link
|
||
$confirmLink = site_url('/confirm_authorized_user?token=' . $token);
|
||
|
||
// Compose the email message
|
||
$message = "
|
||
<p>You have been added as an authorized user for another account. Click the link below to confirm your access:</p>
|
||
<p><a href='{$confirmLink}'>Confirm Access</a></p>
|
||
<p>If you did not request this, please ignore this email.</p>
|
||
";
|
||
|
||
// Create an instance of the EmailController
|
||
$emailController = new \App\Controllers\View\EmailController();
|
||
$subject = 'Authorized User Confirmation';
|
||
|
||
// Send email
|
||
if ($emailController->sendEmail($email, $subject, $message)) {
|
||
log_message('info', 'Authorized user confirmation email sent to ' . $email);
|
||
} else {
|
||
log_message('error', 'Failed to send authorized user confirmation email to ' . $email);
|
||
}
|
||
}
|
||
}
|