diff --git a/app/Entities/Paste.php b/app/Entities/Paste.php index 5cf7ef0..1e6f3da 100644 --- a/app/Entities/Paste.php +++ b/app/Entities/Paste.php @@ -17,11 +17,14 @@ final class Paste extends Model protected $table = self::TABLE_NAME; /** {@inheritdoc} */ - protected $fillable = ["paste_id", "language_id", "slug", "name", "extension", "code", "description"]; + protected $fillable = ["paste_id", "language_id", "slug", "name", "extension", "code", "description", "password"]; /** {@inheritdoc} */ protected $guarded = ["id"]; + /** {@inheritdoc} */ + protected $hidden = ["password"]; + /** {@inheritdoc} */ protected $casts = [ "paste_id" => "integer", @@ -45,6 +48,18 @@ public function getFileNameAttribute() : string return "{$this->name}.{$this->extension}"; } + /** + * Use built-in function to store the password for + * a paste + * + * @param string|null $value + * @return void + */ + public function setPasswordAttribute($value) + { + $this->attributes["password"] = !is_null($value) ? bcrypt($value) : $value; + } + /** * Determine if a paste is a fork * of another one diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 206540a..44b2ea4 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -43,14 +43,4 @@ public function render($request, Exception $exception) { return parent::render($request, $exception); } - - /** {@inheritdoc} */ - protected function unauthenticated($request, AuthenticationException $exception) - { - if ($request->expectsJson()) { - return response()->json(["error" => "Unauthenticated."], 401); - } - - return redirect()->guest(route("login")); - } } diff --git a/app/Http/Handlers/Paste/EditPasteHandler.php b/app/Http/Handlers/Paste/EditPasteHandler.php new file mode 100644 index 0000000..b21008b --- /dev/null +++ b/app/Http/Handlers/Paste/EditPasteHandler.php @@ -0,0 +1,23 @@ +with("paste", $request->paste); + } +} diff --git a/app/Http/Handlers/Paste/UpdatePasteHandler.php b/app/Http/Handlers/Paste/UpdatePasteHandler.php new file mode 100644 index 0000000..9d39e7a --- /dev/null +++ b/app/Http/Handlers/Paste/UpdatePasteHandler.php @@ -0,0 +1,26 @@ +paste; + $paste->update($request->only(["name", "description", "language_id", "extension", "code"])); + + return redirect()->route("paste.show", $paste); + } +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php deleted file mode 100644 index 7e1da91..0000000 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ /dev/null @@ -1,31 +0,0 @@ -check()) { - return redirect("/home"); - } - - return $next($request); - } -} diff --git a/app/Http/Requests/AddPasteRequest.php b/app/Http/Requests/AddPasteRequest.php index d8735e6..53e9791 100644 --- a/app/Http/Requests/AddPasteRequest.php +++ b/app/Http/Requests/AddPasteRequest.php @@ -36,12 +36,19 @@ public function rules() : array "min:3", ], "code" => [ - "required" + "required", + ], + "password" => [ + "sometimes", + "confirmed", ], "language_id" => [ "required", - Rule::exists(Language::TABLE_NAME, "id") - ] + Rule::exists(Language::TABLE_NAME, "id"), + ], + "extension" => [ + "required", + ], ]; } } diff --git a/app/Http/Requests/UpdatePasteRequest.php b/app/Http/Requests/UpdatePasteRequest.php new file mode 100644 index 0000000..5163a40 --- /dev/null +++ b/app/Http/Requests/UpdatePasteRequest.php @@ -0,0 +1,54 @@ + [ + "required", + "min:3", + ], + "code" => [ + "required", + ], + "password" => [ + "required", + new PasswordMatch($this->paste->password), + ], + "language_id" => [ + "required", + Rule::exists(Language::TABLE_NAME, "id"), + ], + "extension" => [ + "required", + ], + ]; + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f96ef13..5c037f7 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -23,7 +23,7 @@ public function boot() Route::model("language", Language::class); Route::bind("paste", function ($slug) { - return Paste::select(["id", "language_id", "paste_id", "user_id", "slug", "name", "extension", "code", "description", "created_at", "updated_at"]) + return Paste::select(["id", "language_id", "paste_id", "user_id", "slug", "name", "extension", "code", "description", "password", "created_at", "updated_at"]) ->where("slug", $slug)->firstOrFail(); }); } diff --git a/app/Rules/PasswordMatch.php b/app/Rules/PasswordMatch.php new file mode 100644 index 0000000..0bc604e --- /dev/null +++ b/app/Rules/PasswordMatch.php @@ -0,0 +1,47 @@ +password = $password; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return Hash::check($value, $this->password); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return "La password non corrisponde."; + } +} diff --git a/database/factories/LanguageFactory.php b/database/factories/LanguageFactory.php index 652b959..2890450 100644 --- a/database/factories/LanguageFactory.php +++ b/database/factories/LanguageFactory.php @@ -3,7 +3,7 @@ /** @var \Illuminate\Database\Eloquent\Factory $factory */ $factory->define(\Wdi\Entities\Language::class, function (Faker\Generator $faker) { return [ - "name" => $faker->unique()->word, + "name" => $faker->lexify("???????"), "extensions" => $faker->randomElements(config("procedural.extensions"), 3), ]; }); diff --git a/database/factories/PasteFactory.php b/database/factories/PasteFactory.php index 71cfaf5..423b6a5 100644 --- a/database/factories/PasteFactory.php +++ b/database/factories/PasteFactory.php @@ -28,3 +28,9 @@ "code" => $faker->paragraph, ]; }); + +$factory->state(\Wdi\Entities\Paste::class, "with-password", function (Faker\Generator $faker) { + return [ + "password" => $faker->password, + ]; +}); diff --git a/database/migrations/2017_11_07_065216_add_password_column_to_pastes_table.php b/database/migrations/2017_11_07_065216_add_password_column_to_pastes_table.php new file mode 100644 index 0000000..9ef0309 --- /dev/null +++ b/database/migrations/2017_11_07_065216_add_password_column_to_pastes_table.php @@ -0,0 +1,34 @@ +string("password")->nullable()->after("description"); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table("pastes", function (Blueprint $table) { + $table->dropColumn("password"); + }); + } +} diff --git a/database/seeds/PastesTableSeeder.php b/database/seeds/PastesTableSeeder.php index 249daa6..a877c98 100644 --- a/database/seeds/PastesTableSeeder.php +++ b/database/seeds/PastesTableSeeder.php @@ -14,13 +14,20 @@ final class PastesTableSeeder extends Seeder */ public function run() { + // Create a password fixed test + factory(\Wdi\Entities\Paste::class)->states("with-password")->create([ + "language_id" => \Wdi\Entities\Language::first()->id, + "name" => "DatabaseFactory", + "password" => "foobar", + ]); + // Create a bunch of anon pastes - factory(\Wdi\Entities\Paste::class, 100)->create([ + factory(\Wdi\Entities\Paste::class, 100)->states("with-password")->create([ "language_id" => \Wdi\Entities\Language::first()->id, ]); - + // Create forks of a single paste - factory(\Wdi\Entities\Paste::class, 10)->create([ + factory(\Wdi\Entities\Paste::class, 10)->states("with-password")->create([ "paste_id" => \Wdi\Entities\Paste::first()->id, ]); } diff --git a/package.json b/package.json index 477e231..0df35d5 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "moment": "^2.19.1", "popper.js": "^1.12.5", "tether": "^1.4.0", - "vue": "^2.5.2", + "vue": "^2.5.3", "vuex": "^2.5.0" }, "devDependencies": { "cross-env": "^5.1.1", - "laravel-mix": "^1.5.0" + "laravel-mix": "^1.6.1" } } diff --git a/resources/assets/sass/_components/_callouts.scss b/resources/assets/sass/_components/_callouts.scss new file mode 100644 index 0000000..401bfd6 --- /dev/null +++ b/resources/assets/sass/_components/_callouts.scss @@ -0,0 +1,40 @@ +// +// Callouts +// + +.bd-callout { + padding: 1.25rem; + margin-top: 1.25rem; + margin-bottom: 1.25rem; + border: 1px solid #eee; + border-left-width: .25rem; + border-radius: .25rem; +} + +.bd-callout h4 { + margin-top: 0; + margin-bottom: .25rem; +} + +.bd-callout p:last-child { + margin-bottom: 0; +} + +.bd-callout code { + border-radius: .25rem; +} + +.bd-callout + .bd-callout { + margin-top: -.25rem; +} + +// Variations +@mixin bs-callout-variant($color) { + border-left-color: $color; + + h4 { color: $color; } +} + +.bd-callout-info { @include bs-callout-variant($bd-info); } +.bd-callout-warning { @include bs-callout-variant($bd-warning); } +.bd-callout-danger { @include bs-callout-variant($bd-danger); } diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss new file mode 100644 index 0000000..c80159f --- /dev/null +++ b/resources/assets/sass/_variables.scss @@ -0,0 +1,9 @@ +// Local variables +$bd-purple: #563d7c !default; +$bd-purple-bright: lighten(saturate($bd-purple, 5%), 15%) !default; +$bd-purple-light: lighten(saturate($bd-purple, 5%), 45%) !default; +$bd-dark: #2a2730 !default; +$bd-download: #ffe484 !default; +$bd-info: #5bc0de !default; +$bd-warning: #f0ad4e !default; +$bd-danger: #d9534f !default; diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 5f2c76f..3157721 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -1 +1,3 @@ @import "vendor"; +@import "variables"; +@import "_components/callouts"; diff --git a/resources/views/layout/base.blade.php b/resources/views/layout/base.blade.php index 3358786..e3e7d32 100644 --- a/resources/views/layout/base.blade.php +++ b/resources/views/layout/base.blade.php @@ -8,7 +8,7 @@ -
+
@include("components.navbar") @include("components.flash-message")
diff --git a/resources/views/paste/create.blade.php b/resources/views/paste/create.blade.php index eeb814a..59e4d23 100644 --- a/resources/views/paste/create.blade.php +++ b/resources/views/paste/create.blade.php @@ -13,19 +13,37 @@
- +
- +
- + +
+ +
+
+
+

Nota sull'impostazione password

+ +

Impostando una password a questo paste sarai in grado di modificarlo in futuro. Ricorda non è possibile recuperare la password o impostarla di nuovo, perciò se te la dimentichi non sarà più possibile effettuare nessuna modifica.

+
+
+
+ + +
+
+ + +
diff --git a/resources/views/paste/edit.blade.php b/resources/views/paste/edit.blade.php new file mode 100644 index 0000000..24bc47a --- /dev/null +++ b/resources/views/paste/edit.blade.php @@ -0,0 +1,39 @@ +@extends("layout.base") + +@section("title", "Wdi Paste - Modifica {$paste->name}") + +@section("container") +

Modifica {{ $paste->name }}

+ + @includeWhen($errors->any(), "components.form-errors") + +
slug) }}" method="post" role="form"> + {{ method_field("PUT") }} + {{ csrf_field() }} + +
+
+ + name) }}"> +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+@endsection diff --git a/resources/views/paste/fork.blade.php b/resources/views/paste/fork.blade.php index 7cf050b..0a3e235 100644 --- a/resources/views/paste/fork.blade.php +++ b/resources/views/paste/fork.blade.php @@ -28,6 +28,24 @@
+
+
+
+

