diff --git a/server/web_app/composer.json b/server/web_app/composer.json index 2075e56099dcbd0eab3da5f38c027a8c5e5bd563..f407a8cce070bbada204355dc15c7d17728134f7 100644 --- a/server/web_app/composer.json +++ b/server/web_app/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", + "symfony/asset": "7.2.*", "symfony/console": "7.2.*", "symfony/dotenv": "7.2.*", "symfony/flex": "^2", @@ -18,8 +19,11 @@ "symfony/runtime": "7.2.*", "symfony/security-bundle": "7.2.*", "symfony/security-core": "7.2.*", + "symfony/twig-bundle": "7.2.*", "symfony/validator": "7.2.*", - "symfony/yaml": "7.2.*" + "symfony/yaml": "7.2.*", + "twig/extra-bundle": "^2.12|^3.0", + "twig/twig": "^2.12|^3.0" }, "config": { "allow-plugins": { diff --git a/server/web_app/config/bundles.php b/server/web_app/config/bundles.php index d1c745acc529dbea17aac1040902965183b399a1..53c245c59fa2c94e3a785d2902d664f084e7e8c8 100644 --- a/server/web_app/config/bundles.php +++ b/server/web_app/config/bundles.php @@ -7,4 +7,6 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], ]; diff --git a/server/web_app/config/packages/framework.yaml b/server/web_app/config/packages/framework.yaml index 7e1ee1f1e0d1183963de0d822c064178a2c4422c..23797d50b9d9645d7a31f5d27813f454d23f4a76 100644 --- a/server/web_app/config/packages/framework.yaml +++ b/server/web_app/config/packages/framework.yaml @@ -10,6 +10,8 @@ framework: when@test: framework: + router: + strict_requirements: null test: true session: storage_factory_id: session.storage.factory.mock_file diff --git a/server/web_app/config/packages/security.yaml b/server/web_app/config/packages/security.yaml index 367af25a56d2decba5041becc5770b16c91b60d4..e02fa5807db32ee5e86328cc6e9ecc76d71bb3cf 100644 --- a/server/web_app/config/packages/security.yaml +++ b/server/web_app/config/packages/security.yaml @@ -4,14 +4,25 @@ security: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + app_user_provider: + entity: + class: App\Entity\User + property: userName + firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider + + form_login: + login_path: app_login + check_path: app_login_check + default_target_path: home + + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -22,18 +33,19 @@ security: # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + - { path: ^/login, roles: PUBLIC_ACCESS } # Allow access to the login page + - { path: ^/dashboard, roles: ROLE_ADMIN } # Secure the dashboard + - { path: ^/profile, roles: ROLE_USER } # Secure user profiles -when@test: - security: - password_hashers: - # By default, password hashers are resource intensive and take time. This is - # important to generate secure password hashes. In tests however, secure hashes - # are not important, waste resources and increase test times. The following - # reduces the work factor to the lowest possible values. - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: - algorithm: auto - cost: 4 # Lowest possible value for bcrypt - time_cost: 3 # Lowest possible value for argon - memory_cost: 10 # Lowest possible value for argon +# when@test: +# security: +# password_hashers: +# # By default, password hashers are resource intensive and take time. This is +# # important to generate secure password hashes. In tests however, secure hashes +# # are not important, waste resources and increase test times. The following +# # reduces the work factor to the lowest possible values. +# Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: +# algorithm: auto +# cost: 4 # Lowest possible value for bcrypt +# time_cost: 3 # Lowest possible value for argon +# memory_cost: 10 # Lowest possible value for argon diff --git a/server/web_app/config/packages/twig.yaml b/server/web_app/config/packages/twig.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8f597df0bd1a6e78f44f6097bcb40735daa470f6 --- /dev/null +++ b/server/web_app/config/packages/twig.yaml @@ -0,0 +1,9 @@ +twig: + file_name_pattern: '*.twig' + default_path: '%kernel.project_dir%/templates' + paths: + '%kernel.project_dir%/templates': ~ + +when@test: + twig: + strict_variables: true diff --git a/server/web_app/config/routes.yaml b/server/web_app/config/routes.yaml index 41ef8140ba811c0da46ce1962ffa50d4c56b9840..dbdcf2c70a5677b874b54bb1b0585ee13a8f3ccb 100644 --- a/server/web_app/config/routes.yaml +++ b/server/web_app/config/routes.yaml @@ -3,3 +3,7 @@ controllers: path: ../src/Controller/ namespace: App\Controller type: attribute + +app_login_check: + path: /login_check + diff --git a/server/web_app/migrations/Version20250312182453.php b/server/web_app/migrations/Version20250319155149.php similarity index 90% rename from server/web_app/migrations/Version20250312182453.php rename to server/web_app/migrations/Version20250319155149.php index 0fd37f30a0f73d43ebd6d96854db3465b97aae36..d2e3ff7063d03173db878d94cafe1409452d8bf0 100644 --- a/server/web_app/migrations/Version20250312182453.php +++ b/server/web_app/migrations/Version20250319155149.php @@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration; /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20250312182453 extends AbstractMigration +final class Version20250319155149 extends AbstractMigration { public function getDescription(): string { @@ -25,12 +25,12 @@ final class Version20250312182453 extends AbstractMigration $this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, user_name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, surname VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, is_blocked TINYINT(1) DEFAULT 0 NOT NULL, role VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_8D93D64924A232CF (user_name), UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE wish_list (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, expiration_date DATETIME DEFAULT NULL, is_active TINYINT(1) DEFAULT 1 NOT NULL, url_view_mode VARCHAR(255) NOT NULL, url_edit_mode VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_5B8739BDA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE wish_list_member (id INT AUTO_INCREMENT NOT NULL, wish_list_id INT NOT NULL, user_id INT NOT NULL, can_edit TINYINT(1) DEFAULT NULL, is_accepted TINYINT(1) DEFAULT 0, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_F4CA81DFD69F3311 (wish_list_id), INDEX IDX_F4CA81DFA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('ALTER TABLE item ADD CONSTRAINT FK_1F1B251ED69F3311 FOREIGN KEY (wish_list_id) REFERENCES wish_list (id)'); - $this->addSql('ALTER TABLE purchase ADD CONSTRAINT FK_6117D13BA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); - $this->addSql('ALTER TABLE purchase ADD CONSTRAINT FK_6117D13B126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); - $this->addSql('ALTER TABLE wish_list ADD CONSTRAINT FK_5B8739BDA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); - $this->addSql('ALTER TABLE wish_list_member ADD CONSTRAINT FK_F4CA81DFD69F3311 FOREIGN KEY (wish_list_id) REFERENCES wish_list (id)'); - $this->addSql('ALTER TABLE wish_list_member ADD CONSTRAINT FK_F4CA81DFA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE item ADD CONSTRAINT FK_1F1B251ED69F3311 FOREIGN KEY (wish_list_id) REFERENCES wish_list (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE purchase ADD CONSTRAINT FK_6117D13BA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE purchase ADD CONSTRAINT FK_6117D13B126F525E FOREIGN KEY (item_id) REFERENCES item (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE wish_list ADD CONSTRAINT FK_5B8739BDA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE wish_list_member ADD CONSTRAINT FK_F4CA81DFD69F3311 FOREIGN KEY (wish_list_id) REFERENCES wish_list (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE wish_list_member ADD CONSTRAINT FK_F4CA81DFA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE'); } public function down(Schema $schema): void diff --git a/server/web_app/public/css/dashboard.css b/server/web_app/public/css/dashboard.css new file mode 100644 index 0000000000000000000000000000000000000000..2f2ec2ec0de53b5953848019ac48fbdb72f04c0b --- /dev/null +++ b/server/web_app/public/css/dashboard.css @@ -0,0 +1,48 @@ +@import url('global.css'); + +.container { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + gap: var(--margin-space-normal); +} + +.card { + display: block; + width: 16rem; + max-height: 26rem; + color: black; + border-radius: 15px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + text-decoration: none; + transition: transform 0.3s ease, box-shadow 0.3s ease; + background-color: var(--color-complementary); +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2); +} + +.card article { + text-align: center; +} + +.card img { + width: 100%; + object-fit: cover; + border-radius: 15px 0px; +} + +.card h4 { + font-size: var(--font-size-big); + color: var(--color-hover); + margin: 10px 0; +} + +.card p { + font-size: var(--font-size-normal); + line-height: 1.5; + padding: var(--padding-space-normal); +} diff --git a/server/web_app/public/css/global.css b/server/web_app/public/css/global.css new file mode 100644 index 0000000000000000000000000000000000000000..a868886906392770e04b221d96d5f7d5b91db5cf --- /dev/null +++ b/server/web_app/public/css/global.css @@ -0,0 +1,52 @@ +:root { + --font-size-normal: 1rem; + --font-size-small: 0.8rem; + --font-size-big: 1.5rem; + --font-size-extrabig: 2rem; + + --line-heigth-small: 1.2rem; + --line-heigth-normal: 1.5rem; + --line-heigth-large: 1.7rem; + + --padding-space-large: 2rem; + --padding-space-normal: 1rem; + --padding-space-small: 0.5rem; + + --margin-space-large: 3rem; + --margin-space-normal: 2rem; + --margin-space-small: 1rem; + --margin-space-xsmall: 0.5rem; + + --border-thin: 1px; + --border-normal: 3px; + --border-thick: 5px; + + --font-family-style: Helvetica, Arial, sans-serif; + + --color-basic: #00B8DE; + --color-hover:#14223C; + --color-secondary: #99CC99; + --color-light: #EDF3F4; + --color-complementary: #FFFFFF; + + --font-weight-light: 300; + --font-weight-regular: 500; + --font-weight-bold: 700; +} + +* { + box-sizing: border-box; + font-family: var(--font-family-style); + line-height: var(--line-heigth-normal); + font-weight: var(--font-weight-regular); + margin: 0; + padding: 0; +} + +body { + background-color: var(--color-light); +} + +.container { + margin: var(--padding-space-large); +} diff --git a/server/web_app/public/css/insights.css b/server/web_app/public/css/insights.css new file mode 100644 index 0000000000000000000000000000000000000000..20d32d76fcd3da6fc04a81066945a287b525a26a --- /dev/null +++ b/server/web_app/public/css/insights.css @@ -0,0 +1 @@ +@import url('global.css'); diff --git a/server/web_app/public/css/login.css b/server/web_app/public/css/login.css new file mode 100644 index 0000000000000000000000000000000000000000..20d32d76fcd3da6fc04a81066945a287b525a26a --- /dev/null +++ b/server/web_app/public/css/login.css @@ -0,0 +1 @@ +@import url('global.css'); diff --git a/server/web_app/public/css/menu.css b/server/web_app/public/css/menu.css new file mode 100644 index 0000000000000000000000000000000000000000..f47392a260800e4cb6f68a5b12c2fd563bdba957 --- /dev/null +++ b/server/web_app/public/css/menu.css @@ -0,0 +1,42 @@ +@import url('global.css'); + +nav { + background-color: var(--color-hover); + width: 100%; +} + +nav ul { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; +} + +nav ul li { + margin: var(--margin-space-xsmall) 0; +} + +nav ul li a { + color: var(--color-complementary); + text-decoration: none; + font-size: var(--font-size-normal); + display: block; + padding: var(--padding-space-normal); + border-radius: var(--padding-space-small) var(--border-thick); + transition: background-color 0.3s, color 0.3s; +} + +nav ul li a:hover { + background-color: var(--color-secondary); +} + +.login { + margin-left: auto; +} + +h3 { + margin: var(--margin-space-small) auto; + text-align: center; + font-size: var(--font-size-extrabig); +} diff --git a/server/web_app/public/css/users.css b/server/web_app/public/css/users.css new file mode 100644 index 0000000000000000000000000000000000000000..ace1e04072466a8f47b640b533029a44db6d04e6 --- /dev/null +++ b/server/web_app/public/css/users.css @@ -0,0 +1,67 @@ +@import url('global.css'); + +table { + width: 100%; + border-collapse: collapse; + background-color: var(--color-complementary); +} + +table th, table td { + border: var(--border-thin) solid var(--color-hover); + padding: var(--padding-space-small); + text-align: left; +} + +table th { + background-color: var(--color-hover); + color: var(--color-complementary); +} + +button { + padding: 6px 12px; + margin: var(--margin-space-xsmall); + border: none; + border-radius: var(--border-thick); + color: var(--color-complementary); + cursor: pointer; + background-color: var(--color-basic); +} + +button:hover { + background-color: var(--color-hover); + color: var(--color-complementary); +} + +button:disabled { + background-color: var(--color-light); + color: black; +} + +/* Basic Styles for the Popup */ +.popup { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.popup p, .popup h4 { + margin: var(--margin-space-xsmall) 0; +} + +.popup-content { + background-color: #fff; + padding: 20px; + border-radius: 5px; + text-align: center; +} + +.popup button { + margin: 10px; +} diff --git a/server/web_app/public/img/gifts.png b/server/web_app/public/img/gifts.png new file mode 100644 index 0000000000000000000000000000000000000000..7db1ba5883eecbb7abe073eb98118e88568d8808 Binary files /dev/null and b/server/web_app/public/img/gifts.png differ diff --git a/server/web_app/public/img/users.png b/server/web_app/public/img/users.png new file mode 100644 index 0000000000000000000000000000000000000000..d4a17a125919cecb86df413b2d58453b074490b4 Binary files /dev/null and b/server/web_app/public/img/users.png differ diff --git a/server/web_app/public/js/insights.js b/server/web_app/public/js/insights.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/server/web_app/public/js/login.js b/server/web_app/public/js/login.js new file mode 100644 index 0000000000000000000000000000000000000000..2739a137af27ff6ae936cebdca6917a73908a096 --- /dev/null +++ b/server/web_app/public/js/login.js @@ -0,0 +1,31 @@ +ROUTE='http://localhost'; +PORT='8080'; + +const loginForm = document.getElementById('login-form'); +loginForm.addEventListener('submit', async function(event) { + event.preventDefault(); + + try { + const response = await fetch(`${ROUTE}:${PORT}/users/authenticate`, { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + username: username, + password: password + }) + }); + + console.log(response) + + if (response.ok) { + window.location.href = '/dashboard'; + } else { + const error = await response.json(); + alert('Login failed: ' + error.message); + } + } catch (error) { + console.error('Error:', error); + } +}); diff --git a/server/web_app/public/js/users.js b/server/web_app/public/js/users.js new file mode 100644 index 0000000000000000000000000000000000000000..c5ee46131284e3b99013c4b33fa7b1d6cb55fdb9 --- /dev/null +++ b/server/web_app/public/js/users.js @@ -0,0 +1,112 @@ +const ROUTE = 'http://localhost'; +const PORT = '8080'; +let currentAction = null; +let currentUserId = null; +let actionType = null; // New variable to track whether it's block or unblock + +async function fetchUsers() { + const response = await fetch(`${ROUTE}:${PORT}/users`); + const data = await response.json(); + const tableBody = document.querySelector('#usersTable tbody'); + tableBody.innerHTML = ''; // Clear current table rows + + data.users.forEach(user => { + const row = document.createElement('tr'); + row.innerHTML = ` + <td>${user.username}</td> + <td>${user.name} ${user.surname}</td> + <td>${user.mail}</td> + <td> + <button onclick="toggleBlock(${user.id}, ${user.isBlocked})"> + ${user.isBlocked ? 'Unblock' : 'Block'} + </button> + <button onclick="prepareDeleteUser(${user.id})">Delete</button> + <button>Details</button> + </td> + `; + tableBody.appendChild(row); + }); +} + +// Toggle block/unblock +async function toggleBlock(userId, isBlocked) { + actionType = isBlocked ? 'Unblock' : 'Block'; // Set action type based on block status + showConfirmationPopup(actionType, () => { + updateBlockStatus(userId, !isBlocked); + }); +} + +// Prepare for user deletion +function prepareDeleteUser(userId) { + actionType = 'Delete'; + showConfirmationPopup("Delete this user", () => { + deleteUser(userId); + }); +} + +function disabledButtons() { + const button = document.getElementById('confirmBtn'); + const buttonCancel = document.getElementById('cancelBtn'); + button.disabled = true; + buttonCancel.disabled = true; + button.textContent = "Loading..."; +} + +// Show confirmation pop-up +function showConfirmationPopup(message, action) { + document.getElementById('popupMessage').innerText = message; + document.getElementById('confirmationPopup').style.display = 'flex'; + currentAction = action; +} + +// Confirm the action +document.getElementById('confirmBtn').addEventListener('click', () => { + if (currentAction) { + currentAction(); + disabledButtons(); + } +}); + +// Cancel the action +document.getElementById('cancelBtn').addEventListener('click', hideConfirmationPopup); + +// Hide the pop-up +function hideConfirmationPopup() { + document.getElementById('confirmationPopup').style.display = 'none'; +} + +// Update block/unblock status +async function updateBlockStatus(userId, isBlocked) { + try { + const response = await fetch(`${ROUTE}:${PORT}/users/${userId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ isBlocked }), + }); + const data = await response.json(); + } catch (error) { + console.error(error); + } finally { + hideConfirmationPopup(); + location.reload(); + } +} + +// Delete user +async function deleteUser(userId) { + try { + const response = await fetch(`${ROUTE}:${PORT}/users/${userId}`, { + method: 'DELETE' + }); + const data = await response.json(); + } catch (error) { + console.error(error); + } finally { + hideConfirmationPopup(); + location.reload(); + } +} +// Fetch users when the page loads +window.onload = fetchUsers; diff --git a/server/web_app/src/Controller/DashboardController.php b/server/web_app/src/Controller/DashboardController.php new file mode 100644 index 0000000000000000000000000000000000000000..0c9a0ec0b15f678b56c6c5a1887063f446048990 --- /dev/null +++ b/server/web_app/src/Controller/DashboardController.php @@ -0,0 +1,41 @@ +<?php + +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +#[Route('/dashboard')] +class DashboardController extends AbstractController +{ + #[Route('', name: 'dashboard')] + public function index(): Response + { + if (!$this->isGranted('ROLE_ADMIN')) { + return $this->redirectToRoute('error'); + } + + return $this->render('dashboard.html.twig'); + } + + #[Route('/users', name: 'manageUsers')] + public function users(): Response + { + if (!$this->isGranted('ROLE_ADMIN')) { + return $this->redirectToRoute('error'); + } + + return $this->render('usersManagement.html.twig'); + } + + #[Route('/insights', name: 'manageInsights')] + public function items(): Response + { + if (!$this->isGranted('ROLE_ADMIN')) { + return $this->redirectToRoute('error'); + } + + return $this->render('insights.html.twig'); + } +} diff --git a/server/web_app/src/Controller/ErrorController.php b/server/web_app/src/Controller/ErrorController.php new file mode 100644 index 0000000000000000000000000000000000000000..c94d3ec3bb2fffda2e050165c58ce7504ee30c6b --- /dev/null +++ b/server/web_app/src/Controller/ErrorController.php @@ -0,0 +1,17 @@ +<?php + +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + + +class ErrorController extends AbstractController +{ + #[Route('/error', name: 'error')] + public function index(): Response + { + return $this->render('error.html.twig'); + } +} diff --git a/server/web_app/src/Controller/MainController.php b/server/web_app/src/Controller/MainController.php new file mode 100644 index 0000000000000000000000000000000000000000..9296ac25c7ebdf365262db70ed8c6d5e1a18e0a8 --- /dev/null +++ b/server/web_app/src/Controller/MainController.php @@ -0,0 +1,34 @@ +<?php + +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; + +class MainController extends AbstractController +{ + #[Route('/', name: 'home')] + public function index(): Response + { + return $this->render('home.html.twig'); + } + + #[Route('/login', name: 'app_login')] + public function login(AuthenticationUtils $authenticationUtils): Response + { + $error = $authenticationUtils->getLastAuthenticationError(); + $lastUsername = $authenticationUtils->getLastUsername(); + return $this->render('login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + + #[Route('/logout', name: 'app_logout')] + public function logout(): void + { + + } +} diff --git a/server/web_app/src/Controller/UserController.php b/server/web_app/src/Controller/UserController.php index 2cca27f8b8df67f622635e42a7fc186b2701ce9a..346ca5fccad990b1a37935ca87c93260d330695a 100644 --- a/server/web_app/src/Controller/UserController.php +++ b/server/web_app/src/Controller/UserController.php @@ -106,7 +106,12 @@ final class UserController extends AbstractController implements UserControllerI $userRole = UserRole::from($roleParam); } - $isBlocked = filter_var($req->query->get('isBlocked', null), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + $isBlockedRaw = $req->query->get('isBlocked', null); + if ($isBlockedRaw === null) { + $isBlocked = null; + } else { + $isBlocked = filter_var($isBlockedRaw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + } // Validate "sortBy" to allow only 'createdAt' or 'username' $allowedSortBy = ['createdAt', 'username']; @@ -137,6 +142,8 @@ final class UserController extends AbstractController implements UserControllerI $userArray = array_map(fn($user) => [ 'id' => $user->getId(), + 'name' => $user->getName(), + 'surname' => $user->getSurname(), 'username' => $user->getUserName(), 'mail' => $user->getEmail(), 'role' => $user->getRoles(), @@ -268,15 +275,6 @@ final class UserController extends AbstractController implements UserControllerI ]); } - #[Route('/authenticate', methods: ['POST'])] - public function authenticateUser(string $username, string $password): JsonResponse - { - return $this->json([ - 'message' => 'Welcome to your new controller!', - 'path' => 'src/Controller/UserController.php', - ]); - } - #[Route('/authenticate/changePassword', methods: ['POST'])] public function changePassword(int $userId, string $oldPassword, string $newPassword): JsonResponse { diff --git a/server/web_app/src/Entity/Item.php b/server/web_app/src/Entity/Item.php index 84d0437096d340d7d109ac3a6c77b773e8038791..2de51983b9744e0b3badca30a8010327874d9362 100644 --- a/server/web_app/src/Entity/Item.php +++ b/server/web_app/src/Entity/Item.php @@ -15,7 +15,7 @@ class Item private ?int $id = null; #[ORM\ManyToOne(targetEntity: WishList::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] #[Assert\NotNull(message: "WishList must be selected.")] private ?WishList $wishList = null; diff --git a/server/web_app/src/Entity/Purchase.php b/server/web_app/src/Entity/Purchase.php index edb95c7c14e3f8f5fb51f9ee54c829f4618a4535..0640329e5ed7653be406c6b06db16596ecf66e55 100644 --- a/server/web_app/src/Entity/Purchase.php +++ b/server/web_app/src/Entity/Purchase.php @@ -11,12 +11,12 @@ class Purchase { #[ORM\Id] #[ORM\ManyToOne(targetEntity: User::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] private ?User $user = null; #[ORM\id] #[ORM\ManyToOne(targetEntity: Item::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] private ?Item $item = null; #[ORM\Column(length: 255, nullable: true)] diff --git a/server/web_app/src/Entity/User.php b/server/web_app/src/Entity/User.php index c7dc58636082b26da39926b8273b0496a9df4767..7ecc797216da40c9b2ade8941a977e173d86baaf 100644 --- a/server/web_app/src/Entity/User.php +++ b/server/web_app/src/Entity/User.php @@ -154,7 +154,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface //Methods neeeded to be implemented here because of the extension of UserInterface, PasswordAuthenticatedUserInterface public function getRoles(): array { - return [$this->role->value]; // Assuming UserRole is an enum with string values + return ['ROLE_' . strtoupper($this->role->value)]; // Assuming UserRole is an enum with string values } public function getUserIdentifier(): string diff --git a/server/web_app/src/Entity/WishList.php b/server/web_app/src/Entity/WishList.php index 8849ca9b43b3bce5f2ef5bb33057fce6ef77a19c..3566b69e30e533bba255e1118535058b14b3f3c7 100644 --- a/server/web_app/src/Entity/WishList.php +++ b/server/web_app/src/Entity/WishList.php @@ -18,7 +18,7 @@ class WishList private ?int $id = null; #[ORM\ManyToOne(targetEntity: User::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] #[Assert\NotNull(message: "User must be provided.")] private ?User $user = null; diff --git a/server/web_app/src/Entity/WishListMember.php b/server/web_app/src/Entity/WishListMember.php index 0d4deabd41ebfbc859fc2e7e816eea97723b8e1e..6c3e35e25ce4c534feb0ad4ba65f17d032b74147 100644 --- a/server/web_app/src/Entity/WishListMember.php +++ b/server/web_app/src/Entity/WishListMember.php @@ -15,12 +15,12 @@ class WishListMember private ?int $id = null; #[ORM\ManyToOne(targetEntity: WishList::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] #[Assert\NotNull(message: "WishList must be provided.")] private ?WishList $wishList = null; #[ORM\ManyToOne(targetEntity: User::class)] - #[ORM\JoinColumn(nullable: false)] + #[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")] #[Assert\NotNull(message: "User must be provided.")] private ?User $user = null; diff --git a/server/web_app/src/Interface/UserControllerInterface.php b/server/web_app/src/Interface/UserControllerInterface.php index 92a41838ddc07c2083948d10982a3d3160436dbb..fc77f16a020b3af3a3e4abfb6f430dbca368a8aa 100644 --- a/server/web_app/src/Interface/UserControllerInterface.php +++ b/server/web_app/src/Interface/UserControllerInterface.php @@ -16,7 +16,5 @@ interface UserControllerInterface public function deleteUser(int $userId): Response; - public function authenticateUser(string $username, string $password): Response; - public function changePassword(int $userId, string $oldPassword, string $newPassword): Response; } diff --git a/server/web_app/symfony.lock b/server/web_app/symfony.lock index 4ddf444e42085914d334d9b5aff511569e222a5a..a0a4776bd1ea97edbf7854c11b7c2eb53a9fd8a6 100644 --- a/server/web_app/symfony.lock +++ b/server/web_app/symfony.lock @@ -117,6 +117,19 @@ "config/routes/security.yaml" ] }, + "symfony/twig-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, "symfony/validator": { "version": "7.2", "recipe": { @@ -128,5 +141,8 @@ "files": [ "config/packages/validator.yaml" ] + }, + "twig/extra-bundle": { + "version": "v3.20.0" } } diff --git a/server/web_app/templates/base.html.twig b/server/web_app/templates/base.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..68d3bc31b9010af25e98a9d6b137450d1a8e272f --- /dev/null +++ b/server/web_app/templates/base.html.twig @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + {% block styles %}{% endblock %} + <title>{% block title %}Wishlist app{% endblock %}</title> +</head> +<body> + {% include 'menu.html.twig' %} + {% block body %}{% endblock %} +</body> +</html> \ No newline at end of file diff --git a/server/web_app/templates/dashboard.html.twig b/server/web_app/templates/dashboard.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..75ab75c400a85f2ce1b8480f041808c8ecd164cc --- /dev/null +++ b/server/web_app/templates/dashboard.html.twig @@ -0,0 +1,24 @@ +{% extends 'base.html.twig' %} + +{% block styles %} + <link rel="stylesheet" href="{{ asset('css/dashboard.css') }}"> +{% endblock %} + +{% block body %} + <section class="container"> + <a class="card" href="{{ path('manageInsights') }}"> + <article> + <img src="{{ asset('img/gifts.png') }}" alt=""> + <h4>Insights</h4> + <p>View insights about system's lists and system's items.</p> + </article> + </a> + <a class="card" href="{{ path('manageUsers') }}"> + <article> + <img src="{{ asset('img/users.png') }}" alt=""> + <h4>Users</h4> + <p>Administrate users.</p> + </article> + </a> + </section> +{% endblock %} diff --git a/server/web_app/templates/error.html.twig b/server/web_app/templates/error.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..f17a6b72a569ff2ebaa3e0119345b2d9bc9671e5 --- /dev/null +++ b/server/web_app/templates/error.html.twig @@ -0,0 +1 @@ +{% block body %} This page does not exist or you do not have rigths to see {% endblock %} \ No newline at end of file diff --git a/server/web_app/templates/home.html.twig b/server/web_app/templates/home.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..06d07ac7e2611893faa199e9a9867c77c5078bb5 --- /dev/null +++ b/server/web_app/templates/home.html.twig @@ -0,0 +1,3 @@ +{% extends 'base.html.twig' %} + +{% block body %} Welcome {% endblock %} diff --git a/server/web_app/templates/insights.html.twig b/server/web_app/templates/insights.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..f682d0e2ad52b5b4f7114c4dca850b0f4d3f3992 --- /dev/null +++ b/server/web_app/templates/insights.html.twig @@ -0,0 +1,9 @@ +{% extends 'base.html.twig' %} + +{% block styles %} + <link rel="stylesheet" href="{{ asset('css/insights.css') }}"> +{% endblock %} + +{% block body %} + +{% endblock %} diff --git a/server/web_app/templates/login.html.twig b/server/web_app/templates/login.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..7bb685f0d2a021aec66977f46866fb2e72d8de59 --- /dev/null +++ b/server/web_app/templates/login.html.twig @@ -0,0 +1,26 @@ +{% extends 'base.html.twig' %} + +{% block title %}Log in!{% endblock %} + +{% block styles %} + <link rel="stylesheet" href="{{ asset('css/login.css') }}"> +{% endblock %} + +{% block body %} + <form action="{{ path('app_login_check') }}" method="post"> + + <h3>Please sign in</h3> + <label for="username">User Name</label> + <input type="text" value="{{ last_username }}" name="_username" id="username" class="form-control" autocomplete="username" required autofocus> + <label for="password">Password</label> + <input type="password" name="_password" id="password" class="form-control" autocomplete="current-password" required> + + <input type="hidden" name="_csrf_token" + value="{{ csrf_token('authenticate') }}" + > + + <button> + Sign in + </button> + </form> +{% endblock %} diff --git a/server/web_app/templates/menu.html.twig b/server/web_app/templates/menu.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..253cf91836820b5bc5c53f8cd8045ef7576e2605 --- /dev/null +++ b/server/web_app/templates/menu.html.twig @@ -0,0 +1,19 @@ +{% block styles %} + <link rel="stylesheet" href="{{ asset('css/menu.css') }}"> +{% endblock %} + +<nav> + <ul> + <li><a href=#>My wishlists</a></li> + <li><a href=#>Shared wishlists</a></li> + {% if is_granted('ROLE_ADMIN') %} + <li><a href="{{ path('dashboard') }}">Dashboard </a></li> + {% endif %} + + {% if app.user %} + <li class="login"><a href="{{ path('app_logout') }}">Logout</a></li> + {% else %} + <li class="login"><a href="{{ path('app_login') }}">Login</a></li> + {% endif %} + </ul> +</nav> \ No newline at end of file diff --git a/server/web_app/templates/usersManagement.html.twig b/server/web_app/templates/usersManagement.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..1c168895ed9450edd3d99d1175a393901a0e693c --- /dev/null +++ b/server/web_app/templates/usersManagement.html.twig @@ -0,0 +1,36 @@ +{% extends 'base.html.twig' %} + +{% block styles %} + <link rel="stylesheet" href="{{ asset('css/users.css') }}"> +{% endblock %} + +{% block body %} + <h3>User control panel</h3> + + <article class="container"> + <table id="usersTable"> + <thead> + <tr> + <th>Username</th> + <th>Name</th> + <th>Email</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + </article> + + <!-- Popup Modal --> + <div id="confirmationPopup" class="popup" style="display: none;"> + <div class="popup-content"> + <h4>Are you sure?</h4> + <p id="popupMessage"></p> + <button id="confirmBtn" class="btn btn-danger">Yes</button> + <button id="cancelBtn" class="btn btn-secondary">Cancel</button> + </div> + </div> + + <script src="{{ asset('js/users.js') }}"></script> +{% endblock %}