Building Block: Backend
2021-01-20
Lucas van der Vegt
De back-end is het hart van je web-applicatie. De server verwerkt en bewaart alle informatie op één plek, waardoor meerdere clients van dezelfde data gebruik kunnen maken. De applicatie biedt de mogelijkheid tot het beheren van de content met een CMS.
Het is belangrijk om voor je begint te programmeren na te denken over je applicatie. Je maakt een ontwerp voor de technische uitwerking waarin je beschrijft hoe je functionaliteiten technisch uitwerkt. Je onderzoekt onderdelen die ingewikkeld zijn en verantwoordt de keuzes die je maakt.
Op deze manier creëer je een applicatie waarvan de basis stabiel en robuust is, en waar later zo min mogelijk aan veranderd hoeft te worden. Ook zorgt een goed ontwerp ervoor dat áls er iets verandert of toegevoegd wordt dit kan zonder het systeem helemaal aan te moeten passen.
Informatie moet op een veilige manier opgeslagen worden en toegankelijk zijn. Onderdelen als authenticatie en autorisatie, hashing en controle van de data zijn enkele voorbeelden.
Voorwoord
ZIE SOURCECODE:
https://github.com/LuukFTF/projectchoirapp-alphav2
ZIE DEMO VIDEO:
https://youtu.be/LLtygBa8lsU
Voor dit buildingblock gebruik ik een song rehearsing systeem gemaakt in laravel.
Met deze webapp kan je eigen nummers toevoegen en oefenen voor je band of koor.
ZIE BLOGPOST MET MOOIE OPMAAK https://luukftf.github.io/docfx-portfolio/main/hr-cmgt/buildingblocks/bb-backend.html
Index
1. Ontwerp
Je hebt de database vooraf ontworpen in een ERD (met minimaal een 1-op-veel relatie)
[x] ERD
[x] 1-op-veel relatie
[x] SH technisch ontwerp
1.1 Technisch ontwerp
Mijn webapp is gemaakt in Laravel.
De frontend is met Blade & CSS
De backend is in PHP
De database is met SQL
1.2 Database
1.2.1 Algemeen Ontwerp
Users Organisations Roles Songs Playlists
1.2.3 ERD
(min. 1-op-veel)
erDiagram
USERS }|--|| ORGANISATIONS : I
ORGANISATIONS ||--|{ SONGS : I
ORGANISATIONS ||--|{ PLAYLISTS : I
SONGS }|--|| PLAYLISTS : I
USERS {
int id
int organisationID
int roleID
varchar email
bool isValidated
varchar username
varchar password
varchar firstname
varchar lastname
varchar profile_pictureID_filepath
timestamp created_at
timestamp updated_at
}
ORGANISATIONS {
int id
int uniqueCode
varchar name
timestamp created_at
timestamp updated_at
}
SONGS {
int id
int playlistID
varchar title
varchar artist
varchar cover_pictureID_filepath
varchar song_audioID_filepath
varchar version
date inRepertoireSince
bool activeRepertoire
time duration
timestamp created_at
timestamp updated_at
}
PLAYLISTS {
int id
varchar title
varchar author
date versionDate
varchar cover_pictureID_filepath
bool isPinned
timestamp created_at
timestamp updated_at
}
2. Data
Je hebt een webapplicatie gebouwd waarbij de data uit een database komt die op de server staat. De applicatie bevat CRUD functionaliteit en formulieren zijn voorzien van server side validatie.
- [x] data uit database
- [x] crud functionaliteiten
- [x] formulieren
- [x] server side validatie
- [x] uitleg crud
- [x] uitleg formulieren
- [x] uitleg server side validatie
DofD:
- [x] bewijsmateriaal
- [x] uitleg
- [x] opmaak
- [x] checked
2.1 Database
2.1.1 Connection
Om de database connectie te kunnen maken moet er een sql server draaien lokaal of op een externe hosting. Hiermee verbind je en dan kan de webapplicatie zijn werk doen. Laravel handeld alle dingen af, het enige wat je hoeft te doen is correcte database informatie invullen.
Environment Variables
.env
⋮
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=projectchoirapp
DB_USERNAME=root
DB_PASSWORD=admin
⋮
Je kopieert het .env.example
bestand, haalt het .example
stuk eraf en voegt in dit configuration bestand de correcte informatie van de database in.
Database Assignment
config/database.php
⋮
'default' => env('DB_CONNECTION', 'mysql'),
⋮
'connections' => [
⋮
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
⋮
],
Hier gebruikt laravel de informatie uit de .env
configuration om de database connectie tot stand te leggen.
2.1.2 Migrations
Song Migration
database/migrations/2021_10_01_133032_create_songs_table.php
namespace Database\Factories;
use App\Models\Song;
use Illuminate\Database\Eloquent\Factories\Factory;
class CreateSongsTable extends Migration
{
public function up()
{
Schema::create('songs', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('title');
$table->string('artist')->nullable();
$table->string('cover_pictureID_filepath')->nullable();
$table->string('song_audioID_filepath')->nullable();
$table->date('version')->nullable();
$table->date('inRepertoireSince')->nullable();
$table->boolean('activeRepertoire')->nullable();
$table->time('duration')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('songs');
}
}
In de migration word de tabel voor de songs aangemaakt. In de create function worden alle kolommen aangegeven. Als eerst word aangegeven wat voor soort data een field is, vervolgens komt de naam die bij de kolom hoort. Als laatst word er aangegeven of het leeg mag zijn en of de waarde in die rij uniek moet zijn in de gehele kolom.
De down()
function word gebruikt mocht de hele database leeg gehaald worden of wanneer er versies terug moeten worden gerollbacked.
2.1.3 Seeders
Song Seeder
database/seeders/SongSeeder.php
namespace Database\Seeders;
use App\Models\Song;
use Illuminate\Database\Seeder;
class SongSeeder extends Seeder {
public function run() {
Song::create([
'title' => 'Come As You Are',
'slug' => 'come-as-you-are',
'artist' => 'Nirvana',
'version' => '2015-09-30',
'inRepertoireSince' => '2010-01-01',
'activeRepertoire' => 1,
'duration' => '00:04:15'
]);
⋮
Song::create([
'title' => 'Skyfall',
'slug' => 'skyfall',
'artist' => 'Adele',
'duration' => '00:13:37',
'activeRepertoire' => 1,
'inRepertoireSince' => '2000-01-01',
]);
Song::factory(40)->create();
}
}
De seeders worden aangeroepen bij de development versie van de applicatie. Hierin kan je automatisch testdata toevoegen an de database. In dit geval worden er ongeveer 30 voor ingevulde test nummers aan de database toegevoegd en 40 random generated (dit word aan de hand van factories gedaan)
2.1.4 Factories
Song Factories
database/seeders/SongFactory.php
namespace Database\Factories;
use App\Models\Song;
use Illuminate\Database\Eloquent\Factories\Factory;
class SongFactory extends Factory
{
protected $model = Song::class;
public function definition()
{
return [
'title' => $this->faker->word(),
'slug' => $this->faker->unique()->slug(2),
'artist' => $this->faker->words(2, true),
'version' => $this->faker->date(),
'inRepertoireSince' => $this->faker->date(),
'activeRepertoire' => $this->faker->boolean(),
'duration' => $this->faker->time('00:i:s', '00:10:00'),
];
}
}
De song factory genereert random data, het zorgt ervoor dat per kolom de juiste soort data ingevuld word. Je ziet bijvoorbeeld dan bij slug
een slug van 2 woorden toegevoegd word en bij artist
een random twee letter woord. Bij duration word er een random tijd gegenereerd in een specifiek format onder een bepaalde timeframe.
2.2 Crud Functionaliteiten
2.2.1 Create Song
Create Routers
routes/web.php
⋮
Route::get('/songs/create', [SongController::class, 'create'])->middleware(['editor']);
Route::post('/songs', [SongController::class, 'new'])->middleware(['editor']);
⋮
Dit zijn de routes voor het aanmaken van nieuwe songs, de get
route is voor het verkrijgen van de create song form en de post
route is voor het versturen van de ingevulde nieuwe song data.
Get create song page
controllers/SongController.php
⋮
public function create() {
$favorite = Favorite::all()
->where('user_id', auth()->user()->id);
if ($favorite->count() <= 3) {
return view('likesrequired');
} else {
return view('song/create');
}
}
⋮
Bij deze controller word de create song pagina opgehaald. Om dit te mogen moet je eerst 3 of meer songs hebben geliked, als je daar aan voeldoet wod de song create pagina geladen, mocht het niet voldoen word er een pagina getoond waar op staat dat je minimaal 3 songs moet liken voordat je deze actie mag uitvoeren.
Create song page
views/song/create.blade.php
⋮
<form method="POST" action="/songs">
@csrf
<div class="item">
<label for="title">title</label>
<input id="title" type="text" name="title" value="{{ old('title') }}"/>
</div>
⋮
<div class="item">
<label for="duration">duration</label>
<input id="duration" type="text" name="duration" placeholder="00:00:00" value="{{ old('duration') }}"/>
</div>
<!-- Validation Errors -->
<x-auth-validation-errors class="mb-4" :errors="$errors" />
<div class="item datasubmit-btn">
<button class="btn" type="submit">Submit</button>
</div>
</form>
⋮
Op deze pagina krijg je velden te zien voor elke mogelijk veld van de song data. Dit vul je in en dan klik je op submit om de data te versturen. Mocht er een probleem zijn dan word dit op de pagina getoond in de x-auth-validation-errors
component.
Add new song to database
controllers/SongController.php
⋮
public function new() {
// Server Side Validatie
$attributes = request()->validate([
'title' => 'required|max:255',
'slug' => ['required', 'max:255', Rule::unique('songs', 'slug')],
'artist' => 'required|max:255',
'inRepertoireSince' => 'nullable|date',
'activeRepertoire' => 'boolean',
'version' => 'required|date',
'duration' => 'required|date_format:H:i:s',
]);
if (!isset($attributes['activeRepertoire'])) {
$attributes['activeRepertoire'] = 0;
}
Song::create($attributes);
return redirect('/');
}
⋮
De submit in de view triggered een post methode op /songs
met de bijgevoegde post data. Deze data word gelezen en in een attributes
variabele gestopt, hier word het direct gevalideerd. Vervolgens word het attribute activeRepertoire
naar 0
gezet mocht er niks ingevuld zijn.
Dan word de nieuwe song aan gemaakt via eloquent vanuit de attributes
en word de pagina naar het overzicht geredirect.
2.2.2 Song Index
Song index router
routes/web.php
⋮
Route::get('/songs', [SongController::class, 'index'])->middleware(['auth']);
⋮
Om bij de lijst van songs te komen moet je naar de uri /songs
gaan.
Get Song Index
controllers/SongController.php
⋮
public function index() {
return view('songs', [
'songs' => Song::filter(request(['search', 'activeRepertoire']))->get(),
⋮
]);
}
⋮
Met deze function word de song overzicht pagina aangeroepen. De song data word meegegeven in het variabele song
. Deze datalijst word ook gefilterd aan de hand van de search query en de kolom activeRepertoire
.
Song Index Page
views/songs.blade.php
⋮
@foreach ($songs as $song)
<div class="song flexrow">
<a class="clickable flexrow" href="/song/{{ $song->id }}">
<div class="left">
<div class="cover"></div>
<h2>{{ $song->title }}</h2>
<h3><i>Song</i> : {{ $song->artist }}</h3>
</div>
<div class="right flexrow">
@if ($song->activeRepertoire == 1)
<div class="item">
<h5><i>Repertoire</i></h5>
<h4> {{ date('Y', strtotime($song->inRepertoireSince)) }} </h4>
</div>
@endif
</div>
</a>
⋮
</div>
@endforeach
⋮
Alle songs die binnen de filter requirements vallen word op deze pagina getoond. Dit gebeurd door de @foreach
loop, hierin word voor elk song een bepaald stukje html geloopt. Hierin word de informatie title
, artist
en inRepertoireSince
mee weergegeven. De inRepertoireSince
data word alleen weergegeven wanneer activeRepertoire
een true
waarde heeft, dit krijg je voor elkaar door de @if
guard class te gebruiken.
2.2.3 Read Song
Song Detail Page
routes/web.php
⋮
Route::get('/song/{song:id}', [SongController::class, 'details'])->middleware(['auth']);
⋮
Dit is de route voor het verkrijgen van de detail pagina van een nummer. Hierbij kan je een id opgeven in de url, bijvoorbeeld: /song/2
dan krijg je het nummer met id 2
.
Song Detail Controller
controllers/SongController.php
⋮
public function details(Song $song) {
return view('song/details', [
'song' => $song
]);
}
⋮
Met deze function word de song detail page opgevraagd, hierbij word het eerder meegegeven id gebruikt om de juiste song in te verkrijgen en mee te sturen bij de aanvraag van de view song/details
Song Detail Page View
views/song/details.blade.php
⋮
<div class="item">
<h3>{{ $song->title }} </h3>
<p>{{ $song->id }}</p>
</div>
⋮
<div class="item">
<h4>{{ $song->updated_at }}</h4>
<p>updated_at</p>
</div>
@can('editor+')
<div class="webcontainer">
<div class="fab-container flex-row">
<a class="fab-btn btncontent-edit" href="/song/{{ $song->id }}/edit"></a>
</div>
</div>
@endcan
⋮
Op deze pagina worden alle gegevens van de specifiek gekozen song weergegeven. Als je de goede bevoegdheden hebt kan je ook het edit knoppen aanklikken om deze song aan te passen.
2.2.4 Update Song
Update Song Routers
routes/web.php
⋮
Route::get('/song/{song:id}/edit', [SongController::class, 'edit'])->middleware(['editor']);
Route::patch('/song/{song:id}', [SongController::class, 'update'])->middleware(['editor']);
Route::patch('/song/{song:id}/activeRepertoire', [SongController::class, 'updateActiveRepertoire'])->middleware(['editor']);
⋮
De get
router kan de edit pagina van een song opvragen en de eerste patch
route verstuurt de ingevulde veranderde data. Met de tweede patch
route kan je enkel het updateActiveRepertoire
veld aanpassen. Hierbij moet het song:id
meegegeven worden in de uri.
Song Edit Page Controller
controllers/SongController.php
⋮
public function edit(Song $song) {
return view('song/edit', [
'song' => $song
]);
}
⋮
Met deze controller word de edit song page opgehaald. Hierin word de specifieke song meegestuurd in de $song
variabele die in de uri meegegeven is.
Song Edit Page View
views/edit.blade.php
⋮
<form method="POST" action="/song/{{ $song->id }}">
@csrf
@method('PATCH')
<div class="item">
<label for="title">title</label>
<input id="title" type="text" name="title" value="{{ $song->title }}"/>
</div>
⋮
<div class="item">
<label for="duration">duration</label>
<input id="duration" type="text" name="duration" placeholder="00:00:00" value="{{ $song->duration }}"/>
</div>
<!-- Validation Errors -->
<x-auth-validation-errors class="mb-4" :errors="$errors" />
<div class="item datasubmit-btn">
<button class="btn" type="submit">Submit</button>
</div>
</form>
@can('admin+')
<form method="POST" action="/song/{{ $song->id }}">
@csrf
@method('DELETE')
<button class="btn secondary" type="submit">Delete</button>
</form>
@endcan
⋮
Op deze pagina staan de formvelden voor het aanpassen van een song, de gegevens die momenteel in de database staat worden geprefilled. Hier is ook een knop delete waarmee deze specifieke song verwijderd kan worden.
Song Update Controller
controllers/SongController.php
⋮
public function update(Song $song) {
$attributes = request()->validate([
// Server Side Validatie
'title' => 'required|max:255',
'slug' => ['required', Rule::unique('songs', 'slug')->ignore($song->id)],
'artist' => 'required|max:255',
'inRepertoireSince' => 'nullable|date',
'activeRepertoire' => 'boolean',
'version' => 'required|date',
'duration' => 'required|date_format:H:i:s',
]);
if (!isset($attributes['activeRepertoire'])) {
$attributes['activeRepertoire'] = 0;
}
$song->update($attributes);
return redirect('/song/'.$song->id);
}
⋮
Wanneer je bij de view op submit klikt word de update controller aangeroepen, hierin word de request data opgehaald en gevalideerd. Vervolgens het veld activeRepertoire
naar 0
gezet mocht er geen data aanwezig zijn. Dan word de specifieke song geupdate met de waardes die in het $attributes
array staan. Als laatst word de pagina geredirect naar de song detail view page.
Active Repertoire
Active Repertoire component on song index
views/songs.blade.php
⋮
<section class="songs flex">
@foreach ($songs as $song)
<div class="song flexrow">
⋮
@can('editor+')
<form class="item" method="POST" action="/song/{{ $song->id }}/activeRepertoire">
@csrf
@method('PATCH')
<input type="checkbox" id="activeRepertoire" name="activeRepertoire" @if ($song->activeRepertoire == 1) checked @endif value="1" onchange="this.form.submit()"/>
</form>
@endcan
⋮
</div>
@endforeach
</section>
⋮
In de song index view is er een active repertoire onderdeel die laat zien of dit nummer momenteel in het repertoire zit en sinds wanneer. Als je geautoriseerd ben om dit aan te passen verschijnt er ook een checkbox, als deze aangevinkt word gaat de status van de song naar het actieve repertoire.
Active Repertoire Update
controllers/SongController.php
⋮
public function updateActiveRepertoire(Song $song){
$attributes = request()->validate([
'activeRepertoire' => 'boolean',
]);
if (!isset($attributes['activeRepertoire'])) {
$attributes['activeRepertoire'] = 0;
}
$song->update($attributes);
return redirect()->back();
}
⋮
Deze controller word uitgevoerd zodra de checkbox veranderd van status. Eerst word gevalideerd of de data daadwerkelijk een boolean waarde is. Wanneer het niet gezet is word er een waarde van 0
gegeven aan het activeRepertoire
veld. Vervolgens word dit geupdate naar de specifieke song.
2.2.5 Delete Song
Song Delete Router
routes/web.php
⋮
Route::delete('/song/{song:id}', [SongController::class, 'delete'])->middleware(['admin']);
⋮
Dit is de route die aangevraagd word wanneer er een song verwijderd moet worden.
Delete Button on edit screen
⋮
<section class="tabledetails">
⋮
@can('admin+')
<form method="POST" action="/song/{{ $song->id }}">
@csrf
@method('DELETE')
<button class="btn secondary" type="submit">Delete</button>
</form>
@endcan
</section>
⋮
Bij elk edit view is er een delete
button aanwezig, hiermee kan je een delete aanvragen op een specifieke song. Dit word gedaan door een post met delete method te versturen naar /song/id
, hiermee word aangegeven welke song er verwijderd moet worden.
Delete Controller
controllers/SongController.php
⋮
public function delete(Song $song) {
$song->delete();
return redirect('/');
}
⋮
Deze controller verwijderd de song die meegegeven is via de form request. Daarna word de pagina geredirect naar de homepage.
2.3 Server Side Validatie
Songs Validation
controllers/SongController.php
⋮
$attributes = request()->validate([
// Server Side Validatie
'title' => 'required|max:255',
'slug' => ['required', 'max:255',Rule::unique('songs', 'slug')->ignore($song->id)],
'artist' => 'required|max:255',
'inRepertoireSince' => 'nullable|date',
'activeRepertoire' => 'boolean',
'version' => 'required|date',
'duration' => 'required|date_format:H:i:s',
]);
⋮
Bij elke controller die iets toevoegt in de database, word elk input veld gevalideerd.
title
moet aanwezig zijn en mag maximaal 255 characters bevatten.
slug
is verplicht, mag maximaal 255 characters bevatten en moet uniek zijn (dit word gecheckt door middel van de Rule::unique()
function, hierien word de tabel en de kolom meegegeven. Negeert hij zichzelf, mocht dat niet aangepast worden)
artist
is verplicht en mag maximaal 255 characters bevatten.
inRepertoireSince
is niet verplicht, maar moet wel een datum zijn.
activeRepertoire
moet altijd een 1
of 0
zijn (oftewel false
of true
)
version
is een verplicht veld en moet een datum zijn.
duration
is verplicht en moet een datum zijn in het speciefieke format: uur:minuut:seconden (bijvoorbeeld: 10:30:24)
View Validation Errors
views/song/create.blade.php
||update.blade.php
⋮
<x-auth-validation-errors class="mb-4" :errors="$errors" />
⋮
Wanneer een van de validatie criteria niet behaald word, toont dit component een response met de bijbehorende validatie foutmelding.
3. Beveiliging
Een deel van je website is niet toegankelijk zonder in te loggen. Al deze pagina's zijn beveiligd tegen deeplinken. Je hebt de webapplicatie beveiligd tegen XSS en SQL-injecties. Wachtwoorden worden veilig opgeslagen. Van deze onderdelen kan je uitleggen waarom de beveiliging wordt toegepast.
- [x] authenticatie
- [x] login shielding (middleware)
- [x] autorisatie (middleware)
- [x] beveiligd tegen deeplinken
- [x] beveiligd tegen xss
- [x] beveiligd tegen sql injections
- [x] password hashing
- [x] waarom word deze beveiliging toegepast
DofD:
- [x] bewijsmateriaal
- [x] uitleg
- [x] opmaak
- [x] checked
3.1 Authenticatie
De authenticatie is opgedeeld in 3 hoofddelen:
- Login: voor het inloggen op een bestaand account
- Register: voor het aanmaken van een nieuwe account
- Logout: hiermee kan je de login sessie beëindigen
3.1.1 Login
Login Routes
routes/auth.php
Route::get('/login', [AuthenticatedSessionController::class, 'create'])->middleware('guest');
Route::post('/login', [AuthenticatedSessionController::class, 'store'])->middleware('guest');
Wanneer je een get
request doen naar /login
krijg je de pagina waarop je kan inloggen. (Via de url) Maar wanneer er een post
request gedaan word naar /login
word de authenticatie uitgevoerd met de bijgevoegde data.
Login View Controller
Controllers/Auth/AuthenticatedSessionController.php
namespace App\Http\Controllers\Auth;
⋮
class AuthenticatedSessionController extends Controller {
public function create() {
return view('login');
}
⋮
}
Wanneer het get
request binnenkomt op de /login
route word de create
function aangeroepen op de AuthenticatedSessionController
. Dit returned de view login
.
Login View
views/auth/login.blade.php
⋮
<x-slot name="logo">
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</x-slot>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<!-- Validation Errors -->
<x-auth-validation-errors class="mb-4" :errors="$errors" />
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email Address -->
<div>
<x-label for="email" :value="__('Email')" />
<x-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
</div>
<!-- Password -->
<div class="mt-4">
<x-label for="password" :value="__('Password')" />
<x-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
</div>
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" name="remember">
<span class="ml-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 hover:text-gray-900" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
<x-button class="ml-3">
{{ __('Log in') }}
</x-button>
</div>
</form>
⋮
Bij de login view kan je een email en password invullen en deze submitten met een post
request. Ook heb je drie extra opties die relevant zijn aan het inloggen: je kan op register
klikken die je naar de account registreer pagina stuurt. Er kan ook op remember me
geklikt worden en dan blijf de login sessie actief als de browser afgesloten word. Daarnaast kan je ook op forgot your password?
klikken mocht je geen toegang meer kunnen krijgen tot je account.
Login Store Controller
Controllers/Auth/AuthenticatedSessionController.php
namespace App\Http\Controllers\Auth;
⋮
class AuthenticatedSessionController extends Controller {
⋮
public function store(LoginRequest $request) {
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
⋮
}
Bij de AuthenticatedSessionController
word de functie store
aangeroepen, die via de ingebouwde laravel sessie functies een secure sessie starten. En vervolgens stuurt het je naar de home route, wat in dit geval de muziek library is. Je kan niet op deze pagina komen als je niet ingelogd ben, dus mocht er iets niet goed zijn gegaan word er geredirect naar de login pagina.
3.1.2 Register
Register Routes
routes/auth.php
Route::get('/register', [RegisteredUserController::class, 'create'])->middleware('guest');
Route::post('/register', [RegisteredUserController::class, 'store'])->middleware('guest');
Dit zijn de register routes, wanneer er een get
request word gedaan naar /register
word de create
function op de RegisteredUserController
opgevraagd. Wanneer er een post
request word gedaan naar /register
dan word de store
functie aangeroepen en de ingevulde data meegegeven.
register view controller
Controllers/Auth/RegisteredUserController.php
namespace App\Http\Controllers\Auth;
⋮
class RegisteredUserController extends Controller {
public function create(){
return view('register');
}
⋮
}
Deze function roep de view register
aan.
Register View
views/auth/register.blade.php
⋮
<!-- Validation Errors -->
<x-auth-validation-errors class="mb-4" :errors="$errors" />
<form method="POST" action="{{ route('register') }}">
@csrf
<!-- Name -->
<div>
<x-label for="username" :value="__('Username')" />
<x-input id="username" class="block mt-1 w-full" type="text" name="username" :value="old('username')" required autofocus />
</div>
⋮
<!-- Confirm Password -->
<div class="mt-4">
<x-label for="password_confirmation" :value="__('Confirm Password')" />
<x-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 hover:text-gray-900" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<x-button class="ml-4">
{{ __('Register') }}
</x-button>
</div>
</form>
⋮
Dit is de register view, hierin kan je de account data invullen van je nieuwe account. Vervolgens klik je op de submit
button en word deze informatie met een post
request meegestuurd naar een store controller. Op deze pagina is er ook een password confirm
die overeen moet komen anders komt er later een validatie error. Vanaf hier is er ook een button terug te vinden naar de login pagina.
Register Store Controller
Controllers/Auth/RegisteredUserController.php
namespace App\Http\Controllers\Auth;
⋮
class RegisteredUserController extends Controller {
⋮
public function store(Request $request) {
$request->validate([
'username' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'username' => $request->username,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(RouteServiceProvider::HOME);
}
}
Op de RegisteredUserController
is er een function store
die het aanmaken van een nieuw account afhandeld. Alle opgegeven informatie word gevalideerd en zal een client error aangeven wanneer er hier iets niet voldoet. Als alles correct is word de informatie in een nieuwe rij in de database gezet, het wachtwoord word eerst gehashed volgens de nieuwste hashing methode. Vervolgens word de sessie aangemaakt en word de user naar de home route verstuurd.
3.1.3 Logout
Logout Routes
routes/auth.php
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])->middleware('auth');
Route::get('/logout', [AuthenticatedSessionController::class, 'destroy'])->middleware('auth');
Om uit te loggen kan je zowel een post
als get
request versturen naar /logout
, deze routes vragen beide dezelfde destroy
function aan.
Logout Controller
Controllers/Auth/AuthenticatedSessionController.php
namespace App\Http\Controllers\Auth;
⋮
class AuthenticatedSessionController extends Controller {
⋮
public function destroy(Request $request) {
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
}
Op de AuthenticatedSessionController
word de function destroy
aangeroepen en deze beëindigd de session en redirect je naar de loginpagina.
3.2 Autorisatie
3.2.1 Login Shielding (middleware guest vs auth/+)
Middleware logged in vs logged out examples in routes
routes/auth.php
|| web.php
⋮
Route::get('/register', [RegisteredUserController::class, 'create'])->middleware('guest'):
Route::post('/register', [RegisteredUserController::class, 'store'])->middleware('guest');
Route::get('/login', [AuthenticatedSessionController::class, 'create'])->middleware('guest');
Route::post('/login', [AuthenticatedSessionController::class, 'store'])->middleware('guest');
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])->middleware('auth');
Route::get('/logout', [AuthenticatedSessionController::class, 'destroy'])->middleware('auth');
⋮
Route::get('/', [SongController::class, 'library'])->middleware(['auth']);
Route::get('/songs', [SongController::class, 'index'])->middleware(['auth']);
⋮
Sommige routes kan je niet accessen wanneer je niet ingelogd ben of wanneer je wel ingelogd ben.
Dit word geregeld via zo geheten middleware
. In dit geval heb je de auth
en guest
middleware. auth
houd in dat je alleen bij deze route kan komen wanneer je een valide session hebt, oftewel ingelogd ben. guest
houd in dat je alleen bij deze route kan als je geen actieve session hebt, zo kan je als ingelogde persoon nooit bij de login pagina terecht komen.
3.2.2 Role Autorisatie (middleware editor/mod/admin)
3.2.2.1 Roles
Bij deze webapp zijn er de volgende roles:
- sysadmin [99] (superuser, deze gebruiker heeft admin in alle organisaties)
- admin [89] (Administrator binnen een bepaalde organisation)
- mod [81] (De moderator beheerd gebruikers en content)
- editor [80] (De editor managed de content)
- user/member [20] (De member is een reguliere gebruiker binnen een organisatie)
- visitor [10] (Een bezoeker heeft gelimiteerde view rechten binnen een organisatie)
- guest/sessionless [00] (uitgelogd, zonder sessie)
Deze rollen hebben binnen de applicatie verschillende rechten waar ze bij mogenen wat ze mogen aanpassen, dit is geregeld met middleware
en guard classes
.
3.2.2.2 Middleware
Middleware roles examples in routes
routes/web.php
⋮
Route::get('/songs', [SongController::class, 'index'])->middleware(['auth']);
Route::get('/song/{song:id}', [SongController::class, 'details'])->middleware(['auth']);
Route::get('/song/{song:id}/edit', [SongController::class, 'edit'])->middleware(['editor']);
Route::patch('/song/{song:id}', [SongController::class, 'update'])->middleware(['editor']);
Route::get('/songs/create', [SongController::class, 'create'])->middleware(['editor']);
⋮
Route::get('/modpanel/users', [UserController::class, 'index'])->middleware(['mod']);
Route::get('/modpanel/user/{user:id}', [UserController::class, 'details'])->middleware(['mod']);
Route::get('/modpanel/user/{user:id}/edit', [UserController::class, 'edit'])->middleware(['admin']);
Route::patch('/modpanel/user/{user:id}', [UserController::class, 'update'])->middleware(['admin']);
⋮
Op elke route is een middleware van kracht. In dit geval zie je dat elke ingelogde gebruiker bij de route /song
en /song/{song:id}
kan, maar alleen mensen hoger dan editer kunnen ook daadwerkelijk deze song aanpassen. Zo is een patch
request op '/song/{song:id}' beveiligd met de editor
middleware. Ook kunnen bijvoorbeeld alleenmaar de mensen hoger dan een moderator bij het gebruikersbeheer, maar hebben enkel de admins rechten om gebruikers aan te passen.
Admin Middleware
Middleware/MustBeAdmin.php
⋮
class MustBeAdmin {
public function handle(Request $request, Closure $next) {
if (auth()->user()->roleID < 89 ) {
abort(403);
}
return $next($request);
}
}
Bij de middleware admin
word de handle
function van de MustBeAdmin
class aangeroepen. Deze function checkt of een gebruiker (in dit geval) een roleID
hoger heeft dan 89
. Dit houd in dat alleen admins
en superadmins
hier rechten voor hebben, mocht dit er niet aan voeldoen word de function aborted.
Moderator+ Middleware
Middleware/MustBeModOrHigher.php
⋮
class MustBeModOrHigher {
public function handle(Request $request, Closure $next) {
if (auth()->user()->roleID < 81 ) {
abort(403);
}
return $next($request);
}
}
Deze function werkt hetzelfde als de admin, alleen is hier de waarde 81
, dit houd in dat alleen mods
, admins
en superadmins
rechten hebben.
Editor+ MIddleware
Middleware/MustBeEditorOrHigher.php
⋮
class MustBeEditorOrHigher {
public function handle(Request $request, Closure $next) {
if (auth()->user()->roleID < 80 ) {
abort(403);
}
return $next($request);
}
}
Weer zo'n function alleen kunnen hier nog meer roles accessen, namelijk editors
, mods
,admins
en superadmins
.
Middelware quickacces in router
Kernel.php
⋮
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'admin' => \App\Http\Middleware\MustBeAdmin::class,
'mod' => \App\Http\Middleware\MustBeModOrHigher::class,
'editor' => \App\Http\Middleware\MustBeEditorOrHigher::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
⋮
];
⋮
Deze middleware classes worden gelinkt aan een simpele middleware route die in de routes aangeroepen kan worden.
3.2.3 Guard Classes
Song Detail Page View
views/song/details.blade.php
⋮
@can('editor+')
<div class="webcontainer">
<div class="fab-container flex-row">
<a class="fab-btn btncontent-edit" href="/song/{{ $song->id }}/edit"></a>
</div>
</div>
@endcan
⋮
In dit geval kunnen alleen de editors en mensen boven die rol de edit button zien. Ook kunnen de normale gebruikers niet zomaar naar /song/{{ $song->id }}/edit
broswen want dat is beveiligd tegen deeplinken.
Song Edit Page View
views/edit.blade.php
⋮
@can('admin+')
<form method="POST" action="/song/{{ $song->id }}">
@csrf
@method('DELETE')
<button class="btn secondary" type="submit">Delete</button>
</form>
@endcan
⋮
Bij dit scenerio kunnen alleen de admins een song verwijderen.
3.2.4 403 Error (deeplink protection)
Error 403
Middleware/*.php
⋮
if (...) {
abort(403);
}
return $next($request);
⋮
Wanneer een gebruiker niet voldoet aan de authorisatie van een bepaalde middleware word de pagina niet geladen en word er geredirect naar een error 403: forbidden
pagina. Deze error houd in dat de gebruiker niet geautoriseerd is om op deze pagina te komen. Ook als je de url weet van een specifieke pagina en je probeer daar heen te gaan als niet bevoegde gebruiker, krijg je deze niet te zien. Dit is dus de protectie tegen deeplinken.
3.3 Security
3.3.1 XSS & SQL Injection Protection
Vanilla PHP
XSS Escaping
https://www.php.net/manual/en/function.htmlspecialchars.php
htmlspecialchars($string, ENT_QUOTES, 'UTF-8')
Om in vanilla PHP een input te filteren op unsafe characters die in html en css dingen kunnen die niet horen gebruik je htmlspecialchars()
. Bij dingen die niet horen moet je denken aan html code ergens uitvoeren wat als plain code hoort en bijvoorbeeld cross site scripting.
SQL Injection Protection
https://www.php.net/mysql_real_escape_string
mysql_real_escape_string($string)
Om te zorgen dat een user input veld nooit door sql gelezen kan worden en iets doet wat niet hoort doe je eerst de ingevulde userdata door de function mysql_real_escape_string()
. Dit maakt sql injections niet meer mogelijk.
Laravel Escaping
In laravel word alle data in de views standaard geescaped op alle bekende vulnerabilities. Dit gebeurd wanneer je de {{ }}
statements gebruikt in de blade files. Ook worden alle inputs standaard geescaped op bijvoorbeeld sql injections, dit word geregeld door eloquent
Als je wilt dat het niet geescaped word moet je {! !}
gebruiken.
Daarom heb ik in de applicatie vrijwel altijd {{ }}
gebruikt en heb ik met eloquent gewerkt
https://laravel.com/docs/8.x/blade#displaying-unescaped-data https://auth0.com/blog/why-laravel-is-the-recommended-framework-for-secure-mission-critical-applications/
3.3.2 Password Hashing
Vanilla PHP
BCRYPT
https://www.php.net/manual/en/function.password-hash.php
password_hash($password, PASSWORD_BCRYPT)
Wanneer er een nieuw account aangemaakt word er via het CRYPT_BLOWFISH
algoritme het wachtwoord gehashed en opgeslagen in de database. Wanneer er vervolgens ingelogd word, word het ingevulde wachtwoord gehashed volgens hetzelfde algoritme en vergeleken met de hash in de database. Wanneer deze overeenkomen kan er ingelogd worden.
MD5 & SHA1
De md5()
en sha1()
algoritmes zijn niet meer secure aangezien deze een simpeler algoritme hebben en al langer bestaan waardoor het makkelijker te bruteforcen is.
https://www.php.net/manual/en/faq.passwords.php#faq.passwords.fasthash
Laravel
In laravel heeft eloquent een hasing tool ingebouwd. Deze word in mijn applicatie gebruikt.
Register Store Controller
Controllers/Auth/RegisteredUserController.php
⋮
$user = User::create([
⋮
'password' => Hash::make($request->password),
]);
⋮
3.3.3 General Security
CSRF
https://laravel.com/docs/8.x/csrf
https://en.wikipedia.org/wiki/Cross-site_request_forgery
Alle forms in Laravel zijn protected tegen cross-site request forgery, dit word gedaan door de guard class @csrf
toe te voegen aan een form. Zonder deze guard class werken ze niet en zal er een error teruggestuurd worden.
Create song page
views/song/create.blade.php
⋮
<form method="POST" action="/songs">
@csrf
⋮
</form>
⋮
4. Gestructureerd bouwen
Functies en kritieke onderdelen in code (onderdelen die niet in één oogopslag te verklaren zijn) zijn voorzien van comments. De logica en presentatie zijn gescheiden. Variabelen en functies zijn in het Engels en hebben beschrijvende namen. Je bent je bewust van code conventies van de programmeertaal waarin je werkt.
- [x] functies en kritieke onderdelen hebben comments
- [x] logica en presentatie zijn gescheiden
- [x] variabelen en functies in het engels
- [x] variabelen en functies hebben beschrijvende namen
- [x] code conventie (in de programmeertaal, php laravel)
DofD:
- [x] bewijsmateriaal
- [x] uitleg
- [x] opmaak
- [x] checked
4.1 MVC Structuur
De Flowchart van mijn laravel webapp structuur.
flowchart LR
subgraph database["Database (sql)"]
direction LR
migrations["Migrations (php)"]
factories["Factories (php)"]
seeders["Seeders (php)"]
factories--->seeders-->migrations
end
models["Models (php)"]
eloquent["Eloquent (laravel)"]
routes["Routes (php)"]
controllers["Controllers (php)"]
middleware["Middleware (php)"]
subgraph views["Views (blade)"]
direction LR
layouts["Layouts (blade)"]
components["Components (blade)"]
styling["Styling (css)"]
end
database<--->eloquent
models<--->eloquent
eloquent<--->controllers
routes--->controllers
controllers--->views
views--->routes
middleware---views
middleware---routes
Dit schema legt uit hoe in mijn webapp de logica gescheiden is van het stukje presentatie. Hier volgt de uitleg hoe de applicatie een paar acties uitvoerd: Database & Eloquent
- Om data te kunnen tonen moet er als eerst data aanwezig zijn. Om dit aan te maken run je de migration van de database
php artisan migrate fresh seed
. - Via de
migrations
worden de bijbehorende tabbellen aangemaakt en word de seeder opgevraagd om de database te vullen. Met deseeders
word er voorgeschreven testdata toegevoegd en vervolgens worden defactories
aangeroepen. - De
factories
creeeren random gegenereerde data. - Vervolgens moet er een bijbehorende model aangemaakt worden zodat
eloquent
zijn werk perfect kan uitvoeren. Song Get Request - Er komt een
get
request binnen op de/songs
route
, de router checkt eerst of de gebruiker authenticatie heeft via demiddleware
en roept de bijbehorendecontroller
aan. - Deze
controller
haalt de data van alle songs op en geeft deze mee aan eenview
. - De view toont alle meegegeven data en laat
components
op de pagina zijn aan de hand van demiddleware
. - De topbar en navbar komen van
layout
en destyling
komt uit een algemeencss
bestand
4.2 Comments
Voorbeelden:
Song Controller
Controllers/SongController.php
⋮
/**
* Song Controller
*
* @param function library() get song library view
* @param function index() get song index view
* @param function details() get song details view
* @param function create() get create new song view
* @param function new() add new song data to database
* @param function edit() get edit song page
* @param function update() update new song data to database
* @param function updateActiveRepertoire() update activeRepertoire field on song
* @param function fav() if no favorite exists > add new favorite
* @param function delete() delete song
*/
class SongController extends Controller {
⋮
}
User Controller
Controllers/UserController.php
⋮
class UserController extends Controller{
⋮
/**
* get user details view
*
* @param User $user model class
*/
public function details(User $user) {
⋮
}
/**
* get create new user view
*
*/
public function create() {
⋮
}
/**
* add new user data to database
*
*/
public function new() {
⋮
}
/**
* get edit user page
*
* @param User $user model class
*/
public function edit(User $user) {
⋮
}
⋮
}
routes/web.php
⋮
/**
* Song Routes
*
*/
/**
* Overview
*/
Route::get('/', [SongController::class, 'library'])->middleware(['auth']);
Route::get('/songs', [SongController::class, 'index'])->middleware(['auth']);
/**
* Details
*/
Route::get('/song/{song:id}', [SongController::class, 'details'])->middleware(['auth']);
⋮
/**
* Favorite
*/
Route::get('/song/{song_id}/fav', [SongController::class, 'fav'])->middleware(['auth']);
/********/
/**
* Setting Routes
*/
Route::get('/settings', function () {
return view('settings');
})->middleware(['auth']);
Route::get('/settings/account', function () {
return view('settings/account');
})->middleware(['auth']);
⋮
4.3 Benamingen van Classes, Functies, Variabelen
4.3.1 Classes
Song Controller
SongController
Controllers/SongController.php
namespace App\Http\Controllers;
⋮
use App\Models\Song;
⋮
class SongController extends Controller {
⋮
}
Song Model
Song
namespace App\Models;
⋮
use Illuminate\Database\Eloquent\Model;
⋮
class Song extends Model {
⋮
}
4.3.2 Middleware
Editor or higher middleware class
MustBeEditorOrHigher
Middleware/MustBeEditorOrHigher.php
namespace App\Http\Middleware;
⋮
class MustBeEditorOrHigher {
⋮
}
Editor middleware class guard class allocation
editor
Kernel.php
⋮
protected $routeMiddleware = [
⋮
'editor' => \App\Http\Middleware\MustBeEditorOrHigher::class,
⋮
];
⋮
Using the editor middleware in router
editor
routes/web.php
⋮
Route::get('/song/{song:id}/edit', [SongController::class, 'edit'])->middleware(['editor']);
⋮
4.3.3 Functies
Get Songs Index View
index
Controllers/SongController.php
⋮
class Song ... {
⋮
public function index() {
⋮
}
⋮
}
Get Song Edit View
edit
Controllers/SongController.php
⋮
class Song ... {
⋮
public function edit() {
⋮
}
⋮
}
Send Song Edit Information
update
Controllers/SongController.php
⋮
class Song ... {
⋮
public function update() {
⋮
}
⋮
}
4.3.4 Variabelen
$song
Controllers/SongController.php
⋮
public function details(Song $song) {
return view('song/details', [
'song' => $song
]);
}
⋮
$song
views/song/details.blade.php
⋮
<h3>{{ $song->title }} </h3>
<p>{{ $song->id }}</p>
⋮
$attributes
Controllers/SongController.php
⋮
public function new() {
$attributes = request()->validate([
'title' => 'required|max:255',
'slug' => ['required', 'max:255', Rule::unique('songs', 'slug')],
'artist' => 'required|max:255',
'inRepertoireSince' => 'nullable|date',
'activeRepertoire' => 'boolean',
'version' => 'required|date',
'duration' => 'required|date_format:H:i:s',
]);
if (!isset($attributes['activeRepertoire'])) {
$attributes['activeRepertoire'] = 0;
}
Song::create($attributes);
⋮
}
⋮
$user
Controllers/UserController.php
⋮
public function details(User $user) {
return view('user/details', [
'user' => $user
]);
}
⋮
$users
& $user
views/modpanel/user.blade.php
⋮
@foreach ($users as $user)
<tr>
<td>{{ $user->id }}</th>
<td>{{ $user->username }}</td>
<td>{{ $user->roleID }} </td>
<td>{{ $user->email }}</td>
<td>{{ $user->isValidated }}</td>
<td>{{ $user->firstname }}</td>
<td>{{ $user->lastname }}</td>
<td>{{ $user->created_at }}</td>
<td>{{ $user->updated_at }}</td>
<td><a class="btn" href="/modpanel/user/{{ $user->id }}">Details</a></td>
</tr>
@endforeach
⋮
4.3.6 Environment Variables
.env
⋮
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=projectchoirapp
DB_USERNAME=root
DB_PASSWORD=password
⋮