Skip to content

Commit

Permalink
[TM-1385] Admin create user funtionality (#590)
Browse files Browse the repository at this point in the history
* [TM-1385] start up admin create user controller

* [TM-1385] start up send login details controller

* [TM-1385] Start up endpoint to set new password

* [TM-1385] update operationId

* [TM-1385] update path index
  • Loading branch information
cesarLima1 authored Nov 26, 2024
1 parent a3e3a81 commit 9bb6341
Show file tree
Hide file tree
Showing 17 changed files with 688 additions and 0 deletions.
72 changes: 72 additions & 0 deletions app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
use App\Http\Requests\ResendByEmailRequest;
use App\Http\Requests\ResendRequest;
use App\Http\Requests\ResetRequest;
use App\Http\Requests\SendLoginDetailsRequest;
use App\Http\Requests\SetPasswordRequest;
use App\Http\Requests\VerifyRequest;
use App\Http\Resources\V2\User\MeResource;
use App\Jobs\ResetPasswordJob;
use App\Jobs\SendLoginDetailsJob;
use App\Jobs\UserVerificationJob;
use App\Models\PasswordReset as PasswordResetModel;
use App\Models\V2\Projects\ProjectInvite;
Expand All @@ -24,6 +27,7 @@
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

Expand Down Expand Up @@ -160,6 +164,74 @@ public function resetAction(ResetRequest $request): JsonResponse
return JsonResponseHelper::success((object) [], 200);
}

public function sendLoginDetailsAction(SendLoginDetailsRequest $request): JsonResponse
{
$this->authorize('reset', 'App\\Models\\Auth');
$data = $request->json()->all();

try {
$user = UserModel::where('email_address', '=', $data['email_address'])
->whereNull('password')
->firstOrFail();
} catch (Exception $exception) {
return JsonResponseHelper::success((object) [], 200);
}

SendLoginDetailsJob::dispatch($user, isset($data['callback_url']) ? $data['callback_url'] : null);

return JsonResponseHelper::success((object) [], 200);
}

public function getEmailByResetTokenAction(Request $request): JsonResponse
{
$data = $request->query();

$passwordReset = PasswordResetModel::where('token', '=', $data['token'])->first();

if (! $passwordReset) {
return JsonResponseHelper::success((object) [
'email_address' => null,
'token_used' => true,
], 200);
}
if (Carbon::parse($passwordReset->created_at)->addDays(7)->isPast()) {
$passwordReset->delete();

return JsonResponseHelper::success((object) [
'email_address' => null,
'token_used' => true,
], 200);
}

$user = UserModel::findOrFail($passwordReset->user_id);

return JsonResponseHelper::success((object) [
'email_address' => $user->email_address,
'token_used' => false,
], 200);
}

public function setNewPasswordAction(SetPasswordRequest $request): JsonResponse
{
$this->authorize('change', 'App\\Models\\Auth');
$data = $request->json()->all();
$passwordReset = PasswordResetModel::where('token', '=', $data['token'])->firstOrFail();
$user = UserModel::findOrFail($passwordReset->user_id);
if (Hash::check($data['password'], $user->password)) {
throw new SamePasswordException();
}
$user->password = $data['password'];

if (empty($user->email_address_verified_at)) {
$user->email_address_verified_at = new DateTime('now', new DateTimeZone('UTC'));
}

$user->saveOrFail();
$passwordReset->delete();

return JsonResponseHelper::success((object) [], 200);
}

public function changeAction(ChangePasswordRequest $request): JsonResponse
{
$this->authorize('change', 'App\\Models\\Auth');
Expand Down
68 changes: 68 additions & 0 deletions app/Http/Controllers/V2/User/AdminUserCreationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace App\Http\Controllers\V2\User;

use App\Helpers\JsonResponseHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\V2\User\AdminUserCreationRequest;
use App\Http\Resources\V2\User\UserResource;
use App\Models\Framework;
use App\Models\V2\Organisation;
use App\Models\V2\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class AdminUserCreationController extends Controller
{
/**
* Create a new user from the admin panel
*/
public function store(AdminUserCreationRequest $request): JsonResponse
{
$this->authorize('create', User::class);

try {
return DB::transaction(function () use ($request) {
$validatedData = $request->validated();

$user = new User($validatedData);
$user->save();

$user->email_address_verified_at = $user->created_at;
$user->save();

$role = $validatedData['role'];
$user->syncRoles([$role]);

if (! empty($validatedData['organisation'])) {
$organisation = Organisation::isUuid($validatedData['organisation'])->first();
if ($organisation) {
$organisation->partners()->updateExistingPivot($user, ['status' => 'approved'], false);
$user->organisation_id = $organisation->id;
$user->save();
}
}

if (! empty($validatedData['direct_frameworks'])) {
$frameworkIds = Framework::whereIn('slug', $validatedData['direct_frameworks'])
->pluck('id')
->toArray();
$user->frameworks()->sync($frameworkIds);
}

return JsonResponseHelper::success(new UserResource($user), 201);
});
} catch (\Exception $e) {
Log::error('User creation failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

return JsonResponseHelper::error([
'message' => 'Failed to create user',
'details' => $e->getMessage(),
], 500);
}
}
}
25 changes: 25 additions & 0 deletions app/Http/Requests/SendLoginDetailsRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class SendLoginDetailsRequest extends FormRequest
{
public function rules()
{
return [
'email_address' => [
'required',
'string',
'email',
],
'callback_url' => [
'sometimes',
'string',
'url',
'max:5000',
],
];
}
}
24 changes: 24 additions & 0 deletions app/Http/Requests/SetPasswordRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;

class SetPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules()
{
return [
'token' => [
'required',
'string',
'exists:password_resets,token',
],
'password' => ['required', 'string', Password::min(8)->mixedCase()->numbers()],
];
}
}
62 changes: 62 additions & 0 deletions app/Http/Requests/V2/User/AdminUserCreationRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace App\Http\Requests\V2\User;

use Illuminate\Foundation\Http\FormRequest;

class AdminUserCreationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request
*/
public function authorize(): bool
{
return true;
}

public function rules(): array
{
return [
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email_address' => [
'required',
'string',
'email',
'max:255',
'unique:users,email_address',
],
'role' => 'required|string',
'job_role' => 'sometimes|nullable|string|max:255',
'country' => 'sometimes|nullable|string|max:2',
'phone_number' => 'sometimes|nullable|string|max:20',
'program' => 'sometimes|nullable|string|max:255',
'organisation' => [
'sometimes',
'nullable',
'array',
function ($attribute, $value, $fail) {
if (! empty($value) && empty($value['uuid'])) {
$fail('The organisation must contain a uuid.');
}
},
],
'monitoring_organisations' => 'sometimes|array',
'monitoring_organisations.*' => 'uuid|exists:organisations,uuid',
'direct_frameworks' => 'sometimes|array',
'direct_frameworks.*' => 'string|exists:frameworks,slug',
];
}

