diff --git a/app/Http/Controllers/Auth/ApiAuthController.php b/app/Http/Controllers/Auth/ApiAuthController.php index 230aef3..c5be14c 100644 --- a/app/Http/Controllers/Auth/ApiAuthController.php +++ b/app/Http/Controllers/Auth/ApiAuthController.php @@ -38,7 +38,8 @@ class ApiAuthController extends Controller return response()->json([ 'access_token' => $token, 'token_type' => 'Bearer', - ]); + ], 201 + ); } } @@ -52,11 +53,13 @@ class ApiAuthController extends Controller $user = User::where('email', $request['email'])->firstOrFail(); - $token = $user->createToken('auth_token')->plainTextToken; + $token = $user->createToken('auth_token') + ->plainTextToken; return response()->json([ 'access_token' => $token, 'token_type' => 'Bearer', + 'message' => 'Login successful, please remember to logout!' ]); } @@ -64,4 +67,14 @@ class ApiAuthController extends Controller { return $request->user(); } + + public function logout(Request $request) + { + $request->user()->currentAccessToken()->delete(); + return response()->json([ + 'message' => 'Logged out' + ], 200 + ); + } + } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 2b42e5b..36031cb 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -22,16 +22,20 @@ use Illuminate\Support\Facades\Response; class ProjectController extends Controller { + protected mixed $project_api_route_pattern = 'api/*'; /** * Display Listing of all Projects. + * @throws AuthorizationException */ public function index(Request $request): View|Factory|Application|JsonResponse|ProjectResource { // Check if API Call, get userID from request - if ($request->is('api/*')) { + if ($request->expectsJson()) { $user = Auth::user(); + $this->authorize('viewAny', Project::class); $projects = $user->projects()->paginate(4); + return new ProjectResource($projects); } @@ -77,13 +81,15 @@ class ProjectController extends Controller $data = $request->validated(); // Check if API Call, get userID from request - if ($request->is('api/*')) { - $user = User::find($request->user_id); - if (!$user) { - return response()->json(['error' => 'User not found'], 404); - } + if ($request->expectsJson()) { + $user = Auth::user(); + + $this->authorize('create', Project::class); + $user->projects()->create($data); + $data = $user->projects()->latest()->first(); + return response()->json([ 'message' => 'Project created successfully', 'data' => $data, @@ -103,11 +109,8 @@ class ProjectController extends Controller public function show(Request $request, $project_id): RedirectResponse|JsonResponse { // Check if API Call, get userID from request - if ($request->is('api/*')) { - $user = User::find($request->user_id); - if (!$user) { - return response()->json(['error' => 'User not found'], 404); - } + if ($request->expectsJson()) { + $user = Auth::user(); $project = $user->projects()->find($project_id); @@ -117,6 +120,8 @@ class ProjectController extends Controller ], 404); } + $this->authorize('view', $project); + return response()->json([ 'message' => 'Project retrieved successfully', 'data' => $project, @@ -152,11 +157,8 @@ class ProjectController extends Controller $data = $request->validatedWithCompletedAt(); // API Call - if ($request->is('api/*')) { - $user = User::find($request->user_id); - if (!$user) { - return response()->json(['error' => 'User not found'], 404); - } + if ($request->expectsJson()) { + $user = Auth::user(); $project = $user->projects()->find($project_id); @@ -166,6 +168,8 @@ class ProjectController extends Controller ], 404); } + $this->authorize('update', $project); + $project->update($data); return response()->json([ @@ -201,12 +205,8 @@ class ProjectController extends Controller public function destroy($project_id, Request $request): RedirectResponse|JsonResponse { // Check if API Call and $project_id is provided - if ($request->is('api/*')) { - $user_id = $request->user_id; - $user = User::find($user_id); - if (!$user) { - return response()->json(['error' => 'User not found'], 404); - } + if ($request->expectsJson() && $project_id) { + $user = Auth::user(); $project = $user->projects()->find($project_id); @@ -217,6 +217,8 @@ class ProjectController extends Controller ], 404); } + $this->authorize('delete', $project); + $project->delete(); return response()->json([ diff --git a/app/Http/Controllers/ProjectTodoController.php b/app/Http/Controllers/ProjectTodoController.php index bc54398..ebe4dd1 100644 --- a/app/Http/Controllers/ProjectTodoController.php +++ b/app/Http/Controllers/ProjectTodoController.php @@ -4,13 +4,17 @@ namespace App\Http\Controllers; use App\Http\Requests\Project\StoreTodoRequest; use App\Http\Requests\Project\UpdateTodoRequest; +use App\Http\Resources\TodoResource; use App\Models\Todo; use App\Models\User; use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Application; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; +use Illuminate\Http\Request; class ProjectTodoController extends Controller { @@ -19,13 +23,23 @@ class ProjectTodoController extends Controller /** * Display a listing of all Todos for a Project. */ - public function index($project_id): Factory|Application|View|\Illuminate\Contracts\Foundation\Application|RedirectResponse + public function index(Request $request, $project_id): + Factory|Application|View|\Illuminate\Contracts\Foundation\Application|RedirectResponse|TodoResource { - $user = User::find(auth()->user()->id); - $projects = $user->projects; + $user = Auth::user(); + $projects = $user->projects(); $project = $projects->find($project_id); - if (!$project || $project->user->id !== auth()->user()->id) + if ($request->expectsJson()) { + $this->authorize('viewAny', [Todo::class, $project]); + + $todos = $project->todos()->paginate(4); + + return new TodoResource($todos); + } + + + if (!$project || $project->user->id !== $user->id) return back() ->with('error', 'Project not found'); @@ -43,7 +57,7 @@ class ProjectTodoController extends Controller */ public function create($project_id): Factory|View|Application { - $project = auth()->user()->projects->find($project_id); + $project = Auth::user()->projects->find($project_id); return view('todo.create', [ 'project' => $project, ]); @@ -52,33 +66,55 @@ class ProjectTodoController extends Controller /** * Store a newly created Todo in storage. */ - public function store($project_id, StoreTodoRequest $request): RedirectResponse + public function store($project_id, StoreTodoRequest $request): RedirectResponse|JsonResponse { - $user = User::find(auth()->user()->id); + $validatedData = $request->validated(); + $user = Auth::user(); + $project = $user->projects->find($project_id); + $this->authorize('create', [Todo::class, $user]); - $validatedData = $request->validated(); - - $project = $user->projects->find($project_id); // Add the Todo to the Project $project->todos()->save(new Todo($validatedData)); + if ($request->expectsJson()) { + return response()->json([ + 'message' => 'Todo created successfully.', + 'data' => $project->todos()->latest()->first(), + ], 201); + } + return redirect()->route('project.todo.index', $project_id) ->with('success', 'Todo created successfully.'); } + /** * Display the specified resource. */ - public function show($project_id, Todo $todo) + public function show(Request $request, $project_id) { - $user = User::find(auth()->user()->id); - $projects = $user->projects; - $project = $projects->find($project_id); + if ($request->expectsJson()) { + $user = Auth::user(); + $project = $user->projects->find($project_id); + $todo = $project->todos->find($request->todo_id); - $this->authorize('view', [Todo::class, $project, $todo]); + $this->authorize('view', [Todo::class, $project, $todo]); - return view('todo.show', compact('project', 'todo')); + return response()->json([ + 'message' => 'Todo retrieved successfully.', + 'data' => $todo, + ], 200); + } + + return redirect()->route('project.todo.index', $project_id); +// $user = Auth::user(); +// $projects = $user->projects; +// $project = $projects->find($project_id); +// +// $this->authorize('view', [Todo::class, $project, $project->todos->find($request->todo_id)]); +// +// return view('todo.show', compact('project', 'todo')); } /** @@ -99,37 +135,49 @@ class ProjectTodoController extends Controller public function update($project_id, UpdateTodoRequest $request, Todo $todo) { $project = auth()->user()->projects->find($project_id); - $this->authorize('update', [Todo::class, $project, $todo]); // Update other fields $todo->fill($request->validated()); - $todo->due_start = $request->due_start ? - strtotime(Carbon::parse($request->due_start)) : - ($todo->due_start ?: null); + $dueStart = $request->due_start ? strtotime(Carbon::parse($request->due_start)) : null; + $dueEnd = $request->due_end ? strtotime(Carbon::parse($request->due_end)) : null; - $todo->due_end = $request->due_end ? - strtotime(Carbon::parse($request->due_end)) : - ($todo->due_end ?: - ($todo->due_start ? strtotime(Carbon::parse($todo->due_start)) : null)); + if ($dueEnd === null && $dueStart !== null) { + $dueEnd = strtotime(Carbon::parse($todo->due_start)); + } + $todo->due_start = $dueStart; + $todo->due_end = $dueEnd; $todo->save(); - return back() - ->with('success', 'Todo updated successfully'); + if ($request->expectsJson()) { + return response()->json([ + 'message' => 'Todo updated successfully', + 'data' => $todo, + ], 200); + } + + return back()->with('success', 'Todo updated successfully'); } + /** * Remove the specified resource from storage. */ - public function destroy($project_id, Todo $todo): RedirectResponse + public function destroy($project_id, Request $request, Todo $todo): RedirectResponse|JsonResponse { $this->authorize('delete', [Todo::class, $todo]); $todo->delete(); + if (request()->expectsJson()) { + return response()->json([ + 'message' => 'Todo deleted successfully', + ], 200); + } + return redirect()->route('project.todo.index', $project_id) ->with('success', 'Todo deleted successfully'); } diff --git a/app/Http/Resources/ProjectResource.php b/app/Http/Resources/ProjectResource.php index c61696c..b93ad66 100644 --- a/app/Http/Resources/ProjectResource.php +++ b/app/Http/Resources/ProjectResource.php @@ -16,6 +16,9 @@ class ProjectResource extends ResourceCollection { return [ 'data' => $this->collection, + 'links' => [ + 'self' => 'link-value', + ], ]; } } diff --git a/app/Http/Resources/TodoResource.php b/app/Http/Resources/TodoResource.php new file mode 100644 index 0000000..9739010 --- /dev/null +++ b/app/Http/Resources/TodoResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'links' => [ + 'self' => 'link-value', + ], + ]; + } +} diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php index fd2a527..499c77d 100644 --- a/app/Policies/ProjectPolicy.php +++ b/app/Policies/ProjectPolicy.php @@ -13,7 +13,7 @@ class ProjectPolicy */ public function viewAny(User $user): bool { - // + return true; } /** diff --git a/app/Policies/TodoPolicy.php b/app/Policies/TodoPolicy.php index df133d4..ac18f54 100644 --- a/app/Policies/TodoPolicy.php +++ b/app/Policies/TodoPolicy.php @@ -14,7 +14,7 @@ class TodoPolicy */ public function viewAny(User $user): bool { - return false; + return true; } /** diff --git a/config/sanctum.php b/config/sanctum.php index 529cfdc..da6e738 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -46,7 +46,7 @@ return [ | */ - 'expiration' => null, + 'expiration' => 525600, /* |-------------------------------------------------------------------------- diff --git a/resources/views/project/load-projects.blade.php b/resources/views/project/load-projects.blade.php index d8d2e30..adff529 100644 --- a/resources/views/project/load-projects.blade.php +++ b/resources/views/project/load-projects.blade.php @@ -1,6 +1,7 @@ @foreach($projects as $project)
- +
diff --git a/resources/views/todo/todo_list.blade.php b/resources/views/todo/todo_list.blade.php index 3b3fccb..559fa4e 100644 --- a/resources/views/todo/todo_list.blade.php +++ b/resources/views/todo/todo_list.blade.php @@ -77,7 +77,7 @@ diff --git a/routes/api.php b/routes/api.php index 2bcbee6..15d5efe 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,7 @@ use App\Http\Controllers\ProjectController; use App\Http\Controllers\Auth\ApiAuthController; +use App\Http\Controllers\ProjectTodoController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; @@ -20,13 +21,15 @@ Route::middleware('auth:sanctum')->get('/user', function (Request $request) { return $request->user(); }); +// Auth Group to ApiAuthController Route::post('/register', [ApiAuthController::class, 'register']); - Route::post('/login', [ApiAuthController::class, 'login']); // Resources route to /api/projects Route::middleware('auth:sanctum')->group( function () { + Route::post('/logout', [ApiAuthController::class, 'logout']); Route::get('/me', [ApiAuthController::class, 'me']); - Route::apiResource('projects', ProjectController::class); + Route::apiResource('project', ProjectController::class); + Route::apiResource('project.todo', ProjectTodoController::class); }); diff --git a/tests/Feature/API/AuthTest.php b/tests/Feature/API/AuthTest.php new file mode 100644 index 0000000..6221ed1 --- /dev/null +++ b/tests/Feature/API/AuthTest.php @@ -0,0 +1,85 @@ + 'Test User', + 'email' => 'test@mail.com', + 'password' => 'password1234', + ]; + protected array $auth_struct = [ + 'access_token', + 'token_type', + ]; + + protected string $registerRoute = '/api/register'; + protected string $loginRoute = '/api/login'; + protected string $meRoute = '/api/me'; + + protected function setUp(): void + { + parent::setUp(); + + $this->withHeaders([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ]); + } + + protected function registerAndLoginUser(): string + { + $this->postJson($this->registerRoute, $this->user); + + $response = $this->postJson($this->loginRoute, [ + 'email' => $this->user['email'], + 'password' => $this->user['password'], + ]); + + return $response['access_token']; + } + + public function test_user_can_register_with_api(): void + { + $response = $this->postJson($this->registerRoute, $this->user); + + $response->assertCreated()->assertJsonStructure($this->auth_struct); + } + + public function test_user_can_authenticate_with_api(): void + { + $this->test_user_can_register_with_api(); + + $response = $this->postJson($this->loginRoute, [ + 'email' => $this->user['email'], + 'password' => $this->user['password'], + ]); + + $response->assertOk()->assertJsonStructure($this->auth_struct); + } + + public function test_user_can_view_me(): void + { + $token = $this->registerAndLoginUser(); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->getJson($this->meRoute); + + $response->assertOk()->assertJsonStructure([ + 'id', + 'name', + 'email', + 'email_verified_at', + 'created_at', + 'updated_at', + ]); + } +} diff --git a/tests/Feature/API/ProjectTest.php b/tests/Feature/API/ProjectTest.php new file mode 100644 index 0000000..7b22f16 --- /dev/null +++ b/tests/Feature/API/ProjectTest.php @@ -0,0 +1,133 @@ + 'Test User', + 'email' => 'test@test.com', + 'password' => 'password1234' + ]; + + private string $project_route = '/api/project'; + private $access_token; + private $user_id; + private $project_id; + + protected function setUp(): void + { + parent::setUp(); + + // Register user + $this->registerUser(); + + // Authenticate user + $this->authenticateUser(); + } + + protected function registerUser(): void + { + $response = $this->postJson('/api/register', $this->user); + $response->assertCreated(); + } + + protected function authenticateUser(): void + { + $response = $this->postJson('/api/login', [ + 'email' => $this->user['email'], + 'password' => $this->user['password'], + ]); + $response->assertOk(); + + $this->access_token = $response['access_token']; + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->access_token, + ])->getJson('/api/me'); + $response->assertOk(); + + $this->user_id = $response['id']; + } + + public function test_user_can_view_all_projects(): void + { + $response = $this->getJson($this->project_route); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + '*' => $this->getProjectJsonStructure(), + ], + 'links' => [ + 'first', + 'last', + 'prev', + 'next', + ], + ]); + } + + public function test_user_can_create_project(): void + { + $response = $this->postJson($this->project_route, [ + 'name' => 'Test Project', + 'description' => 'Test Description', + ]); + + + $response->assertCreated() + ->assertJsonStructure([ + 'data' => $this->getProjectJsonStructure(), + ]); + + $this->assertDatabaseHas('projects', [ + 'name' => 'Test Project', + 'description' => 'Test Description', + ]); + + $this->assertDatabaseHas('project_user', [ + 'user_id' => $this->user_id, + 'project_id' => $response['data']['id'], + ]); + + $this->project_id = $response['data']['id']; + } + + public function test_user_can_get_created_project_from_api(): void + { + $this->test_user_can_create_project(); + + $response = $this->getJson($this->project_route . '/' . $this->project_id); + + $response->assertOk(); + } + + public function test_user_can_delete_created_project_from_api(): void + { + $this->test_user_can_create_project(); + + $response = $this->deleteJson($this->project_route . '/' . $this->project_id); + + $response->assertOk(); + } + + protected function getProjectJsonStructure() + { + return [ + 'id', + 'name', + 'description', + 'created_at', + 'updated_at', + ]; + } + + +} diff --git a/tests/Feature/API/TodoTest.php b/tests/Feature/API/TodoTest.php new file mode 100644 index 0000000..b19282a --- /dev/null +++ b/tests/Feature/API/TodoTest.php @@ -0,0 +1,260 @@ + 'Test User', + 'email' => 'test@test.com', + 'password' => 'password1234' + ]; + + private string $project_route = '/api/project/'; + private $access_token; + private $user_id; + private $project_id; + private $todo_id; + + protected function setUp(): void + { + parent::setUp(); + + // Register user + $this->registerUser(); + + // Authenticate user + $this->authenticateUser(); + } + + protected function registerUser(): void + { + $response = $this->postJson('/api/register', $this->user); + $response->assertCreated(); + } + + protected function authenticateUser(): void + { + $response = $this->postJson('/api/login', [ + 'email' => $this->user['email'], + 'password' => $this->user['password'], + ]); + $response->assertOk(); + + $this->access_token = $response['access_token']; + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->access_token, + ])->getJson('/api/me'); + $response->assertOk(); + + $this->user_id = $response['id']; + } + + public function create_project_and_todo(): void + { + $this->actingAs(User::find($this->user_id)); + + // Create a project through POST and store it in the project property + $response = $this->post(route('project.store'), [ + 'name' => 'Test Project', + 'description' => 'Test Description', + ]); + $response->assertRedirect(route('project.index')); + $this->assertDatabaseHas('projects', [ + 'name' => 'Test Project', + 'description' => 'Test Description', + ]); + + $this->assertDatabaseHas('project_user', [ + 'project_id' => Project::where('name', 'Test Project')->first()->id, + 'user_id' => $this->user_id, + ]); + + $this->project_id = Project::where('name', 'Test Project')->first()->id; + + $response = $this->post(route('project.todo.store', $this->project_id), [ + 'title' => 'Test Todo', + 'description' => 'Test Description', + ]); + + $response->assertRedirect(route('project.todo.index', $this->project_id)); + + $this->todo_id = Todo::where('title', 'Test Todo')->first()->id; + + $this->assertDatabaseHas('todos', [ + 'title' => 'Test Todo', + 'description' => 'Test Description', + ]); + + $this->assertDatabaseHas('project_todo', [ + 'project_id' => $this->project_id, + 'todo_id' => $this->todo_id, + ]); + + } + + public function test_user_can_see_todo_of_project(): void + { + $this->create_project_and_todo(); + + $response = $this->getJson($this->project_route . $this->project_id . '/todo'); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'title', + 'description', + 'created_at', + 'updated_at', + ], + ], + 'links' => [ + 'first', + 'last', + 'prev', + 'next', + ], + ]); + } + + public function test_user_can_create_todo(): void + { + $this->create_project_and_todo(); + + $response = $this->postJson($this->project_route . $this->project_id . '/todo', [ + 'title' => 'Test Todo', + 'description' => 'Test Description', + ]); + + $response->assertCreated() + ->assertJsonStructure([ + 'data' => [ + 'id', + 'title', + 'description', + 'created_at', + 'updated_at', + ], + ]); + + $this->assertDatabaseHas('todos', [ + 'title' => 'Test Todo', + 'description' => 'Test Description', + ]); + + $this->assertDatabaseHas('project_todo', [ + 'project_id' => $this->project_id, + 'todo_id' => $response['data']['id'], + ]); + } + + public function test_user_can_edit_todo(): void + { + $this->create_project_and_todo(); + + $response = $this->putJson($this->project_route . $this->project_id . '/todo/' . $this->todo_id, [ + 'title' => 'Updated Todo', + 'description' => 'Updated Description', + ]); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + 'id', + 'title', + 'description', + 'created_at', + 'updated_at', + ], + ]); + } + + public function test_user_complete_todo() + { + $this->create_project_and_todo(); + + $response = $this->putJson($this->project_route . $this->project_id . '/todo/' . $this->todo_id, + [ + 'completed_at' => 'on', + ] + ); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + 'id', + 'title', + 'description', + 'completed_at', + 'created_at', + 'updated_at', + ], + ]); + } + + public function test_user_can_uncomplete_todo(): void + { + $this->create_project_and_todo(); + + $response = $this->putJson($this->project_route . $this->project_id . '/todo/' . $this->todo_id, + [ + 'completed_at' => 'on', + ] + ); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + 'id', + 'title', + 'description', + 'completed_at', + 'created_at', + 'updated_at', + ], + ]); + + $response = $this->putJson($this->project_route . $this->project_id . '/todo/' . $this->todo_id, + [] + ); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + 'id', + 'title', + 'description', + 'completed_at', + 'created_at', + 'updated_at', + ], + ]); + + // Check database for "completed_at" is null + $this->assertDatabaseHas('todos', [ + 'id' => $this->todo_id, + 'completed_at' => null, + ]); + } + + public function test_user_can_delete_todo(): void + { + $this->create_project_and_todo(); + + $response = $this->deleteJson($this->project_route . $this->project_id . '/todo/' . $this->todo_id); + + $response->assertOk(); + } +}