Nota sull'impostazione password

+ +

Impostando una password a questo paste sarai in grado di modificarlo in futuro. Ricorda non è possibile recuperare la password o impostarla di nuovo, perciò se te la dimentichi non sarà più possibile effettuare nessuna modifica.

+
+
+
+ + +
+
+ + +
+
+ @endsection diff --git a/resources/views/paste/show.blade.php b/resources/views/paste/show.blade.php index 751128f..9b35343 100644 --- a/resources/views/paste/show.blade.php +++ b/resources/views/paste/show.blade.php @@ -7,9 +7,14 @@

{{ $paste->name }}.{{$paste->extension}}

- diff --git a/routes/handler.php b/routes/handler.php index c92d657..8aa872e 100644 --- a/routes/handler.php +++ b/routes/handler.php @@ -5,7 +5,7 @@ ListLanguagesHandler, ShowLanguageHandler }; use Wdi\Http\Handlers\Paste\{ - AddForkHandler, AddPasteHandler, CreateForkHandler, CreatePasteHandler, ShowPasteForksHandler, ShowPasteHandler + AddForkHandler, AddPasteHandler, CreateForkHandler, CreatePasteHandler, EditPasteHandler, ShowPasteForksHandler, ShowPasteHandler, UpdatePasteHandler }; Route::get("", HomePageHandler::class)->name("home"); @@ -14,17 +14,17 @@ Route::get("create", CreatePasteHandler::class)->name("paste.create"); Route::post("", AddPasteHandler::class)->name("paste.add"); - Route::prefix("{paste}")->group(function () { Route::get("", ShowPasteHandler::class)->name("paste.show"); Route::get("fork", CreateForkHandler::class)->name("fork.create"); Route::post("fork", AddForkHandler::class)->name("fork.add"); Route::get("forks", ShowPasteForksHandler::class)->name("paste.forks"); + Route::get("edit", EditPasteHandler::class)->name("paste.edit"); + Route::match(["put", "patch"], "edit", UpdatePasteHandler::class)->name("paste.update"); }); }); Route::prefix("languages")->group(function () { Route::get("", ListLanguagesHandler::class)->name("language.list"); - Route::get("{language}", ShowLanguageHandler::class)->name("language.show"); }); diff --git a/tests/Feature/Handlers/Paste/AddForkHandlerTest.php b/tests/Feature/Handlers/Paste/AddForkHandlerTest.php index b930aae..c9e3e18 100644 --- a/tests/Feature/Handlers/Paste/AddForkHandlerTest.php +++ b/tests/Feature/Handlers/Paste/AddForkHandlerTest.php @@ -42,6 +42,8 @@ public function an_user_can_fork_a_paste() "name" => $stub->name, "code" => $stub->code, "description" => $stub->description, + "password" => "foobar", + "password_confirmation" => "foobar", ]) ->assertStatus(Response::HTTP_FOUND) ->assertSessionHas("flash_notification"); @@ -49,6 +51,32 @@ public function an_user_can_fork_a_paste() $this->assertDatabaseHas(Paste::TABLE_NAME, [ "paste_id" => $this->paste->id, "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], + "name" => $stub->name, + "code" => $stub->code, + "description" => $stub->description, + ]); + } + + /** @test */ + public function an_user_can_fork_a_new_paste_without_password() + { + $stub = factory(Paste::class)->states("plain")->make(); + + $this->post("pastes/{$this->paste->slug}/fork", [ + "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], + "name" => $stub->name, + "code" => $stub->code, + "description" => $stub->description, + ]) + ->assertStatus(Response::HTTP_FOUND) + ->assertSessionHas("flash_notification"); + + $this->assertDatabaseHas(Paste::TABLE_NAME, [ + "paste_id" => $this->paste->id, + "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], "name" => $stub->name, "code" => $stub->code, "description" => $stub->description, @@ -75,7 +103,7 @@ public function name_must_be_at_least_3_chars_long_in_order_to_fork_a_paste() { $this->withExceptionHandling(); $stub = factory(Paste::class)->make([ - "name" => str_random(2) + "name" => str_random(2), ]); $this->post("pastes/{$this->paste->slug}/fork", [ @@ -133,4 +161,22 @@ public function language_must_exists_in_order_to_fork_a_paste() ->assertStatus(Response::HTTP_FOUND) ->assertSessionHasErrors(["language_id"]); } + + /** @test */ + public function password_must_be_confirmed_in_order_to_fork_a_paste() + { + $this->withExceptionHandling(); + $stub = factory(Paste::class)->make(); + + $this->post("pastes/{$this->paste->slug}/fork", [ + "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], + "name" => $stub->name, + "code" => $stub->code, + "description" => $stub->description, + "password" => "foobar", + ]) + ->assertStatus(Response::HTTP_FOUND) + ->assertSessionHasErrors(["password"]); + } } diff --git a/tests/Feature/Handlers/Paste/AddPasteHandlerTest.php b/tests/Feature/Handlers/Paste/AddPasteHandlerTest.php index 6040d9b..cba2158 100644 --- a/tests/Feature/Handlers/Paste/AddPasteHandlerTest.php +++ b/tests/Feature/Handlers/Paste/AddPasteHandlerTest.php @@ -39,12 +39,39 @@ public function an_user_can_add_a_new_paste() "name" => $stub->name, "code" => $stub->code, "description" => $stub->description, + "password" => "foobar", + "password_confirmation" => "foobar", ]) ->assertStatus(Response::HTTP_FOUND) ->assertSessionHas("flash_notification"); $this->assertDatabaseHas(Paste::TABLE_NAME, [ "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], + "name" => $stub->name, + "code" => $stub->code, + "description" => $stub->description, + ]); + } + + /** @test */ + public function an_user_can_add_a_new_paste_without_password() + { + $stub = factory(Paste::class)->states("plain")->make(); + + $this->post("pastes", [ + "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], + "name" => $stub->name, + "code" => $stub->code, + "description" => $stub->description, + ]) + ->assertStatus(Response::HTTP_FOUND) + ->assertSessionHas("flash_notification"); + + $this->assertDatabaseHas(Paste::TABLE_NAME, [ + "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], "name" => $stub->name, "code" => $stub->code, "description" => $stub->description, @@ -56,7 +83,7 @@ public function name_is_required_in_order_to_add_a_new_paste() { $this->withExceptionHandling(); $stub = factory(Paste::class)->make(); - + $this->post("pastes", [ "language_id" => $this->language->id, "code" => $stub->code, @@ -71,7 +98,7 @@ public function name_must_be_at_least_3_chars_long_in_order_to_add_a_new_paste() { $this->withExceptionHandling(); $stub = factory(Paste::class)->make([ - "name" => str_random(2) + "name" => str_random(2), ]); $this->post("pastes", [ @@ -83,7 +110,7 @@ public function name_must_be_at_least_3_chars_long_in_order_to_add_a_new_paste() ->assertStatus(Response::HTTP_FOUND) ->assertSessionHasErrors(["name"]); } - + /** @test */ public function code_is_required_in_order_to_add_a_new_paste() { @@ -104,7 +131,7 @@ public function language_is_required_in_order_to_add_a_new_paste() { $this->withExceptionHandling(); $stub = factory(Paste::class)->make(); - + $this->post("pastes", [ "name" => $stub->name, "code" => $stub->code, @@ -129,4 +156,22 @@ public function language_must_exists_in_order_to_add_a_new_paste() ->assertStatus(Response::HTTP_FOUND) ->assertSessionHasErrors(["language_id"]); } + + /** @test */ + public function password_must_be_confirmed_in_order_to_add_a_new_paste() + { + $this->withExceptionHandling(); + $stub = factory(Paste::class)->make(); + + $this->post("pastes", [ + "language_id" => $this->language->id, + "extension" => $this->language->extensions[1], + "name" => $stub->name, + "code" => $stub->code, + "description" => $stub->description, + "password" => "foobar", + ]) + ->assertStatus(Response::HTTP_FOUND) + ->assertSessionHasErrors(["password"]); + } } diff --git a/tests/Feature/Handlers/Paste/EditPasteHandlerTest.php b/tests/Feature/Handlers/Paste/EditPasteHandlerTest.php new file mode 100644 index 0000000..67fd3c3 --- /dev/null +++ b/tests/Feature/Handlers/Paste/EditPasteHandlerTest.php @@ -0,0 +1,26 @@ +create(); + + $this->get("pastes/{$paste->slug}/edit") + ->assertStatus(Response::HTTP_OK) + ->assertViewIs("paste.edit"); + } +} diff --git a/tests/Feature/Handlers/Paste/UpdatePasteHandlerTest.php b/tests/Feature/Handlers/Paste/UpdatePasteHandlerTest.php new file mode 100644 index 0000000..145b0bb --- /dev/null +++ b/tests/Feature/Handlers/Paste/UpdatePasteHandlerTest.php @@ -0,0 +1,85 @@ +paste = factory(Paste::class)->states("with-password")->create([ + "password" => "foobar", + ]); + } + + /** @test */ + public function it_updates_the_current_paste() + { + $this->put("pastes/{$this->paste->slug}/edit", [ + "name" => "foo", + "code" => $this->paste->code, + "language_id" => $this->paste->language->id, + "extension" => $this->paste->extension, + "description" => "bar", + "password" => "foobar", + ]) + ->assertStatus(Response::HTTP_FOUND) + ->assertRedirect(route("paste.show", $this->paste->slug)); + + $this->assertDatabaseHas(Paste::TABLE_NAME, [ + "id" => $this->paste->id, + "name" => "foo", + "description" => "bar", + ]); + } + + /** @test */ + public function password_is_required_in_order_to_edit_a_paste() + { + $this->withExceptionHandling(); + + $this->put("pastes/{$this->paste->slug}/edit", [ + "name" => "foo", + "code" => $this->paste->code, + "language_id" => $this->paste->language->id, + "extension" => $this->paste->extension, + "description" => "bar", + ]) + ->assertStatus(Response::HTTP_FOUND) + ->assertSessionHasErrors(["password"]); + } + + /** @test */ + public function password_must_match_in_order_to_edit_a_paste() + { + $this->withExceptionHandling(); + + $this->put("pastes/{$this->paste->slug}/edit", [ + "name" => "foo", + "code" => $this->paste->code, + "language_id" => $this->paste->language->id, + "extension" => $this->paste->extension, + "description" => "bar", + "password" => "barfoo" + ]) + ->assertStatus(Response::HTTP_FOUND) + ->assertSessionHasErrors(["password"]); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 6dba00f..130d252 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,4 +23,18 @@ protected function setUp() $this->withoutExceptionHandling(); } + + /** + * Handy function to determine the hash status + * between plain text and hashed string + * + * @param string $plain + * @param string $hashed + */ + public function assertHash(string $plain, string $hashed) + { + static::assertTrue( + app("hash")->check($plain, $hashed) + ); + } } diff --git a/tests/Unit/Entities/PasteTest.php b/tests/Unit/Entities/PasteTest.php index 2b1813a..c86d1d7 100644 --- a/tests/Unit/Entities/PasteTest.php +++ b/tests/Unit/Entities/PasteTest.php @@ -41,6 +41,27 @@ public function it_may_displays_file_name_and_extension_altogether() $this->assertEquals("foo.bar", $paste->fileName); } + /** @test */ + public function it_may_have_a_password_for_editing_purpose() + { + $paste = factory(Paste::class)->states("with-password")->make([ + "password" => "foobar" + ]); + + $this->assertNotNull($paste->password); + $this->assertHash("foobar", $paste->password); + } + + /** @test */ + public function it_may_have_a_null_password() + { + $paste = factory(Paste::class)->make([ + "password" => null + ]); + + $this->assertNull($paste->password); + } + /** @test */ public function it_can_tell_if_its_a_fork() {