Compare commits

...

29 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
147 changed files with 3392 additions and 731 deletions
+96 -1
View File
@@ -1,3 +1,85 @@
# Ü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 # Voraussetzungen um das React Projekt zu bauen
- Node 18 - Node 18
- PNPM - PNPM
@@ -6,6 +88,9 @@
## Tabelle `egb_benutzer` ## Tabelle `egb_benutzer`
- Neue Spalten `token` (Auth token): VarChar(36), Nullable, UNIQUE Constraint - 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 `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 `benutzername`: Non-Nullable gemacht, UNIQUE Constraint
- Abänderung der Spalte `email`: Non-Nullable gemacht, UNIQUE Constraint - Abänderung der Spalte `email`: Non-Nullable gemacht, UNIQUE Constraint
@@ -15,10 +100,19 @@
## Tabelle `egb_gaestebuch` ## Tabelle `egb_gaestebuch`
- Abänderung der Spalte `benutzer_id`: Non-Nullable gemacht - Abänderung der Spalte `benutzer_id`: Non-Nullable gemacht
- Abänderung der Spalte `beitrag`: Non-Nullable gemacht - Abänderung der Spalte `beitrag`: Non-Nullable gemacht
- Hinzufüge eines Foreign Key Constraints auf `benutzer_id` - 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 # 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 ## PHP
### `classes/Models/User.php` ### `classes/Models/User.php`
@@ -28,6 +122,7 @@
- Alle Pfade - Alle Pfade
## JS ## JS
**WICHTIG:** Nach allen Änderungen muss das React Projekt neu gebaut werden
### `react/vite.config.ts` ### `react/vite.config.ts`
- `base` Pfad - `base` Pfad
+15
View File
@@ -9,8 +9,23 @@ use Khofmann\Input\Input;
use Khofmann\Response\Response; use Khofmann\Response\Response;
use Khofmann\Models\User\User; use Khofmann\Models\User\User;
/**
* Login route handlers
*/
class Login extends Api 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 public function post(): void
{ {
// Fetch all required inputs. // Fetch all required inputs.
+12
View File
@@ -7,8 +7,20 @@ use Khofmann\Models\User\User;
use Khofmann\Request\Request; use Khofmann\Request\Request;
use Khofmann\Response\Response; use Khofmann\Response\Response;
/**
* Logout route handlers
*/
class Logout extends Api 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 public function post(): void
{ {
// Get user auth token. // Get user auth token.
+42 -1
View File
@@ -11,8 +11,16 @@ use Khofmann\Models\User\User;
use Khofmann\Request\Request; use Khofmann\Request\Request;
use Khofmann\Response\Response; use Khofmann\Response\Response;
/**
* Posts route handlers
*/
class Posts extends Api 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() public function get()
{ {
// Fetch and constrain all parameters. // Fetch and constrain all parameters.
@@ -24,6 +32,15 @@ class Posts extends Api
Response::json(Post::list($page, $limit, $authed)); 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 public function post(): void
{ {
// Fetch all required inputs. // Fetch all required inputs.
@@ -47,6 +64,19 @@ class Posts extends Api
} }
} }
/**
* 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 public function patch($id): void
{ {
// Fetch all inputs. // Fetch all inputs.
@@ -75,9 +105,20 @@ class Posts extends Api
} }
} }
/**
* 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 public function delete($id): void
{ {
// Fetch ax(0, intval(Input::get("p", 0))); // Fetch and constrain all parameters.
$limit = constrain(0, 30, intval(Input::get("l", 10))); $limit = constrain(0, 30, intval(Input::get("l", 10)));
// Try delete, 404 if post was not found. // Try delete, 404 if post was not found.
try { try {
+14
View File
@@ -10,8 +10,22 @@ use Khofmann\Response\Response;
use Khofmann\Models\User\User; use Khofmann\Models\User\User;
use Khofmann\Request\Request; use Khofmann\Request\Request;
/**
* Refresh route handlers
*/
class Refresh extends Api 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 public function post(): void
{ {
// Fetch all required inputs. // Fetch all required inputs.
+25
View File
@@ -9,8 +9,22 @@ use Khofmann\Input\Input;
use Khofmann\Response\Response; use Khofmann\Response\Response;
use Khofmann\Models\User\User; use Khofmann\Models\User\User;
/**
* Register route handlers
*/
class Register extends Api 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 public function post(): void
{ {
// Fetch all required inputs. // Fetch all required inputs.
@@ -39,6 +53,17 @@ class Register extends Api
} }
} }
/**
* 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 public function patch(): void
{ {
// Fetch all required inputs. // Fetch all required inputs.
+25
View File
@@ -10,8 +10,23 @@ use Khofmann\Response\Response;
use Khofmann\ApiError\ApiError; use Khofmann\ApiError\ApiError;
use Khofmann\Request\Request; use Khofmann\Request\Request;
/**
* User image route handlers
*/
class Image extends Api 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 public function post($id): void
{ {
// Fetch all inputs. // Fetch all inputs.
@@ -33,6 +48,16 @@ class Image extends Api
} }
} }
/**
* 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 public function postSelf(): void
{ {
// Fetch all inputs. // Fetch all inputs.
@@ -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;
}
}
}
}
+12
View File
@@ -9,8 +9,20 @@ use Khofmann\Response\Response;
use Khofmann\ApiError\ApiError; use Khofmann\ApiError\ApiError;
use Khofmann\Input\Input; use Khofmann\Input\Input;
/**
* User posts route handlers
*/
class Posts extends Api 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 public function get($id): void
{ {
// Fetch and constrain all parameters. // Fetch and constrain all parameters.
+57 -1
View File
@@ -10,8 +10,18 @@ use Khofmann\Response\Response;
use Khofmann\ApiError\ApiError; use Khofmann\ApiError\ApiError;
use Khofmann\Request\Request; use Khofmann\Request\Request;
/**
* Users route handlers
*/
class Users extends Api 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() public function list()
{ {
// Fetch and constrain all parameters. // Fetch and constrain all parameters.
@@ -22,6 +32,17 @@ class Users extends Api
Response::json(User::list($page, $limit)); 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 public function get($id): void
{ {
// Try and get a user, 404 if not found. // Try and get a user, 404 if not found.
@@ -37,6 +58,18 @@ class Users extends Api
} }
} }
/**
* 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 public function patch($id): void
{ {
// Fetch all inputs. // Fetch all inputs.
@@ -59,6 +92,16 @@ class Users extends Api
} }
} }
/**
* 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 public function patchSelf(): void
{ {
// Fetch all inputs. // Fetch all inputs.
@@ -82,11 +125,24 @@ class Users extends Api
} }
} }
/**
* 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 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 to delete user, 404 if not found.
try { try {
Response::json(User::getByID($id)->delete()); Response::json(User::getByID($id)->delete($limit));
} catch (Exception $err) { } catch (Exception $err) {
switch ($err->getMessage()) { switch ($err->getMessage()) {
case "NotFound": case "NotFound":
+72 -1
View File
@@ -560,13 +560,21 @@ paths:
schema: schema:
type: integer type: integer
format: int14 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: responses:
200: 200:
description: Success. description: Success.
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/UserResponse" $ref: "#/components/schemas/UserDeleteResponse"
404: 404:
description: User not found. description: User not found.
content: content:
@@ -632,6 +640,57 @@ paths:
} }
tags: tags:
- User - 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: /users/{id}/posts:
get: get:
summary: Get user posts summary: Get user posts
@@ -808,6 +867,11 @@ components:
type: string type: string
email: email:
type: string type: string
UserPermissionsUpdateRequest:
type: object
properties:
isAdmin:
type: boolean
UserImageUpdateRequest: UserImageUpdateRequest:
type: object type: object
properties: properties:
@@ -903,6 +967,13 @@ components:
refreshToken: refreshToken:
type: string type: string
format: uuid4 format: uuid4
UserDeleteResponse:
type: object
properties:
pages:
type: number
post:
$ref: "#/components/schemas/UserResponse"
securitySchemes: securitySchemes:
BasicAuth: BasicAuth:
type: apiKey type: apiKey
File diff suppressed because one or more lines are too long
+5
View File
@@ -4,6 +4,11 @@ namespace Khofmann\Api;
use Khofmann\Response\Response; use Khofmann\Response\Response;
/**
* Base class for all API handler classes.
*
* Sets common headers
*/
class Api class Api
{ {
public function __construct() public function __construct()
+39
View File
@@ -4,6 +4,9 @@ namespace Khofmann\ApiError;
use Exception; use Exception;
/**
* Facade for common API errors
*/
class ApiError extends Exception class ApiError extends Exception
{ {
private function __construct($message = "", $code = 0) private function __construct($message = "", $code = 0)
@@ -11,6 +14,11 @@ class ApiError extends Exception
parent::__construct($message, $code); 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 public static function missingField(array $fields): ApiError
{ {
return new ApiError(json_encode([ return new ApiError(json_encode([
@@ -19,6 +27,11 @@ class ApiError extends Exception
]), 400); ]), 400);
} }
/**
* Error for duplicates
*
* @param string entity Entity for which a duplicate exists
*/
public static function duplicate(string $entity): ApiError public static function duplicate(string $entity): ApiError
{ {
return new ApiError(json_encode([ return new ApiError(json_encode([
@@ -27,6 +40,11 @@ class ApiError extends Exception
]), 400); ]), 400);
} }
/**
* Error for missing permissions
*
* @param string message Message specifics
*/
public static function notAllowed(string $message) public static function notAllowed(string $message)
{ {
return new ApiError(json_encode([ return new ApiError(json_encode([
@@ -35,6 +53,11 @@ class ApiError extends Exception
]), 401); ]), 401);
} }
/**
* Error for missing authentication
*
* @param string $message Message specifics
*/
public static function unauthorized(string $message) public static function unauthorized(string $message)
{ {
return new ApiError(json_encode([ return new ApiError(json_encode([
@@ -43,6 +66,11 @@ class ApiError extends Exception
]), 401); ]), 401);
} }
/**
* Error for not found
*
* @param string entity Entity for which a duplicate exists
*/
public static function notFound(string $entity) public static function notFound(string $entity)
{ {
return new ApiError(json_encode([ return new ApiError(json_encode([
@@ -51,6 +79,11 @@ class ApiError extends Exception
]), 404); ]), 404);
} }
/**
* Generic error
*
* @param string message Message specifics
*/
public static function failed(string $message) public static function failed(string $message)
{ {
return new ApiError(json_encode([ return new ApiError(json_encode([
@@ -59,6 +92,12 @@ class ApiError extends Exception
]), 500); ]), 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) public static function failedUpdate(array $fields, array $reasons)
{ {
return new ApiError(json_encode([ return new ApiError(json_encode([
+17
View File
@@ -8,8 +8,20 @@ use Pecee\Http\Request;
use Khofmann\Models\User\User; use Khofmann\Models\User\User;
use Khofmann\Response\Response; use Khofmann\Response\Response;
/**
* Middleware for admin authenticated routes
*/
class AdminAuth implements IMiddleware 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 public function handle(Request $request): void
{ {
$token = $request->getHeader("token"); $token = $request->getHeader("token");
@@ -26,7 +38,9 @@ class AdminAuth implements IMiddleware
} }
try { try {
// Get user
$user = User::getByToken($token); $user = User::getByToken($token);
// Check if user is admin
if (!$user->getIsAdmin()) { if (!$user->getIsAdmin()) {
Response::response() Response::response()
->header("Cache-control: no-cache") ->header("Cache-control: no-cache")
@@ -36,6 +50,9 @@ class AdminAuth implements IMiddleware
->httpCode(401) ->httpCode(401)
->json(["code" => "NotAllowed", "message" => "Not Authorized"]); ->json(["code" => "NotAllowed", "message" => "Not Authorized"]);
} }
// Keep fresh
$user->keepFresh();
} catch (Exception $err) { } catch (Exception $err) {
// No user with this token exists // No user with this token exists
Response::response() Response::response()
+17 -1
View File
@@ -8,8 +8,20 @@ use Pecee\Http\Request;
use Khofmann\Models\User\User; use Khofmann\Models\User\User;
use Khofmann\Response\Response; use Khofmann\Response\Response;
/**
* Middleware for authenticated routes
*/
class Auth implements IMiddleware 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 public function handle(Request $request): void
{ {
$token = $request->getHeader("token"); $token = $request->getHeader("token");
@@ -26,7 +38,11 @@ class Auth implements IMiddleware
} }
try { try {
User::getByToken($token); // Get user
$user = User::getByToken($token);
// Keep fresh
$user->keepFresh();
} catch (Exception $err) { } catch (Exception $err) {
// No user with this token exists // No user with this token exists
Response::response() Response::response()
+18 -2
View File
@@ -8,19 +8,35 @@ use Pecee\Http\Request;
use Khofmann\Models\User\User; use Khofmann\Models\User\User;
use Khofmann\Response\Response; use Khofmann\Response\Response;
/**
* Middleware for optional authenticated routes
*/
class OptAuth implements IMiddleware 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 public function handle(Request $request): void
{ {
$token = $request->getHeader("token"); $token = $request->getHeader("token");
// No token // No token, since authentication is optional, pass
if ($token === null) { if ($token === null) {
return; return;
} }
try { try {
User::getByToken($token); // Get user
$user = User::getByToken($token);
// Keep fresh
$user->keepFresh();
} catch (Exception $err) { } catch (Exception $err) {
// No user with this token exists // No user with this token exists
Response::response() Response::response()
+42
View File
@@ -4,29 +4,50 @@ namespace Khofmann\Config;
use Exception; use Exception;
/**
* Facade for application configuration
*/
class Config class Config
{ {
// Instances array to ensure singleton pattern
private static array $instances = []; private static array $instances = [];
// Configuration arrays
private array $app; private array $app;
private array $database; private array $database;
/**
* Loads configurations into arrays.
*
* Private since singleton pattern.
*/
private function __construct() private function __construct()
{ {
$this->app = require_once __DIR__ . "/../../config/app.php"; $this->app = require_once __DIR__ . "/../../config/app.php";
$this->database = require_once __DIR__ . "/../../config/database.php"; $this->database = require_once __DIR__ . "/../../config/database.php";
} }
/**
* Disallow clone
*/
private function __clone() private function __clone()
{ {
throw new Exception("Cannot clone a singleton."); throw new Exception("Cannot clone a singleton.");
} }
/**
* Disallow wakeup
*/
private function __wakeup() private function __wakeup()
{ {
throw new Exception("Cannot unserialize a singleton."); 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 private static function getInstance(): Config
{ {
$cls = static::class; $cls = static::class;
@@ -37,36 +58,57 @@ class Config
return self::$instances[$cls]; return self::$instances[$cls];
} }
/**
* @return string Application base URI path
*/
public static function getBasePath(): string public static function getBasePath(): string
{ {
return Config::getInstance()->app["basePath"]; return Config::getInstance()->app["basePath"];
} }
/**
* @return string Application base file system path
*/
public static function getBaseFSPath(): string public static function getBaseFSPath(): string
{ {
return Config::getInstance()->app["baseFSPath"]; return Config::getInstance()->app["baseFSPath"];
} }
/**
* @return string Application storage URI path
*/
public static function getStoragePath(): string public static function getStoragePath(): string
{ {
return Config::getInstance()->app["storagePath"]; return Config::getInstance()->app["storagePath"];
} }
/**
* @return string Application storage file system path
*/
public static function getStorageFSPath(): string public static function getStorageFSPath(): string
{ {
return Config::getInstance()->app["storageFSPath"]; return Config::getInstance()->app["storageFSPath"];
} }
/**
* @return string Application base URI path
*/
public static function getDatabase(): array public static function getDatabase(): array
{ {
return Config::getInstance()->database; return Config::getInstance()->database;
} }
/**
* @return string Application base URI path
*/
public static function getTokenExpiry(): string public static function getTokenExpiry(): string
{ {
return Config::getInstance()->app["tokenExpiry"]; return Config::getInstance()->app["tokenExpiry"];
} }
/**
* @return string Application base URI path
*/
public static function getRefreshTokenExpiry(): string public static function getRefreshTokenExpiry(): string
{ {
return Config::getInstance()->app["refreshTokenExpiry"]; return Config::getInstance()->app["refreshTokenExpiry"];
+18
View File
@@ -6,25 +6,43 @@ use PDO;
use Khofmann\Config\Config; use Khofmann\Config\Config;
use Exception; use Exception;
/**
* Facade for database access
*/
class Database extends PDO class Database extends PDO
{ {
// Instances array to ensure singleton pattern
private static array $instances = []; private static array $instances = [];
/**
* Private since singleton pattern.
*/
private function __construct(string $dsn, string $username = null, string $password = null, array $options = null) private function __construct(string $dsn, string $username = null, string $password = null, array $options = null)
{ {
parent::__construct($dsn, $username, $password, $options); parent::__construct($dsn, $username, $password, $options);
} }
/**
* Disallow clone
*/
private function __clone() private function __clone()
{ {
throw new Exception("Cannot clone a singleton."); throw new Exception("Cannot clone a singleton.");
} }
/**
* Disallow wakeup
*/
private function __wakeup() private function __wakeup()
{ {
throw new Exception("Cannot unserialize a singleton."); throw new Exception("Cannot unserialize a singleton.");
} }
/**
* Get the instance. Ensures singleton pattern.
*
* Loads configuration from `Config` facade
*/
public static function getInstance(): Database public static function getInstance(): Database
{ {
$cls = static::class; $cls = static::class;
+13
View File
@@ -2,12 +2,25 @@
namespace Khofmann\GUID; namespace Khofmann\GUID;
/**
* Facade for GUID generators
*/
class GUID class GUID
{ {
/**
* Private since facade.
*/
private function __construct() private function __construct()
{ {
} }
/**
* Generate a UUID v4.
*
* @param mixed $data Optional data
*
* @return string UUID v4
*/
public static function v4($data = null): string public static function v4($data = null): string
{ {
// Generate 16 bytes (128 bits) of random data or use the data passed into the function. // Generate 16 bytes (128 bits) of random data or use the data passed into the function.
+30
View File
@@ -4,30 +4,60 @@ namespace Khofmann\Input;
use Khofmann\Request\Request; use Khofmann\Request\Request;
/**
* Facade for Input (wraps SimpleRouter)
*/
class Input class Input
{ {
/**
* Private since facade.
*/
private function __construct() 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) public static function post(string $index, $defaultValue = null)
{ {
$value = Request::request()->getInputHandler()->post($index, $defaultValue); $value = Request::request()->getInputHandler()->post($index, $defaultValue);
return !is_object($value) ? $value : $value->getValue(); 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) public static function patch(string $index, $defaultValue = null)
{ {
$value = Request::request()->getInputHandler()->post($index, $defaultValue); $value = Request::request()->getInputHandler()->post($index, $defaultValue);
return !is_object($value) ? $value : $value->getValue(); 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) public static function get(string $index, $defaultValue = null)
{ {
$value = Request::request()->getInputHandler()->get($index, $defaultValue); $value = Request::request()->getInputHandler()->get($index, $defaultValue);
return !is_object($value) ? $value : $value->getValue(); 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) public static function file(string $index, $defaultValue = null)
{ {
$value = Request::request()->getInputHandler()->file($index, $defaultValue); $value = Request::request()->getInputHandler()->file($index, $defaultValue);
+64 -1
View File
@@ -2,7 +2,6 @@
namespace Khofmann\Models\Post; namespace Khofmann\Models\Post;
use Api\User\User as UserUser;
use DateTime; use DateTime;
use Exception; use Exception;
use Khofmann\Models\User\User; use Khofmann\Models\User\User;
@@ -12,6 +11,11 @@ use Khofmann\Config\Config;
use Khofmann\Database\Database; use Khofmann\Database\Database;
use PDO; use PDO;
/**
* Post database model
*
* Abstracts database access
*/
class Post implements JsonSerializable class Post implements JsonSerializable
{ {
private int $id; private int $id;
@@ -37,6 +41,16 @@ class Post implements JsonSerializable
* Statics * 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 public static function getByID(int $id): Post
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -59,6 +73,15 @@ class Post implements JsonSerializable
return new Post($data["id"], $user, null, null, $data["beitrag"], $data["zeitstempel"]); 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 public static function create(User $user, string $content, int $limit): array
{ {
$content = substr(trim($content), 0, 250); $content = substr(trim($content), 0, 250);
@@ -91,6 +114,15 @@ class Post implements JsonSerializable
]; ];
} }
/**
* 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 public static function list(int $page, int $limit, bool $authed = false): array
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -128,10 +160,20 @@ class Post implements JsonSerializable
* Members * 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 public function update(?string $content): Post
{ {
$db = Database::getInstance(); $db = Database::getInstance();
// Make sure we do all changes or none
$db->beginTransaction(); $db->beginTransaction();
$failed = []; $failed = [];
@@ -156,16 +198,25 @@ class Post implements JsonSerializable
} }
} }
if (count($failed) > 0) { if (count($failed) > 0) {
// We failed, go back
$db->rollBack(); $db->rollBack();
throw ApiError::failedUpdate($failed, $reason); throw ApiError::failedUpdate($failed, $reason);
} }
// Commit the changes
$db->commit(); $db->commit();
return Post::getByID($this->id); 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 public function delete(int $limit): array
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -189,21 +240,33 @@ class Post implements JsonSerializable
* Getters * Getters
*/ */
/**
* Get post ID
*/
public function getId(): int public function getId(): int
{ {
return $this->id; return $this->id;
} }
/**
* Get post creator
*/
public function getUser(): User public function getUser(): User
{ {
return $this->user; return $this->user;
} }
/**
* Get post content
*/
public function getContent(): string public function getContent(): string
{ {
return $this->content; return $this->content;
} }
/**
* Get time when post was created
*/
public function getPostedAt(): DateTime public function getPostedAt(): DateTime
{ {
return $this->postedAt; return $this->postedAt;
+268 -17
View File
@@ -11,8 +11,12 @@ use JsonSerializable;
use Khofmann\ApiError\ApiError; use Khofmann\ApiError\ApiError;
use Khofmann\GUID\GUID; use Khofmann\GUID\GUID;
use Khofmann\Models\Post\Post; use Khofmann\Models\Post\Post;
use PDOException;
/**
* User database model
*
* Abstracts database access
*/
class User implements JsonSerializable class User implements JsonSerializable
{ {
private int $id; private int $id;
@@ -36,6 +40,13 @@ class User implements JsonSerializable
$this->postCount = $postCount; $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 private static function getByConfirmCode(string $code): User
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -62,6 +73,13 @@ class User implements JsonSerializable
* Statics * Statics
*/ */
/**
* Get a user by ID.
*
* @param int $id User ID
*
* @throws NotFound User not found
*/
public static function getByID(int $id): User public static function getByID(int $id): User
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -83,6 +101,13 @@ class User implements JsonSerializable
return new User($id, $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]); 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 public static function getByEmail(string $email): User
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -104,6 +129,13 @@ class User implements JsonSerializable
return new User($data["id"], $data["benutzer"], $data["status"], $email, $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]); 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 public static function getByToken(string $token): User
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -126,6 +158,18 @@ class User implements JsonSerializable
return new User($data["id"], $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]); 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 public static function logIn(string $email, string $password): array
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -191,6 +235,15 @@ class User implements JsonSerializable
} }
} }
/**
* 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 public static function create(string $username, string $email, string $password): User
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -214,7 +267,7 @@ class User implements JsonSerializable
mail( mail(
$email, $email,
"Account activation GuestBookDB", "Account activation GuestBookDB",
"Hello $username. To activate your account, visit https://khofmann.userpage.fu-berlin.de/phpCourse/exam/confirm?c=$guid" "Hello $username. To activate your account, visit https://khofmann.userpage.fu-berlin.de/phpCourse/exam/confirm?code=$guid"
); );
return $user; return $user;
@@ -225,6 +278,13 @@ class User implements JsonSerializable
} }
} }
/**
* 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 public static function confirm(string $confirmCode): User
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -239,9 +299,18 @@ class User implements JsonSerializable
WHERE id = :UID" WHERE id = :UID"
); );
$stmt->bindValue(":UID", $user->getID()); $stmt->bindValue(":UID", $user->getID());
$stmt->execute();
return User::getByID($user->getID()); 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) public static function list(int $page, int $limit)
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -270,9 +339,18 @@ class User implements JsonSerializable
$data $data
); );
return ["pages" => intdiv($count, $limit + 1), "data" => $list]; 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) public static function refresh(string $token, string $refreshToken)
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -346,10 +424,22 @@ class User implements JsonSerializable
return $stmt->execute(); 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 public function update(?string $username, ?string $password, ?string $email): User
{ {
$db = Database::getInstance(); $db = Database::getInstance();
// Make sure we do all changes or none
$db->beginTransaction(); $db->beginTransaction();
$failed = []; $failed = [];
@@ -412,38 +502,47 @@ class User implements JsonSerializable
} }
if (count($failed) > 0) { if (count($failed) > 0) {
// We failed, go back
$db->rollBack(); $db->rollBack();
throw ApiError::failedUpdate($failed, $reasons); throw ApiError::failedUpdate($failed, $reasons);
} }
// Commit the changes
$db->commit(); $db->commit();
return User::getByID($this->id); 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 public function updateImage($image, ?string $predefined): User
{ {
$db = Database::getInstance(); $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(); $db->beginTransaction();
$failed = []; $failed = [];
$reasons = []; $reasons = [];
try {
$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);
if (strpos($oldImage, "default") === false) unlink(Config::getStorageFSPath() . $oldImage);
} catch (Exception $e) {
}
if (!empty($image)) { if (!empty($image)) {
// Generate new filename
$destinationFilename = sprintf('%s.%s', uniqid(), $image->getExtension()); $destinationFilename = sprintf('%s.%s', uniqid(), $image->getExtension());
$image->move(Config::getStorageFSPath() . "profilbilder/$destinationFilename"); // Try and update user record
try { try {
$stmt = $db->prepare("UPDATE egb_benutzer SET image = :IMG WHERE id = :ID"); $stmt = $db->prepare("UPDATE egb_benutzer SET image = :IMG WHERE id = :ID");
$stmt->bindValue(":IMG", "profilbilder/$destinationFilename"); $stmt->bindValue(":IMG", "profilbilder/$destinationFilename");
@@ -453,6 +552,8 @@ class User implements JsonSerializable
array_push($failed, "image"); array_push($failed, "image");
array_push($reasons, "generic"); array_push($reasons, "generic");
} }
// Move file
$image->move(Config::getStorageFSPath() . "profilbilder/$destinationFilename");
} catch (Exception $e) { } catch (Exception $e) {
array_push($failed, "image"); array_push($failed, "image");
if ($e->getCode() === "23000") { if ($e->getCode() === "23000") {
@@ -481,25 +582,127 @@ class User implements JsonSerializable
} }
if (count($failed) > 0) { if (count($failed) > 0) {
// We failed, go back
$db->rollBack(); $db->rollBack();
throw ApiError::failedUpdate($failed, $reasons); 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(); $db->commit();
return User::getByID($this->id); return User::getByID($this->id);
} }
public function delete(): User /**
* 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(); $db = Database::getInstance();
$stmt = $db->prepare("DELETE FROM egb_benutzer WHERE id = :ID"); $stmt = $db->prepare("DELETE FROM egb_benutzer WHERE id = :ID");
$stmt->bindValue(":ID", $this->id); $stmt->bindValue(":ID", $this->id);
$stmt->execute();
return $this; $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) public function posts(int $page, int $limit, string $sort)
{ {
$db = Database::getInstance(); $db = Database::getInstance();
@@ -542,45 +745,93 @@ class User implements JsonSerializable
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $list]; 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 * Getters
*/ */
/**
* Get user ID
*/
public function getID(): int public function getID(): int
{ {
return $this->id; return $this->id;
} }
/**
* Get username
*/
public function getUsername(): string public function getUsername(): string
{ {
return $this->username; return $this->username;
} }
/**
* Get user status
*
* * 0: Not confirmed
* * 1: Confirmed
*/
public function getStatus(): int public function getStatus(): int
{ {
return $this->status; return $this->status;
} }
/**
* Get user email
*/
public function getEmail(): string public function getEmail(): string
{ {
return $this->email; return $this->email;
} }
/**
* Get user image path
*/
public function getImage(): ?string public function getImage(): ?string
{ {
return $this->image; return $this->image;
} }
/**
* Get user admin status
*/
public function getIsAdmin(): bool public function getIsAdmin(): bool
{ {
return $this->isAdmin; return $this->isAdmin;
} }
/**
* Get time of user creation
*/
public function getMemberSince(): DateTime public function getMemberSince(): DateTime
{ {
return $this->memberSince; return $this->memberSince;
} }
/**
* Get count of posts by user
*/
public function getPostCount(): int public function getPostCount(): int
{ {
return $this->postCount; return $this->postCount;
+19
View File
@@ -5,22 +5,41 @@ namespace Khofmann\Request;
use Pecee\Http\Request as PRequest; use Pecee\Http\Request as PRequest;
use Pecee\SimpleRouter\SimpleRouter; use Pecee\SimpleRouter\SimpleRouter;
/**
* Facade for Request access (wraps SimpleRouter)
*/
class Request class Request
{ {
/**
* Private since facade.
*/
private function __construct() private function __construct()
{ {
} }
/**
* Get current request
*/
public static function request(): PRequest public static function request(): PRequest
{ {
return SimpleRouter::request(); 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 public static function header(string $name, $defaultValue = null, bool $tryParse = true): ?string
{ {
return Request::request()->getHeader($name, $defaultValue, $tryParse); return Request::request()->getHeader($name, $defaultValue, $tryParse);
} }
/**
* Get authentication header field
*/
public static function token() public static function token()
{ {
return Request::header("token"); return Request::header("token");
+28
View File
@@ -5,17 +5,33 @@ namespace Khofmann\Response;
use Pecee\SimpleRouter\SimpleRouter; use Pecee\SimpleRouter\SimpleRouter;
use Pecee\Http\Response as PResponse; use Pecee\Http\Response as PResponse;
/**
* Facade for Response creation (wraps SimpleRouter)
*/
class Response class Response
{ {
/**
* Private since facade.
*/
private function __construct() private function __construct()
{ {
} }
/**
* Get current response
*/
public static function response(): PResponse public static function response(): PResponse
{ {
return SimpleRouter::response(); 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 public static function json($value, int $options = 0, int $dept = 512): void
{ {
if (is_bool($value)) { if (is_bool($value)) {
@@ -26,6 +42,12 @@ class Response
SimpleRouter::response()->json($value, $options, $dept); 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 public static function apiError(string $value, int $code): void
{ {
Response::response()->header('Content-Type: application/json; charset=utf-8')->httpCode($code); Response::response()->header('Content-Type: application/json; charset=utf-8')->httpCode($code);
@@ -33,6 +55,12 @@ class Response
exit(0); 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 public static function redirect(string $url, ?int $code = null): void
{ {
if ($code !== null) { if ($code !== null) {
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
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
-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
-1
View File
@@ -1 +0,0 @@
import"./react-C_FdcE2X.js";
+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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/favicon/ms-icon-70x70.png"/>
<square150x150logo src="/favicon/ms-icon-150x150.png"/>
<square310x310logo src="/favicaon/ms-icon-310x310.png"/>
<TileColor>#000b29</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+41
View File
@@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "/favicon/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "/favicon/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/favicon/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "/favicon/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "/favicon/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "/favicon/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+26 -7
View File
@@ -2,14 +2,33 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
<link rel="apple-touch-icon" sizes="57x57" href="/phpCourse/exam/dist/favicon/apple-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/phpCourse/exam/dist/favicon/apple-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/phpCourse/exam/dist/favicon/apple-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/phpCourse/exam/dist/favicon/apple-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/phpCourse/exam/dist/favicon/apple-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/phpCourse/exam/dist/favicon/apple-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/phpCourse/exam/dist/favicon/apple-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/phpCourse/exam/dist/favicon/apple-icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/phpCourse/exam/dist/favicon/apple-icon-180x180.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/phpCourse/exam/dist/favicon/android-icon-192x192.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/phpCourse/exam/dist/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="96x96" href="/phpCourse/exam/dist/favicon/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/phpCourse/exam/dist/favicon/favicon-16x16.png" />
<link rel="manifest" href="/phpCourse/exam/dist/favicon/manifest.json" />
<meta name="msapplication-TileColor" content="#000b29" />
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png" />
<meta name="theme-color" content="#000b29" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>GuestBook</title>
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-CfSUaRjT.js"></script> <script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-SI5snNZz.js"></script>
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C9_qfvjK.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-C4H8cxTH.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BnAUJOoN.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-C0csOcmc.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DpDh5IPY.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DyW0LrNj.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/zustand-DKxXQGKw.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-Be01V9yD.js">
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css"> <link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css"> <link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
</head> </head>
+32 -2
View File
@@ -14,12 +14,15 @@
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon", "FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon", "FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
"NotAllowed_newPost": "Keine Berechtigung", "Unauthorized_newPost": "Keine Berechtigung",
"MissingField_content:newPost": "Beitrag darf nicht leer sein", "MissingField_content:newPost": "Beitrag darf nicht leer sein",
"NotAllowed_postUpdate": "Keine Berechtigung", "NotAllowed_postUpdate": "Keine Berechtigung",
"NotFound_post:postUpdate": "Post nicht gefunden", "NotFound_post:postUpdate": "Post nicht gefunden",
"NotAllowed_delete|PermissionsUser": "Keine Berechtigung",
"NotFound_user:delete|PermissionsUser": "Benutzer nicht gefunden",
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon", "Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
"username": "Benutzername", "username": "Benutzername",
@@ -82,5 +85,32 @@
"Register": "Konto anlegen", "Register": "Konto anlegen",
"Confirm header": "Fast geschafft!", "Confirm header": "Fast geschafft!",
"Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.", "Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.",
"Close": "Schließen" "Close": "Schließen",
"Dark": "Dunkel",
"Light": "Hell",
"System": "System",
"Confirm success header": "Benutzer aktiviert!",
"Confirm error header": "Benutzer existiert nicht oder ist schon aktiviert.",
"Confirm pending header": "Benutzer wird aktiviert...",
"Back to main": "Zurück zur Hauptseite",
"Page not found": "Die Seite konnte nicht gefunden werden.",
"Session expired": "Deine Sitzung ist abgelaufen.",
"General error": "Da ist wohl was schief gelaufen.",
"Favicon": "Gästebuch Icons erstellt von Smashicons - Flaticon",
"Password confirm": "Passwort bestätigen",
"Password match": "Passwörter stimmen nicht überein",
"Change password": "Passwort ändern",
"Confirm user delete title": "Diesen User löschen?",
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
"Manage users": "Benutzer verwalten",
"Admin": "Administration",
"Make Admin": "Administratorrecht erteilen",
"Demote Admin": "Administratorrecht entziehen"
} }
+36 -6
View File
@@ -1,25 +1,28 @@
{ {
"Unauthorized": "NotAllowed", "Unauthorized": "Not allowed",
"NotAllowed_login": "Invalid email or password", "NotAllowed_login": "Invalid email or password",
"NotFound_user:login": "User does not exist", "NotFound_user:login": "User does not exist",
"MissingField_email:login": "E-Mail required", "MissingField_email:login": "E-Mail required",
"MissingField_password:login": "Password required", "MissingField_password:login": "Password required",
"NotAllowed_deletPost": "NotAllowed", "NotAllowed_deletPost": "Not allowed",
"NotFound_post:deletePost": "Post not found", "NotFound_post:deletePost": "Post not found",
"NotAllowed_userUpdate": "NotAllowed", "NotAllowed_userUpdate": "Not allowed",
"NotFound_user:userUpdate": "User not found", "NotFound_user:userUpdate": "User not found",
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists", "FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists", "FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
"NotAllowed_newPost": "NotAllowed", "Unauthorized_newPost": "Not allowed",
"MissingField_content:newPost": "Content required", "MissingField_content:newPost": "Content required",
"NotAllowed_postUpdate": "NotAllowed", "NotAllowed_postUpdate": "Not allowed",
"NotFound_post:postUpdate": "Post not found", "NotFound_post:postUpdate": "Post not found",
"NotAllowed_delete|PermissionsUser": "Not allowed",
"NotFound_user:deleteUserdelete|PermissionsUser": "User not found",
"Duplicate_user:register": "A user with this username or email already exists", "Duplicate_user:register": "A user with this username or email already exists",
"username": "username", "username": "username",
@@ -83,5 +86,32 @@
"Register": "Create account", "Register": "Create account",
"Confirm header": "Almost there!", "Confirm header": "Almost there!",
"Confirm mail": "Check your email for a confirmation mail.", "Confirm mail": "Check your email for a confirmation mail.",
"Close": "Close" "Close": "Close",
"Dark": "Dark",
"Light": "Light",
"System": "System",
"Confirm success header": "User confirmed!",
"Confirm error header": "User does not exist or is already confirmed.",
"confirm pending header": "User is getting confirmed...",
"Back to main": "Back to the front page",
"Page not found": "This page was not found",
"Session expired": "Your session has expired.",
"General error": "Looks like something went wrong.",
"Favicon": "Guests book icons created by Smashicons - Flaticon",
"Password confirm": "Confirm password",
"Password match": "Password do not match",
"Change password": "Change password",
"Confirm user delete title": "Diesen User löschen?",
"Confirm user delete body": "Möchtest du {{name}} wirklich Löschen?",
"Manage users": "Manage users",
"Admin": "Administration",
"Make Admin": "Make Admin",
"Demote Admin": "Demote Admin"
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+2
View File
@@ -12,5 +12,7 @@ module.exports = {
plugins: ['react-refresh'], plugins: ['react-refresh'],
rules: { rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
}, },
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+20 -2
View File
@@ -2,9 +2,27 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/favicon/manifest.json" />
<meta name="msapplication-TileColor" content="#000b29" />
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png" />
<meta name="theme-color" content="#000b29" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>GuestBook</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+3 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "react", "name": "react",
"private": true, "private": true,
"version": "1.0.0", "version": "3.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -18,11 +18,13 @@
"@tanstack/react-form": "^0.26.4", "@tanstack/react-form": "^0.26.4",
"@tanstack/react-query": "^5.51.11", "@tanstack/react-query": "^5.51.11",
"@tanstack/react-router": "^1.45.8", "@tanstack/react-router": "^1.45.8",
"@types/react-helmet": "^6.1.11",
"i18next": "^23.12.2", "i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2", "i18next-http-backend": "^2.5.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-i18next": "^15.0.0", "react-i18next": "^15.0.0",
"zustand": "^4.5.4" "zustand": "^4.5.4"
}, },
+43 -3
View File
@@ -32,9 +32,9 @@ importers:
'@tanstack/react-router': '@tanstack/react-router':
specifier: ^1.45.8 specifier: ^1.45.8
version: 1.45.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.45.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/node': '@types/react-helmet':
specifier: ^20.14.12 specifier: ^6.1.11
version: 20.14.12 version: 6.1.11
i18next: i18next:
specifier: ^23.12.2 specifier: ^23.12.2
version: 23.12.2 version: 23.12.2
@@ -50,6 +50,9 @@ importers:
react-dom: react-dom:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
react-helmet:
specifier: ^6.1.0
version: 6.1.0(react@18.3.1)
react-i18next: react-i18next:
specifier: ^15.0.0 specifier: ^15.0.0
version: 15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -72,6 +75,9 @@ importers:
'@tanstack/router-plugin': '@tanstack/router-plugin':
specifier: ^1.45.8 specifier: ^1.45.8
version: 1.45.8(vite@5.3.4(@types/node@20.14.12)) version: 1.45.8(vite@5.3.4(@types/node@20.14.12))
'@types/node':
specifier: ^20.14.12
version: 20.14.12
'@types/react': '@types/react':
specifier: ^18.3.3 specifier: ^18.3.3
version: 18.3.3 version: 18.3.3
@@ -818,6 +824,9 @@ packages:
'@types/react-dom@18.3.0': '@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
'@types/react-helmet@6.1.11':
resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==}
'@types/react-transition-group@4.4.10': '@types/react-transition-group@4.4.10':
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
@@ -1618,6 +1627,14 @@ packages:
peerDependencies: peerDependencies:
react: ^18.3.1 react: ^18.3.1
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-helmet@6.1.0:
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==}
peerDependencies:
react: '>=16.3.0'
react-i18next@15.0.0: react-i18next@15.0.0:
resolution: {integrity: sha512-2O3IgF4zivg57Q6p6i+ChDgJ371IDcEWbuWC6gvoh5NbkDMs0Q+O7RPr4v61+Se32E0V+LmtwePAeqWZW0bi6g==} resolution: {integrity: sha512-2O3IgF4zivg57Q6p6i+ChDgJ371IDcEWbuWC6gvoh5NbkDMs0Q+O7RPr4v61+Se32E0V+LmtwePAeqWZW0bi6g==}
peerDependencies: peerDependencies:
@@ -1641,6 +1658,11 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
react-side-effect@2.1.2:
resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==}
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0
react-transition-group@4.4.5: react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies: peerDependencies:
@@ -2678,6 +2700,10 @@ snapshots:
dependencies: dependencies:
'@types/react': 18.3.3 '@types/react': 18.3.3
'@types/react-helmet@6.1.11':
dependencies:
'@types/react': 18.3.3
'@types/react-transition-group@4.4.10': '@types/react-transition-group@4.4.10':
dependencies: dependencies:
'@types/react': 18.3.3 '@types/react': 18.3.3
@@ -3519,6 +3545,16 @@ snapshots:
react: 18.3.1 react: 18.3.1
scheduler: 0.23.2 scheduler: 0.23.2
react-fast-compare@3.2.2: {}
react-helmet@6.1.0(react@18.3.1):
dependencies:
object-assign: 4.1.1
prop-types: 15.8.1
react: 18.3.1
react-fast-compare: 3.2.2
react-side-effect: 2.1.2(react@18.3.1)
react-i18next@15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-i18next@15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.24.8 '@babel/runtime': 7.24.8
@@ -3534,6 +3570,10 @@ snapshots:
react-refresh@0.14.2: {} react-refresh@0.14.2: {}
react-side-effect@2.1.2(react@18.3.1):
dependencies:
react: 18.3.1
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.24.8 '@babel/runtime': 7.24.8
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/favicon/ms-icon-70x70.png"/>
<square150x150logo src="/favicon/ms-icon-150x150.png"/>
<square310x310logo src="/favicaon/ms-icon-310x310.png"/>
<TileColor>#000b29</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+41
View File
@@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "/favicon/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "/favicon/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/favicon/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "/favicon/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "/favicon/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "/favicon/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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