bumped symfony to 5.3

This commit is contained in:
tracer 2021-06-01 18:48:20 +02:00
parent c39ac0d299
commit c18d5dc339
58 changed files with 7128 additions and 455 deletions

4
.env
View File

@ -26,3 +26,7 @@ APP_SECRET=cd0ae68f915f2a06b82007f2906e54e8
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
DATABASE_URL="mysql://24unix:24.unix@127.0.0.1:3306/24unix"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
MAILER_DSN=smtp://localhost
###< symfony/mailer ###

7
.gitignore vendored
View File

@ -8,3 +8,10 @@
/var/
/vendor/
###< symfony/framework-bundle ###
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

12
assets/app.js Normal file
View File

@ -0,0 +1,12 @@
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';
// start the Stimulus application
import './bootstrap';

11
assets/bootstrap.js vendored Normal file
View File

@ -0,0 +1,11 @@
import { startStimulusApp } from '@symfony/stimulus-bridge';
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.(j|t)sx?$/
));
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

4
assets/controllers.json Normal file
View File

@ -0,0 +1,4 @@
{
"controllers": [],
"entrypoints": []
}

View File

@ -0,0 +1,16 @@
import { Controller } from 'stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 153 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.0" width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
<g transform="matrix(0.1, 0, 0, -0.1, -6.266793, 1030.994141)" fill="#000000" stroke="none">
<path d="M 1568.308 10309.078 C 564.548 10309.078 62.668 9803.508 62.668 8792.368 C 62.668 8339.535 62.668 1727.029 62.668 1714.388 C 62.668 703.248 564.548 197.678 1568.308 197.678 C 1591.463 197.678 7590.868 197.678 8594.628 197.678 C 9598.388 197.678 10100.268 703.248 10100.268 1714.388 C 10100.268 2725.528 10100.268 6486.982 10100.268 8792.368 C 10100.268 9803.508 9598.388 10309.078 8594.628 10309.078 C 5583.348 10309.078 1568.308 10309.078 1568.308 10309.078 Z M 3672.54 7275.658 C 4244.339 8047.825 5331.794 8472.407 6085.228 8392.819 C 7558.724 8237.167 7711.517 7222.701 7669.179 6891 C 7601.342 6359.52 7261.927 6120.025 7215.766 6046.83 C 7054.424 5790.996 7112.956 5590 7129.487 5573 C 7140.184 5562 7284.674 5550.452 7381.343 5616 C 7983.187 6016.375 8092.748 6264.518 8434.473 6410 C 9054.145 6673.811 9255.358 5797.308 9262.001 5720 C 9313.839 5116.734 9046.297 5272.999 8804.116 5495.788 C 8923.874 4704.112 8630.922 4160.925 8514.503 4156.928 C 8067.033 4141.565 7890.303 4557.157 7878.388 4495.295 C 7651.349 3316.524 6684.687 4203.602 6680.894 4195 C 6408.836 3577.95 6794.683 3431.998 6849.431 3394 C 7072.287 3239.325 7829.534 3522.262 7901.588 3527 C 8353.345 3556.705 8404.386 3093.747 8337.202 2842.622 C 8300.167 2704.19 8116.008 3110.391 8098.694 3056.379 C 7905.831 2454.74 7286.79 1799.512 5252.72 1970 C 2634.673 2189.435 3169.554 3982.907 3145.643 3965.989 C 2508.377 3515.099 2335 4324.288 2233.361 4750 C 2228.393 4770.808 2005.604 4213.617 1673.29 4210.931 C 1340.976 4208.245 1180.756 5173.068 1418.474 5904 C 1278.78 5813.12 1110.84 5657.594 908.927 5784 C 768.062 5872.188 787.568 6952.627 1704.288 7206 C 1828.193 7240.246 2045.681 7168.896 2123.478 7041 C 2259.345 6817.638 2477.148 6490.068 2526.059 6431 C 2665.475 6262.634 3023.704 6040.791 3220.366 6417 C 3444.851 6846.434 3309.293 6770.088 3672.54 7275.658 Z" style="stroke: rgb(218, 85, 85); fill: #ff8040;"/>
<path d="M 4218 7190 C 3843.862 7161.67 3684.337 6798.411 3725.003 6362.026 C 3760.674 5979.24 4109.523 6050.046 4179.282 6064.14 C 4462.744 6121.409 4624.226 6383.444 4672.836 6857.022 C 4686.564 6990.762 4563.698 7216.176 4218 7190 Z" style="stroke: rgb(218, 85, 85); fill: #ff8040;"/>
<path d="M 5340 6904 C 5231.393 6881.963 5107.694 6745.696 4939 6400 C 4867.75 6253.991 4906.668 6046.567 4987 5945 C 5126.502 5768.621 5353.992 5737.6 5564.869 5909.852 C 5636.594 5968.439 5788.19 6124.729 5832.272 6408.204 C 5894.908 6810.991 5662.601 6969.457 5340 6904 Z" style="stroke: rgb(218, 85, 85); fill: #ff8040;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
assets/images/asteroid.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
assets/images/bg.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

12
assets/js/app.js Normal file
View File

@ -0,0 +1,12 @@
// assets/js/app.js
import '../styles/app.scss';
// import $ from 'jquery';
// global.$ = $;
import 'bootstrap';
require('@fortawesome/fontawesome-free/css/all.min.css');
require('@fortawesome/fontawesome-free/js/all.js');

3
assets/styles/app.css Normal file
View File

@ -0,0 +1,3 @@
body {
background-color: lightgray;
}

185
assets/styles/app.scss Normal file
View File

@ -0,0 +1,185 @@
/*
jet black (tiefschwarz) - RAL 9005: #0e0e10, rgba(14, 14, 16, 1.0);
mango: #ff8040, rgba(255, 130, 67, 1.0);
gray: #a1a1a1, rgba(161, 161, 161, 1.0)
*/
/*
@media (prefers-color-scheme: light) {}
@media (prefers-color-scheme: dark) {}
*/
/* debug */
* {
border: 0 solid gray;
}
// customize some Bootstrap variables
$primary: #FF8040;
$body-bg: #0E0E10;
$body-color: darken(white, 20);
// the ~ allows you to reference things in node_modules
@import "~bootstrap/scss/bootstrap";
body {
padding-top: 95px;
padding-bottom: 155px;
}
.row.content {
height: 350px;
}
.col-center {
margin: 0 auto;
}
.sidenav-left {
padding-top: 20px;
background-color: #f1f1f1;
height: 100%;
margin-left: 3em;
}
.sidenav-right {
padding-top: 20px;
background-color: #f1f1f1;
height: 100%;
}
@media screen and (max-width: 767px) {
.sidenav {
height: auto;
padding: 15px;
}
.row.content {height:auto;}
}
.navbar-top {
border-bottom-width: 1px;
border-bottom-color: #FF8040;
padding-top: 0;
padding-bottom: 0;
opacity: 0.9;
}
.navbar-bottom {
border-top-width: 1px;
border-top-color: #FF8040;
padding-top: 0;
padding-bottom: 0;
height: 125px;
opacity: 0.9;
}
.navbar-nav > li > .dropdown-menu {
background-color: #A1A1A1;
}
@include media-breakpoint-down(sm) {
.dropdown-toggle:after {
content: none;
}
#dropdown-menu {
display: block;
}
}
@include media-breakpoint-up(md) {
.navbar {
padding-left: 100px;
padding-right: 100px;
}
}
.button-login {
margin-top: 25px;
}
.box {
border-width: 1px;
border-style: solid;
border-color: #ff8040;
border-radius: 10px;
padding: 15px;
background-image: url('../images/bg.jpeg');
background-position: center;
align-content: center;
}
/* BlogPosts */
.main-article {
border: 2px solid #efefee;
Background: #fff;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.main-article img {
width: 100%;
height: 250px;
border-top-right-radius: 5px;
border-top-left-radius: 5px;
border-top: 5px solid lightblue;
}
.blog-container {
border: 1px solid #FF8040;
border-radius: 5px;
background: #0E0E10;
margin-bottom: 25px;
background-image: url('../images/bg.jpeg');
background-position: center;
}
.main-article-link, .article-container a {
text-decoration: none;
color: #000;
}
.main-article-link:hover {
text-decoration: none;
color: #000;
}
.article-title {
min-width: 300px;
}
@media (max-width: 440px) {
.article-title {
min-width: 100px;
max-width: 245px;
}
}
.blog-img {
height: 100px;
width: 100px;
border-radius: 5px;
margin: 7px;
}
.article-author-img {
height: 25px;
border: 1px solid darkgray;
margin-left: 25px;
}
.blog-details {
font-size: .8em;
margin-right: 15px;
}
.blog-teaser {
margin-left: 15px;
}

