Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TM-1385] Admin create user funtionality #590

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading