Compare commits

..

146 Commits

Author SHA1 Message Date
Kilian a283d755de Adj. README 2024-07-30 21:37:49 +02:00
Kilian 4da6e6fb5f Permission change session invalidation 2024-07-30 21:25:05 +02:00
Kilian 0fbbfdc997 Admin 2024-07-30 21:20:51 +02:00
Kilian b1061e67ac Make admin 2024-07-30 21:09:42 +02:00
Kilian ae31f57ee0 Exec. delete 2024-07-30 19:06:40 +02:00
Kilian 28955feda7 Fix pages on users 2024-07-30 16:47:03 +02:00
Kilian c192eeba65 Refresh on profile with valid session 2024-07-30 00:24:43 +02:00
Kilian 45778d83d0 Merge branch 'master' of https://git.hofmannnet.myhome-server.de/Kilian/PHP-Course 2024-07-30 00:14:38 +02:00
Kilian bf3cebcf86 PW Change 2024-07-30 00:13:55 +02:00
Kilian 478f2429f5 File deletes 2024-07-29 23:48:36 +02:00
Kilian f6a10c8133 Conditional retry 2024-07-29 23:29:56 +02:00
Kilian 386472e680 Profile does not retry 2024-07-29 23:27:18 +02:00
Kilian 9cb34839ec Delete only if exists 2024-07-29 23:24:12 +02:00
Kilian bc5c04c7be Build it lol 2024-07-29 23:10:52 +02:00
Kilian 702bb94004 App version 2024-07-29 23:09:48 +02:00
Kilian b455fdb20e Readme 2024-07-29 23:09:37 +02:00
Kilian 34b7b47d0e Logo 2024-07-29 22:29:21 +02:00
Kilian a85cdc6356 Docs pt. 2 2024-07-29 22:07:26 +02:00
Kilian 6a62ae58fc Docs 2024-07-29 22:06:57 +02:00
Kilian 5ce2215e44 Favicon 2024-07-29 19:47:40 +02:00
Kilian e20206d4a5 API Usage refreshes 2024-07-29 19:33:00 +02:00
Kilian 053724f9b7 PageTitle 2024-07-29 16:14:18 +02:00
Kilian 5f9654b5af Fix predefined path 2024-07-29 16:07:20 +02:00
Kilian bf2f4954ee ErrorComponent, 404Component, Theming 2024-07-29 16:03:27 +02:00
Kilian e6fcd54e22 Light theme 2024-07-29 05:07:35 +02:00
Kilian 9deff439d7 Theming 2024-07-29 03:21:29 +02:00
Kilian 2008888a16 Errant logs 2024-07-29 00:43:12 +02:00
Kilian 7d92daeccd Merge branch 'master' of https://git.hofmannnet.myhome-server.de/Kilian/PHP-Course 2024-07-29 00:41:10 +02:00
Kilian 0b661c7ccc Reauth 2024-07-29 00:40:35 +02:00
Kilian 45d4ebcded Reauth params in config 2024-07-29 00:29:35 +02:00
Kilian 9828ee762a Reauth endpoint 2024-07-28 22:37:12 +02:00
Kilian 7723dd0722 Autocompletes 2024-07-28 03:09:52 +02:00
Kilian 96bdbfc62a Split 401 2024-07-28 03:04:26 +02:00
Kilian 73c7dc1b5e Distinct auth from perm 2024-07-28 02:57:45 +02:00
Kilian 93e5cebce0 Images return full URI (excl. host) 2024-07-28 01:58:16 +02:00
Kilian 215ed1bc7f New picture link output, register modal 2024-07-28 01:46:16 +02:00
Kilian 72a0ad6364 Path again 2024-07-27 23:10:26 +02:00
Kilian 9d6bd65583 Path 2024-07-27 23:06:37 +02:00
Kilian 2c9f8caff4 Recent posts 2024-07-27 23:02:17 +02:00
Kilian a950f6770a User posts 2024-07-27 22:53:27 +02:00
Kilian a3ec9ae318 Image 2 2024-07-27 21:49:23 +02:00
Kilian eb3516bfb2 Fix Image 2024-07-27 21:06:21 +02:00
Kilian 627654e0a7 Profile Image edit 2024-07-27 21:00:31 +02:00
Kilian 15ca7e8879 Cleanup 2024-07-27 20:55:53 +02:00
Kilian 42529a66d4 Image Endpoint 2024-07-27 20:43:57 +02:00
Kilian 5ebdee09d8 Switch to post 2024-07-27 19:42:06 +02:00
Kilian ee9126b9e8 Dump InputHandler 2024-07-27 19:27:32 +02:00
Kilian c1b35aeef4 Dump request 2024-07-27 19:26:26 +02:00
Kilian 619f704c4e all file 2024-07-27 19:19:13 +02:00
Kilian 3beb06ceb0 Superglobal 2024-07-27 19:10:30 +02:00
Kilian f25b0127e4 Switch output 2024-07-27 19:09:07 +02:00
Kilian f8fecbae4f Test Echo 2024-07-27 19:08:09 +02:00
Kilian e850d14c2b CORS 2024-07-27 19:06:38 +02:00
Kilian 24fad3bb59 Demo image route 2024-07-27 19:03:18 +02:00
Kilian 7144b017b3 Docs 2024-07-27 17:05:00 +02:00
Kilian 4dcb7531d3 Docs 2024-07-27 17:03:47 +02:00
Kilian 6018e8bee5 Delete return pages 2024-07-27 17:01:24 +02:00
Kilian ce7ba50b9d paramater fix 2024-07-27 16:46:17 +02:00
Kilian 08731f8559 Post Create/Edit 2024-07-27 16:42:03 +02:00
Kilian 74fd55084f Redo nl2br 2024-07-27 16:14:39 +02:00
Kilian 43e2c6c0ad Remove nl2br 2024-07-27 16:12:03 +02:00
Kilian 64311f2765 Fix post update 2024-07-27 16:08:13 +02:00
Kilian d71eaf2ef2 Email Edit 2024-07-27 03:39:52 +02:00
Kilian 12f7176467 Email change on user 2024-07-27 02:52:24 +02:00
Kilian 683b6020b7 Missign translation 2024-07-27 01:40:45 +02:00
Kilian 581cacb636 Post delete, Profile edit 2024-07-27 00:54:23 +02:00
Kilian a515c447e0 Delete Post 2024-07-26 20:56:32 +02:00
Kilian 0929371c10 Logout invalidate 2024-07-26 16:14:45 +02:00
Kilian ff0e192768 Chunking 2024-07-26 03:38:43 +02:00
Kilian a0b327f744 Reorder imports 2024-07-26 02:41:39 +02:00
Kilian 103b93184f Better splitting 2024-07-26 01:47:15 +02:00
Kilian 7665b7185d Srcmap 2024-07-26 01:32:58 +02:00
Kilian d8f02675a0 React load first, mui then 2024-07-26 01:30:13 +02:00
Kilian 96fa86d53a Mui + react 2024-07-26 01:22:05 +02:00
Kilian 922aea543d Mui and Mui Incos in one bundle 2024-07-26 01:20:03 +02:00
Kilian 0a0baca051 Keep mui in main vendor bundle 2024-07-26 01:18:00 +02:00
Kilian 36a4659915 Auth redirects, language switch, basic profile 2024-07-26 01:14:12 +02:00
Kilian 2091bdb4e3 CORS Unauth repsonse 2024-07-25 22:19:38 +02:00
Kilian 202e89382a Post list fixes 2024-07-25 21:00:34 +02:00
Kilian bb1e0eebf5 Initial Post list 2024-07-25 20:55:35 +02:00
Kilian 9a2673aba2 REST Naming convention 2024-07-25 13:51:19 +02:00
Kilian 83c519fdfd Newest api docs 2024-07-25 01:35:28 +02:00
Kilian 91cdf15cc1 Fix Tab 2024-07-25 01:30:08 +02:00
Kilian d9755c732c Fix again 2024-07-25 01:20:02 +02:00
Kilian 7e30d515b6 Fix translations 2024-07-25 01:18:38 +02:00
Kilian 95555a30a5 More translations 2024-07-25 01:14:34 +02:00
Kilian 65848f094b Login/Logout + Menu + i18next 2024-07-25 01:09:20 +02:00
Kilian dd48f72c42 Exclude Devtools 2024-07-24 01:29:53 +02:00
Kilian 1b62b890b9 Begin consumption of API 2024-07-24 01:25:12 +02:00
Kilian b40ae7d701 Begin Tanstack 2024-07-23 19:22:01 +02:00
Kilian f303c7423c React start 2024-07-23 02:02:13 +02:00
Kilian 1bff7f46d7 Cleanup 2024-07-23 01:22:16 +02:00
Kilian 85d20e034a Better Errors 2024-07-23 01:12:05 +02:00
Kilian 30849019af Enable CORS 2024-07-22 22:58:01 +02:00
Kilian 700faf4351 Endpoints 2024-07-22 21:38:21 +02:00
Kilian 5251c43a6b Pagination on the list, optional auth 2024-07-22 16:19:01 +02:00
Kilian 7ee04b0a4b User List 2024-07-22 14:18:48 +02:00
Kilian 2fde820942 Confirm Register 2024-07-22 13:49:33 +02:00
Kilian 06ce614c2b Routes for React 2024-07-22 04:27:02 +02:00
Kilian 20db2301cb Rewrites and Formatting 2024-07-22 03:34:56 +02:00
Kilian 3e22aaf30c Move react over 2024-07-22 02:50:28 +02:00
Kilian 2168e37413 Return types 2024-07-22 02:36:28 +02:00
Kilian af588ab174 Register endpoint 2024-07-22 02:21:30 +02:00
Kilian d94c342bd6 Post Count 2024-07-22 01:14:36 +02:00
Kilian 7b897071f5 More Facades, User delete 2024-07-22 00:56:04 +02:00
Kilian 4b89a7e9ca Most User endpoints 2024-07-21 20:22:39 +02:00
Kilian b3c5841e36 Image now a file 2024-07-21 18:28:19 +02:00
Kilian 8d91e805dd LoginLogout 2024-07-21 15:49:43 +02:00
Kilian 132e8790a3 Loader update 2024-07-21 12:32:34 +02:00
Kilian 976313305f Prep autoloader 2024-07-21 12:32:11 +02:00
Kilian e9407c6c72 Begin docs 2024-07-20 20:20:56 +02:00
Kilian 0ba01eb9f3 Loader update 2024-07-20 19:23:59 +02:00
Kilian 3fa2f07ed3 Basis 2024-07-20 19:23:43 +02:00
Kilian d608a990b6 Start of proj 2024-07-20 14:07:00 +02:00
Kilian 066251b3a7 Loader update 2024-07-20 14:05:19 +02:00
Kilian 7461a42586 More loader 2024-07-20 14:04:08 +02:00
Kilian 0951285ff8 autoloader 2024-07-20 13:41:54 +02:00
Kilian b8edde019f autoloader 2024-07-20 12:38:11 +02:00
Kilian 87d949361e Initial Class tryout 2024-07-20 12:30:53 +02:00
Kilian ab1df63788 Simple Route 2024-07-19 15:47:19 +02:00
Kilian 156d277e77 Fix API 2024-07-19 13:05:21 +02:00
Kilian 88f535a1ed HTACCESS 2024-07-19 10:59:21 +02:00
Kilian 797b166fd0 Asset path 2024-07-19 10:51:17 +02:00
Kilian 10e5e8fc5a Base path 2024-07-19 10:40:01 +02:00
Kilian 9149cb25fe Output dir 2024-07-19 10:31:01 +02:00
Kilian f06bb1549e Config 2024-07-19 10:26:29 +02:00
Kilian eb20e236e5 Include React 2024-07-19 10:15:23 +02:00
Kilian edf8b7cecf Basic User and Auth 2024-07-13 20:17:59 +02:00
Kilian 03da043be3 Docs 2024-07-13 17:59:54 +02:00
Kilian 4d25387a47 Basic Framework 2024-07-13 17:22:53 +02:00
Kilian c1fffec124 Rem Laravel 2024-07-12 19:26:47 +02:00
Kilian 1d93997d82 Laravel 2024-07-12 18:57:24 +02:00
Kilian c5842e58f2 Setup counter for days 2024-07-12 12:26:34 +02:00
Kilian 49a65d4d1a OOP Counter 2024-07-12 12:12:31 +02:00
Kilian 6bb19ad6b0 Validators for Form Fields 2024-07-05 13:39:02 +02:00
Kilian 7043d56329 EoD 2024-06-28 13:39:21 +02:00
Kilian fac22f76c6 Counter styling 2024-06-27 16:37:58 +02:00
Kilian 15b264ded2 Start on HW 2024-06-21 13:51:15 +02:00
Kilian 32d4a62246 Fis JS undefined for non logged in users 2024-06-21 12:48:45 +02:00
Kilian 5f573a468f Login/Register/confirm and new comment feature 2024-06-21 12:47:08 +02:00
Kilian 70da3e66fd New DB based GuestBook 2024-06-14 13:48:24 +02:00
Kilian 15d3507b04 Article List 2024-06-07 13:33:01 +02:00
Kilian f6626019ae Arrow funcs 2024-05-31 13:37:41 +02:00
Kilian 3d8b9928d6 DB Test 2024-05-31 13:07:37 +02:00
Kilian c62321f854 Repos header 2024-05-31 12:49:47 +02:00
Kilian 0da314a31d Access 2024-05-31 12:32:26 +02:00
405 changed files with 33093 additions and 3886 deletions
+3 -2
View File
@@ -1,2 +1,3 @@
DENY FROM ALL Order deny,allow
ALLOW FROM 127.0.0.1 Deny from all
Allow from 127.0.0.1
+7
View File
@@ -0,0 +1,7 @@
<?php
spl_autoload_register(function ($name) {
$_name = lcfirst($name);
require_once "/home/k/khofmann/public_html/phpCourse/tasks/oop/classes/$_name/$_name.php";
});
+5 -5
View File
@@ -6,16 +6,16 @@ class DB extends PDO
parent::__construct($dsn, $username, $password, $options); parent::__construct($dsn, $username, $password, $options);
} }
static function openConnection($config = "../configs/db.ini") static function openConnection($config = "/home/k/khofmann/public_html/phpCourse/configs/db.ini")
{ {
$conn = null; $conn = null;
try { try {
$dataAccess = parse_ini_file($config, true); $dataAccess = parse_ini_file($config, true)["ZEDAT"];
$conn = new PDO( $conn = new PDO(
"mysql:host={$dataAccess["ZEDAT"]["host"]};dbname={$dataAccess["ZEDAT"]["database"]};charset={$dataAccess["ZEDAT"]["charset"]}", "mysql:host={$dataAccess["host"]};dbname={$dataAccess["database"]};charset={$dataAccess["charset"]}",
$dataAccess["ZEDAT"]["user"], $dataAccess["user"],
$dataAccess["ZEDAT"]["passwd"], $dataAccess["passwd"],
[ [
PDO::ATTR_PERSISTENT => false, PDO::ATTR_PERSISTENT => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+16
View File
@@ -0,0 +1,16 @@
<?php
function guidv4($data = null)
{
// Generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = $data ?? random_bytes(16);
assert(strlen($data) == 16);
// Set version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
// Output the 36 character UUID.
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
+7
View File
@@ -2,4 +2,11 @@
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16"> <symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" /> <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</symbol> </symbol>
<symbol id="arrow-left" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8" />
</symbol>
<symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" />
</symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 1.0 KiB

+21
View File
@@ -0,0 +1,21 @@
RewriteEngine On
RewriteBase /phpCourse/exam/
## API routes
RewriteCond %{REQUEST_FILENAME} /api/.*
RewriteCond %{REQUEST_FILENAME} !/api/docs
RewriteRule ^ api/index.php [L,NC,QSA]
## Rewrite all routes other than the specified paths
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !/api/docs
RewriteCond %{REQUEST_FILENAME} !/dist
RewriteRule ^ dist/index.html [L,NC,QSA]
## Rewrite all files unless in the directories specified
RewriteCond %{REQUEST_FILENAME} -f
RewriteCond %{REQUEST_FILENAME} !/storage/.*
RewriteCond %{REQUEST_FILENAME} !/dist/.*
RewriteCond %{REQUEST_FILENAME} !/api/docs
RewriteRule ^ dist/index.html [L,NC,QSA]
+144
View File
@@ -0,0 +1,144 @@
# Übersicht der Inhalt
### Fett markierte Verzeichnisse und deren Inhalt sind von besonderem Interesse
- **api**
- docs
- API Dokumentation (openAPI 3.0)
- Login
- `Login.php` Endpunkt Klasse für `api/api/login`
- Logout
- `Logout.php` Endpunkt Klasse für `api/api/login`
- Posts
- `Posts.php` Endpunkt Klasse für `api/api/posts`
- `Posts.php` Endpunkt Klasse für `api/api/posts/{id}`
- Refresh
- `Refresh.php` Endpunkt Klasse für `api/api/refresh`
- Register
- `Register.php` Endpunkt Klasse für `api/api/register`
- Users
- `Users.php` Endpunkt Klasse für `api/users`
- `Users.php` Endpunkt Klasse für `api/users/{id}`
- Image
- `Image.php` Endpunkt Klasse für `api/users{id}/image`
- Permissions
- `Permissions.php` Endpunkt Klasse für `api/users/{id}/permissions`
- Posts
- `Posts.php` Endpunkt Klasse für `api/users{id}/posts`
- `index.php`
- API Einstiegspunkt
- **classes**
- Api
- `Api.php`: Basisklasse für Endpunkte
- ApiError
- `ApiError.php`: Facade für Fehler die das Api zurück gibt
- Auth
- `AdminAuth.php`: Middleware für Authentifizierung zuzüglich Admin Rechte
- `Auth.php`: Middleware für Authentifizierung
- `OptAuth.php`: Middleware für optionale Authentifizierung
- Config
- `Config.php`: Singleton für Applikationskonfiguration
- Database
- `Database.php`: Singleton für Datenbankzugriffe (Wrapper um PDO)
- GUID
- `GUID.php`: Facade für GUID Algorithmen
- Input
- `Input.php`: Facade die SimpleRouters `Input` Klasse wrapped für
einfacheren Zugriff auf häufig verwendete Methoden
- Models
- Post
- `Post.php`: Modellklasse für Posts. Abstrahiert und kapselt
Datenbankzugriffe
- User
- `User.php`: Modellklasse für User. Abstrahiert und kapselt
Datenbankzugriffe
- Request
- `Request.php`: Facade die SimpleRouters `Request` Klasse wrapped für
einfacheren Zugriff auf häufig verwendete Methoden
- Response
- `Response.php`: Facade die SimpleRouters `Response` Klasse wrapped für
einfacheren Zugriff auf häufig verwendete Methoden
- **config**
- `app.php`: Applikationskonfiguration
- `database.php`: Datenbankverbindungsdaten
- dist
- React Buildartefakte.
- react
- React Projekt
- **routes**
- `routes.php`: SimpleRouter Routenkonfiguration für die Applikation
- storage:
- Speicherort für Profilbilder
- **utils**:
- `helpers.php`: Kleine Helferlein
- vendor:
- Verzeichnis für Composer Pakete
# Autoloader
Verwendet wird hier der Composer Autoloader. Konfiguriert werden dessen
Namespaces in der `composer.json`. Durch `composer dump-autoload` wird
der Autoloader aktualisiert sollten neue Namespaces hinzugefügt werden.
# Voraussetzungen um das React Projekt zu bauen
- Node 18
- PNPM
# Veränderungen Datenbank
## Tabelle `egb_benutzer`
- Neue Spalten `token` (Auth token): VarChar(36), Nullable, UNIQUE Constraint
- Neue Spalten `tokenExpiry` (Auth token verfall): DateTime, Nullable
- Neue Spalten `refreshToken` (Auth refresh token): VarChar(36), Nullable, UNIQUE Constraint
- Neue Spalten `refreshExpiry` (Auth refresh token verfall): VarChar(36), DateTime
- Abänderung der Spalte `zeitstempel`: Entfernen des `ON UPDATE` (da sonst die Mitgliedszeit beim Ändern der Daten sich ändert)
- Abänderung der Spalte `benutzername`: Non-Nullable gemacht, UNIQUE Constraint
- Abänderung der Spalte `email`: Non-Nullable gemacht, UNIQUE Constraint
- Abänderung der Spalte `passwort`: Non-Nullable gemacht
- Abänderung der Spalte `confirmationcode`: Default auf `NULL` gesetzt
## Tabelle `egb_gaestebuch`
- Abänderung der Spalte `benutzer_id`: Non-Nullable gemacht
- Abänderung der Spalte `beitrag`: Non-Nullable gemacht
- Hinzufüge eines Foreign Key Constraints auf `benutzer_id` mit CASCADE (Löschen des Users löscht Beiträge)
# Notwendige Anpassung für die Verzeichnisstruktur eines anderen Hosters
## HTACCESS
### `.htaccess`
- RewriteBase anpassen
### `react/public/.htaccess`
- RewriteBase anpassen
- **WICHTIG:** React Projekt neu bauen damit die Datei an den korrekten Platz kopiert wird
## PHP
### `classes/Models/User.php`
- Link in der Confirmation E-Mail
### `config/app.php`
- Alle Pfade
## JS
**WICHTIG:** Nach allen Änderungen muss das React Projekt neu gebaut werden
### `react/vite.config.ts`
- `base` Pfad
### `react/src/i18n.ts`
- `loadPath` Pfad
### `react/src/main.tsx`
- `createRouter.basepath` Pfad
### `react/src/api/Api.ts`
- `BASE` API Url
### Projekt bauen
```
cd react
pnpm install
pnpm build
```
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace Api\Login;
use Exception;
use Khofmann\Api\Api;
use Khofmann\ApiError\ApiError;
use Khofmann\Input\Input;
use Khofmann\Response\Response;
use Khofmann\Models\User\User;
/**
* Login route handlers
*/
class Login extends Api
{
/**
* Login POST handler
*
* Log in a user. Required inputs are `email` and `password`.
*
* Returns user and tokens
*
* @throws 400 Missing field
* @throws 401 Invalid credentials (login fails)
* @throws 404 User not found
* @throws 500 Failed to log in user
*/
public function post(): void
{
// Fetch all required inputs.
// Throw 400 error if a required one is missing.
$missing = [];
$email = Input::post("email");
$password = Input::post("password");
if (empty($email)) array_push($missing, "email");
if (empty($password)) array_push($missing, "password");
if (count($missing) > 0) throw ApiError::missingField($missing);
// Try and log in user.
// Throw errors according to situation.
try {
Response::json(User::logIn($email, $password));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "Failed":
throw ApiError::failed("Login failed");
case "NotFound":
throw ApiError::notFound("user");
case "Invalid":
throw ApiError::notAllowed("Invalid username or password");
default:
throw $err;
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Api\Logout;
use Khofmann\Api\Api;
use Khofmann\Models\User\User;
use Khofmann\Request\Request;
use Khofmann\Response\Response;
/**
* Logout route handlers
*/
class Logout extends Api
{
/**
* Logout POST handler
*
* Logout a user. User is retrieved using the authentication `token`.
*
* Returns user.
*
* @throws 404 User not found
*/
public function post(): void
{
// Get user auth token.
$token = Request::token();
// Log out.
Response::json(User::getByToken($token)->logOut());
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
namespace Api\Posts;
use Exception;
use Khofmann\Api\Api;
use Khofmann\ApiError\ApiError;
use Khofmann\Input\Input;
use Khofmann\Models\Post\Post;
use Khofmann\Models\User\User;
use Khofmann\Request\Request;
use Khofmann\Response\Response;
/**
* Posts route handlers
*/
class Posts extends Api
{
/**
* Posts GET handler
*
* Lists posts. Optional parameters are `l` (limit of returned list) and `p` (page, i.e. offset).
*/
public function get()
{
// Fetch and constrain all parameters.
$page = max(0, intval(Input::get("p", 0)));
$limit = constrain(0, 30, intval(Input::get("l", 10)));
$authed = Request::token() !== null;
// Return list of posts.
Response::json(Post::list($page, $limit, $authed));
}
/**
* Posts POST handler
*
* Create a new posts. Required inputs are `content`. Optional parameter is `l` (limit of list for which the returned pages is calculated).
*
* Returns created post and resulting amount of pages for a given limit.
*
* @throws 400 Missing fields
*/
public function post(): void
{
// Fetch all required inputs.
// Throw 400 error if a required one is missing.
$content = Input::post("content");
// This one is optional
$limit = constrain(0, 30, intval(Input::get("l", 10)));
if (empty($content)) throw ApiError::missingField(["content"]);
// Get logged in user
$self = User::getByToken(Request::token());
// Try to create a new post for logged in user.
try {
Response::json(Post::create($self, $content, $limit));
} catch (Exception $err) {
switch ($err->getMessage()) {
default:
throw $err;
}
}
}
/**
* Posts PATCH handler
*
* Update a posts.
*
* Returns updated post.
*
* @param mixed $id ID of post to update
*
* @throws 401 Not authorized (trying to edit a different users post if not admin)
* @throws 404 Post not found
* @throws 500 Failed to update user
*/
public function patch($id): void
{
// Fetch all inputs.
$content = Input::patch("content");
// Fetch authed user.
$self = User::getByToken(Request::token());
try {
// Try fetch the post in question, 404 if not found.
$post = Post::getByID($id);
// Throw 400 if we aren't admin but trying to edit another users post.
if (!$self->getIsAdmin() && $post->getUser()->getID() !== $self->getID()) throw ApiError::notAllowed("Not allowed");
// Try update.
Response::json($post->update($content));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("post");
default:
// Due to how the failed field is handled, it's ApiError is inside the models update
throw $err;
}
}
}
/**
* Posts DELETE handler
*
* Delete a post. Optional parameter is `l` (limit of list for which the returned pages is calculated).
*
* Returns deleted post and resulting amount of pages for a given limit.
*
* @param mixed $id ID of posts to delete
*
* @throws 404 Post not found
*/
public function delete($id): void
{
// Fetch and constrain all parameters.
$limit = constrain(0, 30, intval(Input::get("l", 10)));
// Try delete, 404 if post was not found.
try {
Response::json(Post::getByID($id)->delete($limit));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("post");
default:
throw $err;
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Api\Refresh;
use Exception;
use Khofmann\Api\Api;
use Khofmann\ApiError\ApiError;
use Khofmann\Input\Input;
use Khofmann\Response\Response;
use Khofmann\Models\User\User;
use Khofmann\Request\Request;
/**
* Refresh route handlers
*/
class Refresh extends Api
{
/**
* Refresh POST handler
*
* Refresh a users session. User is retrieved using the authentication `token`.
*
* Returns user and tokens.
*
* @throws 401 Missing field
* @throws 404 User not found
* @throws 500 Failed to refresh tokens
*/
public function post(): void
{
// Fetch all required inputs.
// Throw 400 error if a required one is missing.
$token = Request::token();
$refreshToken = Input::post("refreshToken");
if (empty($refreshToken)) throw ApiError::missingField(["refreshToken"]);
// Try and log in user.
// Throw errors according to situation.
try {
Response::json(User::refresh($token, $refreshToken));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "Failed":
throw ApiError::failed("Refresh failed");
case "NotFound":
throw ApiError::unauthorized("Not authorized");
default:
throw $err;
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace Api\Register;
use Exception;
use Khofmann\Api\Api;
use Khofmann\ApiError\ApiError;
use Khofmann\Input\Input;
use Khofmann\Response\Response;
use Khofmann\Models\User\User;
/**
* Register route handlers
*/
class Register extends Api
{
/**
* Register POST handler
*
* Register a new user. Required inputs are `username`, `email`, and `password`.
*
* Returns user.
*
* @throws 400 Missing fields
* @throws 400 Duplicate
* @throws 404 Failure to create
*/
public function post(): void
{
// Fetch all required inputs.
// Throw 400 error if a required one is missing.
$missing = [];
$username = Input::post("username");
$email = Input::post("email");
$password = Input::post("password");
if (empty($username)) array_push($missing, "username");
if (empty($email)) array_push($missing, "email");
if (empty($password)) array_push($missing, "password");
if (count($missing) > 0) throw ApiError::missingField($missing);
// Try and create a new user, 400 if duplicate.
try {
Response::json(User::create($username, $email, $password));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::failed("Failed to create user");
case "Duplicate":
throw ApiError::duplicate("user");
default:
throw $err;
}
}
}
/**
* Register PATCH handler
*
* Confirms a user. Required input is `code`.
*
* Returns user.
*
* @throws 400 Missing field
* @throws 404 User not found
* @throws 404 User already confirmed
*/
public function patch(): void
{
// Fetch all required inputs.
// Throw 400 error if a required one is missing.
$code = Input::post("code");
if (empty($code)) throw ApiError::missingField(["code"]);
// Try and confirm user, 404 if user not found.
try {
Response::json(User::confirm($code));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
throw $err;
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace Api\Users\Image;
use Exception;
use Khofmann\Api\Api;
use Khofmann\Input\Input;
use Khofmann\Models\User\User;
use Khofmann\Response\Response;
use Khofmann\ApiError\ApiError;
use Khofmann\Request\Request;
/**
* User image route handlers
*/
class Image extends Api
{
/**
* Image POST handler
*
* Set a new user image.
*
* Returns updated user.
*
* @param mixed $id User ID
*
* @throws 404 User not found
* @throws 500 Failed to update user image
*/
public function post($id): void
{
// Fetch all inputs.
$image = Input::file("image");
$predefined = Input::post("predefined");
// Try and update user image.
// Throw errors according to situation.
try {
Response::json(User::getByID($id)->updateImage($image, $predefined));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
// Due to how the failed field is handled, it's ApiError is inside the models update
throw $err;
}
}
}
/**
* Image POST handler
*
* Set a new user image. User is retrieved using the authentication `token`.
*
* Returns updated user.
*
* @throws 404 User not found
* @throws 500 Failed to update user image
*/
public function postSelf(): void
{
// Fetch all inputs.
$token = Request::token();
$image = Input::file("image");
$predefined = Input::post("predefined");
// Try and update user image.
// Throw errors according to situation.
try {
Response::json(User::getByToken($token)->updateImage($image, $predefined));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
// Due to how the failed field is handled, it's ApiError is inside the models update
throw $err;
}
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace Api\Users\Permissions;
use Exception;
use Khofmann\Api\Api;
use Khofmann\Input\Input;
use Khofmann\Models\User\User;
use Khofmann\Response\Response;
use Khofmann\ApiError\ApiError;
use Khofmann\Request\Request;
/**
* User permissions route handlers
*/
class Permissions extends Api
{
/**
* Permissions PATCH handler
*
* Sets user admin or not.
*
* Returns updated user.
*
* @param mixed $id User ID
*
* @throws 404 User not found
* @throws 500 Failed to update user permissions
*/
public function patch($id): void
{
// Fetch all inputs.
$isAdmin = Input::post("isAdmin");
// Try and update user image.
// Throw errors according to situation.
try {
Response::json(User::getByID($id)->updatePermissions($isAdmin));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
// Due to how the failed field is handled, it's ApiError is inside the models update
throw $err;
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace Api\Users\Posts;
use Exception;
use Khofmann\Api\Api;
use Khofmann\Models\User\User;
use Khofmann\Response\Response;
use Khofmann\ApiError\ApiError;
use Khofmann\Input\Input;
/**
* User posts route handlers
*/
class Posts extends Api
{
/**
* Posts GET handler
*
* Lists posts for a user. Optional parameters are `l` (limit of returned list), `p` (page, i.e. offset), `s` (sort order).
*
* Returns list of posts.
*
* @throws 404 User not found
*/
public function get($id): void
{
// Fetch and constrain all parameters.
$page = max(0, intval(Input::get("p", 0)));
$limit = constrain(0, 30, intval(Input::get("l", 10)));
$sort = Input::get("s", "asc");
$sort = in_array($sort, ["asc", "desc"]) ? $sort : "asc";
// Try and get users posts
// Throw errors according to situation.
try {
Response::json(User::getByID($id)->posts($page, $limit, $sort));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
// Due to how the failed field is handled, it's ApiError is inside the models update
throw $err;
}
}
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace Api\Users;
use Exception;
use Khofmann\Api\Api;
use Khofmann\Input\Input;
use Khofmann\Models\User\User;
use Khofmann\Response\Response;
use Khofmann\ApiError\ApiError;
use Khofmann\Request\Request;
/**
* Users route handlers
*/
class Users extends Api
{
/**
* Users GET handler
*
* Lists users. Optional parameters are `l` (limit of returned list) and `p` (page, i.e. offset).
*
* Returns list of users.
*/
public function list()
{
// Fetch and constrain all parameters.
$page = max(0, intval(Input::get("p", 0)));
$limit = constrain(0, 30, intval(Input::get("l", 10)));
// Return list of users.
Response::json(User::list($page, $limit));
}
/**
* User GET handler
*
* Get a single user.
*
* Returns user.
*
* @param mixed $id User ID
*
* @throws 404 User not found
*/
public function get($id): void
{
// Try and get a user, 404 if not found.
try {
Response::json(User::getByID($id));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
throw $err;
}
}
}
/**
* Users PATCH handler
*
* Update a user.
*
* Returns updated user.
*
* @param mixed $id User ID
*
* @throws 404 User not found
* @throws 500 Failed to update user
*/
public function patch($id): void
{
// Fetch all inputs.
$username = Input::patch("username");
$password = Input::patch("password");
$email = Input::patch("email");
// Try and update user.
// Throw errors according to situation.
try {
Response::json(User::getByID($id)->update($username, $password, $email));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
// Due to how the failed field is handled, it's ApiError is inside the models update
throw $err;
}
}
}
/**
* Users PATCH handler
*
* Update a user. User is retrieved using the authentication `token`.
*
* Returns updated user.
*
* @throws 404 User not found
* @throws 500 Failed to update user
*/
public function patchSelf(): void
{
// Fetch all inputs.
$token = Request::token();
$username = Input::patch("username");
$password = Input::patch("password");
$email = Input::patch("email");
// Try and update user.
// Throw errors according to situation.
try {
Response::json(User::getByToken($token)->update($username, $password, $email));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
// Due to how the failed field is handled, it's ApiError is inside the models update
throw $err;
}
}
}
/**
* Users DELETE handler
*
* Deletes a user. Optional parameter is `l` (limit of list for which the returned pages is calculated).
*
* Returns deleted user and resulting amount of pages for a given limit.
*
* @param mixed $id User ID
*
* @throws 404 User not found
*/
public function delete($id): void
{
// Fetch and constrain all parameters.
$limit = constrain(0, 30, intval(Input::get("l", 10)));
// Try to delete user, 404 if not found.
try {
Response::json(User::getByID($id)->delete($limit));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "NotFound":
throw ApiError::notFound("user");
default:
throw $err;
}
}
}
}
+987
View File
@@ -0,0 +1,987 @@
openapi: 3.0.0
info:
title: PHP Course Exam
version: 1.0.0
contact:
name: Kilian Kurt Hofmann
email: khofmann@zedat.fu-berlin.de
description: PHP Course (ABV FU Berlin) 2024 Exam
paths:
/login:
post:
summary: Login
description: Log in user.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LoginRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/LoginResponse"
400:
description: Missing fields.
content:
application/json:
schema:
$ref: "#/components/schemas/MissingFieldResponse"
examples:
Missing fields:
value:
{ "code": "MissingField", "fields": ["email", "password"] }
401:
description: Invalid credentials.
content:
application/json:
schema:
$ref: "#/components/schemas/UnauthorizedResponse"
examples:
Invalid username or password:
value:
{
"code": "Unauthorized",
"message": "Invalid username or password",
}
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
500:
description: Failed.
content:
application/json:
schema:
$ref: "#/components/schemas/FailedResponse"
examples:
Failed:
value: { "code": "Failed", "message": "Login failed" }
tags:
- Login/Logout
/logout:
post:
summary: Logout
description: Log out user.
security:
- BasicAuth: []
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/BooleanResponse"
examples:
Success:
value: true
tags:
- Login/Logout
/posts:
get:
summary: List posts
description: List all posts, return full user data if authenticated.
security:
- {}
- BasicAuth: []
parameters:
- in: query
name: p
schema:
type: integer
minimum: 0
default: 0
description: Current page.
- in: query
name: l
schema:
type: integer
minimum: 0
maximum: 30
default: 10
description: The number of items to return.
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/PostListResponse"
examples:
Not authenticated:
value:
{
"pages": 0,
"data":
[
{
"id": 0,
"user": { "username": "string", "image": "string" },
"content": "string",
"postedAt":
{
"date": "2019-08-24T14:15:22Z",
"timezone_type": 0,
"timezone": "string",
},
},
],
}
Authenticated:
value:
{
"pages": 1,
"data":
[
{
"id": 1,
"user":
{
"id": 0,
"username": "string",
"status": 0,
"email": "string",
"image": "string",
"isAdmin": true,
"memberSince":
{
"date": "2019-08-24T14:15:22Z",
"timezone_type": 0,
"timezone": "string",
},
"postCount": 0,
},
"content": "string",
"postedAt":
{
"date": "2019-08-24T14:15:22Z",
"timezone_type": 0,
"timezone": "string",
},
},
],
}
tags:
- Post
post:
summary: New post
description: Create a new post.
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PostCreateRequest"
parameters:
- in: query
name: l
schema:
type: integer
minimum: 0
maximum: 30
default: 10
description: Number of items per page, influences returned pages count.
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/PostCreateResponse"
400:
description: Missing fields.
content:
application/json:
schema:
$ref: "#/components/schemas/MissingFieldResponse"
examples:
Missing fields:
value: { "code": "MissingField", "fields": ["content"] }
tags:
- Post
/posts/{id}:
patch:
summary: Update post
description: Update post with ID. <br>
Requires logged in user to have admin permissions for posts not made by them.
security:
- BasicAuth: []
- BasicAuth: [isAdmin]
parameters:
- name: id
in: path
description: Post ID
required: true
schema:
type: integer
format: int14
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/PostUpdateRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/PostResponse"
401:
description: Not allowed.
content:
application/json:
schema:
$ref: "#/components/schemas/UnauthorizedResponse"
examples:
Not allowed:
value: { "code": "Unauthorized", "message": "Not allowed" }
404:
description: Post not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
Post not found:
value: { "code": "NotFound", "entity": "post" }
500:
description: Update failed.
content:
application/json:
schema:
$ref: "#/components/schemas/FailedUpdateResponse"
examples:
Failed:
value:
{
"code": "FailedUpdate",
"fields": ["content"],
"reasons": ["string"],
}
tags:
- Post
delete:
summary: Delete post
description: Delete post with ID.
security:
- BasicAuth: [isAdmin]
parameters:
- name: id
in: path
description: Post ID
required: true
schema:
type: integer
format: int14
- in: query
name: l
schema:
type: integer
minimum: 0
maximum: 30
default: 10
description: Number of items per page, influences returned pages count.
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/PostDeleteResponse"
404:
description: Post not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
Post not found:
value: { "code": "NotFound", "entity": "post" }
tags:
- Post
/refresh:
post:
summary: Refresh
description: Token refresh.
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/LoginResponse"
400:
description: Missing fields.
content:
application/json:
schema:
$ref: "#/components/schemas/MissingFieldResponse"
examples:
Missing fields:
value: { "code": "MissingField", "fields": ["refreshToken"] }
500:
description: Failed.
content:
application/json:
schema:
$ref: "#/components/schemas/FailedResponse"
examples:
Failed:
value: { "code": "Failed", "message": "Refresh failed" }
tags:
- Refresh
/register:
post:
summary: Register
description: Register a new user.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegisterRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
400:
description: Missing fields or duplicate.
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/MissingFieldResponse"
- $ref: "#/components/schemas/DuplicateResponse"
examples:
Missing fields:
value:
{
"code": "MissingField",
"fields": ["username", "email", "password"],
}
Duplicate:
value: { "code": "Duplicate", "entity": "user" }
404:
description: Failed to create
content:
application/json:
schema:
$ref: "#/components/schemas/FailedResponse"
examples:
Failed to create:
value:
{ "code": "Failed", "message": "Failed to create user" }
tags:
- Register
patch:
summary: Confirm register
description: Confirm a registration.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ConfirmRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
400:
description: Missing fields.
content:
application/json:
schema:
$ref: "#/components/schemas/MissingFieldResponse"
examples:
Missing fields:
value: { "code": "MissingField", "fields": ["code"] }
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
tags:
- Register
/users:
get:
summary: List users
description: List all users.
security:
- BasicAuth: [isAdmin]
parameters:
- in: query
name: p
schema:
type: integer
minimum: 0
default: 0
description: Current page.
- in: query
name: l
schema:
type: integer
minimum: 0
maximum: 30
default: 10
description: The number of items to return.
responses:
200:
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/UserListResponse"
tags:
- User
/users/{id}:
get:
summary: Get user
description: Get user by ID.
security:
- BasicAuth: []
parameters:
- name: id
in: path
description: User ID
required: true
schema:
type: integer
format: int14
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
tags:
- User
patch:
summary: Update user
description: Update user with ID. <br>
Use special ID <code>self</code> to update logged in user. <br>
Requires logged in user to have admin permissions for any ID other than <code>self</code>.
security:
- BasicAuth: []
- BasicAuth: [isAdmin]
parameters:
- name: id
in: path
description: User ID
required: true
schema:
type: integer
format: int14
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UserUpdateRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
500:
description: Update failed.
content:
application/json:
schema:
$ref: "#/components/schemas/FailedUpdateResponse"
examples:
Failed username:
value:
{
"code": "FailedUpdate",
"fields": ["username", "password", "email"],
"reasons": ["string", "string", "string"],
}
tags:
- User
delete:
summary: Delete user
description: Delete user with ID.
security:
- BasicAuth: [isAdmin]
parameters:
- name: id
in: path
description: User ID
required: true
schema:
type: integer
format: int14
- in: query
name: l
schema:
type: integer
minimum: 0
maximum: 30
default: 10
description: Number of items per page, influences returned pages count.
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/UserDeleteResponse"
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
tags:
- User
/users/{id}/image:
post:
summary: Update user image
description: Update user image with ID. <br>
Use special ID <code>self</code> to update logged in user. <br>
Requires logged in user to have admin permissions for any ID other than <code>self</code>.
security:
- BasicAuth: []
- BasicAuth: [isAdmin]
parameters:
- name: id
in: path
description: User ID
required: true
schema:
type: integer
format: int14
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UserImageUpdateRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
500:
description: Update failed.
content:
application/json:
schema:
$ref: "#/components/schemas/FailedUpdateResponse"
examples:
Failed username:
value:
{
"code": "FailedUpdate",
"fields": ["image", "predefined"],
"reasons": ["string", "string"],
}
tags:
- User
/users/{id}/permissions:
post:
summary: Update user permissions
description: Update user permissions with ID.
security:
- BasicAuth: [isAdmin]
parameters:
- name: id
in: path
description: User ID
required: true
schema:
type: integer
format: int14
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UserPermissionsUpdateRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
500:
description: Update failed.
content:
application/json:
schema:
$ref: "#/components/schemas/FailedUpdateResponse"
examples:
Failed username:
value:
{
"code": "FailedUpdate",
"fields": ["isAdmin"],
"reasons": ["string"],
}
tags:
- User
/users/{id}/posts:
get:
summary: Get user posts
description: Get a users posts ID.
security:
- BasicAuth: []
parameters:
- name: id
in: path
description: User ID
required: true
schema:
type: integer
format: int14
- in: query
name: p
schema:
type: integer
minimum: 0
default: 0
description: Current page.
- in: query
name: l
schema:
type: integer
minimum: 0
maximum: 30
default: 10
description: The number of items to return.
- in: query
name: s
schema:
type: string
enum:
- asc
- desc
default: asc
description: Sort order by time.
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/PostListResponse"
404:
description: User not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundResponse"
examples:
User not found:
value: { "code": "NotFound", "entity": "user" }
tags:
- User
externalDocs:
url: https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/docs/
security: []
servers:
- url: https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/
components:
schemas:
BooleanResponse:
type: boolean
MissingFieldResponse:
type: object
properties:
code:
type: MissingField
fields:
type: array
items:
type: string
NotFoundResponse:
type: object
properties:
code:
type: NotFound
entity:
type: string
UnauthorizedResponse:
type: object
properties:
code: type:NotAllowed
message:
type: string
FailedResponse:
type: object
properties:
code:
type: Failed
message:
type: string
DuplicateResponse:
type: object
properties:
code:
type: Duplicate
entity:
type: string
FailedUpdateResponse:
type: object
properties:
code:
type: FailedUpdate
fields:
type: array
items:
type: string
reasons:
type: array
items:
type: string
ErrorResponse:
type: object
properties:
message:
type: string
LoginRequest:
type: object
required:
- username
- password
properties:
username:
type: string
password:
type: string
LoginResponse:
type: object
properties:
user:
$ref: "#/components/schemas/UserResponse"
token:
type: string
refreshToken:
type: string
UserResponse:
type: object
properties:
id:
type: number
username:
type: string
status:
type: number
email:
type: string
image:
type: string
nullable: true
isAdmin:
type: boolean
memberSince:
type: object
properties:
date:
type: string
format: date-time
timezone_type:
type: number
timezone:
type: string
postCount:
type: number
UserUpdateRequest:
type: object
properties:
username:
type: string
password:
type: string
email:
type: string
UserPermissionsUpdateRequest:
type: object
properties:
isAdmin:
type: boolean
UserImageUpdateRequest:
type: object
properties:
image:
type: string
format: binary
predefined:
type: string
RegisterRequest:
type: object
required:
- username
- email
- password
properties:
username:
type: string
email:
type: string
password:
type: string
ConfirmRequest:
type: object
required:
- code
properties:
code:
type: string
format: uuid4
UserListResponse:
type: object
properties:
pages:
type: number
data:
type: array
items:
$ref: "#/components/schemas/UserResponse"
PostResponse:
type: object
properties:
id:
type: number
user:
$ref: "#/components/schemas/UserResponse"
content:
type: string
postedAt:
type: object
properties:
date:
type: string
format: date-time
timezone_type:
type: number
timezone:
type: string
PostCreateResponse:
type: object
properties:
pages:
type: number
post:
$ref: "#/components/schemas/PostResponse"
PostDeleteResponse:
$ref: "#/components/schemas/PostCreateResponse"
PostListResponse:
type: object
properties:
pages:
type: number
data:
type: array
items:
$ref: "#/components/schemas/PostResponse"
PostUpdateRequest:
type: object
properties:
content:
type: string
PostCreateRequest:
type: object
required:
- content
properties:
content:
type: string
RefreshRequest:
type: object
required:
- refreshToken
properties:
refreshToken:
type: string
format: uuid4
UserDeleteResponse:
type: object
properties:
pages:
type: number
post:
$ref: "#/components/schemas/UserResponse"
securitySchemes:
BasicAuth:
type: apiKey
name: token
in: header
tags:
- name: Login/Logout
- name: Post
- name: Register
- name: Refresh
- name: User
File diff suppressed because one or more lines are too long
+45
View File
@@ -0,0 +1,45 @@
<?php
// Enable Errors
ini_set("display_errors", 1); // DISABLE FOR RELEASE
ini_set("default_charset", "utf-8");
ini_set("session.cookie_httponly", 1);
ini_set("session.cookie_secure", 1);
ini_set("session.use_only_cookies", 1);
error_reporting(E_ALL);
// Load composer Autoloader
require_once __DIR__ . "/../vendor/autoload.php";
// Helpers
require_once __DIR__ . "/../utils/helpers.php";
// Namespaces
use Pecee\SimpleRouter\SimpleRouter;
use Pecee\SimpleRouter\Handlers\EventHandler;
use Pecee\SimpleRouter\Event\EventArgument;
use Pecee\SimpleRouter\Route\ILoadableRoute;
use Pecee\SimpleRouter\Route\IGroupRoute;
use Khofmann\Config\Config;
// Router base path
$basePath = Config::getBasePath() . "api";
// Router event handler for prepending base path
$eventHandler = new EventHandler();
$eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function (EventArgument $event) use ($basePath) {
$route = $event->route;
// Skip routes added by group as these will inherit the url
if ($event->isSubRoute) {
return;
}
// Prepend based on what type of Route
switch (true) {
case $route instanceof ILoadableRoute:
$route->prependUrl($basePath);
break;
case $route instanceof IGroupRoute:
$route->prependPrefix($basePath);
break;
}
});
// Add the handler
SimpleRouter::addEventHandler($eventHandler);
// Load external routes file
require_once __DIR__ . "/../routes/routes.php";
// Start the routing
SimpleRouter::start();
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Khofmann\Api;
use Khofmann\Response\Response;
/**
* Base class for all API handler classes.
*
* Sets common headers
*/
class Api
{
public function __construct()
{
Response::response()
->header("Cache-control: no-cache")
->header("Access-Control-Allow-Origin: *")
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *");
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace Khofmann\ApiError;
use Exception;
/**
* Facade for common API errors
*/
class ApiError extends Exception
{
private function __construct($message = "", $code = 0)
{
parent::__construct($message, $code);
}
/**
* Error for missing fields
*
* @param array $fields Array of strings denoting which fields were missing
*/
public static function missingField(array $fields): ApiError
{
return new ApiError(json_encode([
"code" => "MissingField",
"fields" => $fields,
]), 400);
}
/**
* Error for duplicates
*
* @param string entity Entity for which a duplicate exists
*/
public static function duplicate(string $entity): ApiError
{
return new ApiError(json_encode([
"code" => "Duplicate",
"entity" => $entity,
]), 400);
}
/**
* Error for missing permissions
*
* @param string message Message specifics
*/
public static function notAllowed(string $message)
{
return new ApiError(json_encode([
"code" => "NotAllowed",
"message" => $message,
]), 401);
}
/**
* Error for missing authentication
*
* @param string $message Message specifics
*/
public static function unauthorized(string $message)
{
return new ApiError(json_encode([
"code" => "Unauthorized",
"message" => $message,
]), 401);
}
/**
* Error for not found
*
* @param string entity Entity for which a duplicate exists
*/
public static function notFound(string $entity)
{
return new ApiError(json_encode([
"code" => "NotFound",
"entity" => $entity,
]), 404);
}
/**
* Generic error
*
* @param string message Message specifics
*/
public static function failed(string $message)
{
return new ApiError(json_encode([
"code" => "Failed",
"message" => $message,
]), 500);
}
/**
* Error for missing fields
*
* @param array $fields Array of strings denoting which fields failed
* @param array $fields Array of strings denoting why the fields failed
*/
public static function failedUpdate(array $fields, array $reasons)
{
return new ApiError(json_encode([
"code" => "FailedUpdate",
"fields" => $fields,
"reasons" => $reasons,
]), 500);
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace Khofmann\Auth;
use Exception;
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
use Khofmann\Models\User\User;
use Khofmann\Response\Response;
/**
* Middleware for admin authenticated routes
*/
class AdminAuth implements IMiddleware
{
/**
* Request handler
*
* Returns 401 if `token`is missing, no user is found with the `token`or user is not admin.
*
* Keeps session fresh if request is authenticated.
*
* @param Request $request Incoming request
*/
public function handle(Request $request): void
{
$token = $request->getHeader("token");
// No token
if ($token === null) {
Response::response()
->header("Cache-control: no-cache")
->header("Access-Control-Allow-Origin: *")
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *")
->httpCode(401)
->json(["code" => "Unauthorized", "message" => "Not Authorized"]);
}
try {
// Get user
$user = User::getByToken($token);
// Check if user is admin
if (!$user->getIsAdmin()) {
Response::response()
->header("Cache-control: no-cache")
->header("Access-Control-Allow-Origin: *")
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *")
->httpCode(401)
->json(["code" => "NotAllowed", "message" => "Not Authorized"]);
}
// Keep fresh
$user->keepFresh();
} catch (Exception $err) {
// No user with this token exists
Response::response()
->header("Cache-control: no-cache")
->header("Access-Control-Allow-Origin: *")
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *")
->httpCode(401)
->json(["code" => "Unauthorized", "message" => "Not Authorized"]);
}
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace Khofmann\Auth;
use Exception;
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
use Khofmann\Models\User\User;
use Khofmann\Response\Response;
/**
* Middleware for authenticated routes
*/
class Auth implements IMiddleware
{
/**
* Request handler
*
* Returns 401 if `token`is missing, or no user is found with the `token`.
*
* Keeps session fresh if request is authenticated.
*
* @param Request $request Incoming request
*/
public function handle(Request $request): void
{
$token = $request->getHeader("token");
// No token
if ($token === null) {
Response::response()
->header("Cache-control: no-cache")
->header("Access-Control-Allow-Origin: *")
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *")
->httpCode(401)
->json(["code" => "Unauthorized", "message" => "Not Authorized"]);
}
try {
// Get user
$user = User::getByToken($token);
// Keep fresh
$user->keepFresh();
} catch (Exception $err) {
// No user with this token exists
Response::response()
->header("Cache-control: no-cache")
->header("Access-Control-Allow-Origin: *")
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *")
->httpCode(401)
->json(["code" => "Unauthorized", "message" => "Not Authorized"]);
}
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Khofmann\Auth;
use Exception;
use Pecee\Http\Middleware\IMiddleware;
use Pecee\Http\Request;
use Khofmann\Models\User\User;
use Khofmann\Response\Response;
/**
* Middleware for optional authenticated routes
*/
class OptAuth implements IMiddleware
{
/**
* Request handler
*
* Returns 401 if no user is found with the `token`.
*
* Keeps session fresh if request is authenticated.
*
* @param Request $request Incoming request
*/
public function handle(Request $request): void
{
$token = $request->getHeader("token");
// No token, since authentication is optional, pass
if ($token === null) {
return;
}
try {
// Get user
$user = User::getByToken($token);
// Keep fresh
$user->keepFresh();
} catch (Exception $err) {
// No user with this token exists
Response::response()
->header("Cache-control: no-cache")
->header("Access-Control-Allow-Origin: *")
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *")
->httpCode(401)
->json(["code" => "Unauthorized", "message" => "Not Authorized"]);
}
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
namespace Khofmann\Config;
use Exception;
/**
* Facade for application configuration
*/
class Config
{
// Instances array to ensure singleton pattern
private static array $instances = [];
// Configuration arrays
private array $app;
private array $database;
/**
* Loads configurations into arrays.
*
* Private since singleton pattern.
*/
private function __construct()
{
$this->app = require_once __DIR__ . "/../../config/app.php";
$this->database = require_once __DIR__ . "/../../config/database.php";
}
/**
* Disallow clone
*/
private function __clone()
{
throw new Exception("Cannot clone a singleton.");
}
/**
* Disallow wakeup
*/
private function __wakeup()
{
throw new Exception("Cannot unserialize a singleton.");
}
/**
* Get the instance. Ensures singleton pattern.
*
* Private to ensure only facade methods can be used.
*/
private static function getInstance(): Config
{
$cls = static::class;
if (!isset(self::$instances[$cls])) {
self::$instances[$cls] = new static();
}
return self::$instances[$cls];
}
/**
* @return string Application base URI path
*/
public static function getBasePath(): string
{
return Config::getInstance()->app["basePath"];
}
/**
* @return string Application base file system path
*/
public static function getBaseFSPath(): string
{
return Config::getInstance()->app["baseFSPath"];
}
/**
* @return string Application storage URI path
*/
public static function getStoragePath(): string
{
return Config::getInstance()->app["storagePath"];
}
/**
* @return string Application storage file system path
*/
public static function getStorageFSPath(): string
{
return Config::getInstance()->app["storageFSPath"];
}
/**
* @return string Application base URI path
*/
public static function getDatabase(): array
{
return Config::getInstance()->database;
}
/**
* @return string Application base URI path
*/
public static function getTokenExpiry(): string
{
return Config::getInstance()->app["tokenExpiry"];
}
/**
* @return string Application base URI path
*/
public static function getRefreshTokenExpiry(): string
{
return Config::getInstance()->app["refreshTokenExpiry"];
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Khofmann\Database;
use PDO;
use Khofmann\Config\Config;
use Exception;
/**
* Facade for database access
*/
class Database extends PDO
{
// Instances array to ensure singleton pattern
private static array $instances = [];
/**
* Private since singleton pattern.
*/
private function __construct(string $dsn, string $username = null, string $password = null, array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
}
/**
* Disallow clone
*/
private function __clone()
{
throw new Exception("Cannot clone a singleton.");
}
/**
* Disallow wakeup
*/
private function __wakeup()
{
throw new Exception("Cannot unserialize a singleton.");
}
/**
* Get the instance. Ensures singleton pattern.
*
* Loads configuration from `Config` facade
*/
public static function getInstance(): Database
{
$cls = static::class;
if (!isset(self::$instances[$cls])) {
$dataAccess = Config::getDatabase();
self::$instances[$cls] = new static(
"mysql:host={$dataAccess["host"]};dbname={$dataAccess["database"]};charset={$dataAccess["charset"]}",
$dataAccess["user"],
$dataAccess["passwd"],
[
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
}
return self::$instances[$cls];
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Khofmann\GUID;
/**
* Facade for GUID generators
*/
class GUID
{
/**
* Private since facade.
*/
private function __construct()
{
}
/**
* Generate a UUID v4.
*
* @param mixed $data Optional data
*
* @return string UUID v4
*/
public static function v4($data = null): string
{
// Generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = $data ?? random_bytes(16);
assert(strlen($data) == 16);
// Set version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
// Output the 36 character UUID.
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Khofmann\Input;
use Khofmann\Request\Request;
/**
* Facade for Input (wraps SimpleRouter)
*/
class Input
{
/**
* Private since facade.
*/
private function __construct()
{
}
/**
* Get POST parameter.
*
* @param string $index Parameter name
* @param mixed $defaultValue Default value if parameter was null
*/
public static function post(string $index, $defaultValue = null)
{
$value = Request::request()->getInputHandler()->post($index, $defaultValue);
return !is_object($value) ? $value : $value->getValue();
}
/**
* Get PATCH parameter.
*
* @param string $index Parameter name
* @param mixed $defaultValue Default value if parameter was null
*/
public static function patch(string $index, $defaultValue = null)
{
$value = Request::request()->getInputHandler()->post($index, $defaultValue);
return !is_object($value) ? $value : $value->getValue();
}
/**
* Get query string parameter.
*
* @param string $index Parameter name
* @param mixed $defaultValue Default value if parameter was null
*/
public static function get(string $index, $defaultValue = null)
{
$value = Request::request()->getInputHandler()->get($index, $defaultValue);
return !is_object($value) ? $value : $value->getValue();
}
/**
* Get FILE entry.
*
* @param string $index File name
* @param mixed $defaultValue Default value if parameter was null
*/
public static function file(string $index, $defaultValue = null)
{
$value = Request::request()->getInputHandler()->file($index, $defaultValue);
return $value;
}
}
+293
View File
@@ -0,0 +1,293 @@
<?php
namespace Khofmann\Models\Post;
use DateTime;
use Exception;
use Khofmann\Models\User\User;
use JsonSerializable;
use Khofmann\ApiError\ApiError;
use Khofmann\Config\Config;
use Khofmann\Database\Database;
use PDO;
/**
* Post database model
*
* Abstracts database access
*/
class Post implements JsonSerializable
{
private int $id;
// User is set if the post was fetched by an authenticated user
private ?User $user;
// Name and image are set if the post was fetched by a non authenticated user
private ?string $name;
private ?string $image;
private string $content;
private DateTime $postedAt;
public function __construct(int $id, ?User $user, ?string $name, ?string $image, string $content, string $postedAt)
{
$this->id = $id;
$this->user = $user;
$this->name = $name;
$this->image = Config::getStoragePath() . $image;
$this->content = $content;
$this->postedAt = new DateTime($postedAt);
}
/*
* Statics
*/
/**
* Get a post by ID.
*
* Also get creator (`User` object).
*
* @param int $id Post ID
*
* @throws NotFound Post not found
* @throws NotFound Creator of post was not found
*/
public static function getByID(int $id): Post
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
*
FROM
egb_gaestebuch
WHERE
id = :ID"
);
$stmt->bindValue(":ID", $id);
$stmt->execute();
$data = $stmt->fetch();
if (!$data) throw new Exception("NotFound");
$user = User::getByID($data["benutzer_id"]);
return new Post($data["id"], $user, null, null, $data["beitrag"], $data["zeitstempel"]);
}
/**
* Create a new post.
*
* @param User $user Creator of post
* @param string $content Post content
* @param int $limit Limit for posts list to calculate number of pages
*
* @return array Number of pages after creation (in accordance with `limit`) and created post
*/
public static function create(User $user, string $content, int $limit): array
{
$content = substr(trim($content), 0, 250);
$db = Database::getInstance();
$stmt = $db->prepare(
"INSERT INTO
egb_gaestebuch(benutzer_id, beitrag)
VALUES(:USR, :CON)"
);
$stmt->bindValue(":USR", $user->getID());
$stmt->bindValue(":CON", nl2br(htmlspecialchars($content)));
$stmt->execute();
$lastId = $db->lastInsertId();
$stmt = $db->prepare(
"SELECT
COUNT(*)
FROM
egb_gaestebuch"
);
$stmt->execute();
$count = $stmt->fetch(PDO::FETCH_COLUMN, 0);
return [
"pages" => intdiv($count, $limit + 1) + 1,
"post" => Post::getByID($lastId)
];
}
/**
* List of posts
*
* @param int $page Current page (offset)
* @param int $limit Posts per page
* @param bool $authed If `true`, include full `User` object. Defaults to `false`
*
* @return array Number of pages and posts of selected page
*/
public static function list(int $page, int $limit, bool $authed = false): array
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
COUNT(*)
FROM
egb_gaestebuch"
);
$stmt->execute();
$count = $stmt->fetch(PDO::FETCH_COLUMN, 0);
$stmt = $db->prepare(
"SELECT
*
FROM
egb_gaestebuch
LIMIT $limit
OFFSET " . ($page * $limit)
);
$stmt->execute();
$data = $stmt->fetchAll();
$list = array_map(
function ($item) use ($authed) {
$user = User::getByID($item["benutzer_id"]);
return new Post($item["id"], $authed ? $user : null, !$authed ? $user->getUsername() : null, !$authed ? $user->getImage() : null, $item["beitrag"], $item["zeitstempel"]);
},
$data
);
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $list];
}
/*
* Members
*/
/**
* Update post
*
* Does nothing if new `content` is empty
*
* @param ?string $content New content
*
* @throws Failed Failed to update content
*/
public function update(?string $content): Post
{
$db = Database::getInstance();
// Make sure we do all changes or none
$db->beginTransaction();
$failed = [];
$reason = [];
if (!empty($content)) {
$content = substr(trim($content), 0, 250);
$stmt = $db->prepare("UPDATE egb_gaestebuch SET beitrag = :CON WHERE id = :ID");
$stmt->bindValue(":CON", nl2br(htmlspecialchars($content)));
$stmt->bindValue(":ID", $this->id);
try {
if (!$stmt->execute()) {
$failed = ["content"];
$reason = ["generic"];
}
} catch (Exception $e) {
$failed = ["content"];
if ($e->getCode() === "23000") {
$pdoErr = $stmt->errorInfo()[1];
$reason = ["SQL: $pdoErr"];
} else $reason = ["{$e->getCode()}"];
}
}
if (count($failed) > 0) {
// We failed, go back
$db->rollBack();
throw ApiError::failedUpdate($failed, $reason);
}
// Commit the changes
$db->commit();
return Post::getByID($this->id);
}
/**
* Delete post
*
* @param int $limit Limit of list for which the returned pages is calculated.
*
* @return array Returns deleted post and resulting amount of pages for a given limit.
*/
public function delete(int $limit): array
{
$db = Database::getInstance();
$stmt = $db->prepare("DELETE FROM egb_gaestebuch WHERE id = :ID");
$stmt->bindValue(":ID", $this->id);
$stmt->execute();
$stmt = $db->prepare(
"SELECT
COUNT(*)
FROM
egb_gaestebuch"
);
$stmt->execute();
$count = $stmt->fetch(PDO::FETCH_COLUMN, 0);
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $this];
}
/*
* Getters
*/
/**
* Get post ID
*/
public function getId(): int
{
return $this->id;
}
/**
* Get post creator
*/
public function getUser(): User
{
return $this->user;
}
/**
* Get post content
*/
public function getContent(): string
{
return $this->content;
}
/**
* Get time when post was created
*/
public function getPostedAt(): DateTime
{
return $this->postedAt;
}
/*
* JSON
*/
public function jsonSerialize(): array
{
$user = $this->user ? $this->user : [
"username" => $this->name,
"image" => $this->image,
];
return [
'id' => $this->id,
'user' => $user,
'content' => $this->content,
'postedAt' => $this->postedAt,
];
}
}
+857
View File
@@ -0,0 +1,857 @@
<?php
namespace Khofmann\Models\User;
use Exception;
use PDO;
use DateTime;
use Khofmann\Database\Database;
use Khofmann\Config\Config;
use JsonSerializable;
use Khofmann\ApiError\ApiError;
use Khofmann\GUID\GUID;
use Khofmann\Models\Post\Post;
/**
* User database model
*
* Abstracts database access
*/
class User implements JsonSerializable
{
private int $id;
private string $username;
private int $status;
private string $email;
private ?string $image;
private bool $isAdmin;
private DateTime $memberSince;
private int $postCount;
private function __construct(int $id, string $username, int $status, string $email, string $timestamp, ?string $image, bool $isAdmin, int $postCount)
{
$this->id = $id;
$this->username = $username;
$this->status = $status;
$this->email = $email;
$this->image = $image;
$this->isAdmin = $isAdmin;
$this->memberSince = new DateTime($timestamp);
$this->postCount = $postCount;
}
/**
* Get a user by their confirmation code.
*
* @param string $code Confirmation code
*
* @throws NotFound User not found
*/
private static function getByConfirmCode(string $code): User
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
b.id, b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
WHERE
confirmationcode = :COD"
);
$stmt->bindValue(":COD", $code);
$stmt->execute();
$data = $stmt->fetch();
if (!$data) throw new Exception("NotFound");
return new User($data["id"], $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
}
/*
* Statics
*/
/**
* Get a user by ID.
*
* @param int $id User ID
*
* @throws NotFound User not found
*/
public static function getByID(int $id): User
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
WHERE
b.id = :ID"
);
$stmt->bindValue(":ID", $id);
$stmt->execute();
$data = $stmt->fetch();
if (!$data) throw new Exception("NotFound");
return new User($id, $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
}
/**
* Get a user by their email.
*
* @param string $email User email
*
* @throws NotFound User not found
*/
public static function getByEmail(string $email): User
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
b.id, b.benutzer, b.status, b.image, b.isadmin, b.zeitstempel,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
WHERE
email = :EMAIL"
);
$stmt->bindValue(":EMAIL", $email);
$stmt->execute();
$data = $stmt->fetch();
if (!$data) throw new Exception("NotFound");
return new User($data["id"], $data["benutzer"], $data["status"], $email, $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
}
/**
* Get a user by their authentication token.
*
* @param string $token Authentication token
*
* @throws NotFound User not found
*/
public static function getByToken(string $token): User
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
b.id, b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
WHERE
token = :TOKEN AND
tokenExpiry > NOW()"
);
$stmt->bindValue(":TOKEN", $token);
$stmt->execute();
$data = $stmt->fetch();
if (!$data) throw new Exception("NotFound");
return new User($data["id"], $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
}
/**
* Log in a user
*
* @param string $email User email
* @param string $password USer password
*
* @return array Tokens and user
*
* @throws Failed Could not generate tokens
* @throws Invalid Password was wrong
* @throws NotFound User not found (email wrong)
*/
public static function logIn(string $email, string $password): array
{
$db = Database::getInstance();
// Get user data
$stmt = $db->prepare(
"SELECT
b.*,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
WHERE
email LIKE :EMAIL AND
status = 1"
);
$stmt->bindValue(":EMAIL", $email);
$stmt->execute();
$data = $stmt->fetch();
if ($data) {
$user = new User($data["id"], $data["benutzer"], $data["status"], $email, $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
if (password_verify($password, $data["passwort"])) {
// REHASH for safety should it somehow change
if (password_needs_rehash($data["passwort"], PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("UPDATE egb_benutzer SET passwort = :PAS WHERE id = :ID");
$stmt->bindValue(":PAS", $newHash);
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
}
// Generate tokens only if expired or missing
if (empty($data["token"]) || new DateTime($data["tokenExpiry"]) <= new DateTime()) {
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
token = UUID(),
tokenExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getTokenExpiry() . "),
refreshToken = UUID(),
refreshExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getRefreshTokenExpiry() . ")
WHERE id = :ID"
);
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
}
// Get token
$stmt = $db->prepare("SELECT token, refreshToken FROM egb_benutzer WHERE id = :ID");
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
[$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
// Return user and tokens
if ($token) {
return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
}
// Token generation failed
throw new Exception("Failed");
} else {
// PW wrong
throw new Exception("Invalid");
}
} else {
// User does not exist
throw new Exception("NotFound");
}
}
/**
* Create a user
*
* @param string $username Username
* @param string $email User email
* @param string $password User password
*
* @throws Duplicate User with this username or email already exists
*/
public static function create(string $username, string $email, string $password): User
{
$db = Database::getInstance();
$guid = GUID::v4();
$stmt = $db->prepare(
"INSERT INTO
egb_benutzer(benutzer, passwort, email, confirmationcode)
VALUES(:USR, :PAS, :EMA, :COD)"
);
$stmt->bindValue(":USR", htmlspecialchars($username));
$stmt->bindValue(":PAS", password_hash($password, PASSWORD_DEFAULT));
$stmt->bindValue(":EMA", $email);
$stmt->bindValue(":COD", $guid);
try {
$stmt->execute();
$user = User::getByID($db->lastInsertId());
mail(
$email,
"Account activation GuestBookDB",
"Hello $username. To activate your account, visit https://khofmann.userpage.fu-berlin.de/phpCourse/exam/confirm?code=$guid"
);
return $user;
} catch (Exception $err) {
if ($err->getCode() === "23000") throw new Exception("Duplicate");
throw $err;
}
}
/**
* Confirm a user
*
* @param string $confirmCode Confirmation code
*
* @throws NotFound User not found (invalid code or already confirmed)
*/
public static function confirm(string $confirmCode): User
{
$db = Database::getInstance();
$user = User::getByConfirmCode($confirmCode);
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
status = 1,
confirmationcode = NULL
WHERE id = :UID"
);
$stmt->bindValue(":UID", $user->getID());
$stmt->execute();
return User::getByID($user->getID());
}
/**
* List of users
*
* @param int $page Current page (offset)
* @param int $limit Users per page
*
* @return array Number of pages and posts of selected page
*/
public static function list(int $page, int $limit)
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
COUNT(*)
FROM
egb_gaestebuch"
);
$stmt->execute();
$count = $stmt->fetch(PDO::FETCH_COLUMN, 0);
$stmt = $db->prepare(
"SELECT
b.id, b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
LIMIT $limit
OFFSET " . ($page * $limit)
);
$stmt->execute();
$data = $stmt->fetchAll();
$list = array_map(
fn ($item) => new User($item["id"], $item["benutzer"], $item["status"], $item["email"], $item["zeitstempel"], $item["image"], $item["isadmin"] === 1, $item["postCount"]),
$data
);
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $list];
}
/**
* Refresh a user session
*
* @param string $token Authentication token
* @param string $refreshToken Refresh token
*
* @throws NotFound User not found (tokens do not exist, refresh token expired)
* @throws Failed Could not regenerate tokens
*/
public static function refresh(string $token, string $refreshToken)
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
b.id, b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel, b.tokenExpiry,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
WHERE
token = :TOKEN AND
refreshToken = :REFRESH AND
refreshExpiry > NOW()"
);
$stmt->bindValue(":TOKEN", $token);
$stmt->bindValue(":REFRESH", $refreshToken);
$stmt->execute();
$data = $stmt->fetch();
if (!$data) throw new Exception("NotFound");
$user = new User($data["id"], $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
// Update tokens if expired
if (new DateTime($data["tokenExpiry"]) <= new DateTime()) {
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
token = UUID(),
tokenExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getTokenExpiry() . "),
refreshToken = UUID(),
refreshExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getRefreshTokenExpiry() . ")
WHERE id = :ID"
);
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
}
// Get token
$stmt = $db->prepare("SELECT token, refreshToken FROM egb_benutzer WHERE id = :ID");
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
[$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
// Return user and tokens
if ($token) {
return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
}
// Token generation failed
throw new Exception("Failed");
}
/*
* Members
*/
public function logOut(): bool
{
$db = Database::getInstance();
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
token = NULL,
tokenExpiry = NULL,
refreshToken = NULL,
refreshExpiry = NULL
WHERE id = :ID"
);
$stmt->bindValue(":ID", $this->id);
return $stmt->execute();
}
/**
* Update user
*
* Does nothing if new all fields are empty
*
* @param ?string $username New username
* @param ?string $password New password
* @param ?string $email New email
*
* @throws Failed At least one field failed to update
*/
public function update(?string $username, ?string $password, ?string $email): User
{
$db = Database::getInstance();
// Make sure we do all changes or none
$db->beginTransaction();
$failed = [];
$reasons = [];
if (!empty($username)) {
$stmt = $db->prepare("UPDATE egb_benutzer SET benutzer = :USR WHERE id = :ID");
$stmt->bindValue(":USR", htmlspecialchars($username));
$stmt->bindValue(":ID", $this->id);
try {
if (!$stmt->execute()) {
array_push($failed, "username");
array_push($reasons, "generic");
}
} catch (Exception $e) {
array_push($failed, "username");
if ($e->getCode() === "23000") {
$pdoErr = $stmt->errorInfo()[1];
if ($pdoErr === 1062) array_push($reasons, "Duplicate");
else array_push($reasons, "SQL: $pdoErr");
} else array_push($reasons, "{$e->getCode()}");
}
}
if (!empty($password)) {
$stmt = $db->prepare("UPDATE egb_benutzer SET passwort = :PAS WHERE id = :ID");
$stmt->bindValue(":PAS", password_hash($password, PASSWORD_DEFAULT));
$stmt->bindValue(":ID", $this->id);
try {
if (!$stmt->execute()) {
array_push($failed, "password");
array_push($reasons, "generic");
}
} catch (Exception $e) {
array_push($failed, "password");
if ($e->getCode() === "23000") {
$pdoErr = $stmt->errorInfo()[1];
if ($pdoErr === 1062) array_push($reasons, "Duplicate");
else array_push($reasons, "SQL: $pdoErr");
} else array_push($reasons, "{$e->getCode()}");
}
}
if (!empty($email)) {
$stmt = $db->prepare("UPDATE egb_benutzer SET email = :EMA WHERE id = :ID");
$stmt->bindValue(":EMA", $email);
$stmt->bindValue(":ID", $this->id);
try {
if (!$stmt->execute()) {
array_push($failed, "email");
array_push($reasons, "generic");
}
} catch (Exception $e) {
array_push($failed, "email");
if ($e->getCode() === "23000") {
$pdoErr = $stmt->errorInfo()[1];
if ($pdoErr === 1062) array_push($reasons, "Duplicate");
else array_push($reasons, "SQL: $pdoErr");
} else array_push($reasons, "{$e->getCode()}");
}
}
if (count($failed) > 0) {
// We failed, go back
$db->rollBack();
throw ApiError::failedUpdate($failed, $reasons);
}
// Commit the changes
$db->commit();
return User::getByID($this->id);
}
/**
* Update user
*
* Does nothing if all fields are empty
*
* @param mixed $image New file upload
* @param ?string $predefined Predefined avatar
*
* @param Failed Image failed to update
*/
public function updateImage($image, ?string $predefined): User
{
$db = Database::getInstance();
// Grab old picture
$stmt = $db->prepare("SELECT image FROM egb_benutzer WHERE id = :ID");
$stmt->bindValue(":ID", $this->id);
$stmt->execute();
$oldImage = $stmt->fetch(PDO::FETCH_COLUMN, 0);
// Make sure we do all changes or none
$db->beginTransaction();
$failed = [];
$reasons = [];
if (!empty($image)) {
// Generate new filename
$destinationFilename = sprintf('%s.%s', uniqid(), $image->getExtension());
// Try and update user record
try {
$stmt = $db->prepare("UPDATE egb_benutzer SET image = :IMG WHERE id = :ID");
$stmt->bindValue(":IMG", "profilbilder/$destinationFilename");
$stmt->bindValue(":ID", $this->id);
if (!$stmt->execute()) {
array_push($failed, "image");
array_push($reasons, "generic");
}
// Move file
$image->move(Config::getStorageFSPath() . "profilbilder/$destinationFilename");
} catch (Exception $e) {
array_push($failed, "image");
if ($e->getCode() === "23000") {
$pdoErr = $stmt->errorInfo()[1];
if ($pdoErr === 1062) array_push($reasons, "Duplicate");
else array_push($reasons, "SQL: $pdoErr");
} else array_push($reasons, "{$e->getCode()}");
}
} else if (!empty($predefined)) {
$stmt = $db->prepare("UPDATE egb_benutzer SET image = :IMG WHERE id = :ID");
$stmt->bindValue(":IMG", "profilbilder/default/$predefined.svg");
$stmt->bindValue(":ID", $this->id);
try {
if (!$stmt->execute()) {
array_push($failed, "image");
array_push($reasons, "generic");
}
} catch (Exception $e) {
array_push($failed, "image");
if ($e->getCode() === "23000") {
$pdoErr = $stmt->errorInfo()[1];
if ($pdoErr === 1062) array_push($reasons, "Duplicate");
else array_push($reasons, "SQL: $pdoErr");
} else array_push($reasons, "{$e->getCode()}");
}
}
if (count($failed) > 0) {
// We failed, go back
$db->rollBack();
throw ApiError::failedUpdate($failed, $reasons);
}
// Commit the changes
$db->commit();
// Delete old picture if it isn't a default one and if it exists.
try {
if (strpos($oldImage, "default") === false && is_file(Config::getStorageFSPath() . $oldImage)) unlink(Config::getStorageFSPath() . $oldImage);
} catch (Exception $e) {
}
return User::getByID($this->id);
}
/**
* Update user
*
* Does nothing if new all fields are empty
*
* @param ?bool $isAdmin Admin permission
*
* @throws Failed Failed to update admin status
*/
public function updatePermissions(?bool $isAdmin): User
{
$db = Database::getInstance();
// Make sure we do all changes or none
$db->beginTransaction();
$failed = [];
$reasons = [];
if (isset($isAdmin)) {
// Clear tokens to revoke access if logged in
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
isadmin = :ADM,
token = NULL,
tokenExpiry = NULL,
refreshToken = NULL,
refreshExpiry = NULL
WHERE
id = :ID"
);
$stmt->bindValue(":ADM", $isAdmin);
$stmt->bindValue(":ID", $this->id);
try {
if (!$stmt->execute()) {
array_push($failed, "username");
array_push($reasons, "generic");
}
} catch (Exception $e) {
array_push($failed, "username");
if ($e->getCode() === "23000") {
$pdoErr = $stmt->errorInfo()[1];
if ($pdoErr === 1062) array_push($reasons, "Duplicate");
else array_push($reasons, "SQL: $pdoErr");
} else array_push($reasons, "{$e->getCode()}");
}
}
if (count($failed) > 0) {
// We failed, go back
$db->rollBack();
throw ApiError::failedUpdate($failed, $reasons);
}
// Commit the changes
$db->commit();
return User::getByID($this->id);
}
/**
* Delete user and image
*
* @param int $limit Limit of list for which the returned pages is calculated.
*
* @return array Returns deleted user and resulting amount of pages for a given limit.
*/
public function delete(int $limit): array
{
$db = Database::getInstance();
$stmt = $db->prepare("DELETE FROM egb_benutzer WHERE id = :ID");
$stmt->bindValue(":ID", $this->id);
$stmt->execute();
$stmt = $db->prepare(
"SELECT
COUNT(*)
FROM
egb_benutzer"
);
$stmt->execute();
$count = $stmt->fetch(PDO::FETCH_COLUMN, 0);
// Delete picture if it isn't a default one and if it exists.
try {
if (strpos($this->image, "default") === false && is_file(Config::getStorageFSPath() . $this->image)) unlink(Config::getStorageFSPath() . $this->image);
} catch (Exception $e) {
}
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $this];
}
/**
* List of posts by user
*
* @param int $page Current page (offset)
* @param int $limit Users per page
* @param string $sort Sort direction
*
* @return array Number of pages and posts of selected page
*/
public function posts(int $page, int $limit, string $sort)
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
COUNT(*)
FROM
egb_gaestebuch
WHERE
benutzer_id = :ID"
);
$stmt->bindValue(":ID", $this->id);
$stmt->execute();
$count = $stmt->fetch(PDO::FETCH_COLUMN, 0);
$stmt = $db->prepare(
"SELECT
*
FROM
egb_gaestebuch
WHERE
benutzer_id = :ID
ORDER BY
id $sort
LIMIT $limit
OFFSET " . ($page * $limit)
);
$stmt->bindValue(":ID", $this->id);
$stmt->execute();
$data = $stmt->fetchAll();
$list = array_map(
function ($item) {
return new Post($item["id"], $this, null, null, $item["beitrag"], $item["zeitstempel"]);
},
$data
);
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $list];
}
/**
* Refresh user token expiry
*/
public function keepFresh()
{
try {
$db = Database::getInstance();
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
tokenExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getTokenExpiry() . "),
refreshExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getRefreshTokenExpiry() . ")
WHERE id = :ID"
);
$stmt->bindValue(":ID", $this->getID());
$stmt->execute();
} catch (Exception $err) {
}
}
/*
* Getters
*/
/**
* Get user ID
*/
public function getID(): int
{
return $this->id;
}
/**
* Get username
*/
public function getUsername(): string
{
return $this->username;
}
/**
* Get user status
*
* * 0: Not confirmed
* * 1: Confirmed
*/
public function getStatus(): int
{
return $this->status;
}
/**
* Get user email
*/
public function getEmail(): string
{
return $this->email;
}
/**
* Get user image path
*/
public function getImage(): ?string
{
return $this->image;
}
/**
* Get user admin status
*/
public function getIsAdmin(): bool
{
return $this->isAdmin;
}
/**
* Get time of user creation
*/
public function getMemberSince(): DateTime
{
return $this->memberSince;
}
/**
* Get count of posts by user
*/
public function getPostCount(): int
{
return $this->postCount;
}
/*
* JSON
*/
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'username' => $this->username,
'status' => $this->status,
'email' => htmlspecialchars($this->email),
'image' => Config::getStoragePath() . $this->image,
'isAdmin' => $this->isAdmin,
'memberSince' => $this->memberSince,
'postCount' => $this->postCount,
];
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Khofmann\Request;
use Pecee\Http\Request as PRequest;
use Pecee\SimpleRouter\SimpleRouter;
/**
* Facade for Request access (wraps SimpleRouter)
*/
class Request
{
/**
* Private since facade.
*/
private function __construct()
{
}
/**
* Get current request
*/
public static function request(): PRequest
{
return SimpleRouter::request();
}
/**
* Get header value from current request
*
* @param string $name Field name
* @param mixed $defaultValue Default value if header field was null
* @param bool $tryParse When enabled the method will try to find the header from both from client (http) and server-side variants, if the header is not found.
*/
public static function header(string $name, $defaultValue = null, bool $tryParse = true): ?string
{
return Request::request()->getHeader($name, $defaultValue, $tryParse);
}
/**
* Get authentication header field
*/
public static function token()
{
return Request::header("token");
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace Khofmann\Response;
use Pecee\SimpleRouter\SimpleRouter;
use Pecee\Http\Response as PResponse;
/**
* Facade for Response creation (wraps SimpleRouter)
*/
class Response
{
/**
* Private since facade.
*/
private function __construct()
{
}
/**
* Get current response
*/
public static function response(): PResponse
{
return SimpleRouter::response();
}
/**
* Create JSON response
*
* @param mixed $value Body of response
* @param int $options Options passed to `json_encode`
* @param int $dept Depth passed to `json_encode`
*/
public static function json($value, int $options = 0, int $dept = 512): void
{
if (is_bool($value)) {
Response::response()->header('Content-Type: application/json; charset=utf-8');
echo json_encode($value, $options, $dept);
exit(0);
}
SimpleRouter::response()->json($value, $options, $dept);
}
/**
* Create API error response
*
* @param string $value Body of error
* @param int $code HTTP code of error
*/
public static function apiError(string $value, int $code): void
{
Response::response()->header('Content-Type: application/json; charset=utf-8')->httpCode($code);
echo $value;
exit(0);
}
/**
* Create redirect response
*
* @param string $url New target
* @param ?int $code HTTP code
*/
public static function redirect(string $url, ?int $code = null): void
{
if ($code !== null) {
Response::response()->httpCode($code);
}
Response::response()->redirect($url);
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"require": {
"pecee/simple-router": "^5.4"
},
"autoload": {
"psr-4": {
"Api\\": "api/",
"Config\\": "config/",
"Khofmann\\": "classes/"
}
}
}
+82
View File
@@ -0,0 +1,82 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d067465d9a99de373ba3093d61eda1d8",
"packages": [
{
"name": "pecee/simple-router",
"version": "5.4.1.7",
"source": {
"type": "git",
"url": "https://github.com/skipperbent/simple-php-router.git",
"reference": "a2843d5b1e037f8b61cc99f27eab52a28bf41dfd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/skipperbent/simple-php-router/zipball/a2843d5b1e037f8b61cc99f27eab52a28bf41dfd",
"reference": "a2843d5b1e037f8b61cc99f27eab52a28bf41dfd",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.4"
},
"require-dev": {
"mockery/mockery": "^1",
"phpstan/phpstan": "^1",
"phpstan/phpstan-deprecation-rules": "^1",
"phpstan/phpstan-phpunit": "^1",
"phpstan/phpstan-strict-rules": "^1",
"phpunit/phpunit": "^8"
},
"type": "library",
"autoload": {
"psr-4": {
"Pecee\\": "src/Pecee/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Simon Sessingø",
"email": "simon.sessingoe@gmail.com"
}
],
"description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.",
"keywords": [
"framework",
"input-handler",
"laravel",
"pecee",
"php",
"request-handler",
"route",
"router",
"routing",
"routing-engine",
"simple-php-router",
"url-handling"
],
"support": {
"issues": "https://github.com/skipperbent/simple-php-router/issues",
"source": "https://github.com/skipperbent/simple-php-router/issues"
},
"time": "2023-12-11T21:48:25+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}
+3
View File
@@ -0,0 +1,3 @@
Order deny,allow
Deny from all
Allow from 127.0.0.1
+10
View File
@@ -0,0 +1,10 @@
<?php
return [
"basePath" => "/phpCourse/exam/",
"storagePath" => "/phpCourse/exam/storage/",
"baseFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/",
"storageFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/storage/",
"tokenExpiry" => "5 MINUTE",
"refreshTokenExpiry" => "30 MINUTE"
];
+9
View File
@@ -0,0 +1,9 @@
<?php
return [
"host" => "usersql.zedat.fu-berlin.de",
"user" => "khofmann-sql",
"passwd" => "xz8c7m7p",
"database" => "khofmann-db1",
"charset" => "utf8",
];
+7
View File
@@ -0,0 +1,7 @@
RewriteEngine On
RewriteBase /phpCourse/exam/dist
## Disallow direct entry over the dist
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ / [L,NC,QSA,F]
+23
View File
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="de">
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>
Page not Found
</title>
</head>
<body>
<div id="site-content" class="site-content">
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">404 Page not Found</h1>
<p class="col-md-8 fs-4">Page does not exist.</p>
</div>
</div>
</div>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="de">
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>
Server Error
</title>
</head>
<body>
<div id="site-content" class="site-content">
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">500 Internal Server Error</h1>
<pre class="col-md-8 fs-4"><?= $e->getMessage() ?></pre>
</div>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+178
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+17
View File
@@ -0,0 +1,17 @@
import{r as g,g as R,b as j}from"./react-C9_qfvjK.js";var V={BASE_URL:"/phpCourse/exam/dist",MODE:"production",DEV:!1,PROD:!0,SSR:!1};const y=t=>{let e;const n=new Set,o=(s,f)=>{const c=typeof s=="function"?s(e):s;if(!Object.is(c,e)){const i=e;e=f??(typeof c!="object"||c===null)?c:Object.assign({},e,c),n.forEach(a=>a(e,i))}},r=()=>e,S={setState:o,getState:r,getInitialState:()=>v,subscribe:s=>(n.add(s),()=>n.delete(s)),destroy:()=>{(V?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),n.clear()}},v=e=t(o,r,S);return S},$=t=>t?y(t):y;var b={exports:{}},w={},D={exports:{}},_={};/**
* @license React
* use-sync-external-store-shim.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var d=g;function I(t,e){return t===e&&(t!==0||1/t===1/e)||t!==t&&e!==e}var A=typeof Object.is=="function"?Object.is:I,C=d.useState,P=d.useEffect,T=d.useLayoutEffect,W=d.useDebugValue;function q(t,e){var n=e(),o=C({inst:{value:n,getSnapshot:e}}),r=o[0].inst,u=o[1];return T(function(){r.value=n,r.getSnapshot=e,m(r)&&u({inst:r})},[t,n,e]),P(function(){return m(r)&&u({inst:r}),t(function(){m(r)&&u({inst:r})})},[t]),W(n),n}function m(t){var e=t.getSnapshot;t=t.value;try{var n=e();return!A(t,n)}catch{return!0}}function z(t,e){return e()}var B=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?z:q;_.useSyncExternalStore=d.useSyncExternalStore!==void 0?d.useSyncExternalStore:B;D.exports=_;var F=D.exports;/**
* @license React
* use-sync-external-store-shim/with-selector.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var E=g,L=F;function M(t,e){return t===e&&(t!==0||1/t===1/e)||t!==t&&e!==e}var U=typeof Object.is=="function"?Object.is:M,k=L.useSyncExternalStore,G=E.useRef,H=E.useEffect,J=E.useMemo,K=E.useDebugValue;w.useSyncExternalStoreWithSelector=function(t,e,n,o,r){var u=G(null);if(u.current===null){var l={hasValue:!1,value:null};u.current=l}else l=u.current;u=J(function(){function S(i){if(!v){if(v=!0,s=i,i=o(i),r!==void 0&&l.hasValue){var a=l.value;if(r(a,i))return f=a}return f=i}if(a=f,U(s,i))return a;var h=o(i);return r!==void 0&&r(a,h)?a:(s=i,f=h)}var v=!1,s,f,c=n===void 0?null:n;return[function(){return S(e())},c===null?void 0:function(){return S(c())}]},[e,n,o,r]);var p=k(t,u[0],u[1]);return H(function(){l.hasValue=!0,l.value=p},[p]),K(p),p};b.exports=w;var N=b.exports;const Q=R(N);var O={BASE_URL:"/phpCourse/exam/dist",MODE:"production",DEV:!1,PROD:!0,SSR:!1};const{useDebugValue:X}=j,{useSyncExternalStoreWithSelector:Y}=Q;let x=!1;const Z=t=>t;function tt(t,e=Z,n){(O?"production":void 0)!=="production"&&n&&!x&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),x=!0);const o=Y(t.subscribe,t.getState,t.getServerState||t.getInitialState,e,n);return X(o),o}const et=t=>{(O?"production":void 0)!=="production"&&typeof t!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const e=typeof t=="function"?$(t):t,n=(o,r)=>tt(e,o,r);return Object.assign(n,e),n},rt=t=>et;export{rt as c};
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Some files were not shown because too many files have changed in this diff Show More