View File

@ -13,22 +13,27 @@
"doctrine/doctrine-migrations-bundle": "^3.1",
"doctrine/orm": "^2.9",
"easycorp/easyadmin-bundle": "^3",
"symfony/console": "5.2.*",
"symfony/dotenv": "5.2.*",
"knplabs/knp-time-bundle": "^1.16",
"symfony/console": "5.3.*",
"symfony/dotenv": "5.3.*",
"symfony/flex": "^1.3.1",
"symfony/framework-bundle": "5.2.*",
"symfony/proxy-manager-bridge": "5.2.*",
"symfony/security-bundle": "5.2.*",
"symfony/framework-bundle": "5.3.*",
"symfony/mailer": "5.3.*",
"symfony/proxy-manager-bridge": "5.3.*",
"symfony/security-bundle": "5.3.*",
"symfony/twig-bundle": "^5.2",
"symfony/validator": "5.2.*",
"symfony/yaml": "5.2.*",
"symfony/validator": "5.3.*",
"symfony/webpack-encore-bundle": "^1.11",
"symfony/yaml": "5.3.*",
"symfonycasts/verify-email-bundle": "^1.5",
"twig/extra-bundle": "^2.12|^3.0",
"twig/intl-extra": "^3.3",
"twig/twig": "^2.12|^3.0"
},
"require-dev": {
"symfony/maker-bundle": "^1.31",
"symfony/stopwatch": "^5.2",
"symfony/web-profiler-bundle": "^5.2"
"symfony/stopwatch": "^5.3",
"symfony/web-profiler-bundle": "^5.3"
},
"config": {
"optimize-autoloader": true,
@ -70,7 +75,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "5.2.*"
"require": "5.3.*"
}
}
}

1359
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,4 +10,7 @@ return [
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
];

View File

@ -0,0 +1,3 @@
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

View File

@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'

View File

@ -0,0 +1,4 @@
#webpack_encore:
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Available in version 1.2
#cache: true

View File

@ -0,0 +1,2 @@
#webpack_encore:
# strict_mode: false

View File

@ -0,0 +1,30 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# link_attributes:
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# pass "frontend" as the 3rg arg to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
# frontend: '%kernel.project_dir%/public/frontend/build'
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Put in config/packages/prod/webpack_encore.yaml
# cache: true

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210530183330 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user ADD is_verified TINYINT(1) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user DROP is_verified');
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210601114523 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE blog ADD slug VARCHAR(255) NOT NULL, CHANGE title title VARCHAR(255) DEFAULT NULL, CHANGE content content LONGTEXT DEFAULT NULL, CHANGE created_at created_at DATETIME DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE blog DROP slug, CHANGE title title VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE content content LONGTEXT CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE created_at created_at DATETIME NOT NULL');
}
}

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"devDependencies": {
"@symfony/stimulus-bridge": "^2.0.0",
"@symfony/webpack-encore": "^1.0.0",
"core-js": "^3.0.0",
"file-loader": "^6.0.0",
"regenerator-runtime": "^0.13.2",
"sass": "^1.34.0",
"sass-loader": "^11.0.0",
"stimulus": "^2.0.0",
"webpack-notifier": "^1.6.0"
},
"license": "UNLICENSED",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"@popperjs/core": "^2.9.2",
"bootstrap": "^5.0.1",
"copy-webpack-plugin": "^9.0.0"
}
}

View File

@ -22,6 +22,7 @@ class BlogCrudController extends AbstractCrudController
AssociationField::new('author')
->autocomplete(),
TextField::new('title'),
TextField::new('slug'),
TextEditorField::new('teaser'),
TextEditorField::new('content'),
DateTimeField::new('createdAt'),

View File

@ -29,11 +29,11 @@ class BlogController extends AbstractController
*
* @return \Symfony\Component\HttpFoundation\Response
*/
#[Route('/blog/{id}', name: 'blog')]
public function show($id, BlogRepository $blogRepository): Response
#[Route('/blog/{slug}', name: 'blog')]
public function show($slug, BlogRepository $blogRepository): Response
{
return $this->render('blog/show.html.twig', [
'blog' => $blogRepository->findOneBy(['id' => $id])
'blog' => $blogRepository->findOneBy(['slug' => $slug])
]);
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\EmailVerifier;
use App\Repository\UserRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
class RegistrationController extends AbstractController
{
private $emailVerifier;
public function __construct(EmailVerifier $emailVerifier)
{
$this->emailVerifier = $emailVerifier;
}
#[Route('/register', name: 'app_register')]
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from(new Address('tracer@24unix.net', '24unix'))
->to($user->getEmail())
->subject('Please Confirm your Email')
->htmlTemplate('registration/confirmation_email.html.twig')
);
// do anything else you need here, like send an email
return $this->redirectToRoute('blogs');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
#[Route('/verify/email', name: 'app_verify_email')]
public function verifyUserEmail(Request $request, UserRepository $userRepository): Response
{
$id = $request->get('id');
if (null === $id) {
return $this->redirectToRoute('app_register');
}
$user = $userRepository->find($id);
if (null === $user) {
return $this->redirectToRoute('app_register');
}
// validate email confirmation link, sets User::isVerified=true and persists
try {
$this->emailVerifier->handleEmailConfirmation($request, $user);
} catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('verify_email_error', $exception->getReason());
return $this->redirectToRoute('app_register');
}
// @TODO Change the redirect on success and handle or remove the flash message in your templates
$this->addFlash('success', 'Your email address has been verified.');
return $this->redirectToRoute('app_register');
}
}

View File

@ -6,9 +6,12 @@ use App\Repository\BlogRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JetBrains\PhpStorm\Pure;
use App\Repository\SectionRepository;
/**
* @ORM\Entity(repositoryClass=BlogRepository::class)
* @ORM\HasLifecycleCallbacks()
*/
class Blog
{
@ -22,28 +25,28 @@ class Blog
/**
* @ORM\Column(type="string", length=255)
*/
private $title;
private ?string $title;
/**
* @ORM\Column(type="text", nullable=true)
*/
private $teaser;
private ?string $teaser;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $teaserImage;
private ?string $teaserImage;
/**
* @ORM\Column(type="text")
*/
private $content;
private ?string $content;
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="blogs")
* @ORM\JoinColumn(nullable=false)
*/
private $author;
private ?User $author;
/**
* @ORM\ManyToMany(targetEntity=Section::class, inversedBy="blogs")
@ -53,17 +56,17 @@ class Blog
/**
* @ORM\Column(type="datetime")
*/
private $createdAt;
private ?\DateTimeInterface $createdAt;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $editedAt;
private ?\DateTimeInterface $editedAt;
/**
* @ORM\ManyToOne(targetEntity=User::class)
*/
private $editedBy;
private ?User $editedBy;
/**
* @ORM\Column(type="string", length=255, nullable=true)
@ -75,16 +78,25 @@ class Blog
*/
private $comments;
/**
* @ORM\Column(type="string", length=255)
*/
private $slug;
#[Pure]
public function __construct()
{
$this->section = new ArrayCollection();
$this->comments = new ArrayCollection();
}
public function __toString()
{
return $this->title;
}
/**
* @return null|string
*/
public function __toString()
{
return $this->title;
}
public function getId(): ?int
@ -253,4 +265,16 @@ class Blog
return $this;
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
}

View File

