Skip to content

Commit 080c00a

Browse files
committed
ISSUE-345: set up router
1 parent 9d05218 commit 080c00a

File tree

13 files changed

+327
-3
lines changed

13 files changed

+327
-3
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# .env
2+
API_BASE_URL=http://api.phplist.com/api/v2
3+

.gitignore

100644100755
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
/var/
1515
/vendor/
1616
.phpunit.result.cache
17+
.env

composer.json

100644100755
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
},
3131
"require": {
3232
"php": "^8.1",
33-
"phplist/core": "v5.0.0-alpha7"
33+
"phplist/core": "v5.0.0-alpha7",
34+
"symfony/twig-bundle": "^6.4.0"
3435
},
3536
"require-dev": {
3637
"phpunit/phpunit": "^9.5",
@@ -83,8 +84,18 @@
8384
"symfony-web-dir": "public",
8485
"symfony-tests-dir": "tests",
8586
"phplist/core": {
86-
"bundles": [],
87-
"routes": {}
87+
"bundles": [
88+
"FOS\\RestBundle\\FOSRestBundle",
89+
"Symfony\\Bundle\\TwigBundle\\TwigBundle",
90+
"PhpList\\WebFrontend\\PhpListFrontendBundle"
91+
],
92+
"routes": {
93+
"rest-api": {
94+
"resource": "@PhpListFrontendBundle/Controller/",
95+
"type": "attribute",
96+
"prefix": "/"
97+
}
98+
}
8899
}
89100
}
90101
}

config/.gitkeep

Whitespace-only changes.

config/services.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# config/services.yaml
2+
services:
3+
_defaults:
4+
autowire: true
5+
autoconfigure: true
6+
public: false
7+
8+
PhpList\WebFrontend\:
9+
resource: '../src/'
10+
exclude:
11+
- '../src/DependencyInjection/'
12+
- '../src/Entity/'
13+
- '../src/Kernel.php'
14+
15+
PhpList\WebFrontend\Service\ApiClient:
16+
arguments:
17+
$baseUrl: '${env(API_BASE_URL):http://api.phplist.local/api/v2}'
18+
# calls:
19+
# - setAuthToken: ['%session.auth_token%']
20+
21+
PhpList\WebFrontend\Controller\:
22+
resource: '../src/Controller'
23+
public: true
24+
autowire: true
25+
tags: ['controller.service_arguments']
26+
27+
Symfony\Component\HttpFoundation\Session\SessionInterface: '@session'

src/.gitkeep

Whitespace-only changes.

