Compare commits

..

No commits in common. "a5dffa065a16fd5c93f23402f63ef55745af39a8" and "1df825db2a22717996c14d1a8ed3ab708dba1b44" have entirely different histories.

124 changed files with 228 additions and 3888 deletions

View File

@ -8,7 +8,11 @@ DB_NAME=template
DB_NAME_LOG=log
# API Keys
TEMPLATE_API_KEY=
AUTH_API_KEY=
NOTIFICATION_API_KEY=
FILE_API_KEY=
HOMEPAGE_API_KEY=
BEE_API_KEY=
# Template Setup
INIT_USER_NAME=admin

2
.gitignore vendored
View File

@ -11,8 +11,6 @@
/var/
/vendor/
docker/docker-compose.yml
*.env
composer.lock
composer.development.json

View File

@ -201,8 +201,8 @@ use Psr\\Http\\Server\\RequestHandlerInterface;
class {$apiHandlerName} implements RequestHandlerInterface
{
public function __construct(
private readonly {$cqrsHandlerName} \$handler,
private readonly {$cqrsBuilderName} \$builder,
private readonly {$cqrsHandlerName} \${$cqrsHandlerVariableName},
private readonly {$cqrsBuilderName} \${$cqrsBuilderVariableName},
private readonly {$apiResponseFormatterName} \$responseFormatter,
) {
}
@ -211,10 +211,10 @@ class {$apiHandlerName} implements RequestHandlerInterface
{
\$data = \$request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);
\${$cqrsVariableName} = \$this->builder->build(
\${$cqrsVariableName} = \$this->{$cqrsBuilderVariableName}->build(
\$data
);
\$result = \$this->handler->execute(\${$cqrsVariableName});
\$result = \$this->{$cqrsHandlerVariableName}->execute(\${$cqrsVariableName});
return new SuccessResponse(\$this->responseFormatter->format(\$result));
}
@ -251,10 +251,10 @@ namespace {$cqrsHandlerNamespace};
class {$cqrsName}
{
public function __construct(
#TODO
) {
#TODO
) {
}
#TODO
}
";
@ -270,12 +270,14 @@ class {$cqrsHandlerName}
{
public function __construct(
#TODO
) {
) {
}
public function execute({$cqrsName} \${$cqrsVariableName}): {$cqrsResultName}
{
return new {$cqrsResultName}();
return new {$cqrsResultName}(
);
}
}
";

View File

@ -1,30 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
source $ENV_DIR/bin/denv_msg
source $ENV_DIR/bin/drun
# Install PHP packages
denv_echo_msg "[backend]: Composer install"
drun backend composer install
denv_success_msg "[backend]: Composer install done"
# Dump autoload
denv_echo_msg "[backend]: Dump autoload"
drun backend composer da
denv_success_msg "[backend]: Dump autoload done"
# Migrate databases to current version
denv_echo_msg "[backend]: Migrate db"
drun backend composer dmm
drun backend composer dmlm
denv_success_msg "[backend]: Migrate db done"
# Insert setup for project after this line
denv_echo_msg "[backend]: Initialize data"
drun backend composer console rbac:update
drun backend composer console init:data
denv_success_msg "[backend]: Initialize data done"

View File

@ -3,16 +3,54 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
source $ENV_DIR/bin/denv_msg
EXIT=0
# Check .env file
if [ ! -f "$PROJECT_DIR/.env" ] ; then
if [ ! -f "$PROJECT_DIR/.env" ]
then
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
denv_info_msg "[backend] Created .env"
EXIT=1
fi
# Check docker-compose.yml file
if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] ; then
cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml"
denv_info_msg "[backend] Created docker-compose.yml"
if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ]
then
cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml"
EXIT=1
fi
# Check docker-compose-mac.yml file
if [ ! -f "$PROJECT_DIR/docker/docker-compose-mac.yml" ]
then
cp "$PROJECT_DIR/docker/docker-compose-mac.yml.dist" "$PROJECT_DIR/docker/docker-compose-mac.yml"
EXIT=1
fi
if [ $EXIT -eq 1 ]
then
echo "docker-compose or env files created, please change variables and call init again"
exit 1
fi
# Source key-scripts
source $ENV_DIR/bin/drun
source $ENV_DIR/bin/dexec
# Build and start docker containers
dexec template-backend build
dexec template-backend up -d
# Install PHP packages
drun template-backend composer install
# Dump autoload
drun template-backend composer da
# Migrate databases to current version
drun template-backend composer dmm
drun template-backend composer dmlm
# Insert setup for project after this line
drun template-backend composer console rbac:update
drun template-backend composer console init:data

View File

@ -1,39 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
SYSTEM_ENV_FILE="$PROJECT_DIR/.env"
CWD=$(pwd)
source $ENV_DIR/bin/denv_msg
source $ENV_DIR/bin/drun
# Pull branch in project directory
denv_echo_msg "[backend]: Git pull"
cd "$PROJECT_DIR"
git pull
denv_success_msg "[backend]: Git pull done"
# Install PHP packages
denv_echo_msg "[backend]: Composer install"
drun backend composer install
denv_success_msg "[backend]: Composer install done"
# Dump autoload
denv_echo_msg "[backend]: Dump autoload"
drun backend composer da
denv_success_msg "[backend]: Dump autoload done"
# Migrate databases to current version
denv_echo_msg "[backend]: Migrate db"
drun backend composer dmm
drun backend composer dmlm
denv_success_msg "[backend]: Migrate db done"
# Insert setup for project after this line
denv_echo_msg "[backend]: Update roles and permissions"
drun backend composer console rbac:update
denv_success_msg "[backend]: Update roles and permissions done"
# Switch back to current working directory
cd "$CWD"

View File

@ -3,7 +3,26 @@
return [
'api' => [
'keys' => [
'template' => $_ENV['TEMPLATE_API_KEY'],
'template' => $_ENV['HOMEPAGE_API_KEY'],
'notification' => $_ENV['NOTIFICATION_API_KEY'],
],
'services' => [
'template' => [
'host' => 'template-backend-nginx',
'apis' => [
]
],
'notification' => [
'host' => 'notification-backend-nginx',
'apis' => [
'send-mail' => [
'path' => '/api/mail/send',
'method' => 'POST'
],
],
],
],
],
];

View File

@ -10,8 +10,7 @@ return [
'user' => [
],
'admin' => [
'user.create',
'user.read-list',
'user.create-user',
],
]
]

View File

