Compare commits
66 Commits
a515c447e0
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a283d755de | |||
| 4da6e6fb5f | |||
| 0fbbfdc997 | |||
| b1061e67ac | |||
| ae31f57ee0 | |||
| 28955feda7 | |||
| c192eeba65 | |||
| 45778d83d0 | |||
| bf3cebcf86 | |||
| 478f2429f5 | |||
| f6a10c8133 | |||
| 386472e680 | |||
| 9cb34839ec | |||
| bc5c04c7be | |||
| 702bb94004 | |||
| b455fdb20e | |||
| 34b7b47d0e | |||
| a85cdc6356 | |||
| 6a62ae58fc | |||
| 5ce2215e44 | |||
| e20206d4a5 | |||
| 053724f9b7 | |||
| 5f9654b5af | |||
| bf2f4954ee | |||
| e6fcd54e22 | |||
| 9deff439d7 | |||
| 2008888a16 | |||
| 7d92daeccd | |||
| 0b661c7ccc | |||
| 45d4ebcded | |||
| 9828ee762a | |||
| 7723dd0722 | |||
| 96bdbfc62a | |||
| 73c7dc1b5e | |||
| 93e5cebce0 | |||
| 215ed1bc7f | |||
| 72a0ad6364 | |||
| 9d6bd65583 | |||
| 2c9f8caff4 | |||
| a950f6770a | |||
| a3ec9ae318 | |||
| eb3516bfb2 | |||
| 627654e0a7 | |||
| 15ca7e8879 | |||
| 42529a66d4 | |||
| 5ebdee09d8 | |||
| ee9126b9e8 | |||
| c1b35aeef4 | |||
| 619f704c4e | |||
| 3beb06ceb0 | |||
| f25b0127e4 | |||
| f8fecbae4f | |||
| e850d14c2b | |||
| 24fad3bb59 | |||
| 7144b017b3 | |||
| 4dcb7531d3 | |||
| 6018e8bee5 | |||
| ce7ba50b9d | |||
| 08731f8559 | |||
| 74fd55084f | |||
| 43e2c6c0ad | |||
| 64311f2765 | |||
| d71eaf2ef2 | |||
| 12f7176467 | |||
| 683b6020b7 | |||
| 581cacb636 |
@@ -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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -33,7 +48,7 @@ class Login extends Api
|
|||||||
case "NotFound":
|
case "NotFound":
|
||||||
throw ApiError::notFound("user");
|
throw ApiError::notFound("user");
|
||||||
case "Invalid":
|
case "Invalid":
|
||||||
throw ApiError::unauthorized("Invalid username or password");
|
throw ApiError::notAllowed("Invalid username or password");
|
||||||
default:
|
default:
|
||||||
throw $err;
|
throw $err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,11 +32,22 @@ 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.
|
||||||
// Throw 400 error if a required one is missing.
|
// Throw 400 error if a required one is missing.
|
||||||
$content = Input::post("content");
|
$content = Input::post("content");
|
||||||
|
// This one is optional
|
||||||
|
$limit = constrain(0, 30, intval(Input::get("l", 10)));
|
||||||
if (empty($content)) throw ApiError::missingField(["content"]);
|
if (empty($content)) throw ApiError::missingField(["content"]);
|
||||||
|
|
||||||
// Get logged in user
|
// Get logged in user
|
||||||
@@ -36,7 +55,7 @@ class Posts extends Api
|
|||||||
|
|
||||||
// Try to create a new post for logged in user.
|
// Try to create a new post for logged in user.
|
||||||
try {
|
try {
|
||||||
Response::json(Post::create($self, $content));
|
Response::json(Post::create($self, $content, $limit));
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
switch ($err->getMessage()) {
|
switch ($err->getMessage()) {
|
||||||
default:
|
default:
|
||||||
@@ -45,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.
|
||||||
@@ -58,7 +90,7 @@ class Posts extends Api
|
|||||||
$post = Post::getByID($id);
|
$post = Post::getByID($id);
|
||||||
|
|
||||||
// Throw 400 if we aren't admin but trying to edit another users post.
|
// Throw 400 if we aren't admin but trying to edit another users post.
|
||||||
if (!$self->getIsAdmin() && $post->getUser()->getID() !== $self->getID()) throw ApiError::unauthorized("Not allowed");
|
if (!$self->getIsAdmin() && $post->getUser()->getID() !== $self->getID()) throw ApiError::notAllowed("Not allowed");
|
||||||
|
|
||||||
// Try update.
|
// Try update.
|
||||||
Response::json($post->update($content));
|
Response::json($post->update($content));
|
||||||
@@ -73,11 +105,24 @@ 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 and constrain all parameters.
|
||||||
|
$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 {
|
||||||
Response::json(Post::getByID($id)->delete());
|
Response::json(Post::getByID($id)->delete($limit));
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
switch ($err->getMessage()) {
|
switch ($err->getMessage()) {
|
||||||
case "NotFound":
|
case "NotFound":
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,17 +58,29 @@ 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.
|
||||||
$username = Input::patch("username");
|
$username = Input::patch("username");
|
||||||
$password = Input::patch("password");
|
$password = Input::patch("password");
|
||||||
$image = Input::file("image");
|
$email = Input::patch("email");
|
||||||
|
|
||||||
// Try and update user.
|
// Try and update user.
|
||||||
// Throw errors according to situation.
|
// Throw errors according to situation.
|
||||||
try {
|
try {
|
||||||
Response::json(User::getByID($id)->update($username, $password, $image));
|
Response::json(User::getByID($id)->update($username, $password, $email));
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
switch ($err->getMessage()) {
|
switch ($err->getMessage()) {
|
||||||
case "NotFound":
|
case "NotFound":
|
||||||
@@ -59,18 +92,28 @@ 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.
|
||||||
$token = Request::token();
|
$token = Request::token();
|
||||||
$username = Input::patch("username");
|
$username = Input::patch("username");
|
||||||
$password = Input::patch("password");
|
$password = Input::patch("password");
|
||||||
$image = Input::file("image");
|
$email = Input::patch("email");
|
||||||
|
|
||||||
// Try and update user.
|
// Try and update user.
|
||||||
// Throw errors according to situation.
|
// Throw errors according to situation.
|
||||||
try {
|
try {
|
||||||
Response::json(User::getByToken($token)->update($username, $password, $image));
|
Response::json(User::getByToken($token)->update($username, $password, $email));
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
switch ($err->getMessage()) {
|
switch ($err->getMessage()) {
|
||||||
case "NotFound":
|
case "NotFound":
|
||||||
@@ -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":
|
||||||
|
|||||||
@@ -182,13 +182,22 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PostCreateRequest"
|
$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:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Success.
|
description: Success.
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PostResponse"
|
$ref: "#/components/schemas/PostCreateResponse"
|
||||||
400:
|
400:
|
||||||
description: Missing fields.
|
description: Missing fields.
|
||||||
content:
|
content:
|
||||||
@@ -254,7 +263,12 @@ paths:
|
|||||||
$ref: "#/components/schemas/FailedUpdateResponse"
|
$ref: "#/components/schemas/FailedUpdateResponse"
|
||||||
examples:
|
examples:
|
||||||
Failed:
|
Failed:
|
||||||
value: { "code": "FailedUpdate", "fields": ["content"] }
|
value:
|
||||||
|
{
|
||||||
|
"code": "FailedUpdate",
|
||||||
|
"fields": ["content"],
|
||||||
|
"reasons": ["string"],
|
||||||
|
}
|
||||||
tags:
|
tags:
|
||||||
- Post
|
- Post
|
||||||
delete:
|
delete:
|
||||||
@@ -270,13 +284,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/PostResponse"
|
$ref: "#/components/schemas/PostDeleteResponse"
|
||||||
404:
|
404:
|
||||||
description: Post not found.
|
description: Post not found.
|
||||||
content:
|
content:
|
||||||
@@ -288,6 +310,45 @@ paths:
|
|||||||
value: { "code": "NotFound", "entity": "post" }
|
value: { "code": "NotFound", "entity": "post" }
|
||||||
tags:
|
tags:
|
||||||
- Post
|
- 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:
|
/register:
|
||||||
post:
|
post:
|
||||||
summary: Register
|
summary: Register
|
||||||
@@ -469,7 +530,7 @@ paths:
|
|||||||
$ref: "#/components/schemas/NotFoundResponse"
|
$ref: "#/components/schemas/NotFoundResponse"
|
||||||
examples:
|
examples:
|
||||||
User not found:
|
User not found:
|
||||||
value: { "code": "NotFound", "entity": "username" }
|
value: { "code": "NotFound", "entity": "user" }
|
||||||
500:
|
500:
|
||||||
description: Update failed.
|
description: Update failed.
|
||||||
content:
|
content:
|
||||||
@@ -481,7 +542,8 @@ paths:
|
|||||||
value:
|
value:
|
||||||
{
|
{
|
||||||
"code": "FailedUpdate",
|
"code": "FailedUpdate",
|
||||||
"fields": ["username", "email", "password"],
|
"fields": ["username", "password", "email"],
|
||||||
|
"reasons": ["string", "string", "string"],
|
||||||
}
|
}
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
@@ -498,6 +560,54 @@ 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:
|
||||||
|
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:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Success.
|
description: Success.
|
||||||
@@ -514,6 +624,127 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
User not found:
|
User not found:
|
||||||
value: { "code": "NotFound", "entity": "user" }
|
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:
|
tags:
|
||||||
- User
|
- User
|
||||||
|
|
||||||
@@ -545,8 +776,7 @@ components:
|
|||||||
UnauthorizedResponse:
|
UnauthorizedResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
code:
|
code: type:NotAllowed
|
||||||
type: Unauthorized
|
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
FailedResponse:
|
FailedResponse:
|
||||||
@@ -572,6 +802,10 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
reasons:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
ErrorResponse:
|
ErrorResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -594,6 +828,8 @@ components:
|
|||||||
$ref: "#/components/schemas/UserResponse"
|
$ref: "#/components/schemas/UserResponse"
|
||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
|
refreshToken:
|
||||||
|
type: string
|
||||||
UserResponse:
|
UserResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -629,9 +865,21 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
UserPermissionsUpdateRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
isAdmin:
|
||||||
|
type: boolean
|
||||||
|
UserImageUpdateRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
image:
|
image:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
|
predefined:
|
||||||
|
type: string
|
||||||
RegisterRequest:
|
RegisterRequest:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -681,6 +929,15 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
timezone:
|
timezone:
|
||||||
type: string
|
type: string
|
||||||
|
PostCreateResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pages:
|
||||||
|
type: number
|
||||||
|
post:
|
||||||
|
$ref: "#/components/schemas/PostResponse"
|
||||||
|
PostDeleteResponse:
|
||||||
|
$ref: "#/components/schemas/PostCreateResponse"
|
||||||
PostListResponse:
|
PostListResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -702,6 +959,21 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
content:
|
content:
|
||||||
type: string
|
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:
|
securitySchemes:
|
||||||
BasicAuth:
|
BasicAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
@@ -711,4 +983,5 @@ tags:
|
|||||||
- name: Login/Logout
|
- name: Login/Logout
|
||||||
- name: Post
|
- name: Post
|
||||||
- name: Register
|
- name: Register
|
||||||
|
- name: Refresh
|
||||||
- name: User
|
- name: User
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// Enable Errors
|
// Enable Errors
|
||||||
ini_set("display_errors", 1);
|
ini_set("display_errors", 1); // DISABLE FOR RELEASE
|
||||||
ini_set("default_charset", "utf-8");
|
ini_set("default_charset", "utf-8");
|
||||||
ini_set("session.cookie_httponly", 1);
|
ini_set("session.cookie_httponly", 1);
|
||||||
ini_set("session.cookie_secure", 1);
|
ini_set("session.cookie_secure", 1);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,24 @@ class ApiError extends Exception
|
|||||||
]), 400);
|
]), 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)
|
public static function unauthorized(string $message)
|
||||||
{
|
{
|
||||||
return new ApiError(json_encode([
|
return new ApiError(json_encode([
|
||||||
@@ -35,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([
|
||||||
@@ -43,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([
|
||||||
@@ -51,11 +92,18 @@ class ApiError extends Exception
|
|||||||
]), 500);
|
]), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function failedUpdate(array $fields)
|
/**
|
||||||
|
* 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([
|
return new ApiError(json_encode([
|
||||||
"code" => "FailedUpdate",
|
"code" => "FailedUpdate",
|
||||||
"fields" => $fields,
|
"fields" => $fields,
|
||||||
|
"reasons" => $reasons,
|
||||||
]), 500);
|
]), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -34,8 +48,11 @@ class AdminAuth implements IMiddleware
|
|||||||
->header("Access-Control-Allow-Methods: *")
|
->header("Access-Control-Allow-Methods: *")
|
||||||
->header("Access-Control-Allow-Headers: *")
|
->header("Access-Control-Allow-Headers: *")
|
||||||
->httpCode(401)
|
->httpCode(401)
|
||||||
->json(["code" => "Unauthorized", "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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,23 +58,59 @@ 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
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return Config::getInstance()->app["tokenExpiry"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string Application base URI path
|
||||||
|
*/
|
||||||
|
public static function getRefreshTokenExpiry(): string
|
||||||
|
{
|
||||||
|
return Config::getInstance()->app["refreshTokenExpiry"];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -4,33 +4,63 @@ 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);
|
||||||
return !is_object($value) ? $value : $value->getValue();
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,20 @@
|
|||||||
|
|
||||||
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;
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
use Khofmann\ApiError\ApiError;
|
use Khofmann\ApiError\ApiError;
|
||||||
|
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;
|
||||||
@@ -22,12 +27,12 @@ class Post implements JsonSerializable
|
|||||||
private string $content;
|
private string $content;
|
||||||
private DateTime $postedAt;
|
private DateTime $postedAt;
|
||||||
|
|
||||||
private function __construct(int $id, ?User $user, ?string $name, ?string $image, string $content, string $postedAt)
|
public function __construct(int $id, ?User $user, ?string $name, ?string $image, string $content, string $postedAt)
|
||||||
{
|
{
|
||||||
$this->id = $id;
|
$this->id = $id;
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->image = $image;
|
$this->image = Config::getStoragePath() . $image;
|
||||||
$this->content = $content;
|
$this->content = $content;
|
||||||
$this->postedAt = new DateTime($postedAt);
|
$this->postedAt = new DateTime($postedAt);
|
||||||
}
|
}
|
||||||
@@ -36,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();
|
||||||
@@ -58,7 +73,16 @@ 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"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function create(User $user, string $content): Post
|
/**
|
||||||
|
* 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);
|
$content = substr(trim($content), 0, 250);
|
||||||
|
|
||||||
@@ -70,13 +94,35 @@ class Post implements JsonSerializable
|
|||||||
VALUES(:USR, :CON)"
|
VALUES(:USR, :CON)"
|
||||||
);
|
);
|
||||||
$stmt->bindValue(":USR", $user->getID());
|
$stmt->bindValue(":USR", $user->getID());
|
||||||
$stmt->bindValue(":CON", $content);
|
$stmt->bindValue(":CON", nl2br(htmlspecialchars($content)));
|
||||||
|
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
$lastId = $db->lastInsertId();
|
||||||
|
|
||||||
return Post::getByID($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
|
public static function list(int $page, int $limit, bool $authed = false): array
|
||||||
{
|
{
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
@@ -107,17 +153,31 @@ class Post implements JsonSerializable
|
|||||||
$data
|
$data
|
||||||
);
|
);
|
||||||
|
|
||||||
return ["pages" => intdiv($count, $limit) + 1, "data" => $list];
|
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $list];
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 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();
|
||||||
|
|
||||||
|
$failed = [];
|
||||||
|
$reason = [];
|
||||||
if (!empty($content)) {
|
if (!empty($content)) {
|
||||||
$content = substr(trim($content), 0, 250);
|
$content = substr(trim($content), 0, 250);
|
||||||
|
|
||||||
@@ -125,44 +185,88 @@ class Post implements JsonSerializable
|
|||||||
$stmt->bindValue(":CON", nl2br(htmlspecialchars($content)));
|
$stmt->bindValue(":CON", nl2br(htmlspecialchars($content)));
|
||||||
$stmt->bindValue(":ID", $this->id);
|
$stmt->bindValue(":ID", $this->id);
|
||||||
try {
|
try {
|
||||||
if (!$stmt->execute()) throw ApiError::failedUpdate(["content"]);
|
if (!$stmt->execute()) {
|
||||||
|
$failed = ["content"];
|
||||||
|
$reason = ["generic"];
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
throw ApiError::failedUpdate(["content"]);
|
$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);
|
return Post::getByID($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(): Post
|
/**
|
||||||
|
* 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();
|
$db = Database::getInstance();
|
||||||
$stmt = $db->prepare("DELETE FROM egb_gaestebuch WHERE id = :ID");
|
$stmt = $db->prepare("DELETE FROM egb_gaestebuch WHERE id = :ID");
|
||||||
$stmt->bindValue(":ID", $this->id);
|
$stmt->bindValue(":ID", $this->id);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
return $this;
|
$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
|
* 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;
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ use Khofmann\Config\Config;
|
|||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
use Khofmann\ApiError\ApiError;
|
use Khofmann\ApiError\ApiError;
|
||||||
use Khofmann\GUID\GUID;
|
use Khofmann\GUID\GUID;
|
||||||
use PDOException;
|
use Khofmann\Models\Post\Post;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User database model
|
||||||
|
*
|
||||||
|
* Abstracts database access
|
||||||
|
*/
|
||||||
class User implements JsonSerializable
|
class User implements JsonSerializable
|
||||||
{
|
{
|
||||||
private int $id;
|
private int $id;
|
||||||
@@ -35,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();
|
||||||
@@ -61,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();
|
||||||
@@ -82,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();
|
||||||
@@ -103,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();
|
||||||
@@ -113,7 +146,8 @@ class User implements JsonSerializable
|
|||||||
FROM
|
FROM
|
||||||
egb_benutzer AS b
|
egb_benutzer AS b
|
||||||
WHERE
|
WHERE
|
||||||
token = :TOKEN"
|
token = :TOKEN AND
|
||||||
|
tokenExpiry > NOW()"
|
||||||
);
|
);
|
||||||
$stmt->bindValue(":TOKEN", $token);
|
$stmt->bindValue(":TOKEN", $token);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
@@ -124,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();
|
||||||
@@ -153,18 +199,29 @@ class User implements JsonSerializable
|
|||||||
$stmt->bindValue(":ID", $user->getID());
|
$stmt->bindValue(":ID", $user->getID());
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
// Generate token
|
// Generate tokens only if expired or missing
|
||||||
$stmt = $db->prepare("UPDATE egb_benutzer SET token = UUID() WHERE id = :ID");
|
if (empty($data["token"]) || new DateTime($data["tokenExpiry"]) <= new DateTime()) {
|
||||||
$stmt->bindValue(":ID", $user->getID());
|
$stmt = $db->prepare(
|
||||||
$stmt->execute();
|
"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
|
// Get token
|
||||||
$stmt = $db->prepare("SELECT token FROM egb_benutzer WHERE id = :ID");
|
$stmt = $db->prepare("SELECT token, refreshToken FROM egb_benutzer WHERE id = :ID");
|
||||||
$stmt->bindValue(":ID", $user->getID());
|
$stmt->bindValue(":ID", $user->getID());
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$token = $stmt->fetch(PDO::FETCH_COLUMN, 0);
|
[$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
|
||||||
// Return user and token
|
// Return user and tokens
|
||||||
if ($token) {
|
if ($token) {
|
||||||
return ["user" => $user, "token" => $token];
|
return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
|
||||||
}
|
}
|
||||||
// Token generation failed
|
// Token generation failed
|
||||||
throw new Exception("Failed");
|
throw new Exception("Failed");
|
||||||
@@ -178,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();
|
||||||
@@ -201,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;
|
||||||
@@ -212,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();
|
||||||
@@ -226,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();
|
||||||
@@ -245,7 +327,9 @@ class User implements JsonSerializable
|
|||||||
b.id, b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel,
|
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
|
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
|
||||||
FROM
|
FROM
|
||||||
egb_benutzer AS b"
|
egb_benutzer AS b
|
||||||
|
LIMIT $limit
|
||||||
|
OFFSET " . ($page * $limit)
|
||||||
);
|
);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$data = $stmt->fetchAll();
|
$data = $stmt->fetchAll();
|
||||||
@@ -255,7 +339,68 @@ 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)
|
||||||
|
{
|
||||||
|
$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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -265,24 +410,56 @@ class User implements JsonSerializable
|
|||||||
public function logOut(): bool
|
public function logOut(): bool
|
||||||
{
|
{
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
$stmt = $db->prepare("UPDATE egb_benutzer SET token = NULL WHERE id = :ID");
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE
|
||||||
|
egb_benutzer
|
||||||
|
SET
|
||||||
|
token = NULL,
|
||||||
|
tokenExpiry = NULL,
|
||||||
|
refreshToken = NULL,
|
||||||
|
refreshExpiry = NULL
|
||||||
|
WHERE id = :ID"
|
||||||
|
);
|
||||||
$stmt->bindValue(":ID", $this->id);
|
$stmt->bindValue(":ID", $this->id);
|
||||||
return $stmt->execute();
|
return $stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(?string $username, ?string $password, $image = null): User
|
/**
|
||||||
|
* 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();
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Make sure we do all changes or none
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
$failed = [];
|
$failed = [];
|
||||||
|
$reasons = [];
|
||||||
if (!empty($username)) {
|
if (!empty($username)) {
|
||||||
$stmt = $db->prepare("UPDATE egb_benutzer SET benutzer = :USR WHERE id = :ID");
|
$stmt = $db->prepare("UPDATE egb_benutzer SET benutzer = :USR WHERE id = :ID");
|
||||||
$stmt->bindValue(":USR", htmlspecialchars($username));
|
$stmt->bindValue(":USR", htmlspecialchars($username));
|
||||||
$stmt->bindValue(":ID", $this->id);
|
$stmt->bindValue(":ID", $this->id);
|
||||||
try {
|
try {
|
||||||
if (!$stmt->execute()) array_push($failed, "username");
|
if (!$stmt->execute()) {
|
||||||
|
array_push($failed, "username");
|
||||||
|
array_push($reasons, "generic");
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
array_push($failed, "username");
|
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()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,78 +468,370 @@ class User implements JsonSerializable
|
|||||||
$stmt->bindValue(":PAS", password_hash($password, PASSWORD_DEFAULT));
|
$stmt->bindValue(":PAS", password_hash($password, PASSWORD_DEFAULT));
|
||||||
$stmt->bindValue(":ID", $this->id);
|
$stmt->bindValue(":ID", $this->id);
|
||||||
try {
|
try {
|
||||||
if (!$stmt->execute()) array_push($failed, "password");
|
if (!$stmt->execute()) {
|
||||||
|
array_push($failed, "password");
|
||||||
|
array_push($reasons, "generic");
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
array_push($failed, "password");
|
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($image)) {
|
if (!empty($email)) {
|
||||||
$destinationFilename = sprintf('%s.%s', uniqid(), $image->getExtension());
|
$stmt = $db->prepare("UPDATE egb_benutzer SET email = :EMA WHERE id = :ID");
|
||||||
$image->move(Config::getStoragePath() . "profilbilder/$destinationFilename");
|
$stmt->bindValue(":EMA", $email);
|
||||||
|
|
||||||
$stmt = $db->prepare("UPDATE egb_benutzer SET image = :IMG WHERE id = :ID");
|
|
||||||
$stmt->bindValue(":IMG", $destinationFilename);
|
|
||||||
$stmt->bindValue(":ID", $this->id);
|
$stmt->bindValue(":ID", $this->id);
|
||||||
try {
|
try {
|
||||||
if (!$stmt->execute()) array_push($failed, "image");
|
if (!$stmt->execute()) {
|
||||||
|
array_push($failed, "email");
|
||||||
|
array_push($reasons, "generic");
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
array_push($failed, "image");
|
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) throw ApiError::failedUpdate($failed);
|
|
||||||
|
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);
|
return User::getByID($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(): User
|
/**
|
||||||
|
* 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();
|
$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)
|
||||||
|
{
|
||||||
|
$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
|
* 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;
|
||||||
@@ -379,7 +848,7 @@ class User implements JsonSerializable
|
|||||||
'username' => $this->username,
|
'username' => $this->username,
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
'email' => htmlspecialchars($this->email),
|
'email' => htmlspecialchars($this->email),
|
||||||
'image' => $this->image,
|
'image' => Config::getStoragePath() . $this->image,
|
||||||
'isAdmin' => $this->isAdmin,
|
'isAdmin' => $this->isAdmin,
|
||||||
'memberSince' => $this->memberSince,
|
'memberSince' => $this->memberSince,
|
||||||
'postCount' => $this->postCount,
|
'postCount' => $this->postCount,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"basePath" => "phpCourse/exam/",
|
"basePath" => "/phpCourse/exam/",
|
||||||
|
"storagePath" => "/phpCourse/exam/storage/",
|
||||||
"baseFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/",
|
"baseFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/",
|
||||||
"storagePath" => "/home/k/khofmann/public_html/phpCourse/exam/storage/"
|
"storageFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/storage/",
|
||||||
|
"tokenExpiry" => "5 MINUTE",
|
||||||
|
"refreshTokenExpiry" => "30 MINUTE"
|
||||||
];
|
];
|
||||||
|
|||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -1 +0,0 @@
|
|||||||
import"./react-DXd9vB-a.js";
|
|
||||||
@@ -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};
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
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>
|
||||||
|
After Width: | Height: | Size: 865 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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-DRZup-fm.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-DXd9vB-a.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C9_qfvjK.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-DQviNP-p.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BnAUJOoN.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-CLt5K1Fy.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DpDh5IPY.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DJgSTqOl.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>
|
||||||
|
|||||||
@@ -1,6 +1,35 @@
|
|||||||
{
|
{
|
||||||
|
"Unauthorized": "Keine Berechtigung",
|
||||||
|
|
||||||
|
"NotAllowed_login": "Ungültige E-Mail oder Passwort",
|
||||||
"NotFound_user:login": "Benutzer existiert nicht",
|
"NotFound_user:login": "Benutzer existiert nicht",
|
||||||
"Unauthorized_login": "Ungültige E-Mail oder Passwort",
|
"MissingField_email:login": "E-Mail darf nicht leer sein",
|
||||||
|
"MissingField_password:login": "Passwort darf nicht leer sein",
|
||||||
|
|
||||||
|
"NotAllowed_deletePost": "Keine Berechtigung",
|
||||||
|
"NotFound_post:deletePost": "Post nicht gefunden",
|
||||||
|
|
||||||
|
"NotAllowed_userUpdate": "Keine Berechtigung",
|
||||||
|
"NotFound_user:userUpdate": "Benutzer nicht gefunden",
|
||||||
|
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
|
||||||
|
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
|
||||||
|
|
||||||
|
"Unauthorized_newPost": "Keine Berechtigung",
|
||||||
|
"MissingField_content:newPost": "Beitrag darf nicht leer sein",
|
||||||
|
|
||||||
|
"NotAllowed_postUpdate": "Keine Berechtigung",
|
||||||
|
"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",
|
||||||
|
|
||||||
|
"username": "Benutzername",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"password": "Passwort",
|
||||||
|
"image": "Bild",
|
||||||
|
"content": "Beitrag",
|
||||||
|
|
||||||
"GuestBook": "Gästebuch",
|
"GuestBook": "Gästebuch",
|
||||||
|
|
||||||
@@ -9,14 +38,79 @@
|
|||||||
|
|
||||||
"Email required": "E-Mail darf nicht leer sein",
|
"Email required": "E-Mail darf nicht leer sein",
|
||||||
"Password required": "Passwort darf nicht leer sein",
|
"Password required": "Passwort darf nicht leer sein",
|
||||||
|
"Username required": "Benutzername darf nicht leer sein",
|
||||||
|
|
||||||
"Log in": "Anmelden",
|
"Log in": "Anmelden",
|
||||||
"Log out": "Abmelden",
|
"Log out": "Abmelden",
|
||||||
"Profile": "Profil",
|
"Profile": "Profil",
|
||||||
|
|
||||||
"Updating": "Aktualisiert",
|
"Updating": "Aktualisiert...",
|
||||||
|
|
||||||
"Username": "Benutzername",
|
"Username": "Benutzername",
|
||||||
"Member since": "Mitglied seit",
|
"Member since": "Mitglied seit",
|
||||||
"Post count": "Anzahl Posts"
|
"Post count": "Anzahl Posts",
|
||||||
|
|
||||||
|
"Edit": "Bearbeiten",
|
||||||
|
"Delete": "Löschen",
|
||||||
|
"Save": "Speichern",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Yes": "Ja",
|
||||||
|
"No": "Nein",
|
||||||
|
|
||||||
|
"Confirm post delete title": "Diesen Post löschen?",
|
||||||
|
"Confirm post delete body": "Möchtest du diesen Post von {{name}} wirklich Löschen?",
|
||||||
|
|
||||||
|
"Deleting": "Löscht...",
|
||||||
|
"Edit data": "Daten ändern",
|
||||||
|
|
||||||
|
"Recent posts": "Letzte Posts",
|
||||||
|
|
||||||
|
"Comment": "Beitrag",
|
||||||
|
"Content required": "Beitrag darf nicht leer sein",
|
||||||
|
"Post comment": "Beitrag posten",
|
||||||
|
"Edit Post": "Post editieren",
|
||||||
|
|
||||||
|
"Edit image": "Profilbild bearbeiten",
|
||||||
|
"Predefined": "Vorgefertigt",
|
||||||
|
"Upload image": "Bild hochladen",
|
||||||
|
"Upload named": "{{name}} gewählt",
|
||||||
|
"Avatar": "Avatar {{name}}",
|
||||||
|
"Remove": "Entfernen",
|
||||||
|
"or": "oder",
|
||||||
|
|
||||||
|
"Leave comment header": "Du benötigst ein Konto um einen Kommentar zu hinterlassen.",
|
||||||
|
"Leave comment action": "Klicke oben rechts auf das Icon um dich anzumelden oder zu registrieren.",
|
||||||
|
|
||||||
|
"Register prompt": "<0>Noch kein Konto?</0> <1>Registriere</1> <2>dich jetzt!</2>",
|
||||||
|
"Register": "Konto anlegen",
|
||||||
|
"Confirm header": "Fast geschafft!",
|
||||||
|
"Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,35 @@
|
|||||||
{
|
{
|
||||||
|
"Unauthorized": "Not allowed",
|
||||||
|
|
||||||
|
"NotAllowed_login": "Invalid email or password",
|
||||||
"NotFound_user:login": "User does not exist",
|
"NotFound_user:login": "User does not exist",
|
||||||
"Unauthorized_login": "Invalid email or password",
|
"MissingField_email:login": "E-Mail required",
|
||||||
|
"MissingField_password:login": "Password required",
|
||||||
|
|
||||||
|
"NotAllowed_deletPost": "Not allowed",
|
||||||
|
"NotFound_post:deletePost": "Post not found",
|
||||||
|
|
||||||
|
"NotAllowed_userUpdate": "Not allowed",
|
||||||
|
"NotFound_user:userUpdate": "User not found",
|
||||||
|
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
|
||||||
|
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
|
||||||
|
|
||||||
|
"Unauthorized_newPost": "Not allowed",
|
||||||
|
"MissingField_content:newPost": "Content required",
|
||||||
|
|
||||||
|
"NotAllowed_postUpdate": "Not allowed",
|
||||||
|
"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",
|
||||||
|
|
||||||
|
"username": "username",
|
||||||
|
"email": "email",
|
||||||
|
"password": "password",
|
||||||
|
"image": "image",
|
||||||
|
"content": "content",
|
||||||
|
|
||||||
"GuestBook": "GuestBook",
|
"GuestBook": "GuestBook",
|
||||||
|
|
||||||
@@ -9,14 +38,80 @@
|
|||||||
|
|
||||||
"Email required": "Email required",
|
"Email required": "Email required",
|
||||||
"Password required": "Password required",
|
"Password required": "Password required",
|
||||||
|
"Username required": "Username required",
|
||||||
|
|
||||||
"Log in": "Log in",
|
"Log in": "Log in",
|
||||||
"Log out": "Log out",
|
"Log out": "Log out",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
|
|
||||||
"Updating": "Updating",
|
"Updating": "Updating...",
|
||||||
|
|
||||||
"Username": "Username",
|
"Username": "Username",
|
||||||
"Member since": "Member since",
|
"Member since": "Member since",
|
||||||
"Post count": "Post count"
|
"Post count": "Post count",
|
||||||
|
|
||||||
|
"Edit": "Edit",
|
||||||
|
"Delete": "Delete",
|
||||||
|
"Save": "Save",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Yes": "Yes",
|
||||||
|
"No": "No",
|
||||||
|
|
||||||
|
"Confirm post delete title": "Delete this post?",
|
||||||
|
"Confirm post delete body": "Do you really want to delete this post by {{name}}?",
|
||||||
|
|
||||||
|
"Deleting": "Deleting...",
|
||||||
|
|
||||||
|
"Edit data": "Edit date",
|
||||||
|
|
||||||
|
"Recent posts": "Last posts",
|
||||||
|
|
||||||
|
"Comment": "Comment",
|
||||||
|
"Content required": "Content required",
|
||||||
|
"Post comment": "Post comment",
|
||||||
|
"Edit post": "Edit post",
|
||||||
|
|
||||||
|
"Edit image": "Edit image",
|
||||||
|
"Predefined": "Predefined",
|
||||||
|
"Upload image": "Upload image",
|
||||||
|
"Upload named": "{{name}} chosen",
|
||||||
|
"Avatar": "Avatar {{name}}",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"or": "or",
|
||||||
|
|
||||||
|
"Leave comment header": "You need an account to leave a Comment.",
|
||||||
|
"Leave comment action": "Click on the icon in the top right corner to log in or register.",
|
||||||
|
|
||||||
|
"Register prompt": "<0>No account yet?</0> <1>Register</1> <2>now!</2>",
|
||||||
|
"Register": "Create account",
|
||||||
|
"Confirm header": "Almost there!",
|
||||||
|
"Confirm mail": "Check your email for a confirmation mail.",
|
||||||
|
"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 +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 |
@@ -5,14 +5,14 @@ module.exports = {
|
|||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||||
],
|
],
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['react-refresh'],
|
plugins: ['react-refresh'],
|
||||||
rules: {
|
rules: {
|
||||||
'react-refresh/only-export-components': [
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||||
'warn',
|
'no-shadow': 'off',
|
||||||
{ allowConstantExport: true },
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
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>
|
||||||
|
After Width: | Height: | Size: 865 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |