Compare commits

..

4 commits

Author SHA1 Message Date
6b98dd6055 Add quotes around curl argument 2021-06-19 12:43:34 +02:00
929991dcca Change translation key 2021-06-19 12:42:40 +02:00
01db6b0f60 Use API to pull from loco 2021-06-19 12:33:11 +02:00
460005c0b0 Start using translation 2021-06-17 17:25:35 +02:00
82 changed files with 15477 additions and 13380 deletions

13
.env
View file

@ -23,17 +23,16 @@ APP_SECRET=8a390490e448f181dd8d3e6bd38efe6a
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
# #
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
DATABASE_URL="mysql://pflaenzli:develop@127.0.0.1:3306/pflaenzli?serverVersion=mariadb-10.5.13" # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
# DATABASE_URL="postgresql://postgres:develop@localhost:5432/pflaenzli?serverVersion=13&charset=utf8" DATABASE_URL="postgresql://postgres:develop@localhost:5432/plantex?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> symfony/mailer ### ###> symfony/mailer ###
# MAILER_DSN=smtp://localhost MAILER_DSN=smtp://localhost
###< symfony/mailer ### ###< symfony/mailer ###
DEFAULT_URI='http://localhost:8080/' DEFAULT_URI='http://localhost:8080/'
###> Ffiendlycaptcha ### ###> symfony/loco-translation-provider ###
# CAPTCHA_SECRET= LOCO_DSN=loco://RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro@default
# CAPTCHA_SITEKEY= ###< symfony/loco-translation-provider ###
###< Ffiendlycaptcha ###

38
.env.staging Normal file
View file

@ -0,0 +1,38 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=8a390490e448f181dd8d3e6bd38efe6a
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
DATABASE_URL="postgresql://postgres:develop@localhost:5432/plantex?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
MAILER_DSN=smtp://no-reply@xn--pflnz-ira.li:dxS5ooKMEzFEa3YgTvru@mail.infomaniak.com:587
###< symfony/mailer ###
DEFAULT_URI="https://staging.this-server.pflaenz.li/"
###> symfony/loco-translation-provider ###
LOCO_DSN=loco://RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro@default
###< symfony/loco-translation-provider ###

5
.gitignore vendored
View file

@ -1,4 +1,4 @@
/public/uploads/photos/* /public/uploads/*
.DS_Store .DS_Store
/pgadmin_data /pgadmin_data
@ -244,5 +244,4 @@ temp/
# End of https://www.toptal.com/developers/gitignore/api/node,phpunit,symfony,composer,yarn # End of https://www.toptal.com/developers/gitignore/api/node,phpunit,symfony,composer,yarn
# Snyk translations
.dccache

View file

@ -3,9 +3,9 @@
## Prerequisites ## Prerequisites
A good start is to follow this [intro](https://symfony.com/doc/current/the-fast-track/en/1-tools.html). A good start is to follow this [intro](https://symfony.com/doc/current/the-fast-track/en/1-tools.html).
In addition, you will need `yarn` and `npm`, as well as [`docker compose`](https://docs.docker.com/compose/install/). In addition, you will need `yarn` and `npm`, as well as [`docker-compose`](https://docs.docker.com/compose/install/).
For that, install [`node.js`](https://nodejs.org/) by downloading directly or using your package manager (if you're using Linux, you probably knpw how to install). For that, install [`node.js`](https://nodejs.org/) by downloading directly or using your package manager.
### MacOS ### MacOS
Using [homebrew](https://homebrew.sh): Using [homebrew](https://homebrew.sh):
@ -22,27 +22,28 @@ choco install nodejs npm yarn
## Setup the project ## Setup the project
### 1. Clone the repo ### 1. Clone the repo
Using SSH ([set up your ssh-keys](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key) first or use https) Using SSH
``` ```
git clone ssh://git@git.thisfro.ch:222/thisfro/pflaenz.li.git git clone ssh://git@git.thisfro.ch:222/thisfro/plant-exchange.git
``` ```
### 2. Go to the directory ### 2. Go to the directory
``` ```
cd pflaenz.li cd plant-exchange
``` ```
### 3. Setup Database and other stuff ### 3. Setup Database and other stuff
``` ```
docker compose up -d docker-compose up -d
``` ```
To create the database To create the database (or use pgadmin):
``` ```
bin/console doctrine:schema:create docker exec -it plant-exchange_db_1 psql -U postgres -c 'CREATE DATABASE plantex;'
``` ```
and migrate and migrate
``` ```
bin/console doctrine:migrations:migrate -n symfony console doctrine:migrations:migrate -n
``` ```
### 4. Install dependencies ### 4. Install dependencies
@ -61,22 +62,20 @@ symfony serve --port 8080 --no-tls -d
You sholud be able to access the site under [localhost:8080](http://localhost:8080) You sholud be able to access the site under [localhost:8080](http://localhost:8080)
You'll also need to build the webpack files (or use watch files):
```
yarn build
```
### 6. Create a user ### 6. Create a user
Register your own account and set the `role` in the databse to `["ROLE_ADMIN"]` Go to the database and add a user manually. For the password use this to generate the appropriate hash:
```
symfony console security:encode-password
```
### 7. Watch files ### 7. Watch files
If you are editing `.scss` or `.js`, you'll want to run If you are editing `.scss` or other webpack files, you'll want to run
``` ```
yarn watch yarn watch
``` ```
this will automatically rebuild webpack files on save.
--- ---
Thats it for now, you can start developing! :tada: Thats it for now, you can start developing!
If you have any questions, ask [thisfro](https://git.thisfro.ch/thisfro) or create an issue! If you have any questions, ask [thisfro](https://git.thisfro.ch/thisfro) or creat an issue!

35
Dockerfile Normal file
View file

@ -0,0 +1,35 @@
FROM php:8.0.6-fpm-buster
# Install prerequisites
RUN apt-get update && apt-get -y install wget git npm curl zip
RUN apt-get install -y libpq-dev \
&& docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \
&& docker-php-ext-install pdo pdo_pgsql pgsql
# Download composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');"
# Install composer and yarn
RUN npm install --global yarn
RUN wget https://get.symfony.com/cli/installer -O - | bash
COPY . /var/www/html/app/
WORKDIR /var/www/html/app
# Install stuff
RUN php ../composer.phar update
RUN yarn install && \
yarn build
# Pull translation
RUN curl "https://localise.biz/api/export/locale/en.yml?key=RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro&format=symfony" > translations/messages.en.yml
RUN curl "https://localise.biz/api/export/locale/de.yml?key=RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro&format=symfony" > translations/messages.de.yml
RUN cp .env.staging .env
# Run Migration and dev-webserver
CMD /root/.symfony/bin/symfony console doctrine:migrations:migrate && /root/.symfony/bin/symfony serve --port=9999 --no-tls

39
Jenkinsfile vendored
View file

@ -2,39 +2,30 @@ node {
def app def app
stage('Clone repository') { stage('Clone repository') {
// Let's make sure we have the repository cloned to our workspace /* Let's make sure we have the repository cloned to our workspace */
checkout scm checkout scm
} }
stage('Install dependencies') { stage('Build image') {
// Install dependencies for build later /* This builds the actual image; synonymous to
sh 'composer update' * docker build on the command line */
sh 'yarn install'
app = docker.build("thisfro/plantex")
} }
stage('Composer Vulnr test') { stage('Test image') {
snykSecurity( /* Ideally, we would run a test framework against our image.
snykInstallation: 'snyk-local', * For this example, we're using a Volkswagen-type approach ;-) */
targetFile: 'composer.lock',
)
}
stage('npm vulnr test') { app.inside {
snykSecurity( // php 'bin/phpunit'
snykInstallation: 'snyk-local', sh 'echo "success"'
targetFile: 'package.json', }
)
} }
stage('Deploy staging') { stage('Deploy staging') {
// Deploy to staging host sh 'docker-compose --project-directory /opt/plant-exchange --file /opt/plant-exchange/docker-compose.yml up -d'
sh 'vendor/bin/dep deploy lq5xi.ftp.infomaniak.com --no-interaction'
} }
/*
stage('Test staging') {
// Run phpunit tests on staging host
bin/phpunit COMMAND
}
*/
} }

View file

@ -1,27 +1,18 @@
# Pflänz.li # plant-exchange
## Idea ## Idea
A platform where people can trade their plants. You can post what you have and search for others with [filters](#filters). The aim is to make it easier to trade plants and collect as few data as possible. Only the email/username and a postal code is required. A platform where people can exchange plants. They can post what they have and search for others with [filters](#filters).
## Tech stack ## Tech stack
- [Symfony](https://symfony.com/) - [Symfony](https://symfony.com/)
- [MariaDB](https://www.mariadb.org) - [PostgreSQL](https://www.postgresql.org/), maybe should be using MySQL for easier deployment?
- [Deployer](https://deployer.org)
Can easily be depoyed to a LAMP server Deployment: TBD
## Admin dashboard ## Admin dashboard
Find it under `/admin` Find it under `/admin`
## Filters ## Filters
### Implemented
- Distance between postal codes
- Search within title
### Ideas
It would be nice to have categories somehow, but it would be hard to make it comprehensive.
:warning: This list is work in progress! :warning: This list is work in progress!
Searching with filters such as: Searching with filters such as:
@ -31,5 +22,4 @@ Searching with filters such as:
| Name | `string` | textfield | | Name | `string` | textfield |
| Category | `Category` | dropdown | | Category | `Category` | dropdown |
## Development Distance from entered ZIP to the offer ZIP.
To get started with development, see [DEVELOPMENT.md](DEVELOPMENT.md)

View file

@ -8,6 +8,7 @@
// any CSS you import will output into a single css file (app.css in this case) // any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss'; import './styles/app.scss';
const $ = require('jquery');
// start the Stimulus application // start the Stimulus application
require('bootstrap'); require('bootstrap');
@ -17,32 +18,15 @@ import '@fortawesome/fontawesome-free/js/solid'
import '@fortawesome/fontawesome-free/js/regular' import '@fortawesome/fontawesome-free/js/regular'
import '@fortawesome/fontawesome-free/js/brands' import '@fortawesome/fontawesome-free/js/brands'
// Friendly captcha
import "friendly-challenge/widget";
// Dsiplay Filename when uploading
document.querySelector('.custom-file-input').addEventListener('change',function(e){
var fileName = document.getElementById("offering_form_photo").files[0].name;
var nextSibling = e.target.nextElementSibling
nextSibling.innerText = fileName
})
// Cookie-consent // Cookie-consent
import 'cookie-notice/dist/cookie.notice.min'; import 'cookie-notice/dist/cookie.notice.min.js'
new cookieNoticeJS({
// Position for the cookie-notifier (default=bottom)
'cookieNoticePosition': 'bottom',
// The message will be shown again in X days
'expiresIn': 365,
// Specify a custom font family and size in pixels
'fontFamily': 'inherit',
'fontSize': '.9rem',
// Dismiss button background color
'buttonBgColor': '#343a40',
// Dismiss button text color
'buttonTextColor': '#fff',
// Notice background color
'noticeBgColor': '#000',
// Notice text color
'noticeTextColor': '#fff',
// Print debug output to the console (default=false)
'debug': false
});

View file

@ -1,15 +0,0 @@
// Friendly captcha
import { WidgetInstance } from 'friendly-challenge';
const $ = require('jquery');
function doneCallback(solution) {
$('#registration_form_captcha_solution').val(solution);
}
const element = document.querySelector('#captcha');
const options = {
doneCallback: doneCallback,
sitekey: 'FCMVL79DP1G5K1K0',
}
const widget = new WidgetInstance(element, options);
widget.start()

View file

@ -1,5 +0,0 @@
const $ = require('jquery');
$( ".custom-file-input" ).change(function() {
$(".custom-file-label").html(($(".custom-file-input").prop("files")[0]["name"]));
});

View file

@ -8,7 +8,7 @@ $primary: darken(#005035, 20%);
footer { footer {
background-color: #ddd; background-color: #ddd;
height: auto; height: 6rem;
} }
nav { nav {
@ -16,7 +16,7 @@ nav {
} }
.container { .container {
min-height: calc(100vh - 15rem); min-height: calc(100vh - 10rem);
} }
.offer-img { .offer-img {
@ -68,11 +68,6 @@ nav {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.link-list {
list-style: none;
padding: 0;
}
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
.show-img-container { .show-img-container {
margin-right: 2rem; margin-right: 2rem;

View file

@ -8,47 +8,39 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"composer/package-versions-deprecated": "1.11.99.1", "composer/package-versions-deprecated": "1.11.99.1",
"deployer/deployer": "^7.0",
"doctrine/doctrine-bundle": "^2.3", "doctrine/doctrine-bundle": "^2.3",
"doctrine/doctrine-migrations-bundle": "^3.1", "doctrine/doctrine-migrations-bundle": "^3.1",
"doctrine/orm": "^2.8", "doctrine/orm": "^2.8",
"easycorp/easyadmin-bundle": "^3", "easycorp/easyadmin-bundle": "^3",
"imagine/imagine": "^1.2",
"mjaschen/phpgeo": "^3.2", "mjaschen/phpgeo": "^3.2",
"presta/sitemap-bundle": "^3.2",
"samayo/bulletproof": "4.0.1",
"sensio/framework-extra-bundle": "^6.1", "sensio/framework-extra-bundle": "^6.1",
"symfony/asset": "^5.4.20", "symfony/asset": "5.3.*",
"symfony/console": "^5.4.20", "symfony/console": "5.3.*",
"symfony/dotenv": "^5.4.20", "symfony/dotenv": "5.3.*",
"symfony/filesystem": "^5.4.20",
"symfony/flex": "^1.3.1", "symfony/flex": "^1.3.1",
"symfony/form": "^5.4.20", "symfony/form": "5.3.*",
"symfony/framework-bundle": "^5.4.20", "symfony/framework-bundle": "5.3.*",
"symfony/mailer": "^5.4.20", "symfony/loco-translation-provider": "5.3.*",
"symfony/mailer": "5.3.*",
"symfony/monolog-bundle": "^3.7", "symfony/monolog-bundle": "^3.7",
"symfony/proxy-manager-bridge": "^5.4.20", "symfony/proxy-manager-bridge": "5.3.*",
"symfony/security-bundle": "^5.4.20", "symfony/security-bundle": "5.3.*",
"symfony/twig-bundle": "^5.4.20", "symfony/twig-bundle": "5.3.*",
"symfony/validator": "^5.4.20", "symfony/validator": "5.3.*",
"symfony/webpack-encore-bundle": "^1.11", "symfony/webpack-encore-bundle": "^1.11",
"symfony/yaml": "^5.4.20", "symfony/yaml": "5.3.*",
"symfonycasts/reset-password-bundle": "^1.7", "symfonycasts/reset-password-bundle": "^1.7",
"symfonycasts/verify-email-bundle": "^1.4", "symfonycasts/verify-email-bundle": "^1.4",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/intl-extra": "^3.3", "twig/intl-extra": "^3.3",
"twig/twig": "^3.4.3" "twig/twig": "^2.12|^3.0"
}, },
"config": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,
"preferred-install": { "preferred-install": {
"*": "dist" "*": "dist"
}, },
"sort-packages": true, "sort-packages": true
"allow-plugins": {
"composer/package-versions-deprecated": false,
"symfony/flex": true
}
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -83,17 +75,17 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "^5.4.20" "require": "5.3.*"
} }
}, },
"require-dev": { "require-dev": {
"symfony/browser-kit": "^5.4", "symfony/browser-kit": "^5.2",
"symfony/css-selector": "^5.4", "symfony/css-selector": "^5.2",
"symfony/debug-bundle": "^5.4", "symfony/debug-bundle": "^5.2",
"symfony/maker-bundle": "^1.30", "symfony/maker-bundle": "^1.30",
"symfony/phpunit-bridge": "^5.4", "symfony/phpunit-bridge": "^5.2",
"symfony/stopwatch": "^5.4", "symfony/stopwatch": "^5.2",
"symfony/var-dumper": "^5.4", "symfony/var-dumper": "^5.2",
"symfony/web-profiler-bundle": "^5.4" "symfony/web-profiler-bundle": "^5.2"
} }
} }

3286
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,5 +16,4 @@ return [
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true], SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true],
]; ];

View file

@ -1,5 +1,6 @@
doctrine: doctrine:
dbal: dbal:
override_url: true
url: '%env(resolve:DATABASE_URL)%' url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version, # IMPORTANT: You MUST configure your server version,

View file

@ -10,7 +10,6 @@ framework:
handler_id: null handler_id: null
cookie_secure: auto cookie_secure: auto
cookie_samesite: lax cookie_samesite: lax
storage_factory_id: 'session.storage.factory.native'
#esi: true #esi: true
#fragments: true #fragments: true

View file

@ -15,7 +15,3 @@ monolog:
type: console type: console
process_psr_3_messages: false process_psr_3_messages: false
channels: ["!event", "!doctrine"] channels: ["!event", "!doctrine"]
file:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: warning

View file

@ -1,11 +1,6 @@
security: security:
enable_authenticator_manager: true encoders:
password_hashers:
# use your user class name here
App\Entity\User: App\Entity\User:
# Use native password hasher, which auto-selects the best
# possible hashing algorithm (starting from Symfony 5.3 this is "bcrypt")
algorithm: auto algorithm: auto
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
@ -20,10 +15,12 @@ security:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
main: main:
anonymous: true
lazy: true lazy: true
provider: app_user_provider provider: app_user_provider
custom_authenticators: guard:
- App\Security\AppAuthenticator authenticators:
- App\Security\AppAuthenticator
logout: logout:
path: app_logout path: app_logout
# where to redirect after logout # where to redirect after logout
@ -50,4 +47,4 @@ security:
access_control: access_control:
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^(?!/(login|register|reset-password|offers|offer/*|imprint|faq|sitemap.*)), roles: ROLE_USER } - { path: ^(?!/(login|register|reset-password|offers|offer/*|imprint)), roles: ROLE_USER }

View file

@ -1,6 +1,8 @@
framework: framework:
default_locale: en default_locale: en
translator: translator:
default_path: '%kernel.project_dir%/translations' providers:
fallbacks: loco:
- en dsn: '%env(LOCO_DSN)%'
domains: ['messages']
locales: ['en', 'de']

View file

@ -1,6 +1,3 @@
twig: twig:
default_path: '%kernel.project_dir%/templates' default_path: '%kernel.project_dir%/templates'
form_themes: ['bootstrap_4_horizontal_layout.html.twig'] form_themes: ['bootstrap_4_horizontal_layout.html.twig']
globals:
app_env: '%env(APP_ENV)%'

View file

@ -1,2 +0,0 @@
presta_sitemap:
resource: "@PrestaSitemapBundle/config/routing.yml"

View file

@ -4,8 +4,6 @@
# Put parameters here that don't need to change on each machine where the app is deployed # Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters: parameters:
captcha.secret: '%env(CAPTCHA_SECRET)%'
captcha.sitekey: '%env(CAPTCHA_SITEKEY)%'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file

View file

@ -4,69 +4,37 @@ namespace Deployer;
require 'recipe/symfony.php'; require 'recipe/symfony.php';
// Project name // Project name
set('application', 'pflaenz.li'); set('application', 'plant-exchange');
// Project repository // Project repository
set('repository', 'ssh://git@git.thisfro.ch:222/thisfro/pflaenz.li.git'); set('repository', 'ssh://git@git.thisfro.ch:222/thisfro/plant-exchange.git');
// [Optional] Allocate tty for git clone. Default value is false. // [Optional] Allocate tty for git clone. Default value is false.
set('git_tty', true); set('git_tty', true);
set('bin/php', function() {
return '/opt/php8.2/bin/php';
});
set('bin/composer', function() {
return '/opt/php8.2/bin/composer2';
});
// Shared files/dirs between deploys // Shared files/dirs between deploys
add('shared_files', ['public/.htaccess']); add('shared_files', []);
add('shared_dirs', ['public/uploads']); add('shared_dirs', []);
// Writable dirs by web server // Writable dirs by web server
add('writable_dirs', []); add('writable_dirs', []);
// Set composer options
set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-scripts');
// Hosts // Hosts
host('lq5xi.ftp.infomaniak.com')
->set('remote_user', 'lq5xi_thisfro')
->set('deploy_path', '~/sites/{{stage}}.{{application}}')
->set('http_user', 'uid153060')
->set('stage', 'beta');
host('pflaenz.li')
->set('deploy_path', '~/{{application}}');
// Tasks // Tasks
task('upload:build', function() { task('build', function () {
upload('public/build/', '{{release_path}}/public/build/'); run('cd {{release_path}} && build');
}); });
// Build yarn locally
task('deploy:build:assets', function (): void {
runLocally('yarn install');
runLocally('yarn encore production');
})->desc('Install front-end assets');
before('deploy:symlink', 'deploy:build:assets');
// Upload assets
task('upload:assets', function (): void {
upload(__DIR__.'/public/build/', '{{release_path}}/public/build');
});
task('upload:build', function() {
upload("public/build/", '{{release_path}}/public/build/');
});
task('upload:build', function() {
upload("public/build/", '{{release_path}}/public/build/');
});
after('deploy:build:assets', 'upload:assets');
// [Optional] if deploy fails automatically unlock. // [Optional] if deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock'); after('deploy:failed', 'deploy:unlock');
before('deploy:symlink', 'database:migrate'); // Migrate database before symlink new release.
before('deploy:symlink', 'database:migrate');

View file

@ -2,20 +2,29 @@ version: '3'
services: services:
db: db:
image: mariadb:10.5 image: postgres:latest
environment: environment:
MARIADB_USER: pflaenzli POSTGRES_PASSWORD: develop
MARIADB_PASSWORD: develop
MARIADB_DATABASE: pflaenzli
MARIADB_ROOT_PASSWORD: r00tpa55w0rd
ports: ports:
- 3306:3306 - 5432:5432
volumes: volumes:
- mariadb-data:/var/lib/mysql - postgres-data:/var/lib/postgresql/data
pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: root
ports:
- "8001:80"
volumes:
- ./pgadmin_data/servers.json:/pgadmin4/servers.json
- pgadmin-data:/varl/lib/pgadmin
mailer: mailer:
image: schickling/mailcatcher image: schickling/mailcatcher
ports: [1025,1080] ports: [1025,1080]
volumes: volumes:
mariadb-data: postgres-data:
pgadmin-data:

View file

@ -0,0 +1,35 @@
<?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 Version20210422125046 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('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE "user_id_seq" CASCADE');
$this->addSql('DROP TABLE "user"');
}
}

View file

@ -10,22 +10,23 @@ use Doctrine\Migrations\AbstractMigration;
/** /**
* Auto-generated Migration: Please modify to your needs! * Auto-generated Migration: Please modify to your needs!
*/ */
final class Version20220111213438 extends AbstractMigration final class Version20210422132314 extends AbstractMigration
{ {
public function getDescription(): string public function getDescription() : string
{ {
return ''; return '';
} }
public function up(Schema $schema): void public function up(Schema $schema) : void
{ {
// this up() migration is auto-generated, please modify it to your needs // this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD is_verified BOOLEAN NOT NULL DEFAULT TRUE');
} }
public function down(Schema $schema): void public function down(Schema $schema) : void
{ {
// this down() migration is auto-generated, please modify it to your needs // this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "user" DROP is_verified');
} }
} }

View file

@ -0,0 +1,34 @@
<?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 Version20210422155430 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 username VARCHAR(255) NOT NULL DEFAULT \'\'');
$this->addSql('ALTER TABLE "user" ALTER is_verified DROP DEFAULT');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "user" DROP username');
$this->addSql('ALTER TABLE "user" ALTER is_verified SET DEFAULT \'true\'');
}
}

