diff --git a/.env b/.env index 5c5c85f..e0f36bf 100644 --- a/.env +++ b/.env @@ -42,3 +42,9 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages ###< symfony/messenger ### + +###> symfony/lock ### +# Choose one of the stores below +# postgresql+advisory://db_user:db_password@localhost/db_name +LOCK_DSN=flock +###< symfony/lock ### diff --git a/TODO b/TODO index 7d3b7fd..dbf4f6e 100644 --- a/TODO +++ b/TODO @@ -2,6 +2,7 @@ accent color #d43934 add dates and author to pages +blog use turbo make unit tests diff --git a/assets/app.js b/assets/app.js index 48617e4..0bb46da 100644 --- a/assets/app.js +++ b/assets/app.js @@ -10,22 +10,58 @@ import 'fork-awesome/scss/fork-awesome.scss' import './styles/app.scss' import $ from 'jquery' import 'bootstrap' + +// Dropzone stuff move to component +import {Dropzone} from 'dropzone' + +// TODO handle error (Chapter 26) +const formElement = $('#dropzoneForm') +if (formElement) { + const previewContent = $('#preview-content').html() + console.log(previewContent) + const dropzone = new Dropzone('#dropzoneForm', { + acceptedFiles: '.jpg, .jpeg, .png', + maxFiles: 1, + init: function () { + this.hiddenFileInput.removeAttribute('multiple') + this.on('maxfilesexceeded', (file) => { + this.removeAllFiles() + this.addFile(file) + }) + this.on('error', (file, data) => { + console.log('error'); + if (data.detail) { + this.emit('error', file, data.detail) + } + + }) + } + }) + console.log('filename', previewContent) + const mockFile = { name: previewContent } + console.log('file', mockFile) + dropzone.displayExistingFile(mockFile) +} + +// End Dropzone stuff move to component + //import './js/index' // needed for legacy code //global.$ = $ if (window.matchMedia('(prefers-color-scheme)').media !== 'not all') { - console.log('🎉 Dark mode is supported') + console.log('🎉 Dark mode is supported') } $(document).ready(() => { - console.log('ready') - $('#toggleSidebar').on('click', () => { - const toggleIcon = $('#toggleIcon') - toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-left') - toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-right') - $('#sidebar').toggleClass('active') - }) + console.log('ready') + $('#toggleSidebar').on('click', () => { + const toggleIcon = $('#toggleIcon') + toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-left') + toggleIcon.toggleClass('fa fa-lg fa-fw fa-caret-square-o-right') + $('#sidebar').toggleClass('active') + }) }) + // start the Stimulus application //import './bootstrap' diff --git a/assets/js/components/_avatarDropzone.js b/assets/js/components/_avatarDropzone.js new file mode 100644 index 0000000..597a6db --- /dev/null +++ b/assets/js/components/_avatarDropzone.js @@ -0,0 +1 @@ +i \ No newline at end of file diff --git a/assets/styles/app.css b/assets/styles/app.css deleted file mode 100644 index cb33b13..0000000 --- a/assets/styles/app.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - background-color: lightgray; -} diff --git a/assets/styles/app.scss b/assets/styles/app.scss index ad39446..fbf9f2f 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -18,13 +18,17 @@ $primary: #FF8040; $jet-black: #0e0e10; -$body-color: #3f3f3f; -$list-group-bg: $body-color; -$list-group-hover-bg: #232323; -$list-group-active-bg: #232323; -$list-group-action-active-bg: #232323; -@import '~bootstrap'; + +$body-color: #9f9f9f; +$list-group: #232323; +$list-group-bg: $list-group; +$list-group-hover-bg: darken($list-group, 10%); +$list-group-active-bg: $list-group; +$list-group-action-active-bg: $list-group; + +@import 'bootstrap/scss/bootstrap'; +@import 'dropzone/dist/dropzone'; @import './components/sidebar'; @@ -36,7 +40,16 @@ html, body { background: #0e0e10; } +// Dropzone +.dropzone .dz-preview.dz-image-preview { + background: transparent !important; +} + +.article-author-img { + width: 80px; + margin: 15px; +} /// diff --git a/composer.json b/composer.json index 1195f29..b632310 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "easycorp/easyadmin-bundle": "^4.0", "knplabs/knp-time-bundle": "^1.18", "league/commonmark": "^2.3", + "liip/imagine-bundle": "^2.9", "nelmio/cors-bundle": "^2.2", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.4", @@ -39,6 +40,7 @@ "symfony/property-access": "6.1.*", "symfony/property-info": "6.1.*", "symfony/proxy-manager-bridge": "6.1.*", + "symfony/rate-limiter": "6.1.*", "symfony/runtime": "6.1.*", "symfony/security-bundle": "6.1.*", "symfony/security-csrf": "6.1.*", diff --git a/composer.lock b/composer.lock index 1bae413..ccaa754 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a389aa46db42a7da38ebb6610fb97a52", + "content-hash": "1c940d128112ec366d0f81bc6eccee45", "packages": [ { "name": "dflydev/dot-access-data", @@ -1689,6 +1689,68 @@ ], "time": "2022-10-17T19:48:16+00:00" }, + { + "name": "imagine/imagine", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/php-imagine/Imagine.git", + "reference": "ae864f26afbf8859ebd2e2b9df92d77ee175dc13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/ae864f26afbf8859ebd2e2b9df92d77ee175dc13", + "reference": "ae864f26afbf8859ebd2e2b9df92d77ee175dc13", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4 || ^9.3" + }, + "suggest": { + "ext-exif": "to read EXIF metadata", + "ext-gd": "to use the GD implementation", + "ext-gmagick": "to use the Gmagick implementation", + "ext-imagick": "to use the Imagick implementation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Imagine\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bulat Shakirzyanov", + "email": "mallluhuct@gmail.com", + "homepage": "http://avalanche123.com" + } + ], + "description": "Image processing for PHP 5.3", + "homepage": "http://imagine.readthedocs.org/", + "keywords": [ + "drawing", + "graphics", + "image manipulation", + "image processing" + ], + "support": { + "issues": "https://github.com/php-imagine/Imagine/issues", + "source": "https://github.com/php-imagine/Imagine/tree/1.3.2" + }, + "time": "2022-04-01T11:58:30+00:00" + }, { "name": "knplabs/knp-time-bundle", "version": "v1.20.0", @@ -2016,6 +2078,108 @@ ], "time": "2021-08-14T12:15:32+00:00" }, + { + "name": "liip/imagine-bundle", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/liip/LiipImagineBundle.git", + "reference": "ba164fef7be638f28d298f9c89b5a8364c3e0a4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/ba164fef7be638f28d298f9c89b5a8364c3e0a4d", + "reference": "ba164fef7be638f28d298f9c89b5a8364c3e0a4d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "imagine/imagine": "^1.2.4", + "php": "^7.1|^8.0", + "symfony/filesystem": "^3.4|^4.4|^5.3|^6.0", + "symfony/finder": "^3.4|^4.4|^5.3|^6.0", + "symfony/framework-bundle": "^3.4.23|^4.4|^5.3|^6.0", + "symfony/mime": "^4.4|^5.3|^6.0", + "symfony/options-resolver": "^3.4|^4.4|^5.3|^6.0", + "symfony/process": "^3.4|^4.4|^5.3|^6.0", + "twig/twig": "^1.44|^2.9|^3.0" + }, + "require-dev": { + "amazonwebservices/aws-sdk-for-php": "^1.0", + "aws/aws-sdk-php": "^2.4", + "doctrine/cache": "^1.11|^2.0", + "doctrine/persistence": "^1.3|^2.0", + "enqueue/enqueue-bundle": "^0.9|^0.10", + "ext-gd": "*", + "league/flysystem": "^1.0|^2.0|^3.0", + "phpstan/phpstan": "^0.12.64", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/log": "^1.0", + "symfony/browser-kit": "^3.4|^4.4|^5.3|^6.0", + "symfony/cache": "^3.4|^4.4|^5.3|^6.0", + "symfony/console": "^3.4|^4.4|^5.3|^6.0", + "symfony/dependency-injection": "^3.4|^4.4|^5.3|^6.0", + "symfony/form": "^3.4|^4.4|^5.3|^6.0", + "symfony/messenger": "^4.4|^5.3|^6.0", + "symfony/phpunit-bridge": "^5.3", + "symfony/templating": "^3.4|^4.4|^5.3|^6.0", + "symfony/validator": "^3.4|^4.4|^5.3|^6.0", + "symfony/yaml": "^3.4|^4.4|^5.3|^6.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "required for mongodb components", + "amazonwebservices/aws-sdk-for-php": "required to use AWS version 1 cache resolver", + "aws/aws-sdk-php": "required to use AWS version 2/3 cache resolver", + "doctrine/mongodb-odm": "required to use mongodb-backed doctrine components", + "enqueue/enqueue-bundle": "^0.9 add if you like to process images in background", + "ext-exif": "required to read EXIF metadata from images", + "ext-gd": "required to use gd driver", + "ext-gmagick": "required to use gmagick driver", + "ext-imagick": "required to use imagick driver", + "ext-mongodb": "required for mongodb components", + "league/flysystem": "required to use FlySystem data loader or cache resolver", + "monolog/monolog": "A psr/log compatible logger is required to enable logging", + "symfony/messenger": "If you like to process images in background", + "symfony/templating": "required to use deprecated Templating component instead of Twig" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Liip\\ImagineBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Liip and other contributors", + "homepage": "https://github.com/liip/LiipImagineBundle/contributors" + } + ], + "description": "This bundle provides an image manipulation abstraction toolkit for Symfony-based projects.", + "homepage": "http://liip.ch", + "keywords": [ + "bundle", + "image", + "imagine", + "liip", + "manipulation", + "photos", + "pictures", + "symfony", + "transformation" + ], + "support": { + "issues": "https://github.com/liip/LiipImagineBundle/issues", + "source": "https://github.com/liip/LiipImagineBundle/tree/2.9.0" + }, + "time": "2022-10-06T06:33:35+00:00" + }, { "name": "monolog/monolog", "version": "3.2.0", @@ -5076,6 +5240,83 @@ ], "time": "2022-10-23T10:33:34+00:00" }, + { + "name": "symfony/lock", + "version": "v6.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/lock.git", + "reference": "98d6c4b6608d15e403a228c15afb4f3e71b22a57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/lock/zipball/98d6c4b6608d15e403a228c15afb4f3e71b22a57", + "reference": "98d6c4b6608d15e403a228c15afb4f3e71b22a57", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/dbal": "<2.13" + }, + "require-dev": { + "doctrine/dbal": "^2.13|^3.0", + "predis/predis": "~1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Lock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jérémy Derussé", + "email": "jeremy@derusse.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Creates and manages locks, a mechanism to provide exclusive access to a shared resource", + "homepage": "https://symfony.com", + "keywords": [ + "cas", + "flock", + "locking", + "mutex", + "redlock", + "semaphore" + ], + "support": { + "source": "https://github.com/symfony/lock/tree/v6.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-10-28T16:23:08+00:00" + }, { "name": "symfony/mailer", "version": "v6.1.7", @@ -6495,6 +6736,76 @@ ], "time": "2022-03-02T13:21:45+00:00" }, + { + "name": "symfony/rate-limiter", + "version": "v6.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/rate-limiter.git", + "reference": "9e75706446f7c3686773c408f422ffb5ec4ba32b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/9e75706446f7c3686773c408f422ffb5ec4ba32b", + "reference": "9e75706446f7c3686773c408f422ffb5ec4ba32b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/lock": "^5.4|^6.0", + "symfony/options-resolver": "^5.4|^6.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\RateLimiter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Wouter de Jong", + "email": "wouter@wouterj.nl" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a Token Bucket implementation to rate limit input and output in your application", + "homepage": "https://symfony.com", + "keywords": [ + "limiter", + "rate-limiter" + ], + "support": { + "source": "https://github.com/symfony/rate-limiter/tree/v6.1.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T13:46:29+00:00" + }, { "name": "symfony/routing", "version": "v6.1.7", diff --git a/config/bundles.php b/config/bundles.php index dd71193..08dbdfd 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -18,4 +18,5 @@ return [ Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true], + Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true], ]; diff --git a/config/packages/liip_imagine.yaml b/config/packages/liip_imagine.yaml new file mode 100644 index 0000000..79bd742 --- /dev/null +++ b/config/packages/liip_imagine.yaml @@ -0,0 +1,24 @@ +# Documentation on how to configure the bundle can be found at: https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html +liip_imagine: + # valid drivers options include "gd" or "gmagick" or "imagick" + driver: "gd" + + filter_sets: + squared_thumbnail_small: + filters: + thumbnail: + size: [100, 100] + mode: outbound + allow_upscale: true + squared_thumbnail_medium: + filters: + thumbnail: + size: [200, 200] + mode: outbound + allow_upscale: true + squared_thumbnail_large: + filters: + thumbnail: + size: [400, 400] + mode: outbound + allow_upscale: true \ No newline at end of file diff --git a/config/packages/lock.yaml b/config/packages/lock.yaml new file mode 100644 index 0000000..574879f --- /dev/null +++ b/config/packages/lock.yaml @@ -0,0 +1,2 @@ +framework: + lock: '%env(LOCK_DSN)%' diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b7ad3e4..da3b26e 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -28,6 +28,8 @@ security: path: security_logout switch_user: true + login_throttling: true + remember_me: secret: '%kernel.secret%' signature_properties: [password] diff --git a/config/routes/liip_imagine.yaml b/config/routes/liip_imagine.yaml new file mode 100644 index 0000000..201cbd5 --- /dev/null +++ b/config/routes/liip_imagine.yaml @@ -0,0 +1,2 @@ +_liip_imagine: + resource: "@LiipImagineBundle/Resources/config/routing.yaml" diff --git a/config/services.yaml b/config/services.yaml index 73b35dd..112efda 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -11,6 +11,9 @@ services: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + # bind + # $var: 'content' + # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: diff --git a/package.json b/package.json index 0e0ac3e..74f8f8f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@typescript-eslint/eslint-plugin-tslint": "^5.26.0", "axios": "^0.27.1", "bootstrap": "^5.2.2", - "bootswatch": "^5.2.2", + "dropzone": "^6.0.0-beta.2", "eslint": "^8.15.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.26.0", diff --git a/public/uploads/avatars/24_logo_bg_90x90.png b/public/uploads/avatars/24_logo_bg_90x90.png deleted file mode 100644 index b00fda4..0000000 Binary files a/public/uploads/avatars/24_logo_bg_90x90.png and /dev/null differ diff --git a/public/uploads/avatars/tracer_schmolle.png b/public/uploads/avatars/tracer_schmolle.png deleted file mode 100644 index a3147c8..0000000 Binary files a/public/uploads/avatars/tracer_schmolle.png and /dev/null differ diff --git a/src/Controller/Admin/ProjectsCrudController.php b/src/Controller/Admin/ProjectsCrudController.php index 6173a60..dfbae28 100644 --- a/src/Controller/Admin/ProjectsCrudController.php +++ b/src/Controller/Admin/ProjectsCrudController.php @@ -4,6 +4,8 @@ namespace App\Controller\Admin; use App\Entity\Projects; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; +use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; +use EasyCorp\Bundle\EasyAdminBundle\Field\Field; use EasyCorp\Bundle\EasyAdminBundle\Field\IdField; use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; @@ -20,11 +22,13 @@ class ProjectsCrudController extends AbstractCrudController yield IdField::new(propertyName: 'id') ->onlyOnIndex(); yield TextField::new(propertyName: 'name'); - yield TextField::new(propertyName: 'description'); + yield AssociationField::new('developer'); yield TextField::new(propertyName: 'description'); yield ImageField::new(propertyName: 'teaserImage') ->setBasePath(path: 'uploads/projects') ->setUploadDir(uploadDirPath: 'public/uploads/projects') ->setUploadedFileNamePattern(patternOrCallable: '[timestamp]-[slug].[extension]'); + yield Field::new('createdAt') + ->hideOnForm(); } } diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 448d76b..d9e0c0d 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -87,18 +87,7 @@ class SecurityController extends AbstractController } // no else, we already confirmed in the form itself $entityManager->persist(entity: $user); $entityManager->flush(); - - // generate a signed url and email it to the user - $this->sendEmailConfirmation(verifyEmailRouteName: 'security_verify_email', user: $user, - email: (new TemplatedEmail()) - ->from(new Address(address: 'info@24unix.net', name: '24unix.net')) - ->to($user->getEmail()) - ->subject(subject: 'Please Confirm your Email') - ->htmlTemplate(template: '@default/security/mail/registration.html.twig') - ->context(context: [ - 'username' => $user->getUsername() - ]) - ); + $this->generateSignedUrlAndEmailToTheUser($user); return $this->render(view: '@default/security/registration_finished.html.twig'); } @@ -127,7 +116,7 @@ class SecurityController extends AbstractController try { $this->handleEmailConfirmation(request: $request, user: $user); } catch (VerifyEmailExceptionInterface $exception) { - $this->addFlash(type: 'verify_email_error', message: $translator->trans(id: $exception->getReason(), parameters: [], domain: 'VerifyEmailBundle')); + $this->addFlash(type: 'error', message: $translator->trans(id: $exception->getReason(), parameters: [], domain: 'VerifyEmailBundle')); return $this->redirectToRoute(route: 'app_main'); } @@ -315,11 +304,48 @@ class SecurityController extends AbstractController */ public function handleEmailConfirmation(Request $request, User /*UserInterface*/ $user): void { - $this->verifyEmailHelper->validateEmailConfirmation(signedUrl: $request->getUri(), userId: $user->getId(), userEmail: $user->getEmail()); + $this->verifyEmailHelper->validateEmailConfirmation(signedUrl: $request->getUri(), userId: $user->getId(), userEmail: $user->getEmail()); + $user->setIsVerified(isVerified: true); + $this->entityManager->persist(entity: $user); + $this->entityManager->flush(); + } - $user->setIsVerified(isVerified: true); + /** + * @param mixed $user + * @return void + */ + public function generateSignedUrlAndEmailToTheUser(mixed $user): void + { + $this->sendEmailConfirmation(verifyEmailRouteName: 'security_verify_email', user: $user, + email: (new TemplatedEmail()) + ->from(new Address(address: 'info@24unix.net', name: '24unix.net')) + ->to($user->getEmail()) + ->subject(subject: 'Please Confirm your Email') + ->htmlTemplate(template: '@default/security/mail/registration.html.twig') + ->context(context: [ + 'username' => $user->getUsername() + ]) + ); + } - $this->entityManager->persist(entity: $user); - $this->entityManager->flush(); + + #[Route('/security/resend/verify_email', name: 'security_resend_verify_email')] + public function resendVerifyEmail(Request $request, UserRepository $userRepository) + { + + if ($request->isMethod('POST')) { + + $email = $request->getSession()->get('non_verified_email'); + $user = $userRepository->findOneBy(['email' => $email]); + if (!$user) { + throw $this->createNotFoundException('user not found for email'); + } + + $this->generateSignedUrlAndEmailToTheUser(user: $user); + $this->addFlash('success', 'eMail has been sent.'); + + return $this->redirectToRoute('app_main'); + } + return $this->render('@default/security/resend_activation.html.twig'); } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index afad2ef..9f20143 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -6,13 +6,18 @@ use App\Entity\User; use App\Form\EditProfileFormType; use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; +use Sunrise\Slugger\Slugger; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Validator\Constraints\File; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Validator\ValidatorInterface; /** * Class UserController. @@ -56,10 +61,6 @@ class UserController extends BaseController return $this->redirectToRoute(route: 'app_main'); }; - $user = $form->getData(); - // hash the plain password - - return $this->renderForm(view: '@default/user/edit_profile.html.twig', parameters: [ 'user' => $user, 'userForm' => $form @@ -93,4 +94,48 @@ class UserController extends BaseController 'users' => $users, ]); } + + // TODO move to a helper class + function humanFilesize($bytes, $decimals = 2) + { + $sz = 'BKMGTP'; + $factor = floor((strlen($bytes) - 1) / 3); + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; + } + + + #[Route(path: '/user/upload/avatar/{id}', name: 'user_upload_avatar')] + public function uploadAvatar( + Request $request, + UserRepository $userRepository, + EntityManagerInterface $entityManager, + ValidatorInterface $validator, + int $id) + { + $user = $userRepository->find($id); + + if (!$user) { + return $this->json('User not found.', 404); + } + + $postMaxSize = UploadedFile::getMaxFilesize(); + $contentLength = $request->headers->get('Content-length'); + + if ($contentLength > $postMaxSize) { + return $this->json('File is bigger than the allowed ' . $this->humanFilesize($postMaxSize) . ' Bytes.', 400); + } + + $uploadedAvatar = $request->files->get('file'); + $destination = $this->getParameter(name: 'kernel.project_dir') . '/public/uploads/avatars'; + $originalFilename = pathinfo($uploadedAvatar->getClientOriginalName(), PATHINFO_FILENAME); + $slugger = new Slugger(); + $cleanFilename = $slugger->slugify($originalFilename); + $newFilename = $cleanFilename . '-' . uniqid() . '.' . $uploadedAvatar->guessExtension(); + $uploadedAvatar->move($destination, $newFilename); + $user->setAvatar($newFilename); + $entityManager->persist(entity: $user); + $entityManager->flush(); + + return $this->json(data: 'OK', status: 201); + } } diff --git a/src/Entity/Projects.php b/src/Entity/Projects.php index b29bc24..7012762 100644 --- a/src/Entity/Projects.php +++ b/src/Entity/Projects.php @@ -2,15 +2,14 @@ namespace App\Entity; -use ApiPlatform\Core\Annotation\ApiResource; use App\Repository\ProjectsRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Stringable; #[ORM\Entity(repositoryClass: ProjectsRepository::class)] -#[ApiResource] -class Projects implements \Stringable +class Projects implements Stringable { #[ORM\Id] #[ORM\GeneratedValue] @@ -33,7 +32,7 @@ class Projects implements \Stringable private ?string $teaserImage = null; #[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'projects')] - private $developer; + private Collection $developer; public function __construct() { diff --git a/src/EventSubscriber/CheckVerifiedUserSubscriber.php b/src/EventSubscriber/CheckVerifiedUserSubscriber.php new file mode 100644 index 0000000..ec3cd38 --- /dev/null +++ b/src/EventSubscriber/CheckVerifiedUserSubscriber.php @@ -0,0 +1,61 @@ +getPassport(); + /* + * var User $user + */ + $user = $passport->getUser(); + + if (!$user->isVerified()) { + throw new UserNotVerifiedException(); + } + } + + + public function onValidationFailure(LoginFailureEvent $failureEvent) + { + if (!$failureEvent->getException() instanceof UserNotVerifiedException) { + return; + } + + $request = $failureEvent->getRequest(); + $email = $failureEvent->getPassport()->getUser()->getEmail(); + $request->getSession()->set('non_verified_email', $email); + + $response = new RedirectResponse( + $this->router->generate('security_resend_verify_email') + ); + $failureEvent->setResponse($response); + } + + + public static function getSubscribedEvents(): array + { + return [ + CheckPassportEvent::class => ['onCheckPassport', -10], + LoginFailureEvent::class => 'onValidationFailure' + ]; + } +} \ No newline at end of file diff --git a/src/Exception/UserNotVerifiedException.php b/src/Exception/UserNotVerifiedException.php index afc4460..bed0169 100644 --- a/src/Exception/UserNotVerifiedException.php +++ b/src/Exception/UserNotVerifiedException.php @@ -2,62 +2,10 @@ namespace App\Exception; -use JetBrains\PhpStorm\ArrayShape; + use Symfony\Component\Security\Core\Exception\AuthenticationException; -use function is_array; class UserNotVerifiedException extends AuthenticationException { - private ?string $identifier = null; - - /** - * {@inheritdoc} - */ - public function getMessageKey(): string - { - return 'User is not verified.'; - } - - /** - * Get the user identifier (e.g. username or email address). - */ - public function getUserIdentifier(): ?string - { - return $this->identifier; - } - - /** - * Set the user identifier (e.g. username or email address). - */ - public function setUserIdentifier(string $identifier): void - { - $this->identifier = $identifier; - } - - /** - * {@inheritdoc} - */ - #[ArrayShape(shape: ['{{ username }}' => "null|string", '{{ user_identifier }}' => "null|string"])] - public function getMessageData(): array - { - return ['{{ username }}' => $this->identifier, '{{ user_identifier }}' => $this->identifier]; - } - - /** - * {@inheritdoc} - */ - public function __serialize(): array - { - return [$this->identifier, parent::__serialize()]; - } - - /** - * {@inheritdoc} - */ - public function __unserialize(array $data): void - { - [$this->identifier, $parentData] = $data; - $parentData = is_array(value: $parentData) ? $parentData : unserialize(data: $parentData); - parent::__unserialize(data: $parentData); - } -} + // empty body +} \ No newline at end of file diff --git a/src/Form/EditProfileFormType.php b/src/Form/EditProfileFormType.php index 65d98c4..a60f68b 100644 --- a/src/Form/EditProfileFormType.php +++ b/src/Form/EditProfileFormType.php @@ -5,10 +5,12 @@ namespace App\Form; use App\Entity\User; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\Length; class EditProfileFormType extends AbstractType @@ -27,7 +29,7 @@ class EditProfileFormType extends AbstractType 'options' => ['attr' => ['class' => 'password-field', 'autocomplete' => 'off']], 'required' => false, 'first_options' => ['label' => 'Password'], - 'second_options' => ['label' => 'Repeat Password (only needed if you want to update the password'], + 'second_options' => ['label' => 'Repeat Password (only needed if you want to update the password)'], 'constraints' => [new Length(exactly: ['min' => 6])] ]) ; diff --git a/src/Security/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php index 6f72500..8a47316 100644 --- a/src/Security/LoginFormAuthenticator.php +++ b/src/Security/LoginFormAuthenticator.php @@ -9,6 +9,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; @@ -51,10 +52,6 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator throw new UserNotFoundException(); } - if (!$user->isVerified()) { - throw new UserNotVerifiedException(); - } - return $user; }), diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php new file mode 100644 index 0000000..4995c9e --- /dev/null +++ b/src/Twig/AppExtension.php @@ -0,0 +1,27 @@ +