diff --git a/app/Http/Controllers/Dashboard/LogoutController.php b/app/Http/Controllers/Dashboard/LogoutController.php new file mode 100644 index 00000000..06c765f5 --- /dev/null +++ b/app/Http/Controllers/Dashboard/LogoutController.php @@ -0,0 +1,24 @@ +session()->invalidate(); + + $request->session()->regenerateToken(); + + return redirect()->route("main"); + } +} diff --git a/app/Http/Controllers/Dashboard/PasswordUpdateController.php b/app/Http/Controllers/Dashboard/PasswordUpdateController.php new file mode 100644 index 00000000..eac69cad --- /dev/null +++ b/app/Http/Controllers/Dashboard/PasswordUpdateController.php @@ -0,0 +1,35 @@ +validate([ + "current_password" => ["required", "current_password"], + "password" => ["required", Password::defaults(), "confirmed"], + ]); + + $request->user()->update([ + "password" => Hash::make($validated["password"]), + ]); + + return redirect()->back() + ->with("success", "Zaktualizowano hasło"); + } +} diff --git a/app/Http/Controllers/Public/LoginController.php b/app/Http/Controllers/Public/LoginController.php new file mode 100644 index 00000000..7b0975d7 --- /dev/null +++ b/app/Http/Controllers/Public/LoginController.php @@ -0,0 +1,36 @@ + "https://irg2023.collegiumwitelona.pl/assets/logos/cwup.png", + ]); + } + + public function store(Request $request): RedirectResponse + { + $credentials = $request->only("email", "password"); + + if (Auth::attempt($credentials)) { + $request->session()->regenerate(); + + return redirect()->route("dashboard"); + } + + return back()->withErrors([ + "email" => "Niepoprawne dane logowania", + ]); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 8c889471..dd38acd0 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -10,20 +10,21 @@ class HandleInertiaRequests extends Middleware { - protected $rootView = "app"; - - public function version(Request $request): ?string - { - return parent::version($request); - } - public function share(Request $request): array { return array_merge(parent::share($request), [ + "auth" => $this->getAuthData($request), "flash" => $this->getFlashedData($request), ]); } + protected function getAuthData(Request $request): array + { + return [ + "user" => $request->user() ? $request->user()->only("id", "name", "email") : null, + ]; + } + protected function getFlashedData(Request $request): Closure { return fn(): array => [ diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index cc98d7ba..b31efbb8 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -4,7 +4,6 @@ namespace App\Http\Middleware; -use App\Providers\RouteServiceProvider; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -18,7 +17,7 @@ public function handle(Request $request, Closure $next, string ...$guards): Resp foreach ($guards as $guard) { if (Auth::guard($guard)->check()) { - return redirect(RouteServiceProvider::HOME); + return redirect()->route("dashboard"); } } diff --git a/app/Models/User.php b/app/Models/User.php index 5da4bc28..b86a3d27 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,28 +4,31 @@ namespace App\Models; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Laravel\Sanctum\HasApiTokens; +/** + * @property string $id + * @property string $name + * @property string $email + * @property string $password + * @property Carbon $created_at + * @property Carbon $updated_at + */ class User extends Authenticatable { - use HasApiTokens; use HasFactory; use Notifiable; + use HasUlids; - protected $fillable = [ - "name", - "email", - "password", - ]; + protected $guarded = []; protected $hidden = [ "password", - "remember_token", ]; protected $casts = [ - "email_verified_at" => "datetime", "password" => "hashed", ]; } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index aa9c54bd..ce2707fb 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -4,11 +4,12 @@ namespace Database\Factories; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Support\Str; +use Illuminate\Support\Facades\Hash; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> + * @extends Factory */ class UserFactory extends Factory { @@ -21,20 +22,8 @@ public function definition(): array { return [ "name" => fake()->name(), - "email" => fake()->unique()->safeEmail(), - "email_verified_at" => now(), - "password" => "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", - "remember_token" => Str::random(10), + "email" => fake()->email(), + "password" => Hash::make("password"), ]; } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn(array $attributes) => [ - "email_verified_at" => null, - ]); - } } 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 4ceb0bdf..95e81f47 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -10,12 +10,10 @@ public function up(): void { Schema::create("users", function (Blueprint $table): void { - $table->id(); + $table->ulid("id")->primary(); $table->string("name"); $table->string("email")->unique(); - $table->timestamp("email_verified_at")->nullable(); $table->string("password"); - $table->rememberToken(); $table->timestamps(); }); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6425c0d7..3b04c72f 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,14 +4,13 @@ namespace Database\Seeders; +use App\Models\User; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { - /** - * Seed the application's database. - */ public function run(): void { + User::factory(1)->create(); } } diff --git a/resources/js/Layouts/DashboardLayout.vue b/resources/js/Layouts/DashboardLayout.vue index 830d61e5..8eabce1c 100644 --- a/resources/js/Layouts/DashboardLayout.vue +++ b/resources/js/Layouts/DashboardLayout.vue @@ -17,6 +17,7 @@ import { ClipboardIcon, CodeBracketSquareIcon, Cog6ToothIcon, + LockOpenIcon, } from '@heroicons/vue/24/outline' import { watch } from 'vue' import { useToast } from 'vue-toastification' @@ -37,6 +38,7 @@ const navigation = [ elements: [ { name: 'Dashboard', href: '/dashboard', icon: HomeIcon, current: true }, { name: 'Ustawienia', href: '#', icon: Cog6ToothIcon, current: false }, + { name: 'Aktualizacja hasła', href: '/dashboard/password', icon: LockOpenIcon, current: false }, { name: 'Aktualności', href: '#', icon: NewspaperIcon, current: false }, { name: 'FAQ', href: '#', icon: QuestionMarkCircleIcon, current: false }, { name: 'Formy kontaktu', href: '#', icon: AtSymbolIcon, current: false }, @@ -150,7 +152,7 @@ watch(() => props.flash, (flash) => { - + @@ -172,10 +174,10 @@ watch(() => props.flash, (flash) => { Strona główna - + Wyloguj się - +
diff --git a/resources/js/Layouts/PublicLayout.vue b/resources/js/Layouts/PublicLayout.vue index 2463934a..4adede9f 100644 --- a/resources/js/Layouts/PublicLayout.vue +++ b/resources/js/Layouts/PublicLayout.vue @@ -36,10 +36,11 @@ const mobileMenuOpen = ref(false) @@ -64,7 +65,12 @@ const mobileMenuOpen = ref(false)
- + Dashboard + + Logowanie @@ -86,7 +92,8 @@ const mobileMenuOpen = ref(false)

2023 - keating management system + keating + management system developed at Blumilk

diff --git a/resources/js/Pages/Dashboard/PasswordUpdate.vue b/resources/js/Pages/Dashboard/PasswordUpdate.vue new file mode 100644 index 00000000..1690e0dc --- /dev/null +++ b/resources/js/Pages/Dashboard/PasswordUpdate.vue @@ -0,0 +1,69 @@ + + + diff --git a/resources/js/Pages/Public/Login.vue b/resources/js/Pages/Public/Login.vue new file mode 100644 index 00000000..13651941 --- /dev/null +++ b/resources/js/Pages/Public/Login.vue @@ -0,0 +1,73 @@ + + + diff --git a/routes/web.php b/routes/web.php index 048cad30..b5ce5bbe 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,16 +3,27 @@ declare(strict_types=1); use App\Http\Controllers\Dashboard\DashboardController; +use App\Http\Controllers\Dashboard\LogoutController; +use App\Http\Controllers\Dashboard\PasswordUpdateController; use App\Http\Controllers\Dashboard\StudentController; use App\Http\Controllers\Public\HomeController; +use App\Http\Controllers\Public\LoginController; use App\Http\Controllers\Public\NewsController; use Illuminate\Support\Facades\Route; -Route::get("/", HomeController::class); +Route::get("/", HomeController::class)->name("main"); Route::get("/aktualnosci", NewsController::class); -Route::prefix("dashboard")->group(function (): void { - Route::get("/", DashboardController::class); +Route::middleware("guest")->group(function (): void { + Route::get("/login", [LoginController::class, "create"])->name("login"); + Route::post("/login", [LoginController::class, "store"]); +}); + +Route::middleware("auth")->prefix("dashboard")->group(function (): void { + Route::get("/", DashboardController::class)->name("dashboard"); + Route::get("/password", [PasswordUpdateController::class, "edit"])->name("password.edit"); + Route::patch("/password", [PasswordUpdateController::class, "update"])->name("password.update"); + Route::post("/logout", LogoutController::class); Route::controller(StudentController::class)->group(function (): void { Route::get("/students", "index")->name("students.index"); Route::get("/students/create", "create")->name("students.create"); diff --git a/tests/Feature/LoginTest.php b/tests/Feature/LoginTest.php new file mode 100644 index 00000000..49f6f2ab --- /dev/null +++ b/tests/Feature/LoginTest.php @@ -0,0 +1,68 @@ + "test@example.com", + "password" => Hash::make("password"), + ])->create(); + + $this->actingAs($user); + + $this->assertAuthenticatedAs($user); + + $this->get("/login") + ->assertRedirect("/dashboard"); + } + + public function testUserCanLoginWithProperCredentials(): void + { + $user = User::factory([ + "email" => "test@example.com", + "password" => Hash::make("password"), + ])->create(); + + $this->assertGuest(); + + $this->post("/login", [ + "email" => "test@example.com", + "password" => "password", + ]) + ->assertSessionHasNoErrors() + ->assertRedirect(); + + $this->assertAuthenticatedAs($user); + } + + public function testUserCannotLoginWithWrongCredentials(): void + { + User::factory([ + "email" => "test@example.com", + "password" => Hash::make("password"), + ])->create(); + + $this->assertGuest(); + + $this->post("/login", [ + "email" => "test@example.com", + "password" => "wrong-password", + ]) + ->assertSessionHasErrors("email") + ->assertRedirect(); + + $this->assertGuest(); + } +} diff --git a/tests/Feature/LogoutTest.php b/tests/Feature/LogoutTest.php new file mode 100644 index 00000000..96754b40 --- /dev/null +++ b/tests/Feature/LogoutTest.php @@ -0,0 +1,34 @@ +post("/dashboard/logout") + ->assertRedirectToRoute("login"); + } + + public function testUserCanLogout(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + $this->assertAuthenticatedAs($user); + + $this->post("/dashboard/logout") + ->assertRedirectToRoute("main"); + + $this->assertGuest(); + } +} diff --git a/tests/Feature/PasswordUpdateTest.php b/tests/Feature/PasswordUpdateTest.php new file mode 100644 index 00000000..ae78bbaa --- /dev/null +++ b/tests/Feature/PasswordUpdateTest.php @@ -0,0 +1,59 @@ +user = User::factory([ + "email" => "test@example.com", + "password" => Hash::make("password"), + ])->create(); + } + + /** + * @throws JsonException + */ + public function testUserPasswordCanBeUpdated(): void + { + $response = $this + ->actingAs($this->user) + ->patch("/dashboard/password", [ + "current_password" => "password", + "password" => "new-password", + "password_confirmation" => "new-password", + ]); + + $response->assertSessionHasNoErrors(); + + $this->assertTrue(Hash::check("new-password", $this->user->refresh()->password)); + } + + public function testUserPasswordCannotBeUpdatedWithWrongPassword(): void + { + $response = $this + ->actingAs($this->user) + ->patch("/dashboard/password", [ + "current_password" => "wrong-password", + "password" => "new-password", + "password_confirmation" => "new-password", + ]); + + $response->assertSessionHasErrors("current_password"); + } +} diff --git a/tests/Feature/StudentTest.php b/tests/Feature/StudentTest.php index 7b357f13..7514b8c7 100644 --- a/tests/Feature/StudentTest.php +++ b/tests/Feature/StudentTest.php @@ -5,7 +5,9 @@ namespace Tests\Feature\Frontend; use App\Models\Student; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Tests\TestCase; @@ -13,15 +15,29 @@ class StudentTest extends TestCase { use RefreshDatabase; + protected User $user; + + protected function setUp(): void + { + parent::setUp(); + + $this->user = User::factory([ + "email" => "test@example.com", + "password" => Hash::make("password"), + ])->create(); + } + public function testStudentCanBeCreated(): void { $this->assertDatabaseCount("students", 0); - $this->post("/dashboard/students", [ - "name" => "Name", - "surname" => "Surname", - "index_number" => "12345", - ])->assertSessionHasNoErrors(); + $this + ->actingAs($this->user) + ->post("/dashboard/students", [ + "name" => "Name", + "surname" => "Surname", + "index_number" => "12345", + ])->assertSessionHasNoErrors(); $this->assertDatabaseCount("students", 1); } @@ -36,11 +52,13 @@ public function testStudentCanBeUpdated(): void "index_number" => "12345", ]); - $this->patch("/dashboard/students/{$student->id}", [ - "name" => "Name", - "surname" => "Surname", - "index_number" => "12345", - ])->assertSessionHasNoErrors(); + $this + ->actingAs($this->user) + ->patch("/dashboard/students/{$student->id}", [ + "name" => "Name", + "surname" => "Surname", + "index_number" => "12345", + ])->assertSessionHasNoErrors(); $this->assertDatabaseHas("students", [ "name" => "Name", @@ -54,26 +72,30 @@ public function testStudentCannotBeCreatedWithBusyIndex(): void Student::factory()->create(["index_number" => "12345"]); $this->assertDatabaseCount("students", 1); - $this->post("/dashboard/students", [ - "name" => "Name", - "surname" => "Surname", - "index_number" => "12345", - ])->assertSessionHasErrors("index_number"); + $this + ->actingAs($this->user) + ->post("/dashboard/students", [ + "name" => "Name", + "surname" => "Surname", + "index_number" => "12345", + ])->assertSessionHasErrors("index_number"); $this->assertDatabaseCount("students", 1); } public function testStudentCannotBeCreatedWithInvalidData(): void { - $this->post("/dashboard/students", [ - "name" => Str::random(256), - "surname" => Str::random(256), - "index_number" => Str::random(256), - ])->assertSessionHasErrors([ - "name", - "surname", - "index_number", - ]); + $this + ->actingAs($this->user) + ->post("/dashboard/students", [ + "name" => Str::random(256), + "surname" => Str::random(256), + "index_number" => Str::random(256), + ])->assertSessionHasErrors([ + "name", + "surname", + "index_number", + ]); $this->assertDatabaseCount("students", 0); } @@ -83,7 +105,9 @@ public function testStudentCanBeDeleted(): void $student = Student::factory()->create(); $this->assertDatabaseCount("students", 1); - $this->delete("/dashboard/students/{$student->id}"); + $this + ->actingAs($this->user) + ->delete("/dashboard/students/{$student->id}"); $this->assertDatabaseCount("students", 0); }