public function messages(): array
{
return [
'email_address.unique' => 'This email address is already in use.',
'role.in' => 'Invalid role selected.',
'organisation.uuid' => 'Invalid organisation identifier.',
'organisation.exists' => 'Organisation not found.',
'country.max' => 'Country code must be 2 characters long.',
'phone_number.max' => 'Phone number cannot exceed 20 characters.',
];
}
}
54 changes: 54 additions & 0 deletions app/Jobs/SendLoginDetailsJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Jobs;

use App\Mail\SendLoginDetails;
use App\Models\PasswordReset as PasswordResetModel;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

class SendLoginDetailsJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

private $model;

private $callbackUrl;

public function __construct(Model $model, ?string $callbackUrl)
{
$this->model = $model;
$this->callbackUrl = $callbackUrl;
}

public function handle()
{
try {
if (get_class($this->model) !== \App\Models\V2\User::class) {
throw new Exception('Invalid model type');
}

$passwordReset = new PasswordResetModel();
$passwordReset->user_id = $this->model->id;
$passwordReset->token = Str::random(32);
$passwordReset->saveOrFail();
Mail::to($this->model->email_address)
->send(new SendLoginDetails($passwordReset->token, $this->callbackUrl, $this->model));
} catch (\Throwable $e) {
Log::error('Job failed', ['error' => $e->getMessage()]);

throw $e;
}
}
}
25 changes: 25 additions & 0 deletions app/Mail/SendLoginDetails.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Mail;

class SendLoginDetails extends I18nMail
{
public function __construct(String $token, string $callbackUrl = null, $user)
{
parent::__construct($user);
$this->setSubjectKey('send-login-details.subject')
->setTitleKey('send-login-details.title')
->setBodyKey('send-login-details.body')
->setParams([
'{userName}' => e($user->first_name . ' ' . $user->last_name),
'{mail}' => e($user->email_address),
])
->setCta('send-login-details.cta');

$this->link = $callbackUrl ?
$callbackUrl . urlencode($token) :
'/set-password?token=' . urlencode($token);

$this->transactional = true;
}
}
14 changes: 14 additions & 0 deletions database/seeders/LocalizationKeysTableSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ public function run(): void
'If you have any questions, feel free to message us at [email protected].');
$this->createLocalizationKey('reset-password.cta', 'Reset Password');

// send-login-details
$this->createLocalizationKey('send-login-details.subject', 'Welcome to TerraMatch!');
$this->createLocalizationKey('send-login-details.title', 'Welcome to TerraMatch 🌱 !');
$this->createLocalizationKey('send-login-details.body', 'Hi {userName},<br><br>' .
'We\'re thrilled to let you know that your access to TerraMatch is now active!<br><br>' .
'Your user email used for your account is {mail}<br><br>' .
'Please click on the button below to set your new password. This link is valid for 7 days from the day you received this email.<br><br>' .
'If you have any questions or require assistance, our support team is ready to help at [email protected] or +44 7456 289369 (WhatsApp only).<br><br>'.
'We look forward to working with you!<br><br>' .
'<br><br>' .
'Best regards,<br><br>' .
'TerraMatch Support');
$this->createLocalizationKey('send-login-details.cta', 'Set Password');

// satellite-map-created
$this->createLocalizationKey('satellite-map-created.subject', 'Remote Sensing Map Received');
$this->createLocalizationKey('satellite-map-created.title', 'Remote Sensing Map Received');
Expand Down
Loading

0 comments on commit 9bb6341

Please sign in to comment.