@ -7,10 +7,12 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JetBrains\PhpStorm\Pure;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
* @UniqueEntity(fields={"username"}, message="There is already an account with this username")
*/
class User implements UserInterface
{
@ -71,22 +73,27 @@ class User implements UserInterface
* @ORM\OneToMany(targetEntity=Comment::class, mappedBy="author")
*/
private $comments;
/**
* @ORM\Column(type="boolean")
*/
private $isVerified = false;
#[Pure] public function __construct()
{
$this->blogs = new ArrayCollection();
$this->comments = new ArrayCollection();
}
{
$this->blogs = new ArrayCollection();
$this->comments = new ArrayCollection();
}
public function __toString()
{
return $this->username;
}
{
return $this->username;
}
public function getId(): ?int
{
return $this->id;
}
{
return $this->id;
}
/**
* A visual identifier that represents this user.
@ -94,50 +101,50 @@ class User implements UserInterface
* @see UserInterface
*/
public function getUsername(): string
{
return (string)$this->username;
}
{
return (string)$this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
{
$this->username = $username;
return $this;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return $this->password;
}
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
{
$this->password = $password;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
@ -146,136 +153,148 @@ class User implements UserInterface
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
{
return null;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
{
return $this->firstName;
}
public function setFirstName(?string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
{
return $this->lastName;
}
public function setLastName(?string $lastName): self
{
$this->lastName = $lastName;
return $this;
}
{
$this->lastName = $lastName;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
{
$this->email = $email;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
{
$this->createdAt = $createdAt;
return $this;
}
public function getLastLoginAt(): ?\DateTimeInterface
{
return $this->lastLoginAt;
}
{
return $this->lastLoginAt;
}
public function setLastLoginAt(?\DateTimeInterface $lastLoginAt): self
{
$this->lastLoginAt = $lastLoginAt;
return $this;
}
{
$this->lastLoginAt = $lastLoginAt;
return $this;
}
/**
* @return Collection|Blog[]
*/
public function getBlogs(): Collection
{
return $this->blogs;
}
{
return $this->blogs;
}
public function addBlog(Blog $blog): self
{
if (!$this->blogs->contains($blog)) {
$this->blogs[] = $blog;
$blog->setAuthor($this);
}
return $this;
}
{
if (!$this->blogs->contains($blog)) {
$this->blogs[] = $blog;
$blog->setAuthor($this);
}
return $this;
}
public function removeBlog(Blog $blog): self
{
if ($this->blogs->removeElement($blog)) {
// set the owning side to null (unless already changed)
if ($blog->getAuthor() === $this) {
$blog->setAuthor(null);
}
}
return $this;
}
{
if ($this->blogs->removeElement($blog)) {
// set the owning side to null (unless already changed)
if ($blog->getAuthor() === $this) {
$blog->setAuthor(null);
}
}
return $this;
}
/**
* @return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setAuthor($this);
}
return $this;
}
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setAuthor($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getAuthor() === $this) {
$comment->setAuthor(null);
}
}
return $this;
}
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getAuthor() === $this) {
$comment->setAuthor(null);
}
}
return $this;
}
public function isVerified(): bool
{
return $this->isVerified;
}
public function setIsVerified(bool $isVerified): self
{
$this->isVerified = $isVerified;
return $this;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username')
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
])
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'attr' => ['autocomplete' => 'new-password'],
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Security;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
/**
* Class EmailVerifier
* @package App\Security
*/
class EmailVerifier
{
private VerifyEmailHelperInterface $verifyEmailHelper;
private MailerInterface $mailer;
private EntityManagerInterface $entityManager;
public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager)
{
$this->verifyEmailHelper = $helper;
$this->mailer = $mailer;
$this->entityManager = $manager;
}
public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
{
$signatureComponents = $this->verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
$user->getId(),
$user->getEmail(),
['id' => $user->getId()]
);
$context = $email->getContext();
$context['signedUrl'] = $signatureComponents->getSignedUrl();
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
$email->context($context);
try {
$this->mailer->send($email);
} catch (TransportExceptionInterface $e) {
die("Error: " . $e->getMessage());
}
}
/**
* @throws VerifyEmailExceptionInterface
*/
public function handleEmailConfirmation(Request $request, UserInterface $user): void
{
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getEmail());
$user->setIsVerified(true);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}

View File

@ -91,9 +91,15 @@
"ref": "b131e6cbfe1b898a508987851963fff485986285"
}
},
"egulias/email-validator": {
"version": "3.1.1"
},
"friendsofphp/proxy-manager-lts": {
"version": "v1.0.5"
},
"knplabs/knp-time-bundle": {
"version": "v1.16.0"
},
"laminas/laminas-code": {
"version": "4.3.0"
},
@ -216,6 +222,18 @@
"symfony/intl": {
"version": "v5.2.7"
},
"symfony/mailer": {
"version": "4.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "4.3",
"ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2"
},
"files": [
"config/packages/mailer.yaml"
]
},
"symfony/maker-bundle": {
"version": "1.0",
"recipe": {
@ -225,18 +243,27 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/mime": {
"version": "v5.2.9"
},
"symfony/options-resolver": {
"version": "v5.2.4"
},
"symfony/orm-pack": {
"version": "v2.1.0"
},
"symfony/password-hasher": {
"version": "v5.3.0"
},
"symfony/polyfill-intl-grapheme": {
"version": "v1.23.0"
},
"symfony/polyfill-intl-icu": {
"version": "v1.23.0"
},
"symfony/polyfill-intl-idn": {
"version": "v1.23.0"
},
"symfony/polyfill-intl-normalizer": {
"version": "v1.23.0"
},
@ -249,6 +276,9 @@
"symfony/polyfill-php80": {
"version": "v1.23.0"
},
"symfony/polyfill-php81": {
"version": "v1.23.0"
},
"symfony/polyfill-uuid": {
"version": "v1.23.0"
},
@ -383,12 +413,40 @@
"config/routes/dev/web_profiler.yaml"
]
},
"symfony/webpack-encore-bundle": {
"version": "1.9",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "1.9",
"ref": "9399a0bfc6ee7a0c019f952bca46d6a6045dd451"
},
"files": [
"assets/app.js",
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/hello_controller.js",
"assets/styles/app.css",
"config/packages/assets.yaml",
"config/packages/prod/webpack_encore.yaml",
"config/packages/test/webpack_encore.yaml",
"config/packages/webpack_encore.yaml",
"package.json",
"webpack.config.js"
]
},
"symfony/yaml": {
"version": "v5.2.9"
},
"symfonycasts/verify-email-bundle": {
"version": "v1.5.0"
},
"twig/extra-bundle": {
"version": "v3.3.1"
},
"twig/intl-extra": {
"version": "v3.3.0"
},
"twig/twig": {
"version": "v3.3.2"
}