View file

@ -0,0 +1,38 @@
<?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 Version20210424153343 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('CREATE SEQUENCE offering_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE offering (id INT NOT NULL, by_user_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, title VARCHAR(255) NOT NULL, photo_filename VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_A5682AB1DC9C2434 ON offering (by_user_id)');
$this->addSql('ALTER TABLE offering ADD CONSTRAINT FK_A5682AB1DC9C2434 FOREIGN KEY (by_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE "user" ALTER username DROP DEFAULT');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE offering_id_seq CASCADE');
$this->addSql('DROP TABLE offering');
$this->addSql('ALTER TABLE "user" ALTER username SET DEFAULT \'\'');
}
}

View file

@ -0,0 +1,34 @@
<?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 Version20210426205302 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 offering ADD zip_code INT NOT NULL DEFAULT 0');
$this->addSql('ALTER TABLE offering ADD description TEXT NOT NULL DEFAULT \'Lorem ipsum dolor\'');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE offering DROP zip_code');
$this->addSql('ALTER TABLE offering DROP description');
}
}

View file

@ -0,0 +1,42 @@
<?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 Version20210502123444 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('CREATE SEQUENCE reset_password_request_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE reset_password_request (id INT NOT NULL, user_id INT NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_7CE748AA76ED395 ON reset_password_request (user_id)');
$this->addSql('COMMENT ON COLUMN reset_password_request.requested_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN reset_password_request.expires_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE offering ALTER zip_code DROP DEFAULT');
$this->addSql('ALTER TABLE offering ALTER description DROP DEFAULT');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE reset_password_request_id_seq CASCADE');
$this->addSql('DROP TABLE reset_password_request');
$this->addSql('ALTER TABLE offering ALTER zip_code SET DEFAULT 0');
$this->addSql('ALTER TABLE offering ALTER description SET DEFAULT \'Lorem ipsum dolor\'');
}
}

View file

@ -0,0 +1,36 @@
<?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 Version20210503161858 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('CREATE SEQUENCE wish_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE wish (id INT NOT NULL, by_user_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_D7D174C9DC9C2434 ON wish (by_user_id)');
$this->addSql('ALTER TABLE wish ADD CONSTRAINT FK_D7D174C9DC9C2434 FOREIGN KEY (by_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE wish_id_seq CASCADE');
$this->addSql('DROP TABLE wish');
}
}

View file

@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration;
/** /**
* Auto-generated Migration: Please modify to your needs! * Auto-generated Migration: Please modify to your needs!
*/ */
final class Version20220119172053 extends AbstractMigration final class Version20210519090349 extends AbstractMigration
{ {
public function getDescription(): string public function getDescription(): string
{ {
@ -20,12 +20,12 @@ final class Version20220119172053 extends AbstractMigration
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
// this up() migration is auto-generated, please modify it to your needs // this up() migration is auto-generated, please modify it to your needs
$this->addSql('RENAME TABLE offering TO offer'); $this->addSql('ALTER TABLE offering ALTER COLUMN description DROP NOT NULL');
} }
public function down(Schema $schema): void public function down(Schema $schema): void
{ {
// this down() migration is auto-generated, please modify it to your needs // this down() migration is auto-generated, please modify it to your needs
$this->addSql('RENAME TABLE offer TO offering'); $this->addSql('CREATE SCHEMA public');
} }
} }

View file

@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration;
/** /**
* Auto-generated Migration: Please modify to your needs! * Auto-generated Migration: Please modify to your needs!
*/ */
final class Version20220112111528 extends AbstractMigration final class Version20210614104026 extends AbstractMigration
{ {
public function getDescription(): string public function getDescription(): string
{ {
@ -20,12 +20,15 @@ final class Version20220112111528 extends AbstractMigration
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
// this up() migration is auto-generated, please modify it to your needs // this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE offering ADD lat DOUBLE PRECISION DEFAULT NULL, ADD lng DOUBLE PRECISION DEFAULT NULL'); $this->addSql('ALTER TABLE offering ALTER description SET NOT NULL');
$this->addSql('ALTER TABLE "user" ADD zip_code INT DEFAULT NULL');
} }
public function down(Schema $schema): void public function down(Schema $schema): void
{ {
// this down() migration is auto-generated, please modify it to your needs // this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE offering DROP lat, DROP lng'); $this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "user" DROP zip_code');
$this->addSql('ALTER TABLE offering ALTER description DROP NOT NULL');
} }
} }

View file

@ -1,32 +0,0 @@
<?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 Version20220117175334 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add an offer ID to be displayed in the URL';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE offering ADD url_id VARCHAR(13) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE offering DROP url_id');
}
}

View file

@ -1,33 +0,0 @@
<?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 Version20220117193804 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 url_id VARCHAR(13) NOT NULL');
$this->addSql('ALTER TABLE wish ADD url_id VARCHAR(13) 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 url_id');
$this->addSql('ALTER TABLE wish DROP url_id');
}
}

15366
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,11 +19,9 @@
"dev-server": "encore dev-server", "dev-server": "encore dev-server",
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress", "build": "encore production --progress"
"test": "snyk test"
}, },
"dependencies": { "dependencies": {
"@snyk/protect": "^1.834.0",
"cookie-notice": "^1.3.6", "cookie-notice": "^1.3.6",
"friendly-challenge": "^0.8.5" "friendly-challenge": "^0.8.5"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

View file

@ -18,7 +18,5 @@ if ($_SERVER['APP_DEBUG']) {
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals(); $request = Request::createFromGlobals();
$response = $kernel->handle($request); $response = $kernel->handle($request);
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->send(); $response->send();
$kernel->terminate($request, $response); $kernel->terminate($request, $response);

View file

@ -3,7 +3,7 @@
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\User; use App\Entity\User;
use App\Entity\Offer; use App\Entity\Offering;
use App\Entity\Wish; use App\Entity\Wish;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard; use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem; use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
@ -36,7 +36,7 @@ class DashboardController extends AbstractDashboardController
yield MenuItem::linktoRoute('Back to the website', 'fas fa-arrow-left', 'offers'); yield MenuItem::linktoRoute('Back to the website', 'fas fa-arrow-left', 'offers');
yield MenuItem::linktoDashboard('Dashboard', 'fa fa-home'); yield MenuItem::linktoDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linkToCrud('User', 'fas fa-user', User::class); yield MenuItem::linkToCrud('User', 'fas fa-user', User::class);
yield MenuItem::linkToCrud('Offer', 'fas fa-seedling', Offer::class); yield MenuItem::linkToCrud('Offering', 'fas fa-seedling', Offering::class);
yield MenuItem::linkToCrud('Wish', 'fas fa-star', Wish::class); yield MenuItem::linkToCrud('Wish', 'fas fa-star', Wish::class);
} }
} }

View file

@ -2,7 +2,7 @@
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Offer; use App\Entity\Offering;
use App\Entity\User; use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
@ -13,11 +13,11 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField; use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
class OfferCrudController extends AbstractCrudController class OfferingCrudController extends AbstractCrudController
{ {
public static function getEntityFqcn(): string public static function getEntityFqcn(): string
{ {
return Offer::class; return Offering::class;
} }
public function configureFields(string $pageName): iterable public function configureFields(string $pageName): iterable

View file

@ -10,21 +10,15 @@ use Twig\Environment;
class AppController extends AbstractController class AppController extends AbstractController
{ {
#[Route('/', name: 'homepage', options: ["sitemap" => true])] #[Route('/', name: 'homepage')]
public function index(): Response public function index(): Response
{ {
return $this->render('app/index.html.twig'); return $this->render('app/index.html.twig');
} }
#[Route('/imprint', name: 'imprint', options: ["sitemap" => true])] #[Route('/imprint', name: 'imprint')]
public function imprint(): Response public function imprint(): Response
{ {
return $this->render('app/imprint.html.twig'); return $this->render('app/imprint.html.twig');
} }
#[Route('/faq', name: 'faq', options: ["sitemap" => true])]
public function faq(): Response
{
return $this->render('app/faq.html.twig');
}
} }

View file

@ -2,17 +2,14 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\Offer; use App\Entity\Offering;
use App\Form\OfferFormType; use App\Form\OfferingFormType;
use App\Form\OfferFilterFormType;
use App\Repository\OfferRepository; use App\Repository\OfferingRepository;
use App\Repository\WishRepository; use App\Repository\WishRepository;
use App\Service\PlzToCoordinate; use App\Service\PlzToCoordinate;
use App\Service\DistanceCalculator; use App\Service\DistanceCalculator;
use App\Service\PhotoResizer;
use App\Service\OfferPhotoHelper;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -26,62 +23,24 @@ class OfferController extends AbstractController
{ {
private $entityManager; private $entityManager;
public function __construct(EntityManagerInterface $entityManager, PhotoResizer $photoresizer) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
$this->photoresizer = $photoresizer;
} }
#[Route('/offers', name: 'offers', options: ["sitemap" => true])] #[Route('/offers', name: 'offers')]
public function showAll(Request $request, OfferRepository $offerRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response public function index(Environment $twig, OfferingRepository $offerRepository): Response
{ {
$form = $this->createForm(OfferFilterFormType::class); return new Response($twig->render('offer/index.html.twig', [
$form->handleRequest($request); 'offers' => $offerRepository->findAll(),
]));
$filteredOffers = [];
if ($form->isSubmitted() && $form->isValid() && $form->get('search')->getData() != null) {
$allOffers = $offerRepository->findBySearchLiteral($form->get('search')->getData());
}
else {
$allOffers = $offerRepository->findAll();
}
if ($form->isSubmitted() && $form->isValid() && $form->get('distance')->getData() != null && $form->get('zipCode')->getData() != null) {
$filterDistance = $form->get('distance')->getData();
$filterPlz = $form->get('zipCode')->getData();
$filterCoordinate = $plzconverter->convertPlzToCoordinate($filterPlz);
if ($filterCoordinate != null) {
foreach ($allOffers as $offer) {
$offerCoordinate = $offer->getCoordinate();
$distance = $distanceCalculator->calculateDistance($offerCoordinate, $filterCoordinate);
if ($distance < $filterDistance) {
array_push($filteredOffers, $offer);
}
}
}
else {
$this->addFlash("error", "The PLZ was not found!");
}
}
else {
$filteredOffers = $allOffers;
}
return $this->render('offer/index.html.twig', [
'offers' => $filteredOffers,
'filter_form' => $form->createView()
]);
} }
#[Route('/new', name: 'new_offer')] #[Route('/new', name: 'new_offer')]
public function newOffer(Request $request, PlzToCoordinate $plzconverter, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response public function newOffer(Request $request, string $photoDir): Response
{ {
$offer = new Offer(); $offer = new Offering();
$form = $this->createForm(OfferFormType::class, $offer); $form = $this->createForm(OfferingFormType::class, $offer);
$user = $this->getUser(); $user = $this->getUser();
$form->handleRequest($request); $form->handleRequest($request);
@ -89,22 +48,22 @@ class OfferController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$offer->setByUser($user); $offer->setByUser($user);
$offer->setCreatedAt(new \DateTime()); $offer->setCreatedAt(new \DateTime());
$offer->setUrlId(uniqid());
$coordinate = $plzconverter->convertPlzToCoordinate($form['zipCode']->getData());
if ($coordinate != null) {
$offer->setCoordinate($coordinate);
}
if ($photo = $form['photo']->getData()) { if ($photo = $form['photo']->getData()) {
$offerPhotoHelper->uploadOfferPhoto($photoDir, $photo, $offer); $filename = uniqid().'.'.$photo->guessExtension();
try {
$photo->move($photoDir, $filename);
} catch (FileException $e) {
// unable to upload the photo, give up
$this->addFlash("error", "There was an error uploading the photo: ".$e);
return $this->redirectToRoute('new_offer');
}
$offer->setPhotoFilename($filename);
} }
$this->entityManager->persist($offer); $this->entityManager->persist($offer);
$this->entityManager->flush(); $this->entityManager->flush();
$this->photoresizer->resize($photoDir.'/'.$offer->getPhotoFilename());
$this->addFlash("success", "Successfully added the new offer!"); $this->addFlash("success", "Successfully added the new offer!");
return $this->redirectToRoute('offers'); return $this->redirectToRoute('offers');
} }
@ -115,10 +74,10 @@ class OfferController extends AbstractController
]); ]);
} }
#[Route('/offer/{urlId}', name: 'show_offer')] #[Route('/offer/{id}', name: 'show_offer')]
public function showOffer(Offer $offer, WishRepository $wishRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response public function show_offer(Offering $offer, WishRepository $wishRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response
{ {
$distance = null; $distance = 0;
$user = $this->getUser(); $user = $this->getUser();
$offerPlz = $offer->getZipCode(); $offerPlz = $offer->getZipCode();
@ -129,16 +88,7 @@ class OfferController extends AbstractController
if (isset($userPlz)) if (isset($userPlz))
{ {
if (isset($offerPlz)) $distance = $distanceCalculator->calculateDistance($plzconverter->getCoordinates($offerPlz), $plzconverter->getCoordinates($userPlz));
{
$offerCoordinate = $plzconverter->convertPlzToCoordinate($offerPlz);
$userCoordinate = $plzconverter->convertPlzToCoordinate($userPlz);
if ($userCoordinate != null && $offerCoordinate != null)
{
$distance = $distanceCalculator->calculateDistance($offerCoordinate, $userCoordinate);
}
}
} }
return $this->render('app/offer.html.twig', [ return $this->render('app/offer.html.twig', [
@ -149,10 +99,10 @@ class OfferController extends AbstractController
]); ]);
} }
#[Route('/offer/edit/{urlId}', name: 'edit_offer')] #[Route('/offer/edit/{id}', name: 'edit_offer')]
public function editOffer(Offer $offer, Request $request, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response public function editOffer(Offering $offer, OfferingRepository $offerRepository, Request $request, string $photoDir): Response
{ {
$form = $this->createForm(OfferFormType::class, $offer); $form = $this->createForm(OfferingFormType::class, $offer);
$user = $this->getUser(); $user = $this->getUser();
if ($offer->getByUser() === $user) if ($offer->getByUser() === $user)
{ {
@ -163,17 +113,19 @@ class OfferController extends AbstractController
$offer->setCreatedAt(new \DateTime()); $offer->setCreatedAt(new \DateTime());
if ($photo = $form['photo']->getData()) { if ($photo = $form['photo']->getData()) {
$oldFilename = $offer->getPhotoFilename(); $filename = uniqid(random_bytes(6)).'.'.$photo->guessExtension();
$offerPhotoHelper->uploadOfferPhoto($photoDir, $photo, $offer); try {
$offerPhotoHelper->deleteOfferPhoto($photoDir, $oldFilename); $photo->move($photoDir, $filename);
} catch (FileException $e) {
// unable to upload the photo, give up
$this->addFlash("error", "There was an error uploading the photo: ".$e);
return $this->redirectToRoute('new_offer');
}
$offer->setPhotoFilename($filename);
} }
$this->entityManager->persist($offer); $this->entityManager->persist($offer);
$this->entityManager->flush(); $this->entityManager->flush();
$this->addFlash("success", "Successfully updated the offer!");
return $this->redirectToRoute('show_offer', ['urlId' => $offer->getUrlId()]);
} }
return $this->render('offer/edit.html.twig', [ return $this->render('offer/edit.html.twig', [
@ -186,16 +138,16 @@ class OfferController extends AbstractController
throw new HttpException(403, "No permission"); throw new HttpException(403, "No permission");
} }
#[Route('/offer/delete/{urlId}', name: 'delete_offer')] #[Route('/offer/delete/{id}', name: 'delete_offer')]
public function deleteOffer(Offer $offer, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response public function deleteOffer(Offering $offer, string $photoDir): Response
{ {
$user = $this->getUser(); $user = $this->getUser();
if ($offer->getByUser() === $user) if ($offer->getByUser() === $user)
{ {
if($offer->getPhotoFilename() != null) if ($offer->getPhotoFilename())
{ {
$offerPhotoHelper->deleteOfferPhoto($photoDir, $offer->getPhotoFilename()); unlink($photoDir . '/' . $offer->getPhotoFilename());
} }
$this->entityManager->remove($offer); $this->entityManager->remove($offer);
$this->entityManager->flush(); $this->entityManager->flush();
@ -210,23 +162,13 @@ class OfferController extends AbstractController
} }
#[Route('/myoffers', name: 'own_offers')] #[Route('/myoffers', name: 'own_offers')]
public function ownOffers(OfferRepository $offerRepository): Response public function ownOffers(OfferingRepository $offeringRepository): Response
{ {
$user = $this->getUser(); $user = $this->getUser();
return $this->render('user/offers.html.twig', [ return $this->render('user/offers.html.twig', [
'user' => $user, 'user' => $user,
'offers' => $offerRepository->findByUser($user), 'offers' => $offeringRepository->findByUser($user),
]);
}
#[Route('/offers/search', name: 'search', options: ["sitemap" => false])]
public function search(OfferRepository $offerRepository): Response
{
$offers = $offerRepository->findBySearchLiteral('');
return $this->render('offer/search.html.twig', [
'offers' => $offers,
]); ]);
} }
} }

View file

@ -4,9 +4,8 @@ namespace App\Controller;
use App\Entity\User; use App\Entity\User;
use App\Form\RegistrationFormType; use App\Form\RegistrationFormType;
use App\Security\AppAuthenticator;
use App\Security\EmailVerifier; use App\Security\EmailVerifier;
use App\Service\CaptchaVerifier; use App\Security\AppAuthenticator;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -14,7 +13,8 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface; use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
class RegistrationController extends AbstractController class RegistrationController extends AbstractController
@ -26,42 +26,42 @@ class RegistrationController extends AbstractController
$this->emailVerifier = $emailVerifier; $this->emailVerifier = $emailVerifier;
} }
#[Route('/register', name: 'app_register', options: ["sitemap" => true])] #[Route('/register', name: 'app_register')]
public function register(Request $request, UserPasswordHasherInterface $passwordEncoder, CaptchaVerifier $captchaVerifier): Response public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, AppAuthenticator $authenticator): Response
{ {
$user = new User(); $user = new User();
$form = $this->createForm(RegistrationFormType::class, $user); $form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
if ($captchaVerifier->isVerified($form->get('captcha_solution')->getData(), $this->getParameter('captcha.secret'), $this->getParameter('captcha.sitekey')) == true) { // encode the plain password
$user->setUrlId(uniqid()); $user->setPassword(
// encode the plain password $passwordEncoder->encodePassword(
$user->setPassword( $user,
$passwordEncoder->hashPassword( $form->get('plainPassword')->getData()
$user, )
$form->get('plainPassword')->getData() );
)
);
$entityManager = $this->getDoctrine()->getManager(); $entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user); $entityManager->persist($user);
$entityManager->flush(); $entityManager->flush();
// generate a signed url and email it to the user // generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user, $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail()) (new TemplatedEmail())
->from(new Address('no-reply@pflaenz.li', 'Pflänzli no-reply')) ->from(new Address('no-reply@pflaenz.li', 'Pflänzli no-reply'))
->to($user->getEmail()) ->to($user->getEmail())
->subject('Please Confirm your Email') ->subject('Please Confirm your Email')
->htmlTemplate('registration/confirmation_email.html.twig') ->htmlTemplate('registration/confirmation_email.html.twig')
); );
// do anything else you need here, like send an email
return $this->render('registration/created.html.twig'); return $guardHandler->authenticateUserAndHandleSuccess(
} $user,
else { $request,
$this->addFlash('error', 'CAPTCHA failed'); $authenticator,
} 'main' // firewall name in security.yaml
);
} }
return $this->render('registration/register.html.twig', [ return $this->render('registration/register.html.twig', [
@ -70,20 +70,32 @@ class RegistrationController extends AbstractController
} }
#[Route('/verify/email', name: 'app_verify_email')] #[Route('/verify/email', name: 'app_verify_email')]
public function verifyUserEmail(Request $request): Response public function verifyUserEmail(Request $request, UserRepository $userRepository): Response
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $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 // validate email confirmation link, sets User::isVerified=true and persists
try { try {
$this->emailVerifier->handleEmailConfirmation($request, $this->getUser()); $this->emailVerifier->handleEmailConfirmation($request, $user);
} catch (VerifyEmailExceptionInterface $exception) { } catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('verify_email_error', $exception->getReason()); $this->addFlash('verify_email_error', $exception->getReason());
return $this->redirectToRoute('app_register'); return $this->redirectToRoute('app_register');
} }
// @TODO Change the redirect on success and handle or remove the flash message in your templates // @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.'); $this->addFlash('success', 'Your email address has been verified.');
return $this->redirectToRoute('user_page'); return $this->redirectToRoute('user_page');
} }
} }

View file

@ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait; use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
@ -71,7 +71,7 @@ class ResetPasswordController extends AbstractController
* Validates and process the reset URL that the user clicked in their email. * Validates and process the reset URL that the user clicked in their email.
*/ */
#[Route('/reset/{token}', name: 'app_reset_password')] #[Route('/reset/{token}', name: 'app_reset_password')]
public function reset(Request $request, UserPasswordHasherInterface $passwordEncoder, string $token = null): Response public function reset(Request $request, UserPasswordEncoderInterface $passwordEncoder, string $token = null): Response
{ {
if ($token) { if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being // We store the token in session and remove it from the URL, to avoid the URL being
@ -106,13 +106,12 @@ class ResetPasswordController extends AbstractController
$this->resetPasswordHelper->removeResetRequest($token); $this->resetPasswordHelper->removeResetRequest($token);
// Encode the plain password, and set it. // Encode the plain password, and set it.
$user->setPassword( $encodedPassword = $passwordEncoder->encodePassword(
$passwordEncoder->hashPassword( $user,
$user, $form->get('plainPassword')->getData()
$form->get('plainPassword')->getData()
)
); );
$user->setPassword($encodedPassword);
$this->getDoctrine()->getManager()->flush(); $this->getDoctrine()->getManager()->flush();
// The session is cleaned up after the password has been changed. // The session is cleaned up after the password has been changed.

View file

@ -10,7 +10,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController class SecurityController extends AbstractController
{ {
/** /**
* @Route("/login", name="app_login", options={"sitemap"=true}) * @Route("/login", name="app_login")
*/ */
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils): Response
{ {

View file

@ -3,9 +3,9 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\User; use App\Entity\User;
use App\Entity\Offer; use App\Entity\Offering;
use App\Repository\OfferRepository; use App\Repository\OfferingRepository;
use App\Repository\WishRepository; use App\Repository\WishRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
@ -19,8 +19,8 @@ use Symfony\Component\Routing\Annotation\Route;
class TradeController extends AbstractController class TradeController extends AbstractController
{ {
#[Route('/trade/{urlId}', name: 'trade')] #[Route('/trade/{id}', name: 'trade')]
public function sendEmail(MailerInterface $mailer, Offer $offer, OfferRepository $offerRepository, WishRepository $wishRepository): Response public function sendEmail(MailerInterface $mailer, Offering $offer, OfferingRepository $offeringRepository, WishRepository $wishRepository): Response
{ {
$user = $this->getUser(); $user = $this->getUser();
@ -35,7 +35,7 @@ class TradeController extends AbstractController
->htmlTemplate('user/trade/offer_email.html.twig') ->htmlTemplate('user/trade/offer_email.html.twig')
->context([ ->context([
'user' => $user, 'user' => $user,
'urlId' => $user->getUrlId(), 'id' => $user->getId(),
]) ])
; ;
try try
@ -49,6 +49,6 @@ class TradeController extends AbstractController
$this->addFlash('error','You can\'t trade with yourself!'); $this->addFlash('error','You can\'t trade with yourself!');
} }
return $this->redirectToRoute('show_offer', ['urlId' => $offer->getUrlId()]); return $this->redirectToRoute('show_offer', ['id' => $offer->getId()]);
} }
} }

View file

@ -5,9 +5,8 @@ namespace App\Controller;
use App\Entity\Wish; use App\Entity\Wish;
use App\Entity\User; use App\Entity\User;
use App\Form\WishFormType; use App\Form\WishFormType;
use App\Form\ChangePasswordFormType;
use App\Repository\OfferRepository; use App\Repository\OfferingRepository;
use App\Repository\WishRepository; use App\Repository\WishRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -15,9 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment; use Twig\Environment;
class UserController extends AbstractController class UserController extends AbstractController
@ -30,7 +27,7 @@ class UserController extends AbstractController
} }
#[Route('/user', name: 'user_page')] #[Route('/user', name: 'user_page')]
public function user(OfferRepository $offerRepository, Request $request, UserPasswordHasherInterface $passwordEncoder): Response public function user(OfferingRepository $offeringRepository): Response
{ {
$user = $this->getUser(); $user = $this->getUser();
@ -39,48 +36,30 @@ class UserController extends AbstractController
$this->addFlash('error','Your email is not verified, please check your inbox'); $this->addFlash('error','Your email is not verified, please check your inbox');
} }
$form = $this->createForm(ChangePasswordFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user->setPassword(
$passwordEncoder->hashPassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash("success", "Successfully changed the password!");
}
return $this->render('user/index.html.twig', [ return $this->render('user/index.html.twig', [
'user' => $user, 'user' => $user,
'changePassword_form' => $form->createView(), 'offers' => $offeringRepository->findByUser($user),
]); ]);
} }
#[Route('/user/offers', name: 'user_offers')] #[Route('/user/offers', name: 'user_offers')]
public function userOffers(OfferRepository $offerRepository): Response public function userOffers(OfferingRepository $offeringRepository): Response
{ {
$user = $this->getUser(); $user = $this->getUser();
return $this->render('user/public.html.twig', [ return $this->render('user/public.html.twig', [
'user' => $user, 'user' => $user,
'offers' => $offerRepository->findByUser($user), 'offers' => $offeringRepository->findByUser($user),
]); ]);
} }
#[Route('/user/{urlId}', name: 'user_public')] #[Route('/user/{id}', name: 'user_public')]
public function show_user(User $user, OfferRepository $offerRepository, WishRepository $wishRepository): Response public function show_user(User $user, OfferingRepository $offeringRepository, WishRepository $wishRepository): Response
{ {
return $this->render('user/public.html.twig', [ return $this->render('user/public.html.twig', [
'username' => $user->getUsername(), 'username' => $user->getUsername(),
'wishes' => $wishRepository->findByUser($user), 'wishes' => $wishRepository->findByUser($user),
'offers' => $offerRepository->findByUser($user), 'offers' => $offeringRepository->findByUser($user),
]); ]);
} }
@ -95,7 +74,6 @@ class UserController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$wish->setByUser($user); $wish->setByUser($user);
$wish->setUrlId(uniqid());
$this->entityManager->persist($wish); $this->entityManager->persist($wish);
$this->entityManager->flush(); $this->entityManager->flush();
@ -111,7 +89,7 @@ class UserController extends AbstractController
]); ]);
} }
#[Route('/wish/delete/{urlId}', name: 'delete_wish')] #[Route('/wish/delete/{id}', name: 'delete_wish')]
public function deleteWish(Wish $wish): Response public function deleteWish(Wish $wish): Response
{ {
$user = $this->getUser(); $user = $this->getUser();

View file

@ -2,15 +2,14 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\OfferRepository; use App\Repository\OfferingRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Location\Coordinate;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
* @ORM\Entity(repositoryClass=OfferRepository::class) * @ORM\Entity(repositoryClass=OfferingRepository::class)
*/ */
class Offer class Offering
{ {
/** /**
* @ORM\Id * @ORM\Id
@ -20,7 +19,7 @@ class Offer
private $id; private $id;
/** /**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="offers") * @ORM\ManyToOne(targetEntity=User::class, inversedBy="offerings")
* @ORM\JoinColumn(nullable=false) * @ORM\JoinColumn(nullable=false)
*/ */
private $byUser; private $byUser;
@ -52,21 +51,6 @@ class Offer
*/ */
private $description; private $description;
/**
* @ORM\Column(type="float", nullable=true)
*/
private $lat;
/**
* @ORM\Column(type="float", nullable=true)
*/
private $lng;
/**
* @ORM\Column(type="string", length=13)
*/
private $urlId;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -148,31 +132,4 @@ class Offer
{ {
return (string) $this-getTitle(); return (string) $this-getTitle();
} }
}
public function getCoordinate(): ?Coordinate
{
$coordinate = new Coordinate($this->lat, $this->lng);
return $coordinate;
}
public function setCoordinate(Coordinate $coordinate): self
{
$this->lat = $coordinate->getLat();
$this->lng = $coordinate->getLng();
return $this;
}
public function getUrlId(): ?string
{
return $this->urlId;
}
public function setUrlId(string $urlId): self
{
$this->urlId = $urlId;
return $this;
}
}

View file

@ -7,7 +7,6 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
/** /**
@ -15,7 +14,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
* @ORM\Table(name="`user`") * @ORM\Table(name="`user`")
* @UniqueEntity(fields={"email"}, message="There is already an account with this email") * @UniqueEntity(fields={"email"}, message="There is already an account with this email")
*/ */
class User implements UserInterface, PasswordAuthenticatedUserInterface class User implements UserInterface
{ {
/** /**
* @ORM\Id * @ORM\Id
@ -51,9 +50,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private $username; private $username;
/** /**
* @ORM\OneToMany(targetEntity=Offer::class, mappedBy="byUser", orphanRemoval=true) * @ORM\OneToMany(targetEntity=Offering::class, mappedBy="byUser", orphanRemoval=true)
*/ */
private $offers; private $offerings;
/** /**
* @ORM\OneToMany(targetEntity=Wish::class, mappedBy="byUser") * @ORM\OneToMany(targetEntity=Wish::class, mappedBy="byUser")
@ -65,14 +64,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
*/ */
private $zipCode; private $zipCode;
/**
* @ORM\Column(type="string", length=13)
*/
private $urlId;
public function __construct() public function __construct()
{ {
$this->offers = new ArrayCollection(); $this->offerings = new ArrayCollection();
$this->wishes = new ArrayCollection(); $this->wishes = new ArrayCollection();
} }
@ -98,14 +92,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
* *
* @see UserInterface * @see UserInterface
*/ */
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* @deprecated since Symfony 5.3, use getUserIdentifier instead
*/
public function getUsername(): string public function getUsername(): string
{ {
return (string) $this->username; return (string) $this->username;
@ -131,11 +117,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
} }
/** /**
* @see PasswordAuthenticatedUserInterface * @see UserInterface
*/ */
public function getPassword(): string public function getPassword(): string
{ {
return $this->password; return (string) $this->password;
} }
public function setPassword(string $password): self public function setPassword(string $password): self
@ -185,29 +171,29 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
} }
/** /**
* @return Collection|Offer[] * @return Collection|Offering[]
*/ */
public function getOffers(): Collection public function getOfferings(): Collection
{ {
return $this->offers; return $this->offerings;
} }
public function addOffer(Offer $offer): self public function addOffering(Offering $offering): self
{ {
if (!$this->offers->contains($offer)) { if (!$this->offerings->contains($offering)) {
$this->offers[] = $offer; $this->offerings[] = $offering;
$offer->setByUser($this); $offering->setByUser($this);
} }
return $this; return $this;
} }
public function removeOffer(Offer $offer): self public function removeOffering(Offering $offering): self
{ {
if ($this->offers->removeElement($offer)) { if ($this->offerings->removeElement($offering)) {
// set the owning side to null (unless already changed) // set the owning side to null (unless already changed)
if ($offer->getByUser() === $this) { if ($offering->getByUser() === $this) {
$offer->setByUser(null); $offering->setByUser(null);
} }
} }
@ -260,16 +246,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getUrlId(): ?string
{
return $this->urlId;
}
public function setUrlId(string $urlId): self
{
$this->urlId = $urlId;
return $this;
}
} }

View file

@ -27,11 +27,6 @@ class Wish
*/ */
private $byUser; private $byUser;
/**
* @ORM\Column(type="string", length=13)
*/
private $urlId;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -65,16 +60,4 @@ class Wish
{ {
return (string) $this->getTitle(); return (string) $this->getTitle();
} }
public function getUrlId(): ?string
{
return $this->urlId;
}
public function setUrlId(string $urlId): self
{
$this->urlId = $urlId;
return $this;
}
} }

View file

@ -5,7 +5,6 @@ namespace App\Form;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
@ -40,7 +39,6 @@ class ChangePasswordFormType extends AbstractType
// this is read and encoded in the controller // this is read and encoded in the controller
'mapped' => false, 'mapped' => false,
]) ])
->add('submit', SubmitType::class)
; ;
} }

View file

@ -1,39 +0,0 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class OfferFilterFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('search', TextType::class, [
'label' => '<i class="fas fa-search mr-1"></i>Search',
'label_html' => true,
])
->add('zipCode', NumberType::class, [
'label' => '<i class="fas fa-map-marker-alt mr-2"></i>ZIP',
'label_html' => true,
])
->add('distance', NumberType::class, [
'label' => '<i class="fas fa-map-signs mr-1"></i>Distance',
'label_html' => true,
])
->add('Apply', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}

View file

@ -2,7 +2,7 @@
namespace App\Form; namespace App\Form;
use App\Entity\Offer; use App\Entity\Offering;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -12,7 +12,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\Image;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
class OfferFormType extends AbstractType class OfferingFormType extends AbstractType
{ {
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
@ -33,7 +33,7 @@ class OfferFormType extends AbstractType
'required' => false, 'required' => false,
'mapped' => false, 'mapped' => false,
'constraints' => [ 'constraints' => [
new Image(['maxSize' => '10m']) new Image(['maxSize' => '5096k'])
], ],
]) ])
->add('submit', SubmitType::class) ->add('submit', SubmitType::class)
@ -43,7 +43,7 @@ class OfferFormType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => Offer::class, 'data_class' => Offering::class,
]); ]);
} }
} }

View file