src/Controller/SecurityController.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\WebFrontend\Controller;
6+
7+
use Exception;
8+
use GuzzleHttp\Exception\GuzzleException;
9+
use PhpList\WebFrontend\Service\ApiClient;
10+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
11+
use Symfony\Component\HttpFoundation\Request;
12+
use Symfony\Component\HttpFoundation\RequestStack;
13+
use Symfony\Component\HttpFoundation\Response;
14+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
15+
use Symfony\Component\Routing\Attribute\Route;
16+
17+
class SecurityController extends AbstractController
18+
{
19+
private ApiClient $apiClient;
20+
private SessionInterface $session;
21+
22+
public function __construct(ApiClient $apiClient, RequestStack $requestStack)
23+
{
24+
$this->apiClient = $apiClient;
25+
$this->session = $requestStack->getSession();
26+
}
27+
28+
#[Route('', name: 'login', methods: ['GET', 'POST'])]
29+
public function login(Request $request): Response
30+
{
31+
if ($this->session->has('auth_token')) {
32+
return $this->redirectToRoute('dashboard');
33+
}
34+
35+
$error = null;
36+
37+
if ($request->isMethod('POST')) {
38+
$username = $request->request->get('username');
39+
$password = $request->request->get('password');
40+
41+
try {
42+
$authData = $this->apiClient->authenticate($username, $password);
43+
44+
// Store token in session
45+
$this->session->set('auth_token', $authData['token']);
46+
47+
// Store user data if needed
48+
if (isset($authData['user'])) {
49+
$this->session->set('user', $authData['user']);
50+
}
51+
52+
// Set token for future API requests
53+
$this->apiClient->setAuthToken($authData['token']);
54+
55+
// Redirect to dashboard
56+
return $this->redirectToRoute('dashboard');
57+
} catch (Exception $e) {
58+
$error = 'Invalid credentials or server error: ' . $e->getMessage();
59+
} catch (GuzzleException $e) {
60+
$error = 'Invalid credentials or server error: ' . $e->getMessage();
61+
}
62+
}
63+
64+
return $this->render('security/login.html.twig', [
65+
'error' => $error,
66+
]);
67+
}
68+
69+
#[Route('/logout', name: 'logout')]
70+
public function logout(): Response
71+
{
72+
// Clear session data
73+
$this->session->remove('auth_token');
74+
$this->session->remove('user');
75+
76+
return $this->redirectToRoute('login');
77+
}
78+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\WebFrontend\DependencyInjection;
6+
7+
use Exception;
8+
use InvalidArgumentException;
9+
use Symfony\Component\Config\FileLocator;
10+
use Symfony\Component\DependencyInjection\ContainerBuilder;
11+
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
12+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
13+
14+
class PhpListFrontendExtension extends Extension
15+
{
16+
/**
17+
* Loads a specific configuration.
18+
*
19+
* @param array $configs configuration values
20+
* @param ContainerBuilder $container
21+
*
22+
* @return void
23+
*
24+
* @throws InvalidArgumentException|Exception if the provided tag is not defined in this extension
25+
*/
26+
public function load(array $configs, ContainerBuilder $container): void
27+
{
28+
// @phpstan-ignore-next-line
29+
$configs;
30+
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config'));
31+
$loader->load('services.yml');
32+
}
33+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace PhpList\WebFrontend\EventSubscriber;
4+
5+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
6+
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
7+
use Symfony\Component\HttpKernel\KernelEvents;
8+
use GuzzleHttp\Exception\ClientException;
9+
10+
class UnauthorizedSubscriber implements EventSubscriberInterface
11+
{
12+
public static function getSubscribedEvents(): array
13+
{
14+
return [
15+
KernelEvents::EXCEPTION => 'onKernelException',
16+
];
17+
}
18+
19+
public function onKernelException(ExceptionEvent $event): void
20+
{
21+
$exception = $event->getThrowable();
22+
23+
if ($exception instanceof ClientException && $exception->getCode() === 401) {
24+
// Redirect to login page or handle unauthorized access
25+
}
26+
}
27+
}

src/PhpListFrontendBundle.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\WebFrontend;
6+
7+
use Symfony\Component\HttpKernel\Bundle\Bundle;
8+
9+
class PhpListFrontendBundle extends Bundle
10+
{
11+
}

src/Service/ApiClient.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\WebFrontend\Service;
6+
7+
namespace PhpList\WebFrontend\Service;
8+
9+
use GuzzleHttp\Client;
10+
use GuzzleHttp\Exception\GuzzleException;
11+
use JsonException;
12+
13+
class ApiClient
14+
{
15+
private Client $client;
16+
private ?string $authToken = null;
17+
18+
public function __construct(string $baseUrl)
19+
{
20+
$this->client = new Client([
21+
'base_uri' => $baseUrl,
22+
'headers' => [
23+
'Authorization' => 'Bearer ' . $this->authToken,
24+
'Accept' => 'application/json',
25+
]
26+
]);
27+
}
28+
29+
/**
30+
* @throws GuzzleException
31+
* @throws JsonException
32+
*/
33+
public function authenticate(string $username, string $password): array
34+
{
35+
try {
36+
$response = $this->request('POST', '/api/login', [
37+
'json' => [
38+
'username' => $username,
39+
'password' => $password,
40+
]
41+
]);
42+
43+
if (!isset($response['token'])) {
44+
throw new \RuntimeException('Authentication failed: No token received');
45+
}
46+
47+
return $response;
48+
} catch (GuzzleException $e) {
49+
if ($e->getCode() === 401) {
50+
throw new \RuntimeException('Invalid credentials', 401, $e);
51+
}
52+
throw $e;
53+
}
54+
}
55+
56+
public function setAuthToken(string $token): void
57+
{
58+
$this->authToken = $token;
59+
}
60+
61+
/**
62+
* @throws GuzzleException
63+
* @throws JsonException
64+
*/
65+
private function request(string $method, string $endpoint, array $options = []): array
66+
{
67+
if ($this->authToken) {
68+
$options['headers'] = [
69+
'Authorization' => 'Bearer ' . $this->authToken,
70+
...$options['headers'] ?? [],
71+
];
72+
}
73+
74+
$response = $this->client->request($method, $endpoint, $options);
75+
76+
return json_decode(
77+
$response->getBody()->getContents(),
78+
true,
79+
512,
80+
JSON_THROW_ON_ERROR
81+
);
82+
}
83+
}

templates/base.html.twig

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>{% block title %}phpList{% endblock %}</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
8+
{# Optionally include Bootstrap for styling #}
9+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
10+
11+
{# Add your own custom CSS later if needed #}
12+
</head>
13+
<body>
14+
<main class="container mt-5">
15+
{% block body %}{% endblock %}
16+
</main>
17+
18+
{# Optionally include Bootstrap JS if you use interactive components #}
19+
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
20+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
21+
</body>
22+
</html>

templates/security/login.html.twig

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{# templates/security/login.html.twig #}
2+
{% extends 'base.html.twig' %}
3+
4+
{% block title %}phpList - Login{% endblock %}
5+
6+
{% block body %}
7+
<div class="login-container">
8+
<h1>Sign in to phpList</h1>
9+
10+
{% if error %}
11+
<div class="alert alert-danger">{{ error }}</div>
12+
{% endif %}
13+
14+
<form method="post">
15+
<div class="form-group">
16+
<label for="username">Username</label>
17+
<input type="text" id="username" name="username" class="form-control" required autofocus>
18+
</div>
19+
20+
<div class="form-group">
21+
<label for="password">Password</label>
22+
<input type="password" id="password" name="password" class="form-control" required>
23+
</div>
24+
25+
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
26+
</form>
27+
</div>
28+
{% endblock %}

0 commit comments

Comments
 (0)