@ -1,17 +0,0 @@
<?php
return [
'mail' => [
'default' => [
'sender' => $_ENV['MAIL_DEFAULT_SENDER'],
'senderName' => $_ENV['MAIL_DEFAULT_SENDER_NAME']
],
'smtp-server' => [
'host' => $_ENV['SMTP_HOST'],
'port' => (int)$_ENV['SMTP_PORT'] ?? 25,
'encryption' => $_ENV['SMTP_ENCRYPTION'],
'username' => $_ENV['SMTP_USERNAME'],
'password' => $_ENV['SMTP_PASSWORD'],
]
]
];

View File

@ -1,9 +0,0 @@
<?php
return [
'notification' => [
'defaultTitle' => $_ENV['NOTIFICATION_DEFAULT_TITLE'] ?? 'Template',
'host' => $_ENV['NOTIFICATION_HOST'],
'id' => $_ENV['NOTIFICATION_ID'],
]
];

View File

@ -52,9 +52,6 @@ $aggregator = new ConfigAggregator([
\Template\Infrastructure\Rbac\ConfigProvider::class,
\Template\Infrastructure\Request\ConfigProvider::class,
\Template\Infrastructure\Session\ConfigProvider::class,
\Template\Infrastructure\Mail\ConfigProvider::class,
\Template\Infrastructure\Notification\ConfigProvider::class,
\Template\Infrastructure\Schema\ConfigProvider::class,
// HandlingDomain
\Template\Handling\User\ConfigProvider::class,
@ -66,7 +63,7 @@ $aggregator = new ConfigAggregator([
\Template\API\Console\ConfigProvider::class,
/// External
\Template\API\External\Api\ConfigProvider::class,
\Template\API\External\Health\ConfigProvider::class,
\Template\API\External\User\ConfigProvider::class,
\Template\API\External\Authentication\ConfigProvider::class,

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
use Template\Infrastructure\Exception\Middleware\TemplateExceptionHandlerMiddleware;
use Template\Infrastructure\Exception\Middleware\ExceptionHandlerMiddleware;
use Template\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Template\Infrastructure\Request\Middleware\AnalyzeHeaderMiddleware;
use Template\Infrastructure\Session\Middleware\SessionMiddleware;
@ -68,7 +68,7 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac
//// Pre Template Space
$app->pipe(TemplateExceptionHandlerMiddleware::class);
$app->pipe(ExceptionHandlerMiddleware::class);
$app->pipe(AnalyzeHeaderMiddleware::class);
$app->pipe(AnalyzeBodyMiddleware::class);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,66 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Willkommen bei Template!</title>
<style>
body {
}
.message-block {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
margin-top: 20px;
padding: 20px;
}
.header {
background-color: rgb(21 128 61);
width: 100%;
height: 15%;
text-align: center;
}
.title {
display: flex;
flex-wrap: wrap;
}
.wrapper {
display: inline-block;
}
.quote {
font-style: italic;
font-size: x-small;
color: slategray;
}
img {
margin: 1rem;
}
h1 {
margin-top: auto;
margin-bottom: auto;
}
</style>
</head>
<body>
<div id="header" class="header">
<div class="wrapper">
<div id="title" class="title">
<img src="assets/icon.png" />
<h1>Template</h1>
</div>
</div>
</div>
<div id="message-block" class="message-block">
<p>Hallo {$username},</p>
<br />
<p>Herzlich willkommen beim Template!</p>
<br />
<p>Ich konnte deine Anmeldung nun bestätigen.</p>
<p>Bitte klicke auf <a href="{$confirmationLink}">diesen Link</a> um dein Passwort festzulegen.</p>
<p>Danach ist deine Registierung abgeschlossen und du kannst loslegen!</p>
<br />
<p>Mit grünen Grüßen,</p>
<p>{$growerName}</p>
<br />
<p class="quote">"Smoke weed everyday" - Snoop Dogg</p>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,63 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Passwort zurücksetzen</title>
<style>
body {
}
.message-block {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
margin-top: 20px;
padding: 20px;
}
.header {
background-color: rgb(255, 199, 44);
width: 100%;
height: 15%;
text-align: center;
}
.title {
display: flex;
flex-wrap: wrap;
}
.wrapper {
display: inline-block;
}
.quote {
font-style: italic;
font-size: x-small;
color: slategray;
}
img {
margin: 1rem;
}
h1 {
margin-top: auto;
margin-bottom: auto;
}
</style>
</head>
<body>
<div id="header" class="header">
<div class="wrapper">
<div id="title" class="title">
<img src="assets/icon.png" />
<h1>Template</h1>
</div>
</div>
</div>
<div id="message-block" class="message-block">
<p>Hallo {$username},</p>
<br />
<p>Dein Passwort wurde zurückgesetzt.</p>
<p>Bitte klicke auf <a href="{$passwordResetLink}">diesen Link</a> um ein neues Passwort zu vergeben.</p>
<br />
<p>Wenn du dein Passwort nicht zurückgesetzt hast, kannst du diese Nachricht ignorieren!</p>
<br />
<p>Mit fleißigen Grüßen,</p>
<p>Der Template</p>
</div>
</body>
</html>

View File

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Template\Migrations\Bee;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240827155813 extends AbstractMigration
{
public function getDescription(): string
{
return "Create Table 'user_password_token'";
}
public function up(Schema $schema): void
{
$sql = "CREATE TABLE user_password_token (
id binary(16) NOT NULL,
user_id binary(16) NOT NULL,
updated_at datetime NOT NULL,
created_at datetime NOT NULL,
PRIMARY KEY (id)
);";
$this->addSql($sql);
}
public function down(Schema $schema): void
{
$this->addSql("DROP TABLE user_password_token;");
}
}

View File

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Bee\Migrations\Bee;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240911191301 extends AbstractMigration
{
public function getDescription(): string
{
return "Create Table 'mail'";
}
public function up(Schema $schema): void
{
$sql = "CREATE TABLE mail (
id binary(16) NOT NULL,
template varchar(255) NOT NULL,
data json NOT NULL,
recipient varchar(255) NOT NULL,
sender varchar(255) NULL,
sender_name varchar(255) NULL,
failed_at datetime NULL,
created_at datetime NOT NULL,
PRIMARY KEY (id)
);";
$this->addSql($sql);
}
public function down(Schema $schema): void
{
$this->addSql("DROP TABLE mail;");
}
}

View File

@ -0,0 +1,52 @@
networks:
template:
external: true
services:
template-backend-mysql:
image: template-backend-mysql
networks:
- template
build:
context: ./../
dockerfile: ./docker/mysql/dockerfile
volumes:
- /Users/flo/dev/backend/template/var/db:/var/lib/mysql:z
environment:
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- 3306:3306
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 20s
retries: 10
template-backend-app:
image: template-backend-app
networks:
- template
build:
context: ./../
dockerfile: ./docker/php/dockerfile
volumes:
- /Users/flo/dev/backend/template/:/var/www/html:z
ports:
- 9000:9000
depends_on:
template-backend-mysql:
condition: service_healthy
template-backend-nginx:
image: template-backend-nginx
networks:
- template
build:
context: ./../
dockerfile: ./docker/nginx/dockerfile
labels:
- "traefik.http.routers.backend.rule=Host(`template.local`) && PathPrefix(`/api`)"
- "traefik.http.routers.backend.entrypoints=websecure"
- "traefik.http.routers.backend.tls.certresolver=le"
depends_on:
- template-backend-app

View File

@ -1,13 +0,0 @@
{
"get": {
"description": "Checks the health of the API backend",
"responses": {
"200": {
"$ref": "./response.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,10 +0,0 @@
{
"description": "Successfully checked the health of the API backend",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"get": {
"description": "Reads the current schema definition",
"responses": {
"200": {
"$ref": "./response.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,16 +0,0 @@
{
"description": "Successfully read the schema definition",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"openapi",
"info",
"paths",
"components"
]
}
}
}
}

View File

@ -1,44 +0,0 @@
{
"post": {
"description": "Confirms a registration and sets the initial password.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"id",
"password",
"passwordConfirmation"
],
"properties": {
"id": {
"$ref": "../../../Partials/uuid.json"
},
"password": {
"$ref": "../Partials/password.json"
},
"passwordConfirmation": {
"$ref": "../Partials/password.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,34 +0,0 @@
{
"description": "Successfully confirmed registration",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"id",
"username",
"roleIdentifier",
"permissions"
],
"properties": {
"id": {
"$ref": "../../../Partials/uuid.json"
},
"username": {
"$ref": "../Partials/username.json"
},
"roleIdentifier": {
"$ref": "../Partials/role-identifier.json"
},
"permissions": {
"description": "All permissions assigned to the user",
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}

View File

@ -1,36 +0,0 @@
{
"post": {
"description": "Requests a password reset mail, by providing a registered email address",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"mail"
],
"properties": {
"mail": {
"$ref": "../Partials/mail.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,9 +0,0 @@
{
"description": "Password reset mail successfully sent",
"content": {
"application/json": {
"schema": {
}
}
}
}

View File

@ -1,40 +0,0 @@
{
"post": {
"description": "Login with user credentials.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"identifier",
"password"
],
"properties": {
"identifier": {
"$ref": "../Partials/identifier.json"
},
"password": {
"$ref": "../Partials/password.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,18 +0,0 @@
{
"description": "User successfully logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"sessionId"
],
"properties": {
"sessionId": {
"$ref": "../../../Partials/uuid.json"
}
}
}
}
}
}

View File

@ -1,19 +0,0 @@
{
"get": {
"description": "Logout the currently logged in user.",
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,10 +0,0 @@
{
"description": "User successfully logged out",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}

View File

@ -1,5 +0,0 @@
{
"description": "The identifier for the user. May be a mail address or a username",
"example": "max.mustermann@example.com",
"type": "string"
}

View File

@ -1,6 +0,0 @@
{
"description": "The mail address of a user",
"example": "max.mustermann@example.com",
"type": "string",
"format": "email"
}

View File

@ -1,5 +0,0 @@
{
"description": "The password for an account",
"example": "Password123!",
"type": "string"
}

View File

@ -1,9 +0,0 @@
{
"description": "A role identifier",
"example": "admin",
"type": "string",
"enum": [
"admin",
"user"
]
}

View File

@ -1,5 +0,0 @@
{
"description": "The username of a user",
"example": "max.mustermann",
"type": "string"
}

View File

@ -1,40 +0,0 @@
{
"post": {
"description": "Creates a registration for a new user with their username and email address",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"username",
"mail"
],
"properties": {
"username": {
"$ref": "../Partials/username.json"
},
"mail": {
"$ref": "../Partials/mail.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,10 +0,0 @@
{
"description": "Successfully created the registration",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}

View File

@ -1,44 +0,0 @@
{
"post": {
"description": "Sets a new Password by providing a password reset token and the new password",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"passwordToken",
"newPassword",
"passwordConfirmation"
],
"properties": {
"passwordToken": {
"$ref": "../../../Partials/uuid.json"
},
"newPassword": {
"$ref": "../Partials/password.json"
},
"passwordConfirmation": {
"$ref": "../Partials/password.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,9 +0,0 @@
{
"description": "Successfully set new password",
"content": {
"application/json": {
"schema": {
}
}
}
}

View File

@ -1,40 +0,0 @@
{
"post": {
"description": "Changes the currently logged in users password",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"password",
"newPassword"
],
"properties": {
"password": {
"$ref": "../../Authentication/Partials/password.json"
},
"newPassword": {
"$ref": "../../Authentication/Partials/password.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,9 +0,0 @@
{
"description": "Successfully changed the password",
"content": {
"application/json": {
"schema": {
}
}
}
}

View File

@ -1,40 +0,0 @@
{
"post": {
"description": "Changes the currently logged in users username",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"password",
"newUsername"
],
"properties": {
"password": {
"$ref": "../../Authentication/Partials/password.json"
},
"newUsername": {
"$ref": "../../Authentication/Partials/username.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,9 +0,0 @@
{
"description": "Successfully changed the username",
"content": {
"application/json": {
"schema": {
}
}
}
}

View File

@ -1,44 +0,0 @@
{
"post": {
"description": "Creates a User",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"username",
"mail",
"password"
],
"properties": {
"username": {
"$ref": "../../Authentication/Partials/username.json"
},
"mail": {
"$ref": "../../Authentication/Partials/mail.json"
},
"password": {
"$ref": "../../Authentication/Partials/password.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,42 +0,0 @@
{
"description": "Successfully created the user",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"id",
"username",
"role",
"permissions",
"createdAt",
"updatedAt"
],
"properties": {
"id": {
"$ref": "../../../Partials/uuid.json"
},
"username": {
"$ref": "../../Authentication/Partials/username.json"
},
"role": {
"$ref": "../../Authentication/Partials/role-identifier.json"
},
"permissions": {
"description": "All permissions assigned to the user",
"type": "array",
"items": {
"type": "string"
}
},
"createdAt": {
"$ref": "../../../Partials/date-time.json"
},
"updatedAt": {
"$ref": "../../../Partials/date-time.json"
}
}
}
}
}
}

View File

@ -1,44 +0,0 @@
{
"post": {
"description": "Reads a paginated list of users",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"page",
"perPage",
"query"
],
"properties": {
"page": {
"$ref": "../../../Partials/pagination-page.json"
},
"perPage": {
"$ref": "../../../Partials/pagination-per-page.json"
},
"query": {
"$ref": "../../../Partials/pagination-query.json"
}
}
}
}
}
},
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,50 +0,0 @@
{
"description": "Successfully read a list of paginated users",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"total",
"items"
],
"properties": {
"total": {
"$ref": "../../../Partials/pagination-total.json"
},
"items": {
"description": "The resultset",
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"role",
"username",
"mail",
"lastLoginAt"
],
"properties": {
"id": {
"$ref": "../../../Partials/uuid.json"
},
"role": {
"$ref": "../../Authentication/Partials/role-identifier.json"
},
"username": {
"$ref": "../../Authentication/Partials/username.json"
},
"mail": {
"$ref": "../../Authentication/Partials/mail.json"
},
"lastLoginAt": {
"$ref": "../../../Partials/nullable-date-time.json"
}
}
}
}
}
}
}
}
}

View File

@ -1,19 +0,0 @@
{
"get": {
"description": "Reads the state of the currently logged in user",
"responses": {
"200": {
"$ref": "./response.json"
},
"401": {
"$ref": "../../../Partials/Response/unauthorized.json"
},
"403": {
"$ref": "../../../Partials/Response/forbidden.json"
},
"default": {
"$ref": "../../../Partials/Response/bad-request.json"
}
}
}
}

View File

@ -1,46 +0,0 @@
{
"description": "Successfully read the currently logged in users state",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"id",
"username",
"roleIdentifier",
"permissions",
"createdAt",
"updatedAt",
"sessionId"
],
"properties": {
"id": {
"$ref": "../../../Partials/uuid.json"
},
"username": {
"$ref": "../../Authentication/Partials/username.json"
},
"role": {
"$ref": "../../Authentication/Partials/role-identifier.json"
},
"permissions": {
"description": "All permissions assigned to the user",
"type": "array",
"items": {
"type": "string"
}
},
"createdAt": {
"$ref": "../../../Partials/date-time.json"
},
"updatedAt": {
"$ref": "../../../Partials/date-time.json"
},
"sessionId": {
"$ref": "../../../Partials/uuid.json"
}
}
}
}
}
}

View File

@ -1,32 +0,0 @@
{
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"description": "The error response object",
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"type": "string"
}
}
}
},
"example": {
"error": {
"code": "Username.AlreadyExists"
}
}
}
}
}
}

View File

@ -1,10 +0,0 @@
{
"description": "The user is forbidden to perform this action",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}

View File

@ -1,10 +0,0 @@
{
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}

View File

@ -1,10 +0,0 @@
{
"description": "Invalid credentials or invalid user session",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}

View File

@ -1,6 +0,0 @@
{
"description": "A string representing a timestamp",
"example": "2024-11-10T21:24:04+00:00",
"type": "string",
"format": "date-time"
}

View File

@ -1,7 +0,0 @@
{
"description": "A nullable string representing a timestamp",
"nullable": true,
"example": "2024-11-10T21:24:04+00:00",
"type": "string",
"format": "date-time"
}

View File

@ -1,7 +0,0 @@
{
"description": "A nullable string indicating the time of day in a 24h format (HH:MM)",
"nullable": true,
"example": "04:20",
"type": "string",
"pattern": "^([01]\\d|2[0-3]):([0-5]\\d)$"
}

View File

@ -1,6 +0,0 @@
{
"description": "The current page of a paginated list",
"type": "integer",
"example": 6,
"minimum": 1
}

View File

@ -1,6 +0,0 @@
{
"description": "The maximum amount of items displayed on a page of a paginated list",
"type": "integer",
"example": 4,
"minimum": 1
}

View File

@ -1,6 +0,0 @@
{
"description": "The nullable query to search for in a paginated list",
"example": "Growbox",
"type": "string",
"nullable": true
}

View File

@ -1,6 +0,0 @@
{
"description": "The total amount of elements in a paginated list",
"type": "integer",
"example": 3,
"minimum": 0
}

View File

@ -1,6 +0,0 @@
{
"description": "A string indicating the time of day in a 24h format (HH:MM)",
"example": "04:20",
"type": "string",
"pattern": "^([01]\\d|2[0-3]):([0-5]\\d)$"
}

View File

@ -1,6 +0,0 @@
{
"description": "A universally unique identifier",
"example": "071ac920-38dc-11ed-a009-0242ac130005",
"format": "uuid",
"type": "string"
}

View File

@ -1,60 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "Template API",
"description": "The API powering the Template application",
"version": "1.0.0"
},
"components": {
"securitySchemes": {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key"
}
}
},
"paths": {
"/api/health": {
"$ref": "External/Api/Health/request.json"
},
"/api/schema": {
"$ref": "External/Api/Schema/request.json"
},
"/api/auth/login-user": {
"$ref": "External/Authentication/Login/request.json"
},
"/api/auth/logout-user": {
"$ref": "External/Authentication/Logout/request.json"
},
"/api/auth/confirm-registration": {
"$ref": "External/Authentication/ConfirmRegistration/request.json"
},
"/api/auth/register-user": {
"$ref": "External/Authentication/RegisterUser/request.json"
},
"/api/auth/forgot-password": {
"$ref": "External/Authentication/ForgotPassword/request.json"
},
"/api/auth/reset-password": {
"$ref": "External/Authentication/ResetPassword/request.json"
},
"/api/user/create": {
"$ref": "External/User/Create/request.json"
},
"/api/user/change-password": {
"$ref": "External/User/ChangePassword/request.json"
},
"/api/user/change-username": {
"$ref": "External/User/ChangeUsername/request.json"
},
"/api/user/read-list": {
"$ref": "External/User/ReadList/request.json"
},
"/api/user/state": {
"$ref": "External/User/UserState/request.json"
}
}
}

View File

@ -46,7 +46,7 @@ return [
'migrations_configuration' => [
'orm_template' => [
'directory' => 'data/migrations/template',
'directory' => 'data/migrations/business',
'name' => 'Doctrine Database Migrations for Template',
'namespace' => 'Template\Migrations\Template',
'table' => 'migrations',

View File

@ -1,20 +1,20 @@
<?php
use Template\Data\Business\Entity\User;
use Template\Data\Business\Factory\EntityManagerFactory as TemplateEntityManagerFactory;
use Template\Data\Business\Factory\EntityManagerFactory;
use Template\Data\Business\Manager\EntityManager;
use Template\Data\Business\Repository\UserRepository;
use Template\Infrastructure\Database\AutowireRepositoryFactory;
use Roave\PsrContainerDoctrine\ConfigurationFactory;
use Roave\PsrContainerDoctrine\ConnectionFactory;
use Roave\PsrContainerDoctrine\EntityManagerFactory;
use Roave\PsrContainerDoctrine\EntityManagerFactory as BaseEntityManagerFactory;
return [
'factories' => [
'doctrine.entity_manager.orm_template' => [EntityManagerFactory::class, 'orm_template'],
'doctrine.entity_manager.orm_template' => [BaseEntityManagerFactory::class, 'orm_template'],
'doctrine.configuration.orm_template' => [ConfigurationFactory::class, 'orm_template'],
'doctrine.connection.orm_template' => [ConnectionFactory::class, 'orm_template'],
EntityManager::class => TemplateEntityManagerFactory::class,
EntityManager::class => EntityManagerFactory::class,
UserRepository::class => [AutowireRepositoryFactory::class, EntityManager::class, User::class],
],

View File

@ -1,125 +0,0 @@
<?php
namespace Template\Data\Business\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Template\Infrastructure\UuidGenerator\UuidGenerator;
use Ramsey\Uuid\UuidInterface;
/**
* @ORM\Entity(repositoryClass="Template\Data\Business\Repository\MailRepository")
* @ORM\Table(name="mail")
*/
class Mail {
/**
* @ORM\Id
* @ORM\Column(name="id", type="uuid_binary_ordered_time")
*/
private UuidInterface $id;
/** @ORM\Column(name="template", type="string") */
private string $template;
/** @ORM\Column(name="data", type="json") */
private array $data;
/** @ORM\Column(name="recipient", type="string") */
private string $recipient;
/** @ORM\Column(name="sender", type="string", nullable="true") */
private ?string $sender;
/** @ORM\Column(name="sender_name", type="string", nullable="true") */
private ?string $senderName;
/** @ORM\Column(name="failed_at", type="datetime", nullable="true") */
private ?DateTime $failedAt;
/** @ORM\Column(name="created_at", type="datetime") */
private DateTime $createdAt;
public function __construct() {
$this->id = UuidGenerator::generate();
$now = new DateTime();
$this->setCreatedAt($now);
$this->setFailedAt(null);
}
/**
* @ORM\PrePersist
* @ORM\PreUpdate
*/
public function updateTimestamps(): void {
$now = new DateTime();
//$this->setUpdatedAt($now);
}
public function getId(): UuidInterface {
return $this->id;
}
public function getTemplate(): string
{
return $this->template;
}
public function setTemplate(string $template): void
{
$this->template = $template;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): void
{
$this->data = $data;
}
public function getRecipient(): string
{
return $this->recipient;
}
public function setRecipient(string $recipient): void
{
$this->recipient = $recipient;
}
public function getSender(): ?string
{
return $this->sender;
}
public function setSender(?string $sender): void
{
$this->sender = $sender;
}
public function getSenderName(): ?string
{
return $this->senderName;
}
public function setSenderName(?string $senderName): void
{
$this->senderName = $senderName;
}
public function getFailedAt(): ?DateTime
{
return $this->failedAt;
}
public function setFailedAt(?DateTime $failedAt): void
{
$this->failedAt = $failedAt;
}
public function getCreatedAt(): DateTime {
return $this->createdAt;
}
public function setCreatedAt(DateTime $createdAt): void {
$this->createdAt = $createdAt;
}
}

View File

@ -3,8 +3,6 @@
namespace Template\Data\Business\Entity;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Template\Infrastructure\UuidGenerator\UuidGenerator;
use Ramsey\Uuid\UuidInterface;
@ -41,11 +39,8 @@ class User {
/** @ORM\OneToOne(targetEntity="Template\Data\Business\Entity\UserSession", mappedBy="user") */
private ?UserSession $session;
/* @ORM\OneToMany(targetEntity="Bee\Data\Business\Entity\UserPasswordToken", mappedBy="user") */
private Collection $passwordTokens;
/** @ORM\Column(name="last_login_at", type="datetime", nullable=true) */
private ?DateTime $lastLoginAt;
private DateTime $lastLoginAt;
/** @ORM\Column(name="created_at", type="datetime") */
private DateTime $createdAt;
@ -56,9 +51,7 @@ class User {
public function __construct() {
$this->id = UuidGenerator::generate();
$this->passwordTokens = new ArrayCollection();
$now = new DateTime();
$this->setCreatedAt($now);
$this->setUpdatedAt($now);
@ -150,27 +143,6 @@ class User {
public function setUpdatedAt(DateTime $updatedAt): void {
$this->updatedAt = $updatedAt;
}
public function getPasswordTokens(): Collection
{
return $this->passwordTokens;
}
public function setPasswordTokens(Collection $passwordTokens): void
{
$this->passwordTokens = $passwordTokens;
}
public function addPasswordToken(UserPasswordToken $passwordToken): void
{
if (!$this->passwordTokens->contains($passwordToken)) {
$this->passwordTokens->add($passwordToken);
}
}
public function removePasswordToken(UserPasswordToken $passwordToken): void
{
if ($this->passwordTokens->contains($passwordToken)) {
$this->passwordTokens->removeElement($passwordToken);
}
}
}
?>

View File

@ -1,91 +0,0 @@
<?php
namespace Template\Data\Business\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Template\Infrastructure\UuidGenerator\UuidGenerator;
use Ramsey\Uuid\UuidInterface;
/**
* @ORM\Entity(repositoryClass="Template\Data\Business\Repository\UserPasswordTokenRepository")
* @ORM\Table(name="user_password_token")
*/
class UserPasswordToken {
/**
* @ORM\Id
* @ORM\Column(name="id", type="uuid_binary_ordered_time")
*/
private UuidInterface $id;
/** @ORM\Column(name="user_id", type="uuid_binary_ordered_time", nullable=false) */
private UuidInterface $userId;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="passwordTokens")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private User $user;
/** @ORM\Column(name="created_at", type="datetime") */
private DateTime $createdAt;
/** @ORM\Column(name="updated_at", type="datetime") */
private DateTime $updatedAt;
public function __construct() {
$this->id = UuidGenerator::generate();
$now = new DateTime();
$this->setCreatedAt($now);
$this->setUpdatedAt($now);
}
/**
* @ORM\PrePersist
* @ORM\PreUpdate
*/
public function updateTimestamps(): void {
$now = new DateTime();
$this->setUpdatedAt($now);
}
public function getId(): UuidInterface {
return $this->id;
}
public function getUserId(): UuidInterface {
return $this->userId;
}
public function setUserId(UuidInterface $userId): void {
$this->userId = $userId;
}
public function getUser(): User {
return $this->user;
}
public function setUser(User $user): void {
$this->user = $user;
}
public function getCreatedAt(): DateTime {
return $this->createdAt;
}
public function setCreatedAt(DateTime $createdAt): void {
$this->createdAt = $createdAt;
}
public function getUpdatedAt(): DateTime {
return $this->updatedAt;
}
public function setUpdatedAt(DateTime $updatedAt): void {
$this->updatedAt = $updatedAt;
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace Template\Data\Business\Repository;
use Doctrine\ORM\EntityRepository;
class MailRepository extends EntityRepository {
}

View File

@ -1,10 +0,0 @@
<?php
namespace Template\Data\Business\Repository;
use Template\Data\Business\Entity\User;
use Doctrine\ORM\EntityRepository;
use Template\Data\Business\Entity\UserSession;
class UserPasswordTokenRepository extends EntityRepository {
}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Database;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerInterface;
class AutowireRepositoryFactory implements FactoryInterface
{
public function __construct(
private readonly string $entityManagerClass,
private readonly string $entityClass,
) {
var_dump($this->entityClass, $this->entityClass);
}
public function __invoke(
ContainerInterface $container,
$requestedName,
?array $options = null
): ObjectRepository|EntityRepository
{
/** @var EntityManager $em */
$em = $container->get($this->entityManagerClass);
return $em->getRepository($this->entityClass);
}
}

View File

@ -4,15 +4,23 @@ declare(strict_types=1);
namespace Template\Infrastructure\Encryption\Client;
use Laminas\Crypt\Password\Bcrypt;
class EncryptionClient
{
private readonly Bcrypt $bcrypt;
public function __construct(
) {
$this->bcrypt = new Bcrypt();
$this->bcrypt->setCost(10);
}
public function encrypt(string $value): string {
return password_hash($value, PASSWORD_BCRYPT, [
'cost' => 10
]);
return $this->bcrypt->create($value);
}
public function verify(string $value, string $encryptedValue): bool {
return hash_equals($encryptedValue, crypt($value, $encryptedValue));
return $this->bcrypt->verify($value, $encryptedValue);
}
}

View File

@ -2,11 +2,11 @@
declare(strict_types=1);
use Template\Infrastructure\Exception\Middleware\TemplateExceptionHandlerMiddleware;
use Template\Infrastructure\Exception\Middleware\ExceptionHandlerMiddleware;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
return [
'factories' => [
TemplateExceptionHandlerMiddleware::class => AutoWiringFactory::class,
ExceptionHandlerMiddleware::class => AutoWiringFactory::class,
],
];

View File

@ -3,13 +3,10 @@
namespace Template\Infrastructure\Exception;
enum ErrorCode : string {
case AlreadyExists = 'AlreadyExists';
case Failed = 'Failed';
case Invalid = 'Invalid';
case Maintenance = 'Maintenance';
case Mismatch = 'Mismatch';
case NotFound = 'NotFound';
case SomethingWentWrong = 'SomethingWentWrong';
case NotFound = 'NotFound';
case AlreadyExists = 'AlreadyExists';
case WrongCredentials = 'WrongCredentials';
case ValidationFailed = 'ValidationFailed';
case Mismatch = 'Mismatch';
}

View File

@ -3,12 +3,11 @@
namespace Template\Infrastructure\Exception;
enum ErrorDomain : string {
case Schema = 'Schema';
case Generic = 'Generic';
case Mail = 'Mail';
case Notification = 'Notification';
case Registration = 'Registration';
case Role = 'Role';
case User = 'User';
case UserPassword = 'UserPassword';
case Registration = 'Registration';
case Product = 'Product';
}

View File

@ -2,11 +2,10 @@
namespace Template\Infrastructure\Exception\Exception;
use Exception;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
class TemplateException extends Exception {
class Exception extends \Exception {
private readonly ErrorDomain $errorDomain;
private readonly ErrorCode $errorCode;

View File

@ -0,0 +1,37 @@
<?php
namespace Template\Infrastructure\Exception\Middleware;
use Template\Infrastructure\Exception\Exception\Exception;
use Template\Infrastructure\Logging\Logger\Logger;
use Template\Infrastructure\Response\ErrorResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ExceptionHandlerMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly Logger $logger
) {
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
try {
return $handler->handle($request);
} catch (Exception $exception) {
$this->logger->exception($exception);
return new ErrorResponse(
$exception->getErrorDomain(),
$exception->getErrorCode(),
$exception->getMessage()
);
}
}
}

View File

@ -1,71 +0,0 @@
<?php
namespace Template\Infrastructure\Exception\Middleware;
use League\OpenAPIValidation\PSR15\Exception\InvalidResponseMessage;
use League\OpenAPIValidation\PSR15\Exception\InvalidServerRequestMessage;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
use Template\Infrastructure\Exception\Exception\TemplateException;
use Template\Infrastructure\Logging\Logger\Logger;
use Template\Infrastructure\Response\ErrorResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class TemplateExceptionHandlerMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly Logger $logger
) {
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
try {
return $handler->handle($request);
} catch (TemplateException $exception) {
$this->logger->exception($exception);
return new ErrorResponse(
$exception->getErrorDomain(),
$exception->getErrorCode(),
$exception->getMessage()
);
} catch (InvalidResponseMessage $exception) {
$this->logger->exception($exception);
$lines = [];
do {
$lines[] = $exception->getMessage();
$exception = $exception->getPrevious();
} while ($exception->getPrevious() !== null);
return new ErrorResponse(
ErrorDomain::Schema,
ErrorCode::ValidationFailed,
implode(PHP_EOL, $lines)
);
} catch (InvalidServerRequestMessage $exception) {
$this->logger->exception($exception);
$lines = [];
do {
$lines[] = $exception->getMessage();
$exception = $exception->getPrevious();
} while ($exception->getPrevious() !== null);
return new ErrorResponse(
ErrorDomain::Schema,
ErrorCode::ValidationFailed,
implode(PHP_EOL, $lines)
);
}
}
}

View File

@ -2,10 +2,8 @@
namespace Template\Infrastructure\Logging\Logger;
use Template\Infrastructure\Exception\Exception\TemplateException;
use Template\Infrastructure\Logging\Handler\FileStreamHandler;
use Template\Infrastructure\Exception\Exception\Exception;
use Monolog\ErrorHandler;
use Monolog\Handler\StreamHandler;
use Psr\Log\LoggerInterface;
use Stringable;
use Throwable;
@ -26,7 +24,7 @@ class Logger implements LoggerInterface
'trace' => $exception->getTraceAsString()
]*/;
if ($exception instanceof TemplateException) {
if ($exception instanceof Exception) {
$exceptionContext = array_merge([
'errorDomain' => $exception->getErrorDomain()->value,
'errorCode' => $exception->getErrorCode()->value,

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Template\Infrastructure\Mail\Service\MailService;
return [
'factories' => [
MailService::class => AutoWiringFactory::class,
],
];

View File

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Mail;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
];
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace Template\Infrastructure\Mail\Exception;
use Throwable;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
use Template\Infrastructure\Exception\Exception\TemplateException;
class SendMailFailedException extends TemplateException
{
private const MESSAGE = 'Das Mail template %s konnte mit den Daten {%s} nicht an %s gesendet werden. Grund: %s';
public function __construct(
string $templateIdentification,
array $templateData,
string $recipient,
Throwable $reason,
) {
parent::__construct(
sprintf(
self::MESSAGE,
$templateIdentification,
json_encode($templateData),
$recipient,
$reason->getMessage(),
),
ErrorDomain::Mail,
ErrorCode::Failed
);
}
}

View File

@ -1,176 +0,0 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Mail\Service;
use Template\Data\Business\Entity\Mail;
use Template\Data\Business\Manager\EntityManager;
use Template\Data\Business\Repository\MailRepository;
use DateTime;
use Exception;
use Latte\Engine;
use Nette\Mail\Mailer;
use Nette\Mail\Message;
use Nette\Mail\SmtpMailer;
use Reinfi\DependencyInjection\Service\ConfigService;
use Throwable;
use Template\Infrastructure\Logging\Logger\Logger;
use Template\Infrastructure\Mail\Exception\SendMailFailedException;
class MailService
{
private const TEMPLATE_PATH = APP_ROOT . '/data/mails/';
private readonly Engine $engine;
private readonly MailRepository $mailRepository;
public function __construct(
private readonly EntityManager $entityManager,
private readonly ConfigService $configService,
private readonly Logger $logger,
) {
$this->engine = new Engine();
$this->mailRepository = $this->entityManager->getRepository(Mail::class);
}
public function enqueue(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): void {
$mail = new Mail();
$mail->setTemplate($template);
$mail->setData($templateData);
$mail->setRecipient($recipient);
$mail->setSender($sender);
$mail->setSenderName($senderName);
$this->entityManager->persist($mail);
$this->entityManager->flush();
}
public function send(?int $count = null): void {
$qb = $this->mailRepository->createQueryBuilder('m');
$qb->orderBy('m.createdAt', 'asc');
if ($count !== null) {
$qb->setMaxResults($count);
}
$mailsToSend = $qb->getQuery()->execute();
/** @var Mail $mailToSend */
foreach ($mailsToSend as $mailToSend) {
try {
$this->sendMail(
$mailToSend->getTemplate(),
$mailToSend->getData(),
$mailToSend->getRecipient(),
$mailToSend->getSender(),
$mailToSend->getSenderName(),
);
$this->entityManager->remove($mailToSend);
} catch (SendMailFailedException $e) {
// log is done withing sendMail
$mailToSend->setFailedAt(new DateTime());
$this->entityManager->persist($mailToSend);
}
$this->entityManager->flush();
}
}
/**
* @throws SendMailFailedException
*/
private function sendMail(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): void {
try {
$mail = $this->getMail(
template: $template,
templateData: $templateData,
recipient: $recipient,
sender: $sender,
senderName: $senderName
);
$mailer = $this->getMailer();
$mailer->send($mail);
} catch (Throwable $e) {
$this->logger->exception($e);
throw new SendMailFailedException(
$template,
$templateData,
$recipient,
$e
);
}
}
private function getMailer(): Mailer
{
$smtpConfig = $this->configService->resolve('mail.smtp-server');
return new SmtpMailer(
host: $smtpConfig['host'],
username: $smtpConfig['username'],
password: $smtpConfig['password'],
encryption: $smtpConfig['encryption'],
port: $smtpConfig['port'],
);
}
private function getMail(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): Message {
$templatePath = self::TEMPLATE_PATH . $template . '/template.latte';
$assetsPath = self::TEMPLATE_PATH . $template . "/";
if (!file_exists($templatePath)) {
throw new Exception("Template File does not exist");
}
if (!is_dir($assetsPath)) {
$assetsPath = null;
}
$mail = new Message();
$mail->setFrom($this->getSenderName($sender, $senderName));
$mail->addTo($recipient);
$mail->setHtmlBody(
$this->engine->renderToString(
$templatePath,
$templateData,
),
$assetsPath
);
return $mail;
}
private function getSenderName(
?string $sender,
?string $senderName
): string {
$defaultConfig = $this->configService->resolve('mail.default');
$from = $sender ?? $defaultConfig['sender'] ?? throw new Exception('Could not determine Sender');
$name =$sender ?? $defaultConfig['senderName'] ?? throw new Exception('Could not determine Sender Name');
return sprintf('%s <%s>', $name, $from);
}
}

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Template\Infrastructure\Notification\Service\NotificationService;
return [
'factories' => [
NotificationService::class => AutoWiringFactory::class,
],
];

View File

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Notification;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
];
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace Template\Infrastructure\Notification\Exception;
use Throwable;
use Template\Infrastructure\Exception\ErrorCode;
use Template\Infrastructure\Exception\ErrorDomain;
use Template\Infrastructure\Exception\Exception\TemplateException;
class SendNotificationFailed extends TemplateException
{
private const MESSAGE = 'Notification could not be sent. Reason: %s';
public function __construct(
Throwable $reason,
) {
parent::__construct(
sprintf(
self::MESSAGE,
$reason->getMessage(),
),
ErrorDomain::Notification,
ErrorCode::Failed
);
}
}

View File

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace Template\Infrastructure\Notification\Service;
use Exception;
use Reinfi\DependencyInjection\Service\ConfigService;
use Throwable;
use Template\Infrastructure\Notification\Exception\SendNotificationFailed;
class NotificationService
{
private string $defaultTitle;
private ?string $host;
private ?string $id;
public function __construct(
private readonly ConfigService $configService,
) {
$config = $this->configService->resolve('notification');
$this->defaultTitle = $config['defaultTitle'];
$this->host = $config['host'] ?? '';
$this->id = $config['id'] ?? '';
}
/**
* @throws SendNotificationFailed
*/
public function push(
string $message,
string $title = null,
): void {
try {
if (($this->id ?? '') === '') {
throw new Exception('Notification ID is not set.');
}
if (($this->host ?? '') === '') {
throw new Exception('Notification Host is not set.');
}
$command = sprintf(
'curl -H "Title: %s" -H "Tags: broccoli" -d "%s" %s/%s',
$title ?? $this->defaultTitle,
$message,
$this->host,
$this->id
);
shell_exec($command);
} catch (Throwable $e) {
throw new SendNotificationFailed($e);
}
}
}

View File

@ -56,8 +56,7 @@ class EnsureAuthorizationMiddleware implements MiddlewareInterface
): bool {
$role = $user->getRole();
$permissions = $role->getPermissions();
$targetApi = $this->routes[$targetPath] ?? null;
$targetApi = $this->routes[$targetPath];
/** @var Permission $permission */
foreach($permissions as $permission) {

View File

@ -2,14 +2,12 @@
declare(strict_types=1);
use Template\Infrastructure\Rbac\Middleware\EnsureAuthorizationMiddleware;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Template\Infrastructure\Request\Factory\RequestServiceFactory;
use Template\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Template\Infrastructure\Request\Middleware\AnalyzeHeaderMiddleware;
use Template\Infrastructure\Request\Middleware\InternalRequestMiddleware;
use Template\Infrastructure\Request\Service\RequestService;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Reinfi\DependencyInjection\Factory\InjectionFactory;
return [
'factories' => [

View File

@ -2,10 +2,16 @@
namespace Template\Infrastructure\Request\Middleware;
use Template\Data\Business\Entity\Permission;
use Template\Data\Business\Entity\User;
use Template\Infrastructure\Response\ForbiddenResponse;
use Template\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Laminas\Config\Config;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Reinfi\DependencyInjection\Annotation\InjectConfig;
class AnalyzeBodyMiddleware implements MiddlewareInterface
{
@ -27,8 +33,6 @@ class AnalyzeBodyMiddleware implements MiddlewareInterface
$request->getBody()->getContents(),
true
);
$request->getBody()->rewind();
}
return $handler->handle($request->withAttribute(

View File

@ -2,47 +2,36 @@
namespace Template\Infrastructure\Request\Middleware;
use Template\Data\Business\Entity\Permission;
use Template\Data\Business\Entity\User;
use Template\Infrastructure\Response\ForbiddenResponse;
use Template\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Laminas\Config\Config;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Reinfi\DependencyInjection\Annotation\InjectConfig;
class AnalyzeHeaderMiddleware implements MiddlewareInterface
{
public const HOST_ATTRIBUTE = 'host_attribute';
public const CONTENT_TYPE_ATTRIBUTE = 'content_type_attribute';
private const CONTENT_TYPE_HEADER_KEY = 'Content-Type';
private const HOST_HEADER_KEY = 'host';
private const FORWARDED_HOST_HEADER_KEY = 'x-forwarded-host';
const HOST_ATTRIBUTE = 'host_attribute';
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
$headers = $request->getHeaders();
$contentType = $this->readHeader($headers, self::CONTENT_TYPE_HEADER_KEY) ?? null;
$host = explode(
':',
$this->readHeader($headers, self::HOST_HEADER_KEY) ??
$this->readHeader($headers, self::FORWARDED_HOST_HEADER_KEY) ??
'UNKNOWN'
$request->getHeaders()['host'][0]
?? $request->getHeaders()['x-forwarded-host'][0]
?? 'UNKNOWN'
)[0];
return $handler->handle(
$request
->withAttribute(self::HOST_ATTRIBUTE, $host)
->withAttribute(self::CONTENT_TYPE_ATTRIBUTE, $contentType)
);
}
private function readHeader(array $header, string $key): mixed
{
return
($header[$key] ??
$header[strtolower($key)] ??
$header[strtoupper($key)] ?? [])[0] ?? null;
return $handler->handle($request->withAttribute(
self::HOST_ATTRIBUTE,
$host
));
}
}

View File

@ -7,13 +7,9 @@ use Laminas\Diactoros\Response\JsonResponse;
class SuccessResponse extends JsonResponse
{
public function __construct(
mixed $data = 'Success'
mixed $data = 'OK'
)
{
if ($data === []) {
$data = 'Success';
}
parent::__construct(
$data
);

View File

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
use cebe\openapi\spec\OpenApi;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\ClassFileBuilder;
use Template\Infrastructure\Schema\Exporter\BackendExporter;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\PropertyBuilder as FrontendPropertyBuilder;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\PropertyBuilder as BackendPropertyBuilder;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\ServiceBuilder;
use Template\Infrastructure\Schema\Exporter\Frontend\Builder\TypeBuilder as FrontendTypeBuilder;
use Template\Infrastructure\Schema\Exporter\Backend\Builder\TypeBuilder as BackendTypeBuilder;;
use Template\Infrastructure\Schema\Exporter\FrontendExporter;
use Template\Infrastructure\Schema\Factory\OpenApiFactory;
use Template\Infrastructure\Schema\Middleware\SchemaValidationMiddleware;
use Template\Infrastructure\Schema\Reader\ApiDefinitionReader;
return [
'factories' => [
OpenApi::class => OpenApiFactory::class,
/// Reader
ApiDefinitionReader::class => AutoWiringFactory::class,
/// Middleware
SchemaValidationMiddleware::class => AutoWiringFactory::class,
/// Exporter
// Backend
BackendExporter::class => AutoWiringFactory::class,
ClassFileBuilder::class => AutoWiringFactory::class,
BackendPropertyBuilder::class => AutoWiringFactory::class,
BackendTypeBuilder::class => AutoWiringFactory::class,
// Frontend
FrontendExporter::class => AutoWiringFactory::class,
ServiceBuilder::class => AutoWiringFactory::class,
FrontendTypeBuilder::class => AutoWiringFactory::class,
FrontendPropertyBuilder::class => AutoWiringFactory::class,
],
];

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