@ -6,16 +6,12 @@ use App\Entity\User;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue; use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
class RegistrationFormType extends AbstractType class RegistrationFormType extends AbstractType
{ {
@ -24,14 +20,19 @@ class RegistrationFormType extends AbstractType
$builder $builder
->add('email', EmailType::class) ->add('email', EmailType::class)
->add('username') ->add('username')
->add('zipcode', NumberType::class, [ ->add('zipcode')
'label' => 'ZIP' ->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
]) ])
->add('plainPassword', PasswordType::class, [ ->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly, // instead of being set onto the object directly,
// this is read and encoded in the controller // this is read and encoded in the controller
'mapped' => false, 'mapped' => false,
'label' => 'Password',
'constraints' => [ 'constraints' => [
new NotBlank([ new NotBlank([
'message' => 'Please enter a password', 'message' => 'Please enter a password',
@ -44,30 +45,6 @@ class RegistrationFormType extends AbstractType
]), ]),
], ],
]) ])
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'label' => 'Agree to <a href="/imprint" target="_blank">Terms</a>',
'label_html' => true,
'constraints' => [
new IsTrue([
'message' => 'You need to agree to our terms.',
]),
],
])
->add('submit', SubmitType::class, [
'label' => 'Register',
'attr' => [
'class' => 'btn-lg btn-primary',
],
])
->add('captcha_solution', HiddenType::class, [
'mapped' => false,
'constraints' => [
new NotNull([
'message' => 'Please wait for the CAPTCHA to complete',
]),
],
])
; ;
} }

View file

