diff --git a/app/Http/Controllers/BookmarkController.php b/app/Http/Controllers/BookmarkController.php new file mode 100644 index 0000000..11b91b0 --- /dev/null +++ b/app/Http/Controllers/BookmarkController.php @@ -0,0 +1,89 @@ +authorize('viewAny', Bookmark::class); + + return new BookmarkCollection(request()->user()->bookmarks); + } + + public function store(BookmarkRequest $request) + { + $this->authorize('create', Bookmark::class); + + $metas = $this->getMeta($request->url); + $request['name'] = (empty($request->name)) ? $metas['title'] : $request->name; + $request['favicon'] = $metas['favicon']; + + $bookmark = request()->user()->bookmarks()->create($request->all()); + + return (new BookmarkResource($bookmark)) + ->response() + ->setStatusCode(201); + } + + public function show(Bookmark $bookmark) + { + $this->authorize('view', $bookmark); + + return new BookmarkResource($bookmark); + } + + public function update(BookmarkRequest $request, Bookmark $bookmark) + { + $this->authorize('update', $bookmark); + + $bookmark->update($request->all()); + + return (new BookmarkResource($bookmark)) + ->response() + ->setStatusCode(200); + } + + public function destroy(Bookmark $bookmark) + { + $this->authorize('delete', $bookmark); + + $bookmark->delete(); + + return response([], 204); + } + + private function getMeta($url) + { + $metas = []; + $client = new \GuzzleHttp\Client(); + $promise = $client->requestAsync('GET', $url); + $response = $promise->wait(); + $page = $response->getBody()->getContents(); + preg_match("/\(.*)\<\/title\>/i",$page,$title); + $metas['title'] = $title[1]; + + + preg_match('/\/i',$page,$favicon); + if(isset($favicon[1])) { + preg_match('/href="(.*)/i',$favicon[1],$favicon); + $metas['favicon'] = $favicon[1]; + + preg_match('/http/', $metas['favicon'], $matches); + if(empty($matches)) { + $metas['favicon'] = parse_url($url, PHP_URL_SCHEME).'://'.parse_url($url, PHP_URL_HOST).$metas['favicon']; + } + //dd($metas['favicon'], $matches, !isset($matches[1]), empty($matches)); + } else { + $metas['favicon'] = null; + } + + return $metas; + } +} diff --git a/app/Http/Requests/BookmarkRequest.php b/app/Http/Requests/BookmarkRequest.php new file mode 100644 index 0000000..d008ae4 --- /dev/null +++ b/app/Http/Requests/BookmarkRequest.php @@ -0,0 +1,31 @@ + 'max:65', + 'url' => ['required', 'url', 'max:255'], + 'favicon' => ['url', 'max:255'], + ]; + } + + public function attributes() + { + return [ + 'name' => 'Nom du marque-page', + ]; + } + + public function messages() + { + return [ + 'url.required' => 'Une url est nécessaire', + ]; + } +} diff --git a/app/Http/Resources/Bookmark.php b/app/Http/Resources/Bookmark.php new file mode 100644 index 0000000..a7c6271 --- /dev/null +++ b/app/Http/Resources/Bookmark.php @@ -0,0 +1,34 @@ + [ + 'type' => 'bookmark', + 'bookmark_id' => $this->id, + 'attributes' => [ + 'data' => [ + 'name' => $this->name, + 'description' => $this->description, + 'url' => $this->url, + 'favicon' => $this->favicon, + 'created_at' => $this->created_at->diffForHumans(), + 'last_updated' => $this->updated_at->diffForHumans(), + ] + ], + ], + ]; + } +} diff --git a/app/Http/Resources/BookmarkCollection.php b/app/Http/Resources/BookmarkCollection.php new file mode 100644 index 0000000..b6418e0 --- /dev/null +++ b/app/Http/Resources/BookmarkCollection.php @@ -0,0 +1,25 @@ + $this->collection, + 'bookmarks_count' => $this->count(), + 'links' => [ + 'self' => url('/bookmarks'), + ] + ]; + } +} diff --git a/app/Models/Bookmark.php b/app/Models/Bookmark.php new file mode 100644 index 0000000..7e52a12 --- /dev/null +++ b/app/Models/Bookmark.php @@ -0,0 +1,10 @@ +hasMany(ToDoList::class); } + public function bookmarks() : HasMany + { + return $this->hasMany(Bookmark::class); + } + public function images(): MorphMany { return $this->morphMany(Image::class, 'imageable'); diff --git a/app/Policies/BookmarkPolicy.php b/app/Policies/BookmarkPolicy.php new file mode 100644 index 0000000..23b30b2 --- /dev/null +++ b/app/Policies/BookmarkPolicy.php @@ -0,0 +1,94 @@ +id == $bookmark->user_id; + } + + /** + * Determine whether the user can create bookmarks. + * + * @param \App\User $user + * @return mixed + */ + public function create(User $user) + { + return true; + } + + /** + * Determine whether the user can update the bookmark. + * + * @param \App\User $user + * @param \App\Models\Bookmark $bookmark + * @return mixed + */ + public function update(User $user, Bookmark $bookmark) + { + return $user->id == $bookmark->user_id; + } + + /** + * Determine whether the user can delete the bookmark. + * + * @param \App\User $user + * @param \App\Models\Bookmark $bookmark + * @return mixed + */ + public function delete(User $user, Bookmark $bookmark) + { + return $user->id == $bookmark->user_id; + } + + /** + * Determine whether the user can restore the bookmark. + * + * @param \App\User $user + * @param \App\Models\Bookmark $bookmark + * @return mixed + */ + public function restore(User $user, Bookmark $bookmark) + { + return false; + } + + /** + * Determine whether the user can permanently delete the bookmark. + * + * @param \App\User $user + * @param \App\Models\Bookmark $bookmark + * @return mixed + */ + public function forceDelete(User $user, Bookmark $bookmark) + { + return false; + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index a1f63e0..5012a59 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -18,6 +18,7 @@ class AuthServiceProvider extends ServiceProvider 'App\User' => 'App\Policies\UserPolicy', 'App\Models\Memo' => 'App\Policies\MemoPolicy', 'App\Models\ToDoList' => 'App\Policies\ToDoListPolicy', + 'App\Models\Bookmark' => 'App\Policies\BookmarkPolicy', ]; /** diff --git a/database/factories/BookmarkFactory.php b/database/factories/BookmarkFactory.php new file mode 100644 index 0000000..bedc0cd --- /dev/null +++ b/database/factories/BookmarkFactory.php @@ -0,0 +1,15 @@ +define(Bookmark::class, function (Faker $faker) { + return [ + 'user_id' => factory(\App\User::class), + 'name' => $faker->words(3, [false]), + 'url' => $faker->url, + 'favicon' => $faker->imageUrl(), + ]; +}); diff --git a/database/migrations/2020_05_09_123927_create_bookmarks_table.php b/database/migrations/2020_05_09_123927_create_bookmarks_table.php new file mode 100644 index 0000000..bf50dd9 --- /dev/null +++ b/database/migrations/2020_05_09_123927_create_bookmarks_table.php @@ -0,0 +1,36 @@ +id(); + $table->unsignedBigInteger('user_id'); + $table->string('name')->nullable(); + $table->string('description')->nullable(); + $table->string('url'); + $table->string('favicon')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('bookmarks'); + } +} diff --git a/resources/js/components/Nav.vue b/resources/js/components/Nav.vue index 475efab..5c252ed 100644 --- a/resources/js/components/Nav.vue +++ b/resources/js/components/Nav.vue @@ -13,6 +13,10 @@ ToDo Lists + + + Bookmarks + Jeux diff --git a/resources/js/router.js b/resources/js/router.js index 656e1fd..49a1c34 100644 --- a/resources/js/router.js +++ b/resources/js/router.js @@ -11,6 +11,7 @@ import MemoShow from './views/Memo/MemoShow' import MemoEdit from './views/Memo/MemoEdit' import ToDoListIndex from './views/ToDoLists/ToDoListIndex' import ToDoListShow from './views/ToDoLists/ToDoListShow' +import BookmarkIndex from './views/Bookmark/BookmarkIndex' import GameIndex from './views/Games/GameIndex' import Hangman from './views/Games/HangMan/Hangman' import Quizz from './views/Games/Quizz/Quizz' @@ -65,6 +66,11 @@ export default new VueRouter({ meta: {title: 'Details of List'} }, + { + path: '/bookmarks', component: BookmarkIndex, + meta: {title: 'Bookmark Lists'} + }, + { path: '/jeux', component: GameIndex, meta: {title: 'Liste des jeux'} diff --git a/resources/js/views/Bookmark/Bookmark.vue b/resources/js/views/Bookmark/Bookmark.vue new file mode 100644 index 0000000..9891fde --- /dev/null +++ b/resources/js/views/Bookmark/Bookmark.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/resources/js/views/Bookmark/BookmarkIndex.vue b/resources/js/views/Bookmark/BookmarkIndex.vue new file mode 100644 index 0000000..473900e --- /dev/null +++ b/resources/js/views/Bookmark/BookmarkIndex.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/resources/svg/globe.svg b/resources/svg/globe.svg new file mode 100644 index 0000000..4603b43 --- /dev/null +++ b/resources/svg/globe.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/routes/api.php b/routes/api.php index 2641233..be41cd2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -26,6 +26,7 @@ Route::middleware('auth:api')->group(function () { '/meteo' => 'MeteoController', '/to-do-lists' => 'ToDoListController', '/to-do-lists/{toDoList}/to-do' => 'ToDoController', + '/bookmarks' => 'BookmarkController', '/events/categories' => 'EventCategoryController', '/events' => 'EventController', // '/users/{user}/posts' => 'UserPostController', diff --git a/tests/Feature/BookmarkTest.php b/tests/Feature/BookmarkTest.php new file mode 100644 index 0000000..79b0c8f --- /dev/null +++ b/tests/Feature/BookmarkTest.php @@ -0,0 +1,215 @@ +withoutExceptionHandling(); + + $this->actingAs($user = factory(\App\User::class)->create(), 'api'); + + $response = $this->post('/api/bookmarks', $this->data()); + + $bookmark = Bookmark::first(); + + $this->assertCount(1, Bookmark::all()); + $this->assertEquals('Test Name', $bookmark->name); + $this->assertEquals('Test Description', $bookmark->description); + $this->assertEquals('https://portal.bricooli.fr', $bookmark->url); +// $this->assertEquals('https://portal.bricooli.fr/img/logo.svg', $bookmark->favicon); + + $response->assertStatus(201); + $response->assertJson([ + 'data' => [ + 'type' => 'bookmark', + 'bookmark_id' => $bookmark->id, + 'attributes' => [ + 'data' => [ + 'name' => $bookmark->name, + 'description' => $bookmark->description, + 'url' => $bookmark->url, +// 'favicon' => $bookmark->favicon, + ] + ], + ], + ]); + } + + /** @test */ + public function bookmark_url_are_required() + { + $this->actingAs($user = factory(\App\User::class)->create(), 'api'); + $response = $this->post('/api/bookmarks', array_merge($this->data(), ['url' => ''])); + + $response->assertSessionHasErrors('url'); + $this->assertCount(0, Bookmark::all()); + } + + /** @test */ + public function a_bookmark_can_be_retrieved() + { + $this->actingAs($user = factory(User::class)->create(), 'api'); + $bookmark = factory(Bookmark::class)->create(['user_id' => $user->id]); + + $response = $this->get('/api/bookmarks/' . $bookmark->id ); + + $response->assertJson([ + 'data' => [ + 'bookmark_id' => $bookmark->id, + 'attributes' => [ + 'data' => [ + 'name' => $bookmark->name, + 'url' => $bookmark->url, + 'favicon' => $bookmark->favicon, + 'last_updated' => $bookmark->updated_at->diffForHumans(), + ] + ], + ], + ]); + } + + /** @test */ + public function a_user_can_retrueved_all_this_to_do_lists() + { + $this->actingAs($user = factory(User::class)->create(), 'api'); + $bookmarkOne = factory(Bookmark::class)->create(['user_id' => $user->id]); + $bookmarkTwo = factory(Bookmark::class)->create(['user_id' => $user->id]); + + $response = $this->get('/api/bookmarks'); + + $response->assertJson([ + 'data' => [ + [ + 'data' => [ + 'bookmark_id' => $bookmarkOne->id, + 'attributes' => [ + 'data' => [ + 'name' => $bookmarkOne->name, + 'last_updated' => $bookmarkOne->updated_at->diffForHumans(), + ] + ], + ] + ], + [ + 'data' => [ + 'bookmark_id' => $bookmarkTwo->id, + 'attributes' => [ + 'data' => [ + 'name' => $bookmarkTwo->name, + 'last_updated' => $bookmarkTwo->updated_at->diffForHumans(), + ] + ], + ], + ] + ], + 'links' => [ + 'self' => url('/bookmarks'), + ] + ]); + } + + /** @test */ + public function only_owner_bookmark_can_retrieved_it() + { + $user = factory(User::class)->create(); + $bookmark = factory(Bookmark::class)->create(['user_id' => $user->id]); + + $this->actingAs($userAnother = factory(User::class)->create(), 'api'); + + $response = $this->get('/api/bookmarks/' . $bookmark->id ); + + $response->assertStatus(403); + } + + /** @test */ + public function a_to_bookmark_can_be_patch() + { + $this->withoutExceptionHandling(); + $this->actingAs($user = factory(User::class)->create(), 'api'); + $bookmark = factory(Bookmark::class)->create(['user_id' => $user->id]); + + $response = $this->patch('/api/bookmarks/' . $bookmark->id, [ + 'name' => 'Bookmark Update', + 'url' => 'https://portal.bricooli.fr', + ]); + + $bookmark = $bookmark->fresh(); + + $this->assertEquals('Bookmark Update', $bookmark->name); + $this->assertEquals('https://portal.bricooli.fr', $bookmark->url); + + $response->assertStatus(200); + $response->assertJson([ + 'data' => [ + 'bookmark_id' => $bookmark->id, + 'attributes' => [ + 'data' => [ + 'name' => 'Bookmark Update' + ] + ] + ] + ]); + } + + /** @test */ + public function only_the_owner_can_patch_the_bookmark() + { + $user = factory(User::class)->create(); + $bookmark = factory(Bookmark::class)->create(['id' => 123, 'user_id' => $user->id]); + + $this->actingAs($anotherUser = factory(User::class)->create(), 'api'); + + $this->patch('/api/bookmarks/'. $bookmark->id, ['url' => 'https://portal.bricooli.fr']) + ->assertStatus(403); + } + + /** @test */ + public function a_bookmark_can_be_delete() + { + $this->actingAs($user = factory(User::class)->create(), 'api'); + + $bookmark = factory(Bookmark::class)->create(['user_id' => $user->id]); + + $response = $this->delete('/api/bookmarks/' . $bookmark->id); + + $toDoList = $bookmark->fresh(); + + $this->assertCount(0, Bookmark::all()); + + $response->assertStatus(204); + } + + /** @test */ + public function only_the_owner_can_delete_the_bookmark() + { + $user = factory(User::class)->create(); + $bookmark = factory(Bookmark::class)->create(); + + $this->actingAs($anotherUser = factory(User::class)->create(), 'api'); + + $response = $this->delete('/api/bookmarks/' . $bookmark->id); + + $response->assertStatus(403); + } + + private function data() + { + return [ + 'name' => 'Test Name', + 'description' => 'Test Description', + 'url' => 'https://portal.bricooli.fr', + 'favicon' => 'https://portal.bricooli.fr/img/logo.svg', + ]; + } +}