diff --git a/app/Http/Controllers/Api/V1/Admin/EmailTemplateController.php b/app/Http/Controllers/Api/V1/Admin/EmailTemplateController.php index 4d339676..da2a7d9e 100644 --- a/app/Http/Controllers/Api/V1/Admin/EmailTemplateController.php +++ b/app/Http/Controllers/Api/V1/Admin/EmailTemplateController.php @@ -100,7 +100,7 @@ public function store(Request $request) // Validate request data $validatedData = $request->validate([ - 'title' => 'required|string|max:255', + 'title' => 'required|string|max:255|unique:email_templates,title', 'template' => 'required|string', 'status' => 'required|boolean' ]); diff --git a/app/Http/Controllers/Api/V1/Auth/AuthController.php b/app/Http/Controllers/Api/V1/Auth/AuthController.php index 6097eb48..93c94032 100755 --- a/app/Http/Controllers/Api/V1/Auth/AuthController.php +++ b/app/Http/Controllers/Api/V1/Auth/AuthController.php @@ -4,6 +4,8 @@ use App\Http\Controllers\Controller; use App\Http\Requests\StoreUserRequest; +use App\Http\Resources\UserResource; +use App\Models\EmailTemplate; use Illuminate\Http\Request; use App\Traits\HttpResponses; use Illuminate\Http\Response; @@ -13,8 +15,8 @@ use App\Models\User; use App\Models\Organisation; use App\Models\OrganisationUser; +use App\Services\OrganisationService; use Illuminate\Support\Facades\Log; -use App\Models\Validators\AuthValidator; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rules\Password; @@ -22,6 +24,28 @@ class AuthController extends Controller { use HttpResponses; + public function __construct(public OrganisationService $organisationService) + { + } + /** + * Display a listing of the resource. + */ + public function index() + { + // + } + + /** + * Show the form for creating a new resource. + */ + public function register() + { + // + } + + /** + * Store a newly created resource in storage. + */ public function store(Request $request) { $validator = Validator::make($request->all(), [ @@ -33,61 +57,61 @@ public function store(Request $request) ]); if ($validator->fails()) { - return $this->apiResponse(message: $validator->errors(), status_code: 400); + return $this->validationErrorResponseAlign($validator->errors()); } try { DB::beginTransaction(); $user = User::create([ - 'name' => $request->first_name . ' ' . $request->last_name, 'email' => $request->email, 'password' => Hash::make($request->password), 'role' => 'user' ]); - $profile = $user->profile()->create([ + $user->profile()->create([ 'first_name' => $request->first_name, 'last_name' => $request->last_name ]); - $organisations = []; + $name = $request->first_name."'s Organisation"; + $organisation = $this->organisationService->create($user, $name); - if ($request->invite_token) { - // Handle invite logic here - // For now, we'll create a default org - $organization = $this->createDefaultOrganization($user); - $organisations[] = $this->formatOrganisation($organization, 'admin', true); - } else { - $organization = $this->createDefaultOrganization($user); - $organisations[] = $this->formatOrganisation($organization, 'admin', true); - } + $roles = $user->roles()->create([ + 'name' => 'admin', + 'org_id' => $organisation->org_id + ]); + DB::table('users_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roles->id + ]); + // Generate JWT token $token = JWTAuth::fromUser($user); DB::commit(); + $email_template_id = null; + + $emailTemplate = EmailTemplate::where('title', 'welcome-email')->first();; + if ($emailTemplate) { + $email_template_id = $emailTemplate->id; + } + return response()->json([ - 'status_code' => 201, - 'message' => 'User Created Successfully', + 'status_code' => 201, + "message" => "User Created Successfully", + 'email_template_id' => $email_template_id, 'access_token' => $token, 'data' => [ - 'user' => [ - 'id' => $user->id, - 'first_name' => $request->first_name, - 'last_name' => $request->last_name, - 'avatar_url' => $user->profile->avatar_url, - 'email' => $user->email, - 'is_superadmin' => false, - 'role' => $user->role - ], - 'organisations' => $organisations + 'user' => new UserResource($user->load('owned_organisations', 'profile')) ], ], 201); } catch (\Exception $e) { DB::rollBack(); - return $this->apiResponse('Registration unsuccessful', Response::HTTP_BAD_REQUEST); + Log::error($e); + return $this->ap('Registration unsuccessful', Response::HTTP_BAD_REQUEST); } } diff --git a/app/Http/Controllers/Api/V1/Auth/LoginController.php b/app/Http/Controllers/Api/V1/Auth/LoginController.php index c12918bb..acc6b537 100755 --- a/app/Http/Controllers/Api/V1/Auth/LoginController.php +++ b/app/Http/Controllers/Api/V1/Auth/LoginController.php @@ -3,8 +3,10 @@ namespace App\Http\Controllers\Api\V1\Auth; use App\Http\Controllers\Controller; +use App\Http\Resources\UserResource; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\RateLimiter; use Tymon\JWTAuth\Facades\JWTAuth; use Illuminate\Support\Facades\Validator; @@ -20,49 +22,32 @@ public function login(Request $request) if ($validator->fails()) { return response()->json([ - 'status_code' => 400, - 'message' => 'Validation failed', - 'data' => $validator->errors() - ], 400); + 'status_code' => 401, + 'message' => 'Invalid Credentials', + 'error' => 'Invalid Email or Password' + ], 401); } - $credentials = $request->only('email', 'password'); - - if (!$token = JWTAuth::attempt($credentials)) { + if (!$token = JWTAuth::attempt($request->only('email', 'password'))) { + $key = 'login_attempts_'.request()->ip(); + RateLimiter::hit($key,3600); return response()->json([ 'status_code' => 401, 'message' => 'Invalid credentials', - 'data' => [] + 'error' => 'Invalid Email or Password' ], 401); } $user = Auth::user(); - $profile = $user->profile; - - $organisations = $user->organisations->map(function ($org) use ($user) { - return [ - 'organisation_id' => $org->org_id, - 'name' => $org->name, - 'role' => $org->pivot->role ?? null, - 'is_owner' => $org->pivot->user_id === $user->id - ]; - }); + $user->last_login_at = now(); + $user->save(); return response()->json([ 'status_code' => 200, 'message' => 'Login successful', 'access_token' => $token, 'data' => [ - 'user' => [ - 'id' => $user->id, - 'first_name' => $profile->first_name ?? null, - 'last_name' => $profile->last_name ?? null, - 'email' => $user->email, - 'avatar_url' => $profile->avatar_url ?? null, - 'is_superadmin' => $user->role === 'superadmin', - 'role' => $user->role - ], - 'organisations' => $organisations + 'user' => new UserResource($user->load('profile', 'owned_organisations')) ] ], 200); } diff --git a/app/Http/Controllers/Api/V1/User/UserController.php b/app/Http/Controllers/Api/V1/User/UserController.php index 15fb5bf2..f8934ca1 100755 --- a/app/Http/Controllers/Api/V1/User/UserController.php +++ b/app/Http/Controllers/Api/V1/User/UserController.php @@ -36,17 +36,25 @@ public function stats() */ public function index() { - $users = User::paginate(); - - return response()->json( - [ - "status_code" => 200, - "message" => "Users returned successfully", - "data" => $users + $users = User::paginate(15); + + return response()->json([ + 'status_code' => 200, + 'message' => 'Users retrieved successfully', + 'status' => 'success', + 'data' => [ + 'users' => $users->items(), + 'pagination' => [ + 'current_page' => $users->currentPage(), + 'per_page' => $users->perPage(), + 'total' => $users->total(), + 'last_page' => $users->lastPage(), ], - 200 - ); + ] + ]); } + + /** @@ -56,14 +64,69 @@ public function store(Request $request) { // } - - /** - * Display the specified resource. - */ public function show(User $user) { - return $user->load('profile', 'products', 'organisations'); + // Load the necessary relationships + $user->load('profile', 'products', 'organisations'); + + // Format the response data + $response = [ + 'status_code' => 200, + 'user' => [ + 'id' => $user->id, + 'created_at' => $user->created_at, + 'updated_at' => $user->updated_at, + 'first_name' => $user->profile->first_name ?? '', + 'last_name' => $user->profile->last_name ?? '', + 'email' => $user->email, + 'status' => null, + 'phone' => $user->phone, + 'is_active' => $user->is_active, + 'backup_codes' => null, + 'attempts_left' => null, + 'time_left' => null, + 'secret' => null, + 'is_2fa_enabled' => false, + 'deletedAt' => $user->deleted_at, + 'profile' => [ + 'id' => $user->profile->id ?? null, + 'created_at' => $user->profile->created_at ?? null, + 'updated_at' => $user->profile->updated_at ?? null, + 'username' => '', + 'jobTitle' => $user->profile->job_title ?? null, + 'pronouns' => $user->profile->pronoun ?? null, + 'department' => null, + 'email' => $user->email, + 'bio' => $user->profile->bio ?? null, + 'social_links' => null, + 'language' => null, + 'region' => null, + 'timezones' => null, + 'profile_pic_url' => $user->profile->avatar_url ?? null, + 'deletedAt' => $user->profile->deleted_at ?? null + ], + 'owned_organisations' => $user->organisations->map(function ($organisation) { + return [ + 'id' => $organisation->id, + 'created_at' => $organisation->created_at, + 'updated_at' => $organisation->updated_at, + 'name' => $organisation->name, + 'description' => $organisation->description, + 'email' => $organisation->email, + 'industry' => $organisation->industry, + 'type' => $organisation->type, + 'country' => $organisation->country, + 'address' => $organisation->address, + 'state' => $organisation->state, + 'isDeleted' => $organisation->deleted_at ? true : false + ]; + }) + ] + ]; + + return response()->json($response); } + /** * Update the specified resource in storage. @@ -133,16 +196,32 @@ public function update(Request $request, string $id) public function destroy(string $id) { $user = User::find($id); - + if (!$user) { return response()->json([ 'status_code' => 404, 'message' => 'User not found' ], 404); } - + + $authUser = auth()->user(); + + if ($authUser->id !== $user->id) { + if (!in_array($authUser->role, ['superAdmin', 'admin'])) { + return response()->json([ + 'status_code' => 403, + 'message' => 'Unauthorized to delete this user' + ], 403); + } + } + $user->delete(); - return response()->noContent(); + + return response()->json([ + 'status_code' => 200, + 'message' => 'User deleted successfully' + ], 200); } + } diff --git a/app/Http/Resources/OrganisationResource.php b/app/Http/Resources/OrganisationResource.php index bd70f7aa..bba93e4b 100755 --- a/app/Http/Resources/OrganisationResource.php +++ b/app/Http/Resources/OrganisationResource.php @@ -10,17 +10,18 @@ class OrganisationResource extends JsonResource public function toArray(Request $request): array { return [ - 'org_id' => $this->org_id, + 'organisation_id' => $this->org_id, 'name' => $this->name, 'email' => $this->email, 'description' => $this->description, + 'is_owner' => true, + 'role' => 'Admin', 'industry' => $this->industry, 'type' => $this->type, 'country' => $this->country, 'address' => $this->address, 'state' => $this->state, 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, ]; } } diff --git a/app/Http/Resources/OrganisationResourceCollection.php b/app/Http/Resources/OrganisationResourceCollection.php new file mode 100644 index 00000000..df168752 --- /dev/null +++ b/app/Http/Resources/OrganisationResourceCollection.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 00000000..4283915b --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'first_name' => $this->profile->first_name, + 'last_name' => $this->profile->last_name, + 'email' => $this->email, + 'avatar_url' => $this->avatar_url, + 'created_at' => $this->created_at, + 'is_superadmin' => $this->role == 'admin' ? true : false, + 'organisations' => OrganisationResource::collection($this->whenLoaded('owned_organisations')), + ]; + } +} diff --git a/app/Models/EmailTemplate.php b/app/Models/EmailTemplate.php index 1d90e070..5c7874f6 100644 --- a/app/Models/EmailTemplate.php +++ b/app/Models/EmailTemplate.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; class EmailTemplate extends Model { @@ -16,6 +17,15 @@ class EmailTemplate extends Model 'status', ]; + protected static function boot() +{ + parent::boot(); + + static::saving(function ($model) { + $model->title = Str::slug($model->title); + }); +} + public function emailRequests() { return $this->hasMany(EmailRequest::class, 'template_id'); diff --git a/app/Services/OrganisationService.php b/app/Services/OrganisationService.php new file mode 100644 index 00000000..99a40d93 --- /dev/null +++ b/app/Services/OrganisationService.php @@ -0,0 +1,30 @@ +owned_organisations()->create([ + 'name' => $name + ]); + $this->assign_member($user, $organisation); + return $organisation; + } + + public function assign_member(User $user, Organisation $organisation) { + OrganisationUser::create([ + 'user_id' => $user->id, + 'org_id' => $organisation->org_id + ]); + } +} \ No newline at end of file diff --git a/app/Traits/HttpResponses.php b/app/Traits/HttpResponses.php index c8504ccd..bf7305cd 100755 --- a/app/Traits/HttpResponses.php +++ b/app/Traits/HttpResponses.php @@ -3,6 +3,7 @@ namespace App\Traits; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; trait HttpResponses { @@ -26,4 +27,22 @@ protected function apiResponse($message = '', $status_code = 200, $data = null, // Return the JSON response return response()->json($response, $status_code); } + + public function validationErrorResponseAlign($errors) : JsonResponse { + $response = [ + 'status' => Response::HTTP_BAD_REQUEST, + 'title' => 'One or more validation errors occurred.', + 'errors' => $errors, + ]; + return response()->json($response, Response::HTTP_BAD_REQUEST); + } + + public function validationErrorResponse($errors) : JsonResponse { + $response = [ + 'status_code' => Response::HTTP_BAD_REQUEST, + 'message' => 'One or more validation errors occurred.', + 'errors' => $errors, + ]; + return response()->json($response, Response::HTTP_BAD_REQUEST); + } } diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index b408c29c..97520b7e 100755 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -22,7 +22,8 @@ public function up(): void $table->boolean('is_active')->default(1); $table->boolean('is_verified')->default(0); $table->string('signup_type')->default('Token'); - $table->mediumText('social_id')->nullable(); + $table->string('social_id')->nullable(); + $table->timestamp('last_login_at')->nullable(); $table->rememberToken(); $table->timestamps(); }); diff --git a/routes/api.php b/routes/api.php index 61d1510f..2098271d 100755 --- a/routes/api.php +++ b/routes/api.php @@ -88,12 +88,13 @@ Route::get('/auth/login-facebook', [SocialAuthController::class, 'loginUsingFacebook']); Route::get('/auth/facebook/callback', [SocialAuthController::class, 'callbackFromFacebook']); Route::post('/auth/facebook/callback', [SocialAuthController::class, 'saveFacebookRequest']); + + Route::middleware('auth:api')->group(function () { + Route::get('/users/stats', [UserController::class, 'stats']); + Route::apiResource('/users', UserController::class); - Route::get('/users/stats', [UserController::class, 'stats']); - Route::apiResource('/users', UserController::class); - - + }); //jobs Route::get('/jobs', [JobController::class, 'index']); Route::get('/jobs/search', [JobSearchController::class, 'search']); diff --git a/tests/Feature/CookiePreferencesTest.php b/tests/Feature/CookiePreferencesTest.php index 9ee869f7..b5cedd5e 100644 --- a/tests/Feature/CookiePreferencesTest.php +++ b/tests/Feature/CookiePreferencesTest.php @@ -38,7 +38,6 @@ public function test_user_cookie_preferences_workflow() 'first_name', 'last_name', 'email', - 'role', 'avatar_url', ], ] diff --git a/tests/Feature/EmailTemplateControllerTest.php b/tests/Feature/EmailTemplateControllerTest.php index e1ad33d7..c1d02e3e 100644 --- a/tests/Feature/EmailTemplateControllerTest.php +++ b/tests/Feature/EmailTemplateControllerTest.php @@ -173,7 +173,7 @@ public function test_super_admin_can_update_email_template() 'message' => 'Email template updated successfully', 'data' => [ 'id' => $template->id, - 'title' => 'Updated Template Title', + 'title' => 'updated-template-title', 'template' => 'Updated Template Content', 'status' => true, ], @@ -181,7 +181,7 @@ public function test_super_admin_can_update_email_template() $this->assertDatabaseHas('email_templates', [ 'id' => $template->id, - 'title' => 'Updated Template Title', + 'title' => 'updated-template-title', 'template' => 'Updated Template Content', 'status' => true, ]); diff --git a/tests/Feature/OrganisationMemberControllerTest.php b/tests/Feature/OrganisationMemberControllerTest.php index a3018dcc..5ecfb151 100755 --- a/tests/Feature/OrganisationMemberControllerTest.php +++ b/tests/Feature/OrganisationMemberControllerTest.php @@ -6,6 +6,7 @@ use Tests\TestCase; use App\Models\User; use App\Models\Organisation; +use App\Models\Profile; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Carbon\Carbon; @@ -40,6 +41,10 @@ public function it_validates_the_organisation_id_and_returns_paginated_members() 'password' => Hash::make('precious') ]); + Profile::factory()->create([ + 'user_id' => $user->id + ]); + // Login the user $response = $this->postJson('/api/v1/auth/login', [ 'email' => 'precious@example.com', diff --git a/tests/Feature/ProductControllerTest.php b/tests/Feature/ProductControllerTest.php index d3bc6297..7c363d7a 100755 --- a/tests/Feature/ProductControllerTest.php +++ b/tests/Feature/ProductControllerTest.php @@ -42,7 +42,7 @@ public function testUserLogin() $response->assertStatus(200); $response->assertJsonStructure([ 'message', - 'data' => ['user' => ['id', 'first_name', 'last_name', 'email', 'role', 'avatar_url']] + 'data' => ['user' => ['id', 'first_name', 'last_name', 'email', 'avatar_url']] ]); } } diff --git a/tests/Feature/RoleCreationTest.php b/tests/Feature/RoleCreationTest.php index f8e20ec5..0f8037e4 100755 --- a/tests/Feature/RoleCreationTest.php +++ b/tests/Feature/RoleCreationTest.php @@ -46,8 +46,6 @@ public function test_role_creation_is_successful() 'permissions_id' => $this->test_permission->id, ]); - // Print the response content to see the validation errors - // $response->dump(); $response->assertStatus(201) ->assertJsonStructure([ diff --git a/tests/Feature/WaitListControllerTest.php b/tests/Feature/WaitListControllerTest.php index aec56c3c..a20a070b 100644 --- a/tests/Feature/WaitListControllerTest.php +++ b/tests/Feature/WaitListControllerTest.php @@ -90,7 +90,6 @@ public function testStore() public function testStoreValidationFailure() { $payload = [ - 'name' => " ", 'email' => 'invalid-email' ]; diff --git a/tests/Unit/LoginTest.php b/tests/Unit/LoginTest.php index 43fbc1be..5698f445 100755 --- a/tests/Unit/LoginTest.php +++ b/tests/Unit/LoginTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit; +use App\Models\Profile; use Illuminate\Http\Response; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -26,12 +27,15 @@ public function test_login_with_valid_credentials() 'email' => 'test@gmail.com', 'password' => Hash::make('Ed8M7s*)?e:hTb^#&;C!create([ + 'user_id' => $user->id + ]); $response = $this->postJson('/api/v1/auth/login', [ 'email' => 'test@gmail.com', 'password' => 'Ed8M7s*)?e:hTb^#&;C!assertStatus(200) ->assertJsonStructure([ 'status_code', @@ -43,8 +47,8 @@ public function test_login_with_valid_credentials() 'first_name', 'last_name', 'email', - 'role', 'avatar_url', + 'is_superadmin' ], ], ]); @@ -74,6 +78,6 @@ public function test_login_with_missing_fields() 'email' => 'test@gmail.com', ]); - $response->assertStatus(400); + $response->assertStatus(401); } } diff --git a/tests/Unit/RegistrationTest.php b/tests/Unit/RegistrationTest.php index b5fa7520..51abbde5 100755 --- a/tests/Unit/RegistrationTest.php +++ b/tests/Unit/RegistrationTest.php @@ -50,14 +50,12 @@ public function test_registration_returns_jwt_token() 'last_name', 'email', 'avatar_url', - 'role' ] ] ]); // Optionally, decode and verify the token $token = $response->json('access_token'); - $token = $response->json('access_token'); $this->assertNotEmpty($token); } @@ -76,8 +74,8 @@ public function test_fails_if_email_is_not_passed() // Check the status code $response->assertStatus(400); $response->assertJson([ - 'status_code' => 400, - 'message' => [ + 'status' => 400, + 'errors' => [ 'email' => [ 'The email field is required.' ]