@ -2,25 +2,25 @@
namespace App\Repository; namespace App\Repository;
use App\Entity\Offer; use App\Entity\Offering;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
/** /**
* @method Offer|null find($id, $lockMode = null, $lockVersion = null) * @method Offering|null find($id, $lockMode = null, $lockVersion = null)
* @method Offer|null findOneBy(array $criteria, array $orderBy = null) * @method Offering|null findOneBy(array $criteria, array $orderBy = null)
* @method Offer[] findAll() * @method Offering[] findAll()
* @method Offer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method Offering[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */
class OfferRepository extends ServiceEntityRepository class OfferingRepository extends ServiceEntityRepository
{ {
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry)
{ {
parent::__construct($registry, Offer::class); parent::__construct($registry, Offering::class);
} }
// /** // /**
// * @return Offer[] Returns an array of Offer objects // * @return Offering[] Returns an array of Offering objects
// */ // */
public function findByUser($user) public function findByUser($user)
{ {
@ -44,21 +44,8 @@ class OfferRepository extends ServiceEntityRepository
; ;
} }
public function findBySearchLiteral(string $literal)
{
$qb = $this->createQueryBuilder('o');
$qb->andWhere($qb->expr()->like('o.title', ':lit'))
->setParameter('lit', '%' . $literal . '%')
->orderBy('o.id', 'ASC')
;
$qb = $qb->getQuery()->getResult();
return $qb;
}
/* /*
public function findOneBySomeField($value): ?Offer public function findOneBySomeField($value): ?Offering
{ {
return $this->createQueryBuilder('o') return $this->createQueryBuilder('o')
->andWhere('o.exampleField = :val') ->andWhere('o.exampleField = :val')

View file

@ -2,59 +2,105 @@
namespace App\Security; namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait; use Symfony\Component\Security\Http\Util\TargetPathTrait;
class AppAuthenticator extends AbstractLoginFormAuthenticator class AppAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{ {
use TargetPathTrait; use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login'; public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator; private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(UrlGeneratorInterface $urlGenerator) public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{ {
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
} }
public function authenticate(Request $request): PassportInterface public function supports(Request $request)
{ {
$email = $request->request->get('email', ''); return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
$request->getSession()->set(Security::LAST_USERNAME, $email); public function getCredentials(Request $request)
{
return new Passport( $credentials = [
new UserBadge($email), 'email' => $request->request->get('email'),
new PasswordCredentials($request->request->get('password', '')), 'password' => $request->request->get('password'),
[ 'csrf_token' => $request->request->get('_csrf_token'),
new CsrfTokenBadge('authenticate', $request->get('_csrf_token')), ];
] $request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
); );
return $credentials;
} }
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response public function getUser($credentials, UserProviderInterface $userProvider)
{ {
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { $token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function getPassword($credentials): ?string
{
return $credentials['password'];
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath); return new RedirectResponse($targetPath);
} }
return new RedirectResponse($this->urlGenerator->generate('user_page')); return new RedirectResponse($this->urlGenerator->generate('user_page'));
} }
protected function getLoginUrl(Request $request): string protected function getLoginUrl()
{ {
return $this->urlGenerator->generate(self::LOGIN_ROUTE); return $this->urlGenerator->generate(self::LOGIN_ROUTE);
} }
} }

View file

@ -1,33 +0,0 @@
<?php
namespace App\Service;
class CaptchaVerifier
{
public function isVerified(string $solution, string $secret, string $sitekey)
{
$url = "https://api.friendlycaptcha.com/api/v1/siteverify";
$data = array(
'solution' => $solution,
'secret'=> $secret,
'sitekey'=> $sitekey,
);
$options = array(
'http' => array(
'method' => 'POST',
'content' => json_encode( $data ),
'header'=> "Content-Type: application/json\r\n" .
"Accept: application/json\r\n"
)
);
$context = stream_context_create( $options );
$result = file_get_contents( $url, false, $context );
$response = json_decode( $result );
$isVerified = $response->success;
return $isVerified;
}
}

View file

@ -9,19 +9,12 @@ class DistanceCalculator
{ {
public function calculateDistance(Coordinate $coordinate1, Coordinate $coordinate2) public function calculateDistance(Coordinate $coordinate1, Coordinate $coordinate2)
{ {
if ($coordinate1 == null || $coordinate2 == null) $calculator = new Vincenty();
{
$distance = "N/A";
}
else
{
$calculator = new Vincenty();
$distance = $calculator->getDistance($coordinate1, $coordinate2); $distance = $calculator->getDistance($coordinate1, $coordinate2);
$distance = round($distance / 1000);
$distance = round($distance / 1000);
}
return $distance; return $distance;
} }
} }

View file

@ -1,42 +0,0 @@
<?php
namespace App\Service;
use App\Entity\Offer;
use Psr\Log\LoggerInterface;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class OfferPhotoHelper
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->filesystem = new Filesystem();
}
public function uploadOfferPhoto(string $photoDir, UploadedFile $photo, Offer $offer)
{
$filename = uniqid().'.'.$photo->guessExtension();
try {
$photo->move($photoDir, $filename);
} catch (FileException $e) {
// unable to upload the photo, give up
$this->addFlash("error", "There was an error uploading the photo: ".$e);
return $this->redirectToRoute('new_offer');
}
$offer->setPhotoFilename($filename);
}
public function deleteOfferPhoto(string $photoDir, string $filename)
{
$file = $photoDir . '/' . $filename;
if($this->filesystem->exists($file)) {
$this->filesystem->remove($file);
}
}
}

View file

@ -1,37 +0,0 @@
<?php
// Source: https://symfony.com/doc/current/the-fast-track/en/23-imagine.html
namespace App\Service;
use Imagine\Gd\Imagine;
use Imagine\Image\Box;
class PhotoResizer
{
private const MAX_WIDTH = 1000;
private const MAX_HEIGHT = 1000;
private $imagine;
public function __construct()
{
$this->imagine = new Imagine();
}
public function resize(string $filename): void
{
list($iwidth, $iheight) = getimagesize($filename);
$ratio = $iwidth / $iheight;
$width = self::MAX_WIDTH;
$height = self::MAX_HEIGHT;
if ($width / $height > $ratio) {
$width = $height * $ratio;
} else {
$height = $width / $ratio;
}
$photo = $this->imagine->open($filename);
$photo->resize(new Box($width, $height))->save($filename);
}
}

View file

@ -6,25 +6,13 @@ use Location\Coordinate;
class PlzToCoordinate class PlzToCoordinate
{ {
public function convertPlzToCoordinate(int $plz) public function getCoordinates(int $plz)
{ {
$content = file_get_contents("https://swisspost.opendatasoft.com/api/records/1.0/search/?dataset=plz_verzeichnis_v2&q=postleitzahl%3D" . $plz); $content = file_get_contents("https://swisspost.opendatasoft.com/api/records/1.0/search/?dataset=plz_verzeichnis_v2&q=postleitzahl%3D" . $plz);
$result = json_decode($content); $result = json_decode($content);
for($i = 0; $i < count($result->records); $i++) { if (isset($result->records[0]->fields->geo_point_2d[0]) && isset($result->records[0]->fields->geo_point_2d[1])) {
try { $coordinate = new Coordinate($result->records[0]->fields->geo_point_2d[0], $result->records[0]->fields->geo_point_2d[1]);
$lat = $result->records[$i]->fields->geo_point_2d[0];
$long = $result->records[$i]->fields->geo_point_2d[1];
} catch (\Throwable $th) {
// throw $th;
}
}
if (isset($lat) && isset($long)) {
$coordinate = new Coordinate($lat, $long);
}
else {
$coordinate = null;
} }
return $coordinate; return $coordinate;

View file

@ -97,18 +97,9 @@
"friendsofphp/proxy-manager-lts": { "friendsofphp/proxy-manager-lts": {
"version": "v1.0.3" "version": "v1.0.3"
}, },
"imagine/imagine": {
"version": "1.2.4"
},
"laminas/laminas-code": { "laminas/laminas-code": {
"version": "4.2.0" "version": "4.2.0"
}, },
"laminas/laminas-eventmanager": {
"version": "3.3.1"
},
"laminas/laminas-zendframework-bridge": {
"version": "1.2.0"
},
"mjaschen/phpgeo": { "mjaschen/phpgeo": {
"version": "3.2.1" "version": "3.2.1"
}, },
@ -118,9 +109,6 @@
"nikic/php-parser": { "nikic/php-parser": {
"version": "v4.10.4" "version": "v4.10.4"
}, },
"presta/sitemap-bundle": {
"version": "v3.2.1"
},
"psr/cache": { "psr/cache": {
"version": "2.0.0" "version": "2.0.0"
}, },
@ -133,9 +121,6 @@
"psr/log": { "psr/log": {
"version": "1.1.3" "version": "1.1.3"
}, },
"samayo/bulletproof": {
"version": "v4.0.1"
},
"sensio/framework-extra-bundle": { "sensio/framework-extra-bundle": {
"version": "5.2", "version": "5.2",
"recipe": { "recipe": {
@ -258,6 +243,12 @@
"src/Kernel.php" "src/Kernel.php"
] ]
}, },
"symfony/http-client": {
"version": "v5.3.0"
},
"symfony/http-client-contracts": {
"version": "v2.4.0"
},
"symfony/http-foundation": { "symfony/http-foundation": {
"version": "v5.2.4" "version": "v5.2.4"
}, },
@ -267,6 +258,15 @@
"symfony/intl": { "symfony/intl": {
"version": "v5.2.4" "version": "v5.2.4"
}, },
"symfony/loco-translation-provider": {
"version": "5.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.3",
"ref": "c706da68af4ae956f45cffccf99a130e9484cb33"
}
},
"symfony/mailer": { "symfony/mailer": {
"version": "4.3", "version": "4.3",
"recipe": { "recipe": {
@ -316,7 +316,7 @@
"version": "v2.1.0" "version": "v2.1.0"
}, },
"symfony/password-hasher": { "symfony/password-hasher": {
"version": "v5.3.7" "version": "v5.3.0"
}, },
"symfony/phpunit-bridge": { "symfony/phpunit-bridge": {
"version": "5.1", "version": "5.1",
@ -348,9 +348,15 @@
"symfony/polyfill-mbstring": { "symfony/polyfill-mbstring": {
"version": "v1.22.1" "version": "v1.22.1"
}, },
"symfony/polyfill-php73": {
"version": "v1.22.1"
},
"symfony/polyfill-php80": { "symfony/polyfill-php80": {
"version": "v1.22.1" "version": "v1.22.1"
}, },
"symfony/polyfill-php81": {
"version": "v1.23.0"
},
"symfony/polyfill-uuid": { "symfony/polyfill-uuid": {
"version": "v1.22.1" "version": "v1.22.1"
}, },
@ -398,6 +404,9 @@
"symfony/security-csrf": { "symfony/security-csrf": {
"version": "v5.2.4" "version": "v5.2.4"
}, },
"symfony/security-guard": {
"version": "v5.2.4"
},
"symfony/security-http": { "symfony/security-http": {
"version": "v5.2.6" "version": "v5.2.6"
}, },

View file

@ -1,11 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}Privacy Policy{% endblock %}
{% block body %}
<h1 class="mb-3">Frequently Asked Questions</h1>
<h2 class="mb-3">Is it free?</h2>
<p>Yes, pflänz.li is free to use and its source code is <a href="https://git.thisfro.ch/thisfro/pflaenz.li">publically accessible</a>.</p>
<h2 class="mb-3">Can I help?</h2>
Yes, feel free to contact <a href="mailto:jannis@thisfro.ch">@thisfro</a>!</p>
{% endblock %}

View file

@ -57,12 +57,12 @@
</p> </p>
<p>Pflänz.li</p> <p>Pflänzl.i</p>
<p>Marbachweg 22</p> <p>Langgrütstrasse 89</p>
<p>8041 Zürich</p> <p>8047 Zürich</p>
<p>Switzerland</p> <p>Switzerland</p>
<p>Email: contact@pflaenz.li</p> <p>Email: contact@pflaenz.li</p>
<p>Website: pflänz.li</p> <p>Website: pflänzl.i</p>
<h4>3. Cookies</h4> <h4>3. Cookies</h4>
<p>The Internet pages of the Pflänzl.i use cookies. Cookies are text files that are stored in a computer system via an Internet browser.</p> <p>The Internet pages of the Pflänzl.i use cookies. Cookies are text files that are stored in a computer system via an Internet browser.</p>

View file

@ -2,15 +2,11 @@
{% block title %}Home{% endblock %} {% block title %}Home{% endblock %}
{% block meta %}
<meta name="description" content="A platform to trade plants." >
{% endblock %}
{% block body %} {% block body %}
<div class="jumbotron"> <div class="jumbotron">
<h1 class="display-4">Welcome to Pflänz.li</h1> <h1 class="display-4">{% trans %} homepage.welcome.header {% endtrans %}</h1>
<p class="lead">This is a platform to trade plants. You can offer plants and setup a wishlist what you want to trade it for.</p> <p class="lead">{% trans %} homepage.welcome.info {% endtrans %}</p>
<hr class="my-4"> <hr class="my-4">
<p>To offer your plants, please register first.</p> <p>To offer your plants, please register first.</p>
<a class="btn btn-primary btn-lg" href="{{ path('app_register') }}" role="button">Register now</a> <a class="btn btn-primary btn-lg" href="{{ path('app_register') }}" role="button">Register now</a>

View file

@ -1,11 +1,5 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}New Offer{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('fileUpload') }}
{% endblock %}
{% block body %} {% block body %}
{% for message in app.flashes('error') %} {% for message in app.flashes('error') %}
<div class="alert alert-error" role="alert"> <div class="alert alert-error" role="alert">

View file

@ -1,12 +1,5 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Offer: {{ offer.title }}{% endblock %}
{% block meta %}
<meta name="description" content="{{offer.byuser }} offers {{ offer.title}}!" >
<meta name="author" content="{{ offer.byUser }}">
{% endblock %}
{% block body %} {% block body %}
{% for message in app.flashes('error') %} {% for message in app.flashes('error') %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
@ -41,10 +34,10 @@
</p> </p>
<p class="pr-3"> <p class="pr-3">
<i class="fas fa-map-marker-alt"></i> {{ offer.zipCode }} <i class="fas fa-map-marker-alt"></i> {{ offer.zipCode }}
{% if distance > 0 %}
(ca. {{ distance }} km)
{% endif %}
</p> </p>
{% if distance > 0 %}
<p class="pr-3"><i class="fas fa-map-signs mr-1"></i>ca. {{ distance }} km</p>
{% endif %}
</div> </div>
<h3>Description</h3> <h3>Description</h3>
<p>{{ offer.description }}</p> <p>{{ offer.description }}</p>
@ -52,7 +45,7 @@
</div> </div>
{% if offer.byUser == user %} {% if offer.byUser == user %}
<a href="{{ path('edit_offer', {'urlId': offer.urlId}) }}" class="btn btn-info mb-3"><i class="fas fa-pen"></i></a> <a href="{{ path('edit_offer', {'id': offer.id}) }}" class="btn btn-info mb-3"><i class="fas fa-pen"></i></a>
<button type="button" class="btn btn-danger mb-3" data-toggle="modal" data-target="#exampleModal"><i class="fas fa-trash-alt"></i></button> <button type="button" class="btn btn-danger mb-3" data-toggle="modal" data-target="#exampleModal"><i class="fas fa-trash-alt"></i></button>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
@ -69,7 +62,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'urlId': offer.urlId}) }}">Delete</a> <a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'id': offer.id}) }}">Delete</a>
</div> </div>
</div> </div>
</div> </div>
@ -90,6 +83,6 @@
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
<a class="btn btn-primary mb-3" href="{{ path('trade', {'urlId': offer.urlId}) }}">Offer trade</a> <a class="btn btn-primary mb-3" href="{{ path('trade', {'id': offer.id}) }}">Offer trade</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,131 +1,85 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Pflänz.li - <title>{% block title %}Plant Exchange Home{% endblock %}</title>
{% block title %}{% endblock %}
</title>
{% block meta %}{% endblock %}
{% if app_env == 'prod' %}
<meta name="robots" content="index,follow" />
{% endif %}
<meta name="publisher" content="pflänz.li" />
<meta name="keywords" content="trade, share, plants, sustainability, pflanzentausch, pflanzen" />
{% block stylesheets %} {% block stylesheets %}
{{ encore_entry_link_tags('app') }} {{ encore_entry_link_tags('app') }}
{% endblock %} {% endblock %}
{% block javascripts %}{% endblock %} {% block javascripts %}
{{ encore_entry_script_tags('app') }}
{{ encore_entry_script_tags('app') }} <!-- Matomo -->
<script type="text/javascript">
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//analytics.thisfro.ch/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '14']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
{% endblock %}
<!-- Matomo --> <meta name="viewport" content="width=device-width, initial-scale=1">
<script> </head>
var _paq = window._paq = window._paq || []; <body>
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
_paq.push(['trackPageView']); <a class="navbar-brand" href="{{ path('homepage') }}"><i class="fas fa-seedling"></i>Pflänz.li</a>
_paq.push(['enableLinkTracking']); <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
(function () { <span class="navbar-toggler-icon"></span>
var u = "https://analytics.thisfro.ch/"; </button>
_paq.push([
'setTrackerUrl', <div class="collapse navbar-collapse" id="navbarSupportedContent">
u + 'matomo.php' <ul class="navbar-nav mr-auto">
]); <li class="nav-item">
_paq.push(['setSiteId', '2']); <a class="nav-link" href=" {{ path('offers') }} "><i class="fas fa-seedling"></i> Offers</a>
var d = document, </li>
g = d.createElement('script'), <li class="nav-item dropdown">
s = d.getElementsByTagName('script')[0]; <a class="nav-link dropdown-toggle" data-toggle="dropdown"role="button" aria-haspopup="true" aria-expanded="false"><i class="fas fa-user"></i> User</a>
g.async = true; <div class="dropdown-menu">
g.src = u + 'matomo.js'; <a class="dropdown-item" href="{{ path('own_offers') }}"><i class="fas fa-seedling"></i> My Offers</a>
s.parentNode.insertBefore(g, s); <a class="dropdown-item" href="{{ path('wishlist') }}"><i class="fas fa-star"></i> Wishlist</a>
})(); <div class="dropdown-divider"></div>
</script> <a class="dropdown-item" href="{{ path('user_page') }}"><i class="fas fa-user"></i> User settings</a>
<!-- End Matomo Code --> </div>
</li>
<li>
<a class="nav-link" href="{{ path('new_offer') }}"><i class="fas fa-plus-square"></i> New Offer</a>
</li>
</ul>
<span>
{% if is_granted('ROLE_USER') %}
<a type="button" class="btn btn-light" href="{{ path('app_logout') }}">Log out</a>
{% else %}
<a type="button" class="btn btn-light" href="{{ path('app_login') }}">Log in</a>
{% endif %}
</span>
</div>
</nav>
<meta name="viewport" content="width=device-width, initial-scale=1"> <div class="container pt-5">
</head> {% block body %}{% endblock %}
<body> </div>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{{ path('homepage') }}">
<i class="fas fa-seedling"></i>Pflänz.li</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <footer class="text-center text-white">
<ul class="navbar-nav mr-auto"> <div class="pt-1">
<li class="nav-item"> <section class="mb-1">
<a class="nav-link" href=" {{ path('offers') }} "> <a class="btn btn-link btn-floating btn-lg text-dark m-1" href="#!" role="button" data-mdb-ripple-color="dark"><i class="fab fa-mastodon"></i></a>
<i class="fas fa-seedling"></i> <a class="btn btn-link btn-floating btn-lg text-dark m-1" href="#!" role="button" data-mdb-ripple-color="dark"><i class="fab fa-github"></i></a>
Offers</a> </section>
</li> </div>
<li class="nav-item dropdown"> <div class="text-center text-dark">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> <a href="https://creativecommons.org"><i class="fab fa-creative-commons"></i><i class="fab fa-creative-commons-by"></i></a>
<i class="fas fa-user"></i> <a class="text-dark" href="{{ path('homepage') }}">pflänz.li</a>
User</a> </div>
<div class="dropdown-menu"> </footer>
<a class="dropdown-item" href="{{ path('own_offers') }}"> </body>
<i class="fas fa-seedling"></i> </html>
My Offers</a>
<a class="dropdown-item" href="{{ path('wishlist') }}">
<i class="fas fa-star"></i>
Wishlist</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{ path('user_page') }}">
<i class="fas fa-user"></i>
User settings</a>
</div>
</li>
<li>
<a class="nav-link" href="{{ path('new_offer') }}">
<i class="fas fa-plus-square"></i>
New Offer</a>
</li>
</ul>
<span>
{% if is_granted('ROLE_USER') %}
<a class="btn btn-light" href="{{ path('app_logout') }}">Log out</a>
{% else %}
<a class="btn btn-light" href="{{ path('app_login') }}">Log in</a>
{% endif %}
</span>
</div>
</nav>
<div class="container pt-5"> {% block body %}{% endblock %}
</div>
<footer class="text-dark p-3">
<div class="row">
<div class="col-lg"></div>
<div class="col-lg text-center pt-3">
<section class="mb-1">
<h2 class="d-none">Social Links</h2>
<a class="btn btn-link btn-floating btn-lg text-dark m-1" rel="me" href="https://mastodon.social/@pflaenzli" role="button" data-mdb-ripple-color="dark">
<i class="fab fa-mastodon"></i>
</a>
<a class="btn btn-link btn-floating btn-lg text-dark m-1" href="https://git.thisfro.ch/thisfro/pflaenz.li" role="button" data-mdb-ripple-color="dark">
<i class="fab fa-git-alt"></i>
</a>
</section>
</div>
<div class="col-lg pt-4 pl-5">
<section>
<h2 class="h5">Links</h2>
<ul class="link-list">
<li><a href="https://blog.pflaenz.li">Blog</a></li>
<li><a href="{{ path('imprint') }}">Imprint</a></li>
<li><a href="{{ path('faq') }}">FAQ</a></li>
</ul>
</section>
</div>
</div>
<div class="row pt-3">
<div class="text-center text-dark w-100">
<a class="text-dark" href="{{ path('homepage') }}"><i class="far fa-copyright mr-1"></i>{{ 'now' | date('Y') }} pflänz.li</a>
</div>
</div>
</footer>
</body>
</html>

View file

@ -1,7 +1,5 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title%}Edit offer{% endblock %}
{% block body %} {% block body %}
{% for message in app.flashes('error') %} {% for message in app.flashes('error') %}
<div class="alert alert-error" role="alert"> <div class="alert alert-error" role="alert">
@ -9,7 +7,7 @@
</div> </div>
{% endfor %} {% endfor %}
<h1 class="mb-3">Edit offer</h1> <h1 class="mb-3">Add new offer</h1>
{{ form_start(offer_form) }} {{ form_start(offer_form) }}
{{ form_row(offer_form.title) }} {{ form_row(offer_form.title) }}
{{ form_row(offer_form.zipCode) }} {{ form_row(offer_form.zipCode) }}

View file

@ -1,7 +1,5 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Offers{% endblock %}
{% block body %} {% block body %}
{% for message in app.flashes('success') %} {% for message in app.flashes('success') %}
@ -9,20 +7,6 @@
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
{% for message in app.flashes('error') %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endfor %}
<div class="mb-5">
<a class="btn btn-primary" data-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="collapseExample">
<div class="btn btn-primary"><i class="fas fa-filter mr-3"></i>Filter<i class="fas fa-chevron-down ml-3 dropdown-collapse"></i></div>
</a>
<div class="collapse" id="collapseExample">
{{ form(filter_form, {attr: {novalidate: 'novalidate'}}) }}
</div>
</div>
<h1>Offers</h1> <h1>Offers</h1>
{% if offers|length > 0 %} {% if offers|length > 0 %}
@ -30,7 +14,7 @@
{% for offer in offers %} {% for offer in offers %}
<div class="mb-5"> <div class="mb-5">
<div class="card offer h-100"> <div class="card offer h-100">
<a href="{{ path('show_offer', {'urlId': offer.urlId }) }}"> <a href="{{ path('show_offer', {'id': offer.id }) }}">
{% if offer.photoFilename %} {% if offer.photoFilename %}
<img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" /> <img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" />
{% else %} {% else %}
@ -41,7 +25,7 @@
</div> </div>
</a> </a>
<div class="card-footer offer-footer"> <div class="card-footer offer-footer">
<a class="user-link" href="{{ path('user_public', { 'urlId': offer.byuser.urlId }) }}"> <a class="user-link" href="{{ path('user_public', { 'id': offer.byuser.id }) }}">
<p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p> <p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p>
</a> </a>
<p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p> <p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p>
@ -51,6 +35,6 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="alert alert-warning" role="alert">There are no active offers with the current filter.</div> <div class="alert alert-warning" role="alert">There are currently no active offers.</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,9 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}Account created{% endblock %}
{% block body %}
<h1>Your account has been created!</h1>
<p>Check your inbox for the verification email!</p>
{% endblock %}

View file

@ -1,35 +1,32 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Register {% block title %}Register{% endblock %}
{% endblock %}
{% block meta %}
<meta name="description" content="Register for pflänz.li" />
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('captcha') }}
{% endblock %}
{% block body %} {% block body %}
{% for flashError in app.flashes('verify_email_error') %} {% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div> <div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% endfor %} {% endfor %}
{% for message in app.flashes('error') %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endfor %}
<h1>Register</h1> <h1>Register</h1>
{{ form_start(registrationForm) }} {{ form_start(registrationForm) }}
{{ form_widget(registrationForm) }} {{ form_row(registrationForm.email) }}
<div class="form-group row"> {{ form_row(registrationForm.username) }}
<label class="col-form-label col-sm-2">CAPTCHA</label> {{ form_row(registrationForm.zipcode, {
<div class="col-sm-10"> label: 'PLZ'
<div class="frc-captcha" data-sitekey="FCMLGE739LB528NG" id="captcha"></div> }) }}
</div> {{ form_row(registrationForm.plainPassword, {
</div> label: 'Password'
{{ form_end(registrationForm) }} }) }}
{{ form_row(registrationForm.agreeTerms) }}
<div class="form-group row">
<label class="col-form-label col-sm-2">CAPTCHA</label>
<div class="col-sm-10">
<div class="frc-captcha" data-sitekey="FCMLGE739LB528NG"></div>
</div>
</div>
<button type="submit" class="btn btn-lg btn-primary">Register</button>
{{ form_end(registrationForm) }}
{% endblock %} {% endblock %}

View file

@ -6,6 +6,7 @@
<h1>Reset your password</h1> <h1>Reset your password</h1>
{{ form_start(resetForm) }} {{ form_start(resetForm) }}
{{form_widget(resetForm)}} {{ form_row(resetForm.plainPassword) }}
<button class="btn btn-primary">Reset password</button>
{{ form_end(resetForm) }} {{ form_end(resetForm) }}
{% endblock %} {% endblock %}

View file

@ -2,10 +2,6 @@
{% block title %}Log in{% endblock %} {% block title %}Log in{% endblock %}
{% block meta %}
<meta name="description" content="Register for pflänz.li"
{% endblock %}
{% block body %} {% block body %}
<form method="post"> <form method="post">
{% if error %} {% if error %}

View file

@ -1,33 +1,55 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}User {% block title %}User{% endblock %}
{% endblock %}
{% block body %} {% block body %}
{% for message in app.flashes('error') %} {% for message in app.flashes('error') %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
{% for message in app.flashes('success') %} {% for message in app.flashes('success') %}
<div class="alert alert-success" role="alert"> <div class="alert alert-success" role="alert">
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
<div class="mb-5"> <div class="alert alert-info" role="alert">
<h1>Hello Please note: This is not yet functional!
{{ user.username }}!</p> </div>
</div>
<div class="mb-5">
<h2>Change Password</h2>
{{ form_start(changePassword_form) }}
{{ form_widget(changePassword_form) }}
{{ form_end(changePassword_form) }}
</div>
<div class="mb-3"> <div class="mb-3">
<h2>Delete Account</h2> <h1>Hello {{ user.username }}!</p>
<button class="btn btn-danger">Delete Account</button> </div>
</div> <div class="mb-3">
<form method="post">
<h3 class="mb-3 font-weight-normal">Change your user data</h3>
<div class="mb-3">
<label for="inputEmail" class="form-label">Email address</label>
<input name="email" type="email" class="form-control" id="inputEmail" aria-describedby="emailHelp" placeholder="{{ app.user.email }}" readonly>
</div>
<div class="mb-3">
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control">
</div>
<div class="mb-3">
<label for="inputPlz">PLZ</label>
<input name="plz" id="inputPlz" class="form-control" value="{{ user.zipcode }}">
</div>
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
<button class="btn btn-lg btn-primary" type="submit">
Save
</button>
</form>
</div>
<div class="mb-3">
<h3 class="mb-3">Delete Account</h3>
<button class="btn btn-danger">Delete Account</button>
</div>
{% endblock %} {% endblock %}

View file

@ -14,7 +14,7 @@
{% for offer in offers %} {% for offer in offers %}
<div class="mb-5"> <div class="mb-5">
<div class="card offer h-100"> <div class="card offer h-100">
<a href="{{ path('show_offer', {'urlId': offer.urlId }) }}"> <a href="{{ path('show_offer', {'id': offer.id }) }}">
{% if offer.photoFilename %} {% if offer.photoFilename %}
<img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" /> <img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" />
{% else %} {% else %}
@ -26,12 +26,12 @@
</div> </div>
</a> </a>
<div class="card-footer offer-footer"> <div class="card-footer offer-footer">
<a href="{{ path('edit_offer', {'urlId': offer.urlId}) }}" class="btn btn-info"><i class="fas fa-pen"></i></a> <a href="{{ path('edit_offer', {'id': offer.id}) }}" class="btn btn-info"><i class="fas fa-pen"></i></a>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#offer-modal-{{ offer.urlId }}"><i class="fas fa-trash-alt"></i></button> <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#offer-modal-{{ offer.id }}"><i class="fas fa-trash-alt"></i></button>
</div> </div>
</div> </div>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="offer-modal-{{ offer.urlId }}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal fade" id="offer-modal-{{ offer.id }}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -45,7 +45,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'urlId': offer.urlId}) }}">Delete</a> <a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'id': offer.id}) }}">Delete</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}User {{ username }}{% endblock %} {% block title %}Whishlist{% endblock %}
{% block body %} {% block body %}
{% for message in app.flashes('success') %} {% for message in app.flashes('success') %}
@ -36,7 +36,7 @@
{% for offer in offers %} {% for offer in offers %}
<div class="mb-5"> <div class="mb-5">
<div class="card offer h-100"> <div class="card offer h-100">
<a href="{{ path('show_offer', {'urlId': offer.urlId }) }}"> <a href="{{ path('show_offer', {'id': offer.id }) }}">
{% if offer.photoFilename %} {% if offer.photoFilename %}
<img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" /> <img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" />
{% else %} {% else %}
@ -48,7 +48,7 @@
</div> </div>
</a> </a>
<div class="card-footer offer-footer"> <div class="card-footer offer-footer">
<a class="user-link" href="{{ path('user_public', { 'urlId': offer.byuser.id }) }}"> <a class="user-link" href="{{ path('user_public', { 'id': offer.byuser.id }) }}">
<p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p> <p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p>
</a> </a>
<p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p> <p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p>

View file

@ -1,5 +1,5 @@
<h1>{{ user.username }} wants to trade!</h1> <h1>{{ user.username }} wants to trade!</h1>
<p>Checkout {{ user.username}}'s offers:</p> <p>Checkout {{ user.username}}'s offers:</p>
<a href="{{ url('user_public', {'urlId': urlId}) }}">Link</a> <a href="{{ url('user_public', {'id': id}) }}">Link</a>
<p>Reply to this email to start trading.</p> <p>Reply to this email to start trading.</p>

View file

@ -28,7 +28,7 @@
{% for wish in wishes %} {% for wish in wishes %}
<li class="list-group-item d-flex justify-content-between align-items-center"> {{ wish.title }} <li class="list-group-item d-flex justify-content-between align-items-center"> {{ wish.title }}
<span> <span>
<a href="{{ path('delete_wish', {'urlId': wish.urlid}) }}" class="btn btn-danger" aria-label="Delete"><i class="fas fa-trash-alt" aria-hidden="true" title="Delete"></i></a> <a href="{{ path('delete_wish', {'id': wish.id}) }}" class="btn btn-danger" aria-label="Delete"><i class="fas fa-trash-alt" aria-hidden="true" title="Delete"></i></a>
</span> </span>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -21,8 +21,6 @@ Encore
* and one CSS file (e.g. app.css) if your JavaScript imports CSS. * and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/ */
.addEntry('app', './assets/app.js') .addEntry('app', './assets/app.js')
.addEntry('captcha', './assets/captcha.js')
.addEntry('fileUpload', './assets/fileUpload.js')
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
.enableStimulusBridge('./assets/controllers.json') .enableStimulusBridge('./assets/controllers.json')

8172
yarn.lock

File diff suppressed because it is too large Load diff