View File

@ -0,0 +1,10 @@
<footer class="footer">
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-bottom navbar-bottom">
<div id="legal">
</div>
<div class="powered">
powered by <a href="#"><img src="{{ asset('build/images/Spookie/spookie_64x64.png') }}" alt="Spookie"></a>
</div>
</nav>
</footer>

View File

@ -0,0 +1,53 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top navbar-top">
<a class="navbar-brand" href="{{ path('blogs') }}">
<img src="{{ asset('build/images/24unix/24_logo_bg_64x64.png') }}" alt="24unix.net" id="site-logo">
</a>
<button class="navbar-toggler border-0" type="button" data-toggle="collapse" data-target="#CollapsingNavbar">
&#9776;
</button>
<div class="collapse navbar-collapse" id="CollapsingNavbar">
<ul class="navbar-nav ml-auto">
{% if is_granted('ROLE_USER') %}
<li class="nav-item dropdown my-2 my-lg-0">
<button type="button" id="navbar-dropdown" data-toggle="dropdown"
class="btn btn-primary dropdown-toggle ml-auto button-login">
{{ app.user.username }}
</button>
<div class="dropdown-menu dropdown-menu-right" id="dropdown-menu" aria-labelledby="navbar-dropdown">
<a class="dropdown-item" href="{{ path('blogs') }}">
<span class="fas fa-user" aria-hidden="true"></span>
Profile</a>
<a class="dropdown-item" href="#">
<span class="fas fa-cog" aria-hidden="true"></span>
Settings</a>
<div class="dropdown-divider"></div>
{% if is_granted('ROLE_ADMIN') %}
<a class="dropdown-item" href="{{ path('admin') }}">
<span class="fas fa-cog" aria-hidden="true"></span>
Administration
</a>
<div class="dropdown-divider"></div>
{% endif %}
<a class="dropdown-item" href="{{ path('app_logout') }}">
<span class="fas fa-sign-out-alt" aria-hidden="true"></span>
Logout
</a>
</div>
</li>
{% else %}
<li class="nav-item">
<a class="btn btn-primary button-login" href="{{ path('app_login') }}" role="button" id="buttonLogin">
Login
</a>
</li>
{% endif %}
</ul>
</div>
</nav>

View File

