Compare commits

...
This repository has been archived on 2024-10-16. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

122 commits

Author SHA1 Message Date
Jannis Portmann
80ef8caf20 Update command 2023-02-08 17:23:24 +01:00
cc5e0b6b27 Merge branch 'master' into develop 2023-02-08 17:13:22 +01:00
Jannis Portmann
a6e4b205f7 Use deployer 7.x 2023-02-08 17:11:54 +01:00
4e5b69450e Merge pull request 'Update and fix security issues' (#26) from develop into master
Reviewed-on: thisfro/pflaenz.li#26
2023-02-03 15:43:06 +01:00
Jannis Portmann
a543fd2a4a Update and fix security issues 2023-02-03 15:37:46 +01:00
e70157067c Merge pull request 'Update packages' (#25) from develop into master
Reviewed-on: thisfro/pflaenz.li#25
2022-09-02 16:20:31 +02:00
2d31f7cd76 Update packages 2022-09-02 16:09:07 +02:00
1c172ec454 Ignore snyk files 2022-09-02 16:08:48 +02:00
90d6fd0770 Set headers on all requests 2022-01-26 17:13:00 +01:00
5eeb662d6f Add simple favicon 2022-01-26 17:12:34 +01:00
220d6a1f83 Add icon for distance 2022-01-26 12:50:08 +01:00
92c07b02cb Fix password resetting 2022-01-26 12:47:48 +01:00
48bf4f26c4 Add icons for filterForm 2022-01-26 12:37:20 +01:00
c7b400cec3 Add padding 2022-01-26 12:37:07 +01:00
f86319f23c Remove umlaut from url 2022-01-26 10:10:54 +01:00
7b9542f0bd Redirect to offer after editing 2022-01-26 10:10:19 +01:00
3d0ffa3311 Always load app.js and matomo script 2022-01-25 18:32:34 +01:00
42564e085c Split app.js into individual scripts 2022-01-25 18:27:51 +01:00
304c0d4ba6 Always use https for tracking 2022-01-25 17:38:27 +01:00
f87b3a0115 Fix keywords 2022-01-25 16:27:30 +01:00
e327966ef0 Fix typo 2022-01-24 23:57:36 +01:00
0c5f93bc36 Change indention 2022-01-24 23:52:29 +01:00
54fa878115 Remove staging host 2022-01-24 23:49:31 +01:00
34e5132c3d Update readme 2022-01-24 23:49:23 +01:00
61b861b510 Add staging 2022-01-24 17:40:19 +01:00
a034a6118a Use domain without umlaut 2022-01-24 17:31:46 +01:00
3135c26345 Add link to terms 2022-01-24 15:34:35 +01:00
af12833642 Snyk test composer and npm 2022-01-24 10:31:21 +01:00
1f140475b9 Remove again 2022-01-24 10:14:01 +01:00
d4a7bd4228 Use correct name 2022-01-24 10:13:01 +01:00
f0c2bfae3a Add token from env 2022-01-24 10:05:00 +01:00
66e5631c16 Use correct snyk installation 2022-01-23 12:08:23 +01:00
ee36881c49 Remove --no-dev 2022-01-23 12:06:24 +01:00
e4972d1b32 Also run as sh 2022-01-23 10:56:28 +01:00
1296ca7cfb Fix typo 2022-01-23 10:54:52 +01:00
6b1406fde9 Run as sh 2022-01-23 00:42:48 +01:00
775dcfa681 Merge branch 'master' of ssh://git.thisfro.ch:222/thisfro/pflaenz.li 2022-01-23 00:38:47 +01:00
b47e2c1b14 New pipeline 2022-01-23 00:37:00 +01:00
93eef67209 Update packages 2022-01-20 17:51:38 +01:00
865f8ec94f Implement basic title text search 2022-01-20 17:51:02 +01:00
71ca261cab Fix wish accesses 2022-01-19 22:40:11 +01:00
f54f663fb0 Rename offering to offer 2022-01-19 19:59:35 +01:00
a7b4bf09e4 Exclude sitemap from firewall 2022-01-19 16:22:40 +01:00
50e0245d4e Setup sitemap 2022-01-19 16:21:10 +01:00
46b5d4c107 Add meta tags 2022-01-19 15:58:59 +01:00
38b6016e0d Adjust footer size/position 2022-01-19 14:58:25 +01:00
661c519439 Update mastodeon link 2022-01-19 14:58:06 +01:00
ed481d9083 Restyle footer and add FAQ 2022-01-19 12:49:07 +01:00
9d640b9fd3 Fix address 2022-01-19 12:41:10 +01:00
774630ea14 Update docs 2022-01-18 20:25:49 +01:00
b8c6d41dc9 Implement changing of user password 2022-01-18 18:56:03 +01:00
9b3c970bba Verify CAPTCHA 2022-01-18 17:50:54 +01:00
946b30b486 Use uniqid for wishes and users 2022-01-17 21:25:31 +01:00
ee063cd439 Increase upload file size 2022-01-17 19:18:25 +01:00
d190971718 Don't use primary key for offers 2022-01-17 19:11:00 +01:00
539b7edffe Fix typo 2022-01-17 18:44:35 +01:00
9a9bbb310a Redisign filter 2022-01-17 18:31:51 +01:00
b8a5ade3e0 Only log warnings and above to file 2022-01-17 17:35:41 +01:00
b20418b9ef Remove outdated tesing image 2022-01-17 17:31:47 +01:00
2a5c9a000b Install new packages 2022-01-17 17:15:38 +01:00
5ba9e8cc85 Install only prod packages 2022-01-17 17:11:58 +01:00
8e7da7511a Handle nonexistent PLZs 2022-01-17 17:11:38 +01:00
7a3749c105 Try all records 2022-01-17 17:11:17 +01:00
9dbf0c72a1 Add file logging in prod as well 2022-01-17 17:00:15 +01:00
3d14e10ced Set binaries 2022-01-14 14:20:21 +01:00
f10512c46e Add stage 2022-01-14 13:57:44 +01:00
dc130dd9a4 Add shared files/dirs 2022-01-14 13:57:12 +01:00
e52753446b Implement filtering functionality 2022-01-14 13:52:16 +01:00
d0feff7d74 Fix typos 2022-01-12 23:28:25 +01:00
72418dd5a4 Resize image after upload using imagine 2022-01-12 17:51:45 +01:00
27474ad2ea Install imagine 2022-01-12 17:51:23 +01:00
00975df11b Bump versions 2022-01-12 16:32:25 +01:00
a04af6d6aa Update to symfony 5.4 2022-01-12 16:05:10 +01:00
c3a9e13dd0 Keep photos dir 2022-01-12 14:42:39 +01:00
c6eadf742c Generate coordinates when offer added 2022-01-12 14:42:02 +01:00
3a525f397c Remove staging .env 2022-01-12 14:40:58 +01:00
79c7a44123 Improvements, lang=en 2022-01-11 23:06:12 +01:00
fa03bfde3d Working deployer config 2022-01-11 22:43:43 +01:00
f7126dba43 Use mariadb for dev and prod 2022-01-11 22:42:01 +01:00
cb90ba1eb3 deployer WIP 2022-01-11 20:31:44 +01:00
e01500ad0f Run the command in the correct directory 2021-12-27 15:29:56 +01:00
d0e65fc8cb Create photos directory 2021-12-27 15:27:41 +01:00
16c34a0bf7 Pull newly built image first 2021-12-27 15:20:05 +01:00
a3bd7bf423 Fix registration issues 2021-12-27 15:02:55 +01:00
4260034fbe Merge branch 'master' of ssh://git.thisfro.ch:222/thisfro/pflaenz.li
Forgot to pull first
2021-12-27 14:32:55 +01:00
26acf70e1b Update tracking 2021-12-27 14:32:22 +01:00
63d2100c57 Use composer update instead of install 2021-12-25 15:32:16 +01:00
faa16735f5 Remove wrong option 2021-12-25 15:29:41 +01:00
9e39334486 Install dev packages with composer 2021-12-25 15:11:29 +01:00
a35e3e4f7a Fix typo 2021-12-25 14:45:49 +01:00
a832f3fc97 Use correct commands 2021-12-25 14:43:30 +01:00
750f5df4f3 Try other commands 2021-12-25 14:37:53 +01:00
d9098182d4 New staging deployment 2021-12-25 14:33:17 +01:00
6e240f95cf Use new hostname for postgres 2021-12-23 14:08:56 +01:00
249cd9aa59 Merge branch 'master' of ssh://git.thisfro.ch:222/thisfro/pflaenz.li
Amend last commit
2021-12-23 13:49:15 +01:00
58dbba3b1d Change to new staging dir 2021-12-23 13:48:43 +01:00
411a858089 Change to new staging dir 2021-12-23 13:40:48 +01:00
1a627ab7d9 Bump versions for staging 2021-12-23 13:24:39 +01:00
e59f3c05e3 Fix plz error 2021-12-22 17:36:12 +01:00
28f71ee479 Check if photoFilname is set before deleting 2021-09-14 23:43:29 +02:00
d1ba54c9f9 Remove last bits of deprecated parts 2021-09-14 14:26:07 +02:00
4ced33c694 Remove deprecated override URL 2021-09-14 14:18:21 +02:00
f2eed9d848 Switch to new authentication system 2021-09-14 14:03:27 +02:00
d277d9c93e Set titles 2021-09-04 16:08:40 +02:00
b0b0125862 Use new session handling 2021-09-04 13:10:18 +02:00
b2428f6ae3 Use new authentication and hashing system 2021-09-04 12:59:08 +02:00
10e250a1da Set titles 2021-09-04 12:00:46 +02:00
8f204a3685 Update to symfony 5.3 2021-09-03 17:00:00 +02:00
36e804abce Add title for offers overview 2021-09-03 16:41:07 +02:00
b0bb249a7a Fix typo 2021-09-03 16:40:42 +02:00
529a9a62ec Remove default from title 2021-09-03 16:38:26 +02:00
521217c00d Set title for offer view 2021-09-03 16:37:34 +02:00
dde15b8306 Prepend "Pflänz.li - " to title 2021-09-03 16:36:49 +02:00
5c25b036a4 Revert "Save cooridnates as GeoJSON in database"
This reverts commit ddb815d1b4.
2021-07-23 22:25:56 +02:00
34088972f8 Merge branch 'master' of ssh://git.thisfro.ch:222/thisfro/pflaenz.li 2021-07-23 11:32:48 +02:00
ddb815d1b4 Save cooridnates as GeoJSON in database 2021-07-16 11:00:02 +02:00
4656be5a32 Use less amigous function name 2021-07-15 16:00:55 +02:00
75ab34210f Update composer packages 2021-07-15 15:44:48 +02:00
df57a6f303 Refactor uploded file handling 2021-07-14 13:47:42 +02:00
7f485cee91 Set distance to null instead of 0 2021-07-14 12:03:07 +02:00
1758011412 Rename function to camelCase 2021-07-10 12:46:44 +02:00
307e251388 Use generic git icon in footer and set gitea link 2021-06-27 22:31:51 +02:00
81 changed files with 13455 additions and 15375 deletions

11
.env
View file

@ -23,12 +23,17 @@ APP_SECRET=8a390490e448f181dd8d3e6bd38efe6a
# 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"
DATABASE_URL="mysql://pflaenzli:develop@127.0.0.1:3306/pflaenzli?serverVersion=mariadb-10.5.13"
# DATABASE_URL="postgresql://postgres:develop@localhost:5432/pflaenzli?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
# MAILER_DSN=smtp://localhost
###< symfony/mailer ###
DEFAULT_URI='http://localhost:8080/'
DEFAULT_URI='http://localhost:8080/'
###> Ffiendlycaptcha ###
# CAPTCHA_SECRET=
# CAPTCHA_SITEKEY=
###< Ffiendlycaptcha ###

View file

@ -1,34 +0,0 @@
# 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/"

5
.gitignore vendored
View file

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

View file

@ -3,9 +3,9 @@
## Prerequisites
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.
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).
### MacOS
Using [homebrew](https://homebrew.sh):
@ -22,28 +22,27 @@ choco install nodejs npm yarn
## Setup the project
### 1. Clone the repo
Using SSH
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)
```
git clone ssh://git@git.thisfro.ch:222/thisfro/plant-exchange.git
git clone ssh://git@git.thisfro.ch:222/thisfro/pflaenz.li.git
```
### 2. Go to the directory
```
cd plant-exchange
cd pflaenz.li
```
### 3. Setup Database and other stuff
```
docker-compose up -d
docker compose up -d
```
To create the database (or use pgadmin):
To create the database
```
docker exec -it plant-exchange_db_1 psql -U postgres -c 'CREATE DATABASE plantex;'
```
bin/console doctrine:schema:create
```
and migrate
```
symfony console doctrine:migrations:migrate -n
bin/console doctrine:migrations:migrate -n
```
### 4. Install dependencies
@ -62,20 +61,22 @@ symfony serve --port 8080 --no-tls -d
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
Go to the database and add a user manually. For the password use this to generate the appropriate hash:
```
symfony console security:encode-password
```
Register your own account and set the `role` in the databse to `["ROLE_ADMIN"]`
### 7. Watch files
If you are editing `.scss` or other webpack files, you'll want to run
If you are editing `.scss` or `.js`, you'll want to run
```
yarn watch
```
this will automatically rebuild webpack files on save.
---
Thats it for now, you can start developing!
Thats it for now, you can start developing! :tada:
If you have any questions, ask [thisfro](https://git.thisfro.ch/thisfro) or creat an issue!
If you have any questions, ask [thisfro](https://git.thisfro.ch/thisfro) or create an issue!

View file

@ -1,31 +0,0 @@
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
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,30 +2,39 @@ node {
def app
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
}
stage('Build image') {
/* This builds the actual image; synonymous to
* docker build on the command line */
app = docker.build("thisfro/plantex")
stage('Install dependencies') {
// Install dependencies for build later
sh 'composer update'
sh 'yarn install'
}
stage('Test image') {
/* Ideally, we would run a test framework against our image.
* For this example, we're using a Volkswagen-type approach ;-) */
stage('Composer Vulnr test') {
snykSecurity(
snykInstallation: 'snyk-local',
targetFile: 'composer.lock',
)
}
app.inside {
// php 'bin/phpunit'
sh 'echo "success"'
}
stage('npm vulnr test') {
snykSecurity(
snykInstallation: 'snyk-local',
targetFile: 'package.json',
)
}
stage('Deploy staging') {
sh 'docker-compose --project-directory /opt/plant-exchange --file /opt/plant-exchange/docker-compose.yml up -d'
// Deploy to staging host
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,18 +1,27 @@
# plant-exchange
# Pflänz.li
## Idea
A platform where people can exchange plants. They can post what they have and search for others with [filters](#filters).
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.
## Tech stack
- [Symfony](https://symfony.com/)
- [PostgreSQL](https://www.postgresql.org/), maybe should be using MySQL for easier deployment?
- [MariaDB](https://www.mariadb.org)
- [Deployer](https://deployer.org)
Deployment: TBD
Can easily be depoyed to a LAMP server
## Admin dashboard
Find it under `/admin`
## 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!
Searching with filters such as:
@ -22,4 +31,5 @@ Searching with filters such as:
| Name | `string` | textfield |
| Category | `Category` | dropdown |
Distance from entered ZIP to the offer ZIP.
## Development
To get started with development, see [DEVELOPMENT.md](DEVELOPMENT.md)

View file

@ -8,7 +8,6 @@
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';
const $ = require('jquery');
// start the Stimulus application
require('bootstrap');
@ -18,15 +17,32 @@ import '@fortawesome/fontawesome-free/js/solid'
import '@fortawesome/fontawesome-free/js/regular'
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
import 'cookie-notice/dist/cookie.notice.min.js'
import 'cookie-notice/dist/cookie.notice.min';
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
});

15
assets/captcha.js Normal file
View file

@ -0,0 +1,15 @@
// 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()

5
assets/fileUpload.js Normal file
View file

@ -0,0 +1,5 @@
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 {
background-color: #ddd;
height: 6rem;
height: auto;
}
nav {
@ -16,7 +16,7 @@ nav {
}
.container {
min-height: calc(100vh - 10rem);
min-height: calc(100vh - 15rem);
}
.offer-img {
@ -68,6 +68,11 @@ nav {
margin-bottom: 0 !important;
}
.link-list {
list-style: none;
padding: 0;
}
@include media-breakpoint-up(sm) {
.show-img-container {
margin-right: 2rem;

View file

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

3336
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,11 @@
security:
encoders:
enable_authenticator_manager: true
password_hashers:
# use your user class name here
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
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
@ -15,12 +20,10 @@ security:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: app_user_provider
guard:
authenticators:
- App\Security\AppAuthenticator
custom_authenticators:
- App\Security\AppAuthenticator
logout:
path: app_logout
# where to redirect after logout
@ -47,4 +50,4 @@ security:
access_control:
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^(?!/(login|register|reset-password|offers|offer/*|imprint)), roles: ROLE_USER }
- { path: ^(?!/(login|register|reset-password|offers|offer/*|imprint|faq|sitemap.*)), roles: ROLE_USER }

View file

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

View file

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

View file

@ -4,6 +4,8 @@
# 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
parameters:
captcha.secret: '%env(CAPTCHA_SECRET)%'
captcha.sitekey: '%env(CAPTCHA_SITEKEY)%'
services:
# default configuration for services in *this* file

View file

@ -4,37 +4,69 @@ namespace Deployer;
require 'recipe/symfony.php';
// Project name
set('application', 'plant-exchange');
set('application', 'pflaenz.li');
// Project repository
set('repository', 'ssh://git@git.thisfro.ch:222/thisfro/plant-exchange.git');
set('repository', 'ssh://git@git.thisfro.ch:222/thisfro/pflaenz.li.git');
// [Optional] Allocate tty for git clone. Default value is false.
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
add('shared_files', []);
add('shared_dirs', []);
add('shared_files', ['public/.htaccess']);
add('shared_dirs', ['public/uploads']);
// Writable dirs by web server
add('writable_dirs', []);
// Set composer options
set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-scripts');
// 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
task('build', function () {
run('cd {{release_path}} && build');
task('upload:build', function() {
upload('public/build/', '{{release_path}}/public/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.
after('deploy:failed', 'deploy:unlock');
// Migrate database before symlink new release.
before('deploy:symlink', 'database:migrate');
before('deploy:symlink', 'database:migrate');

View file

@ -2,29 +2,20 @@ version: '3'
services:
db:
image: postgres:latest
image: mariadb:10.5
environment:
POSTGRES_PASSWORD: develop
MARIADB_USER: pflaenzli
MARIADB_PASSWORD: develop
MARIADB_DATABASE: pflaenzli
MARIADB_ROOT_PASSWORD: r00tpa55w0rd
ports:
- 5432:5432
- 3306:3306
volumes:
- 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
- mariadb-data:/var/lib/mysql
mailer:
image: schickling/mailcatcher
ports: [1025,1080]
volumes:
postgres-data:
pgadmin-data:
mariadb-data:

View file

@ -1,35 +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 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

@ -1,34 +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 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

@ -1,38 +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 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

@ -1,34 +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 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

@ -1,42 +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 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

@ -1,36 +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 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!
*/
final class Version20210519090349 extends AbstractMigration
final class Version20220111213438 extends AbstractMigration
{
public function getDescription(): string
{
@ -20,12 +20,12 @@ final class Version20210519090349 extends AbstractMigration
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE offering ALTER COLUMN description DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
}
}

View file

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

View file

@ -0,0 +1,32 @@
<?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

@ -0,0 +1,33 @@
<?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');
}
}

View file

@ -10,23 +10,22 @@ use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210422132314 extends AbstractMigration
final class Version20220119172053 extends AbstractMigration
{
public function getDescription() : string
public function getDescription(): string
{
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->addSql('ALTER TABLE "user" ADD is_verified BOOLEAN NOT NULL DEFAULT TRUE');
$this->addSql('RENAME TABLE offering TO offer');
}
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->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "user" DROP is_verified');
$this->addSql('RENAME TABLE offer TO offering');
}
}

15330
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

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

View file

@ -3,7 +3,7 @@
namespace App\Controller\Admin;
use App\Entity\User;
use App\Entity\Offering;
use App\Entity\Offer;
use App\Entity\Wish;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
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::linktoDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linkToCrud('User', 'fas fa-user', User::class);
yield MenuItem::linkToCrud('Offering', 'fas fa-seedling', Offering::class);
yield MenuItem::linkToCrud('Offer', 'fas fa-seedling', Offer::class);
yield MenuItem::linkToCrud('Wish', 'fas fa-star', Wish::class);
}
}

View file

@ -2,7 +2,7 @@
namespace App\Controller\Admin;
use App\Entity\Offering;
use App\Entity\Offer;
use App\Entity\User;
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\DateTimeField;
class OfferingCrudController extends AbstractCrudController
class OfferCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Offering::class;
return Offer::class;
}
public function configureFields(string $pageName): iterable

View file

@ -10,15 +10,21 @@ use Twig\Environment;
class AppController extends AbstractController
{
#[Route('/', name: 'homepage')]
#[Route('/', name: 'homepage', options: ["sitemap" => true])]
public function index(): Response
{
return $this->render('app/index.html.twig');
}
#[Route('/imprint', name: 'imprint')]
#[Route('/imprint', name: 'imprint', options: ["sitemap" => true])]
public function imprint(): Response
{
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,14 +2,17 @@
namespace App\Controller;
use App\Entity\Offering;
use App\Form\OfferingFormType;
use App\Entity\Offer;
use App\Form\OfferFormType;
use App\Form\OfferFilterFormType;
use App\Repository\OfferingRepository;
use App\Repository\OfferRepository;
use App\Repository\WishRepository;
use App\Service\PlzToCoordinate;
use App\Service\DistanceCalculator;
use App\Service\PhotoResizer;
use App\Service\OfferPhotoHelper;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -23,24 +26,62 @@ class OfferController extends AbstractController
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
public function __construct(EntityManagerInterface $entityManager, PhotoResizer $photoresizer)
{
$this->entityManager = $entityManager;
$this->photoresizer = $photoresizer;
}
#[Route('/offers', name: 'offers')]
public function index(Environment $twig, OfferingRepository $offerRepository): Response
#[Route('/offers', name: 'offers', options: ["sitemap" => true])]
public function showAll(Request $request, OfferRepository $offerRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response
{
return new Response($twig->render('offer/index.html.twig', [
'offers' => $offerRepository->findAll(),
]));
$form = $this->createForm(OfferFilterFormType::class);
$form->handleRequest($request);
$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')]
public function newOffer(Request $request, string $photoDir): Response
public function newOffer(Request $request, PlzToCoordinate $plzconverter, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response
{
$offer = new Offering();
$form = $this->createForm(OfferingFormType::class, $offer);
$offer = new Offer();
$form = $this->createForm(OfferFormType::class, $offer);
$user = $this->getUser();
$form->handleRequest($request);
@ -48,22 +89,22 @@ class OfferController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
$offer->setByUser($user);
$offer->setCreatedAt(new \DateTime());
$offer->setUrlId(uniqid());
$coordinate = $plzconverter->convertPlzToCoordinate($form['zipCode']->getData());
if ($coordinate != null) {
$offer->setCoordinate($coordinate);
}
if ($photo = $form['photo']->getData()) {
$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);
$offerPhotoHelper->uploadOfferPhoto($photoDir, $photo, $offer);
}
$this->entityManager->persist($offer);
$this->entityManager->flush();
$this->photoresizer->resize($photoDir.'/'.$offer->getPhotoFilename());
$this->addFlash("success", "Successfully added the new offer!");
return $this->redirectToRoute('offers');
}
@ -74,10 +115,10 @@ class OfferController extends AbstractController
]);
}
#[Route('/offer/{id}', name: 'show_offer')]
public function show_offer(Offering $offer, WishRepository $wishRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response
#[Route('/offer/{urlId}', name: 'show_offer')]
public function showOffer(Offer $offer, WishRepository $wishRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response
{
$distance = 0;
$distance = null;
$user = $this->getUser();
$offerPlz = $offer->getZipCode();
@ -88,7 +129,16 @@ class OfferController extends AbstractController
if (isset($userPlz))
{
$distance = $distanceCalculator->calculateDistance($plzconverter->getCoordinates($offerPlz), $plzconverter->getCoordinates($userPlz));
if (isset($offerPlz))
{
$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', [
@ -99,10 +149,10 @@ class OfferController extends AbstractController
]);
}
#[Route('/offer/edit/{id}', name: 'edit_offer')]
public function editOffer(Offering $offer, OfferingRepository $offerRepository, Request $request, string $photoDir): Response
#[Route('/offer/edit/{urlId}', name: 'edit_offer')]
public function editOffer(Offer $offer, Request $request, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response
{
$form = $this->createForm(OfferingFormType::class, $offer);
$form = $this->createForm(OfferFormType::class, $offer);
$user = $this->getUser();
if ($offer->getByUser() === $user)
{
@ -113,19 +163,17 @@ class OfferController extends AbstractController
$offer->setCreatedAt(new \DateTime());
if ($photo = $form['photo']->getData()) {
$filename = uniqid(random_bytes(6)).'.'.$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);
$oldFilename = $offer->getPhotoFilename();
$offerPhotoHelper->uploadOfferPhoto($photoDir, $photo, $offer);
$offerPhotoHelper->deleteOfferPhoto($photoDir, $oldFilename);
}
$this->entityManager->persist($offer);
$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', [
@ -138,16 +186,16 @@ class OfferController extends AbstractController
throw new HttpException(403, "No permission");
}
#[Route('/offer/delete/{id}', name: 'delete_offer')]
public function deleteOffer(Offering $offer, string $photoDir): Response
#[Route('/offer/delete/{urlId}', name: 'delete_offer')]
public function deleteOffer(Offer $offer, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response
{
$user = $this->getUser();
if ($offer->getByUser() === $user)
{
if ($offer->getPhotoFilename())
if($offer->getPhotoFilename() != null)
{
unlink($photoDir . '/' . $offer->getPhotoFilename());
$offerPhotoHelper->deleteOfferPhoto($photoDir, $offer->getPhotoFilename());
}
$this->entityManager->remove($offer);
$this->entityManager->flush();
@ -162,13 +210,23 @@ class OfferController extends AbstractController
}
#[Route('/myoffers', name: 'own_offers')]
public function ownOffers(OfferingRepository $offeringRepository): Response
public function ownOffers(OfferRepository $offerRepository): Response
{
$user = $this->getUser();
return $this->render('user/offers.html.twig', [
'user' => $user,
'offers' => $offeringRepository->findByUser($user),
'offers' => $offerRepository->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,8 +4,9 @@ namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\EmailVerifier;
use App\Security\AppAuthenticator;
use App\Security\EmailVerifier;
use App\Service\CaptchaVerifier;
use App\Repository\UserRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -13,8 +14,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
class RegistrationController extends AbstractController
@ -26,42 +26,42 @@ class RegistrationController extends AbstractController
$this->emailVerifier = $emailVerifier;
}
#[Route('/register', name: 'app_register')]
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, AppAuthenticator $authenticator): Response
#[Route('/register', name: 'app_register', options: ["sitemap" => true])]
public function register(Request $request, UserPasswordHasherInterface $passwordEncoder, CaptchaVerifier $captchaVerifier): Response
{
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
if ($captchaVerifier->isVerified($form->get('captcha_solution')->getData(), $this->getParameter('captcha.secret'), $this->getParameter('captcha.sitekey')) == true) {
$user->setUrlId(uniqid());
// encode the plain password
$user->setPassword(
$passwordEncoder->hashPassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from(new Address('no-reply@pflaenz.li', 'Pflänzli no-reply'))
->to($user->getEmail())
->subject('Please Confirm your Email')
->htmlTemplate('registration/confirmation_email.html.twig')
);
// do anything else you need here, like send an email
// generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from(new Address('no-reply@pflaenz.li', 'Pflänzli no-reply'))
->to($user->getEmail())
->subject('Please Confirm your Email')
->htmlTemplate('registration/confirmation_email.html.twig')
);
return $guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$authenticator,
'main' // firewall name in security.yaml
);
return $this->render('registration/created.html.twig');
}
else {
$this->addFlash('error', 'CAPTCHA failed');
}
}
return $this->render('registration/register.html.twig', [
@ -70,32 +70,20 @@ class RegistrationController extends AbstractController
}
#[Route('/verify/email', name: 'app_verify_email')]
public function verifyUserEmail(Request $request, UserRepository $userRepository): Response
public function verifyUserEmail(Request $request): Response
{
$id = $request->get('id');
if (null === $id) {
return $this->redirectToRoute('app_register');
}
$user = $userRepository->find($id);
if (null === $user) {
return $this->redirectToRoute('app_register');
}
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
// validate email confirmation link, sets User::isVerified=true and persists
try {
$this->emailVerifier->handleEmailConfirmation($request, $user);
$this->emailVerifier->handleEmailConfirmation($request, $this->getUser());
} catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('verify_email_error', $exception->getReason());
return $this->redirectToRoute('app_register');
}
// @TODO Change the redirect on success and handle or remove the flash message in your templates
$this->addFlash('success', 'Your email address has been verified.');
return $this->redirectToRoute('user_page');
}
}

View file

@ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
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.
*/
#[Route('/reset/{token}', name: 'app_reset_password')]
public function reset(Request $request, UserPasswordEncoderInterface $passwordEncoder, string $token = null): Response
public function reset(Request $request, UserPasswordHasherInterface $passwordEncoder, string $token = null): Response
{
if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being
@ -106,12 +106,13 @@ class ResetPasswordController extends AbstractController
$this->resetPasswordHelper->removeResetRequest($token);
// Encode the plain password, and set it.
$encodedPassword = $passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
$user->setPassword(
$passwordEncoder->hashPassword(
$user,
$form->get('plainPassword')->getData()
)
);
$user->setPassword($encodedPassword);
$this->getDoctrine()->getManager()->flush();
// 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
{
/**
* @Route("/login", name="app_login")
* @Route("/login", name="app_login", options={"sitemap"=true})
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{

View file

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

View file

@ -5,8 +5,9 @@ namespace App\Controller;
use App\Entity\Wish;
use App\Entity\User;
use App\Form\WishFormType;
use App\Form\ChangePasswordFormType;
use App\Repository\OfferingRepository;
use App\Repository\OfferRepository;
use App\Repository\WishRepository;
use Doctrine\ORM\EntityManagerInterface;
@ -14,7 +15,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
class UserController extends AbstractController
@ -27,7 +30,7 @@ class UserController extends AbstractController
}
#[Route('/user', name: 'user_page')]
public function user(OfferingRepository $offeringRepository): Response
public function user(OfferRepository $offerRepository, Request $request, UserPasswordHasherInterface $passwordEncoder): Response
{
$user = $this->getUser();
@ -36,30 +39,48 @@ class UserController extends AbstractController
$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', [
'user' => $user,
'offers' => $offeringRepository->findByUser($user),
'changePassword_form' => $form->createView(),
]);
}
#[Route('/user/offers', name: 'user_offers')]
public function userOffers(OfferingRepository $offeringRepository): Response
public function userOffers(OfferRepository $offerRepository): Response
{
$user = $this->getUser();
return $this->render('user/public.html.twig', [
'user' => $user,
'offers' => $offeringRepository->findByUser($user),
'offers' => $offerRepository->findByUser($user),
]);
}
#[Route('/user/{id}', name: 'user_public')]
public function show_user(User $user, OfferingRepository $offeringRepository, WishRepository $wishRepository): Response
#[Route('/user/{urlId}', name: 'user_public')]
public function show_user(User $user, OfferRepository $offerRepository, WishRepository $wishRepository): Response
{
return $this->render('user/public.html.twig', [
'username' => $user->getUsername(),
'wishes' => $wishRepository->findByUser($user),
'offers' => $offeringRepository->findByUser($user),
'offers' => $offerRepository->findByUser($user),
]);
}
@ -74,6 +95,7 @@ class UserController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
$wish->setByUser($user);
$wish->setUrlId(uniqid());
$this->entityManager->persist($wish);
$this->entityManager->flush();
@ -89,7 +111,7 @@ class UserController extends AbstractController
]);
}
#[Route('/wish/delete/{id}', name: 'delete_wish')]
#[Route('/wish/delete/{urlId}', name: 'delete_wish')]
public function deleteWish(Wish $wish): Response
{
$user = $this->getUser();

View file

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

View file

@ -27,6 +27,11 @@ class Wish
*/
private $byUser;
/**
* @ORM\Column(type="string", length=13)
*/
private $urlId;
public function getId(): ?int
{
return $this->id;
@ -60,4 +65,16 @@ class Wish
{
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,6 +5,7 @@ namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
@ -39,6 +40,7 @@ class ChangePasswordFormType extends AbstractType
// this is read and encoded in the controller
'mapped' => false,
])
->add('submit', SubmitType::class)
;
}

View file

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

View file

@ -6,12 +6,16 @@ use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
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\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
class RegistrationFormType extends AbstractType
{
@ -20,19 +24,14 @@ class RegistrationFormType extends AbstractType
$builder
->add('email', EmailType::class)
->add('username')
->add('zipcode')
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
->add('zipcode', NumberType::class, [
'label' => 'ZIP'
])
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'label' => 'Password',
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
@ -45,6 +44,30 @@ 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;
use App\Entity\Offering;
use App\Entity\Offer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Offering|null find($id, $lockMode = null, $lockVersion = null)
* @method Offering|null findOneBy(array $criteria, array $orderBy = null)
* @method Offering[] findAll()
* @method Offering[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
* @method Offer|null find($id, $lockMode = null, $lockVersion = null)
* @method Offer|null findOneBy(array $criteria, array $orderBy = null)
* @method Offer[] findAll()
* @method Offer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OfferingRepository extends ServiceEntityRepository
class OfferRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Offering::class);
parent::__construct($registry, Offer::class);
}
// /**
// * @return Offering[] Returns an array of Offering objects
// * @return Offer[] Returns an array of Offer objects
// */
public function findByUser($user)
{
@ -44,8 +44,21 @@ class OfferingRepository 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): ?Offering
public function findOneBySomeField($value): ?Offer
{
return $this->createQueryBuilder('o')
->andWhere('o.exampleField = :val')

View file

@ -2,105 +2,59 @@
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
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\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class AppAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
class AppAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
private UrlGeneratorInterface $urlGenerator;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
public function authenticate(Request $request): PassportInterface
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
$email = $request->request->get('email', '');
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
$request->getSession()->set(Security::LAST_USERNAME, $email);
return new Passport(
new UserBadge($email),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->get('_csrf_token')),
]
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$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)) {
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('user_page'));
}
protected function getLoginUrl()
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
}

View file

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

View file

@ -0,0 +1,42 @@
<?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

@ -0,0 +1,37 @@
<?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,13 +6,25 @@ use Location\Coordinate;
class PlzToCoordinate
{
public function getCoordinates(int $plz)
public function convertPlzToCoordinate(int $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);
if (isset($result->records[0]->fields->geo_point_2d[0]) && isset($result->records[0]->fields->geo_point_2d[1])) {
$coordinate = new Coordinate($result->records[0]->fields->geo_point_2d[0], $result->records[0]->fields->geo_point_2d[1]);
for($i = 0; $i < count($result->records); $i++) {
try {
$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;

View file

@ -97,6 +97,9 @@
"friendsofphp/proxy-manager-lts": {
"version": "v1.0.3"
},
"imagine/imagine": {
"version": "1.2.4"
},
"laminas/laminas-code": {
"version": "4.2.0"
},
@ -115,6 +118,9 @@
"nikic/php-parser": {
"version": "v4.10.4"
},
"presta/sitemap-bundle": {
"version": "v3.2.1"
},
"psr/cache": {
"version": "2.0.0"
},
@ -127,6 +133,9 @@
"psr/log": {
"version": "1.1.3"
},
"samayo/bulletproof": {
"version": "v4.0.1"
},
"sensio/framework-extra-bundle": {
"version": "5.2",
"recipe": {
@ -249,9 +258,6 @@
"src/Kernel.php"
]
},
"symfony/http-client-contracts": {
"version": "v2.4.0"
},
"symfony/http-foundation": {
"version": "v5.2.4"
},
@ -309,6 +315,9 @@
"symfony/orm-pack": {
"version": "v2.1.0"
},
"symfony/password-hasher": {
"version": "v5.3.7"
},
"symfony/phpunit-bridge": {
"version": "5.1",
"recipe": {
@ -339,15 +348,9 @@
"symfony/polyfill-mbstring": {
"version": "v1.22.1"
},
"symfony/polyfill-php73": {
"version": "v1.22.1"
},
"symfony/polyfill-php80": {
"version": "v1.22.1"
},
"symfony/polyfill-php81": {
"version": "v1.23.0"
},
"symfony/polyfill-uuid": {
"version": "v1.22.1"
},
@ -395,9 +398,6 @@
"symfony/security-csrf": {
"version": "v5.2.4"
},
"symfony/security-guard": {
"version": "v5.2.4"
},
"symfony/security-http": {
"version": "v5.2.6"
},

View file

@ -0,0 +1,11 @@
{% 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>Pflänzl.i</p>
<p>Langgrütstrasse 89</p>
<p>8047 Zürich</p>
<p>Pflänz.li</p>
<p>Marbachweg 22</p>
<p>8041 Zürich</p>
<p>Switzerland</p>
<p>Email: contact@pflaenz.li</p>
<p>Website: pflänzl.i</p>
<p>Website: pflänz.li</p>
<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>

View file

@ -2,6 +2,11 @@
{% block title %}Home{% endblock %}
{% block meta %}
<meta name="description" content="A platform to trade plants." >
{% endblock %}
{% block body %}
<div class="jumbotron">
<h1 class="display-4">Welcome to Pflänz.li</h1>

View file

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

View file

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

View file

@ -1,85 +1,131 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Plant Exchange Home{% endblock %}</title>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pflänz.li -
{% 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 %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% block javascripts %}{% endblock %}
<!-- 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 %}
{{ encore_entry_script_tags('app') }}
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<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">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href=" {{ path('offers') }} "><i class="fas fa-seedling"></i> Offers</a>
</li>
<li class="nav-item dropdown">
<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>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ path('own_offers') }}"><i class="fas fa-seedling"></i> 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 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>
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function () {
var u = "https://analytics.thisfro.ch/";
_paq.push([
'setTrackerUrl',
u + 'matomo.php'
]);
_paq.push(['setSiteId', '2']);
var d = document,
g = d.createElement('script'),
s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
<div class="container pt-5">
{% block body %}{% endblock %}
</div>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<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>
<footer class="text-center text-white">
<div class="pt-1">
<section class="mb-1">
<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>
<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>
</section>
</div>
<div class="text-center text-dark">
<a href="https://creativecommons.org"><i class="fab fa-creative-commons"></i><i class="fab fa-creative-commons-by"></i></a>
<a class="text-dark" href="{{ path('homepage') }}">pflänz.li</a>
</div>
</footer>
</body>
</html>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href=" {{ path('offers') }} ">
<i class="fas fa-seedling"></i>
Offers</a>
</li>
<li class="nav-item dropdown">
<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>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ path('own_offers') }}">
<i class="fas fa-seedling"></i>
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,5 +1,7 @@
{% extends 'base.html.twig' %}
{% block title%}Edit offer{% endblock %}
{% block body %}
{% for message in app.flashes('error') %}
<div class="alert alert-error" role="alert">
@ -7,7 +9,7 @@
</div>
{% endfor %}
<h1 class="mb-3">Add new offer</h1>
<h1 class="mb-3">Edit offer</h1>
{{ form_start(offer_form) }}
{{ form_row(offer_form.title) }}
{{ form_row(offer_form.zipCode) }}

View file

@ -1,5 +1,7 @@
{% extends 'base.html.twig' %}
{% block title %}Offers{% endblock %}
{% block body %}
{% for message in app.flashes('success') %}
@ -7,6 +9,20 @@
{{ message }}
</div>
{% 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>
{% if offers|length > 0 %}
@ -14,7 +30,7 @@
{% for offer in offers %}
<div class="mb-5">
<div class="card offer h-100">
<a href="{{ path('show_offer', {'id': offer.id }) }}">
<a href="{{ path('show_offer', {'urlId': offer.urlId }) }}">
{% if offer.photoFilename %}
<img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" />
{% else %}
@ -25,7 +41,7 @@
</div>
</a>
<div class="card-footer offer-footer">
<a class="user-link" href="{{ path('user_public', { 'id': offer.byuser.id }) }}">
<a class="user-link" href="{{ path('user_public', { 'urlId': offer.byuser.urlId }) }}">
<p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p>
</a>
<p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p>
@ -35,6 +51,6 @@
{% endfor %}
</div>
{% else %}
<div class="alert alert-warning" role="alert">There are currently no active offers.</div>
<div class="alert alert-warning" role="alert">There are no active offers with the current filter.</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,9 @@
{% 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,32 +1,35 @@
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block title %}Register
{% endblock %}
{% block meta %}
<meta name="description" content="Register for pflänz.li" />
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('captcha') }}
{% endblock %}
{% block body %}
{% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% endfor %}
{% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% 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_row(registrationForm.email) }}
{{ form_row(registrationForm.username) }}
{{ form_row(registrationForm.zipcode, {
label: 'PLZ'
}) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
{{ 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) }}
{{ form_start(registrationForm) }}
{{ form_widget(registrationForm) }}
<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" id="captcha"></div>
</div>
</div>
{{ form_end(registrationForm) }}
{% endblock %}

View file

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

View file

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

View file

@ -1,55 +1,33 @@
{% extends 'base.html.twig' %}
{% block title %}User{% endblock %}
{% block title %}User
{% endblock %}
{% block body %}
{% for message in app.flashes('error') %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endfor %}
{% for message in app.flashes('success') %}
<div class="alert alert-success" role="alert">
{{ message }}
</div>
{% endfor %}
{% for message in app.flashes('error') %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% endfor %}
{% for message in app.flashes('success') %}
<div class="alert alert-success" role="alert">
{{ message }}
</div>
{% endfor %}
<div class="alert alert-info" role="alert">
Please note: This is not yet functional!
</div>
<div class="mb-5">
<h1>Hello
{{ user.username }}!</p>
</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">
<h1>Hello {{ user.username }}!</p>
</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>
<div class="mb-3">
<h2>Delete Account</h2>
<button class="btn btn-danger">Delete Account</button>
</div>
{% endblock %}

View file

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

View file

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

View file

@ -28,7 +28,7 @@
{% for wish in wishes %}
<li class="list-group-item d-flex justify-content-between align-items-center"> {{ wish.title }}
<span>
<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>
<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>
</span>
</li>
{% endfor %}

View file

@ -21,6 +21,8 @@ Encore
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.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)
.enableStimulusBridge('./assets/controllers.json')

8172
yarn.lock

File diff suppressed because it is too large Load diff