@ -1,19 +1,42 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{# Run `composer require symfony/webpack-encore-bundle`
and uncomment the following Encore helpers to start using Symfony UX #}
<title>{% block title %}Spookie{% endblock %}</title>
{% block stylesheets %}
{#{{ encore_entry_link_tags('app') }}#}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{#{{ encore_entry_script_tags('app') }}#}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% include '_header.html.twig' %}
<div class="container-fluid text-center">
<div class="row content d-flex justify-content-sm-center">
<div class="col-sm-2 sidenav-left box" id="main-menu">
<p>
<a href="{{ path('blogs') }}">Blogs</a><br>
<a href="//git.24unix.net">git.24unix.net</a>
<a href="//pastebin.24unix.net">pastebin.24unix.net</a>
</p>
</div>
<div class="col-sm-9 text-left center-block" id="main-content">
{% block body %} {% endblock %}
</div>
<div class="col-sm-1 sidenav" id="spacer">
</div>
</div>
</div>
{% include '_footer.html.twig' %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>

View File

@ -1,12 +1,38 @@
{# templates/blog/blog_show.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Hello BlogController!{% endblock %}
{% block title %} Blogpost {% endblock %}
{% block body %}
{% for blog in blogs %}
<h4>{{ blog }}</h4>
<p>{{ blog.teaser }}</p>
<div class="container">
<div class="row">
<!-- blog List -->
<div class="col-sm-12">
{% for blogpost in blogs %}
<div class="blog-container my-4">
<a href="{{ path('blog', { slug: blogpost.slug }) }} ">
{% if blogpost.teaserImage %}
<img class="blog-img" src="{{ asset('build/images/asteroid.jpeg') }}" alt="asteroid">
{% endif %}
<div class="article-title d-inline-block pl-3 align-middle">
<span>{{ blogpost.title }}</span>
</div>
</a>
<br>
<span class="align-left blog-details">
<img class="article-author-img rounded-circle"
src="{{ asset('build/images/alien-profile.png') }}" alt="profile">
{{ blogpost.author }}
</span>
<span class="pl-5 blog-details float-right">{{ blogpost.createdAt|ago }}</span>
<br>
<span class="blog-teaser">
{{ blogpost.teaser }}
</span>
</div>
{% endfor %}
<a href="{{ path('blog', { id: blog.id }) }}">View more …</a>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
<h1>Hi! Please confirm your email!</h1>
<p>
Please confirm your email address by clicking the following link: <br><br>
<a href="{{ signedUrl }}">Confirm my Email</a>.
This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}.
</p>
<p>
Cheers!
</p>

View File

@ -0,0 +1,21 @@
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block body %}
{% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% endfor %}
<h1>Register</h1>
{{ form_start(registrationForm) }}
{{ form_row(registrationForm.username) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
{{ form_row(registrationForm.agreeTerms) }}
<button type="submit" class="btn">Register</button>
{{ form_end(registrationForm) }}
{% endblock %}

84
webpack.config.js Normal file
View File

@ -0,0 +1,84 @@
const Encore = require("@symfony/webpack-encore");
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || "dev");
}
const CopyWebpackPlugin = require("copy-webpack-plugin");
Encore
// directory where compiled assets will be stored
.setOutputPath("public/build/")
.copyFiles({
from: "./assets/images",
to: "images/[path][name].[ext]"
})
// public path used by the web server to access the output path
.setPublicPath("/build")
// only needed for CDN's or sub-directory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Add 1 entry for each "page" of your app
* (including one that's included on every page - e.g. "app")
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.scss) if your JavaScript imports CSS.
*/
.addEntry("app", "./assets/js/app.js")
//.addEntry('page1', './assets/js/page1.js')
//.addEntry('page2', './assets/js/page2.js')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
//.enableSingleRuntimeChunk()
.disableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// enables @babel/preset-env polyfills
.configureBabel(() => {
}, {
useBuiltIns: "usage",
corejs: 3
})
// enables Sass/SCSS support
.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
// uncomment if you use API Platform Admin (composer req api-admin)
//.enableReactPreset()
//.addEntry('admin', './assets/js/admin.js')
;
module.exports = Encore.getWebpackConfig();

4910
yarn.lock Normal file

File diff suppressed because it is too large Load Diff