Compare commits

..

No commits in common. "develop" and "Release_2305.1" have entirely different histories.

55 changed files with 1238 additions and 1574 deletions

54
Jenkinsfile vendored
View File

@ -13,9 +13,8 @@ pipeline {
}
agent {
dockerfile {
filename 'docker/Dockerfile'
filename 'docker/Dockerfile-test'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
@ -27,9 +26,8 @@ pipeline {
stage('Unit Testing') {
agent {
dockerfile {
filename 'docker/Dockerfile'
filename 'docker/Dockerfile-test'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
@ -37,7 +35,7 @@ pipeline {
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
}
post {
always {
success {
archiveArtifacts 'unit_test_results.xml'
}
}
@ -46,9 +44,8 @@ pipeline {
stage('Static Code Analysis') {
agent {
dockerfile {
filename 'docker/Dockerfile'
filename 'docker/Dockerfile-test'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
@ -56,51 +53,10 @@ pipeline {
sh 'php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
}
post {
always {
success {
archiveArtifacts 'static_code_analysis_results.json'
}
}
}
stage('Prepare Docker release') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
npm_config_cache="${WORKSPACE}/.npm"
}
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
script {
sh script: 'git clean -ffdx', label: 'Clean repository'
env.VERSION = sh(script: 'git describe --tags --always --match "Release_*" HEAD', returnStdout: true).trim()
sh script: 'docker/scripts/release.sh', label: 'Release script'
sh script: "rm -rf ${env.COMPOSER_HOME} ${env.npm_config_cache}"
}
}
}
stage('Release Docker image') {
steps {
script {
withDockerRegistry([credentialsId: 'gitea-system-user', url: 'https://git.esoko.eu/']) {
sh script: 'docker buildx create --use --bootstrap --platform=linux/arm64,linux/amd64 --name multi-platform-builder'
sh script: """docker buildx build \
--platform linux/amd64,linux/arm64 \
-f docker/Dockerfile \
--target mapg_release \
-t git.esoko.eu/esoko/mapguesser:${env.VERSION} \
--push \
.""",
label: 'Build Docker image'
}
}
}
}
}
}

111
README.md
View File

@ -1,16 +1,58 @@
# MapGuesser
[![Build Status](https://ci.esoko.eu/job/mapguesser/job/develop/badge/icon)](https://ci.esoko.eu/job/mapguesser/job/develop/)
[![Build Status](https://jenkins.e5tv.hu/job/mapguesser/job/develop/badge/icon)](https://jenkins.e5tv.hu/job/mapguesser/job/develop/)
This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
## Installation
### Clone the Git repository
The first step is obviously cloning the repository to your machine:
```
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
```
All the commands listed here should be executed from the repository root.
### Setup Docker stack (recommended)
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
All you have to do is executing the following command:
```
docker-compose up -d
```
Attach shell to the container of `mapguesser_app`:
```
docker exec -it mapguesser_app_1 bash
```
All of the following commands should be executed there.
### Manual setup (alternative)
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
### Initialize project
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
```
composer create-project
```
### Set environment variables
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment. Check `.env.example` for reference.
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
**Important: `DEV` should NOT be set for production! See section Development if you want to use the application in development mode.**
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode.
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value.
#### API keys
@ -33,72 +75,31 @@ LEAFLET_TILESERVER_SUBDOMAINS=abc
LEAFLET_TILESERVER_ATTRIBUTION="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
```
### Docker Compose
### (Production only) Create cron job
Create a `docker-compose.yml` file. The example code below assumes that `.env` is placed in the same folder.
```yml
version: '3'
services:
app:
image: git.esoko.eu/esoko/mapguesser:latest
depends_on:
mariadb:
condition: service_healthy
ports:
- 80:80
- 8090:8090
volumes:
- .env:/var/www/mapguesser/.env
mariadb:
image: mariadb:10.3
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: 'root'
MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: 'mapguesser'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
volumes:
mysql:
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour:
```
Execute the following command:
```bash
docker compose up -d
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
```
### Finalize installation
**And you are done!** The application is ready to use. You can create the first administrative user with the following command after attaching to the `app` container:
After you followed the above steps, execute the following command:
```
./mapg user:add EMAIL USERNAME PASSWORD admin
scripts/install.sh
```
## Development
**Warning: Because of a known issue the image `mapguesser_multi` fails to run without the installation steps. You have to relauch `docker-compose up -d` after you finished the installation process.**
### Set environment variables
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **mapg@mapg.dev**, password is **123456**. In production mode you should create the first administrative user with the following command:
`.env.example` should be copied to `.env` into the repo root. Only the variables for external dependencies (API keys, map attribution, etc.) should be adapted in. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value. **`DEV=1` should be set for development!**
### Docker Compose
Execute the following command from the repo root:
```bash
docker compose up -d
```
./mapg user:add EMAIL PASSWORD admin
```
**And you are done!** You can reach the application on http://localhost. The mails that are sent by the application can be found on http://localhost:8080. If needed, the database server can be directly reached on localhost:3306, or you can use Adminer web interface on http://localhost:9090
You might have to attach to the `app` container, e.g. for creating users, `composer update`, etc.
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306.
---

View File

@ -10,10 +10,11 @@
}
],
"require": {
"esoko/soko-web": "0.15"
"esoko/soko-web": "0.10",
"fzaninotto/faker": "^1.9"
},
"require-dev": {
"phpunit/phpunit": "^10.3",
"phpunit/phpunit": "^9.6",
"phpstan/phpstan": "^1.10"
},
"autoload": {

1068
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,14 +15,6 @@ CREATE TABLE `maps` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `migrations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`type` enum('structure', 'data') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
DROP TABLE IF EXISTS `places`;
CREATE TABLE `places` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,

View File

@ -0,0 +1 @@
<?php //empty on purpose

View File

@ -1,20 +0,0 @@
<?php
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Util\UsernameGenerator;
use SokoWeb\Database\Query\Select;
$select = new Select(Container::$dbConnection);
$users = Container::$persistentDataManager->selectMultipleFromDb($select, User::class);
$userRepository = new UserRepository();
$usernameGenerator = new UsernameGenerator();
foreach ($users as $user) {
do {
$username = $usernameGenerator->generate();
} while ($userRepository->getByUsername($username));
$user->setUsername($username);
Container::$persistentDataManager->saveToDb($user);
}

View File

@ -0,0 +1,6 @@
CREATE TABLE `migrations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`type` enum('structure', 'data') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -1,3 +0,0 @@
ALTER TABLE `users`
ADD `username` VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL AFTER `email`,
ADD UNIQUE `username` (`username`);

View File

@ -2,20 +2,22 @@ version: '3'
services:
app:
build:
context: .
dockerfile: docker/Dockerfile
target: mapg_dev
depends_on:
mariadb:
condition: service_healthy
context: ./docker
dockerfile: Dockerfile-app
ports:
- 80:80
volumes:
- .:/var/www/mapguesser
multi:
build:
context: ./docker
dockerfile: Dockerfile-multi
ports:
- 5000:5000
- 8090:8090
- 9229:9229
volumes:
- .:/var/www/mapguesser
working_dir: /var/www/mapguesser
mariadb:
image: mariadb:10.3
ports:
@ -27,13 +29,6 @@ services:
MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: 'mapguesser'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
adminer:
image: adminer:4.8.1-standalone
ports:

View File

@ -1,44 +0,0 @@
FROM ubuntu:22.04 AS mapg_base
ENV DEBIAN_FRONTEND noninteractive
RUN apt update --fix-missing && apt install -y sudo curl git unzip mariadb-client nginx \
php-apcu php8.1-cli php8.1-curl php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-zip php8.1-xml
RUN mkdir -p /run/php
COPY docker/configs/nginx.conf /etc/nginx/sites-available/default
COPY docker/scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh
COPY docker/scripts/install-nodejs.sh install-nodejs.sh
RUN ./install-nodejs.sh
RUN npm install -g uglify-js clean-css-cli svgo yarn
FROM mapg_base AS mapg_dev
RUN apt update --fix-missing && apt install -y php-xdebug
RUN echo "xdebug.remote_enable = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_autostart = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_connect_back = 1" >> /etc/php/8.1/mods-available/xdebug.ini
EXPOSE 80
EXPOSE 5000
EXPOSE 8090
EXPOSE 9229
ENTRYPOINT docker/scripts/entry-point-dev.sh
FROM mapg_base AS mapg_release
RUN apt update --fix-missing && apt install -y cron
WORKDIR /var/www/mapguesser
COPY ./ /var/www/mapguesser
RUN rm -rf /var/www/mapguesser/.git
EXPOSE 80
EXPOSE 8090
ENTRYPOINT docker/scripts/entry-point.sh

30
docker/Dockerfile-app Normal file
View File

@ -0,0 +1,30 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install Nginx, PHP and further necessary packages
RUN apt update --fix-missing
RUN apt install -y curl git unzip mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip php7.4-xml
# Configure Nginx with PHP
RUN mkdir -p /run/php
COPY configs/nginx.conf /etc/nginx/sites-available/default
RUN echo "xdebug.remote_enable = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_autostart = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_connect_back = 1" >> /etc/php/7.4/mods-available/xdebug.ini
# Install Composer
COPY scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
RUN npm install -g uglify-js clean-css-cli svgo yarn
EXPOSE 80
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/sbin/php-fpm7.4 -F & /usr/sbin/nginx -g 'daemon off;'

16
docker/Dockerfile-multi Normal file
View File

@ -0,0 +1,16 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install necessary packages
RUN apt update --fix-missing
RUN apt install -y curl build-essential
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi

6
docker/Dockerfile-test Normal file
View File

@ -0,0 +1,6 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
RUN apt update && apt install -y curl git unzip php7.4-cli php7.4-mbstring php7.4-xml
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View File

@ -1,15 +1,11 @@
map $http_x_forwarded_proto $forwarded_scheme {
default $scheme;
http http;
https https;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/mapguesser/public;
index index.php index.html index.htm index.nginx-debian.html;
server_name mapguesser-dev.ch;
location / {
@ -18,8 +14,7 @@ server {
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param REQUEST_SCHEME $forwarded_scheme;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
}
location ~ /\.ht {

View File

@ -1 +0,0 @@
0 * * * * /var/www/mapguesser/mapg db:maintain

View File

@ -1,40 +0,0 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
if [ -f .env ]; then
composer install
else
composer create-project
fi
echo "Installing NPM packages..."
(cd multi && npm install)
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Migrating DB..."
./mapg db:migrate
echo "Set runner user based on owner of .env..."
if ! getent group mapg; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID mapg
fi
if ! id -u mapg; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID mapg
fi
sed -i -e "s/^user = .*$/user = mapg/g" -e "s/^group = .*$/group = mapg/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
sudo -u mapg -g mapg /usr/bin/node --inspect=0.0.0.0:9229 multi &
wait -n
exit $?

View File

@ -1,32 +0,0 @@
#!/bin/bash
set -e
echo "Migrating DB..."
./mapg db:migrate
echo "Installing crontab..."
/usr/bin/crontab docker/scripts/cron
echo "Set runner user based on owner of .env..."
if ! getent group mapg; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID mapg
fi
if ! id -u mapg; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID mapg
fi
chown mapg:mapg cache
sed -i -e "s/^user = .*$/user = mapg/g" -e "s/^group = .*$/group = mapg/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/cron -f &
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
sudo -u mapg -g mapg /usr/bin/node multi &
wait -n
exit $?

View File

@ -1,14 +0,0 @@
#!/bin/sh
set -e
apt update
apt install -y ca-certificates curl gnupg
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
apt update
apt install -y nodejs

View File

@ -1,30 +0,0 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
composer create-project --no-dev
echo "Installing NPM packages..."
(cd multi && npm install)
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php
echo "Minifying JS, CSS and SVG files..."
find public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
echo "Linking view files..."
./mapg view:link
rm .env

View File

@ -1,43 +1,17 @@
{
"name": "mapguesser-multi",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mapguesser-multi",
"license": "GNU AGPL 3.0",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
}
},
"node_modules/dotenv": {
"lockfileVersion": 1,
"dependencies": {
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"engines": {
"node": ">=8"
}
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"node_modules/ws": {
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
}
}
}

View File

@ -376,7 +376,7 @@ header>p>span {
padding-left: 6px;
}
header>p>span>a:link, header>p>span>a:visited, footer>p>a:link, footer>p>a:visited {
header>p>span>a:link, header>p>span>a:visited {
color: inherit;
}

View File

@ -231,6 +231,17 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
prepare: function () {
var data = new FormData();
var userNames;
if (roomId) {
var userNames = localStorage.userNames ? JSON.parse(localStorage.userNames) : {};
if (!userNames.hasOwnProperty(roomId)) {
userNames[roomId] = prompt('Your name: ');
localStorage.userNames = JSON.stringify(userNames);
}
data.append('userName', userNames[roomId]);
}
document.getElementById('loading').style.visibility = 'visible';
var url = Game.getGameIdentifier() + '/prepare.json';
@ -607,7 +618,7 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
break;
case 'anonymous_user':
MapGuesser.showModalWithContent('Error', 'You have to login to join this game!');
MapGuesser.showModalWithContent('Error', 'You have to login to join a challenge!');
break;
default:

View File

@ -89,9 +89,6 @@ var MapGuesser = {
formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText;
if (typeof grecaptcha !== 'undefined') {
grecaptcha.reset();
}
return;
}
@ -186,23 +183,12 @@ var MapGuesser = {
document.getElementById('cover').style.visibility = 'hidden';
},
observeInput: function (form, observedInputs) {
var anyChanged = false;
for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]];
if (input.type === 'checkbox') {
if (input.defaultChecked !== input.checked) {
anyChanged = true;
}
} else {
if (input.defaultValue !== input.value) {
anyChanged = true;
}
}
observeInput: function (input, buttonToToggle) {
if (input.defaultValue !== input.value) {
buttonToToggle.disabled = false;
} else {
buttonToToggle.disabled = true;
}
form.elements['submit_button'].disabled = !anyChanged;
},
observeInputsInForm: function (form, observedInputs) {
@ -213,19 +199,19 @@ var MapGuesser = {
case 'INPUT':
case 'TEXTAREA':
input.oninput = function () {
MapGuesser.observeInput(form, observedInputs);
MapGuesser.observeInput(this, form.elements.submit);
};
break;
case 'SELECT':
input.onchange = function () {
MapGuesser.observeInput(form, observedInputs);
MapGuesser.observeInput(this, form.elements.submit);
};
break;
}
}
form.onreset = function () {
form.elements['submit_button'].disabled = true;
form.elements.submit.disabled = true;
}
}
};

View File

@ -131,13 +131,11 @@
}, formData);
};
if (document.getElementById('multiButton')) {
document.getElementById('multiButton').onclick = function () {
MapGuesser.showModal('multi');
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select();
document.getElementById('playMode').style.visibility = 'hidden';
}
document.getElementById('multiButton').onclick = function () {
MapGuesser.showModal('multi');
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select();
document.getElementById('playMode').style.visibility = 'hidden';
}
if (document.getElementById('challengeButton')) {

View File

@ -0,0 +1,162 @@
#!/usr/bin/python3
# Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH
import sys
import os
import subprocess
import re
WORKTREE_REGEX = r"^worktree (.*)\nHEAD ([a-f0-9]*)\n(?:branch refs\/heads\/(.*)|detached)$"
if len(sys.argv) < 4:
print("Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH")
exit(1)
REPO = os.path.abspath(sys.argv[1])
WORKTREE_DEVELOPMENT = os.path.abspath(sys.argv[2])
WORKTREE_PRODUCTION = os.path.abspath(sys.argv[3])
class Worktree:
def __init__(self, path, branch, revision, version):
self.path = path
self.branch = branch
self.revision = revision
self.version = version
self.newRevision = None
self.newVersion = None
def getDataForWorktrees():
ret = subprocess.check_output(["git", "worktree", "list", "--porcelain"], cwd=REPO).decode().strip()
blocks = ret.split("\n\n")
worktrees = []
for block in blocks:
matches = re.search(WORKTREE_REGEX, block)
if matches:
path = matches.group(1)
revision = matches.group(2)
branch = matches.group(3)
version = getVersion(revision)
worktrees.append(Worktree(path, branch, revision, version))
return worktrees
def findWorktree(path):
for worktree in worktrees:
if worktree.path == path:
return worktree
return None
def getVersion(branch):
return subprocess.check_output(["git", "describe", "--tags", "--always", "--match", "Release_*", branch], cwd=REPO).decode().strip()
def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag():
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
def cleanWorktree(worktreePath):
subprocess.call(["git", "clean", "-f", "-d"], cwd=worktreePath)
def updateAppInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
def updateAppVersionInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
worktrees = getDataForWorktrees()
updateRepoFromRemote()
print("Repo is updated from origin")
print("----------------------------------------------")
print("----------------------------------------------")
developmentWorktree = findWorktree(WORKTREE_DEVELOPMENT)
developmentWorktree.newRevision = getRevisionForRef(developmentWorktree.branch)
developmentWorktree.newVersion = getVersion(developmentWorktree.revision)
print("DEVELOPMENT (" + developmentWorktree.path + ") is on branch " + developmentWorktree.branch)
print(developmentWorktree.revision + " = " + developmentWorktree.branch + " (" + developmentWorktree.version + ")")
print(developmentWorktree.newRevision + " = origin/" + developmentWorktree.branch + " (" + developmentWorktree.newVersion + ")")
if developmentWorktree.revision != developmentWorktree.newRevision:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") will be UPDATED")
print("----------------------------------------------")
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
cleanWorktree(developmentWorktree.path)
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch + " and cleaned")
updateAppInWorktree(developmentWorktree.path)
updateAppVersionInWorktree(developmentWorktree.path)
print("MapGuesser is updated in " + developmentWorktree.path)
elif developmentWorktree.version != developmentWorktree.newVersion:
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
updateAppVersionInWorktree(developmentWorktree.path)
print("MapGuesser version is updated in " + developmentWorktree.path)
else:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")
productionWorktree = findWorktree(WORKTREE_PRODUCTION)
productionWorktree.newVersion = getLatestReleaseTag()
productionWorktree.newRevision = getRevisionForRef(productionWorktree.newVersion)
print("PRODUCTION (" + productionWorktree.path + ")")
print(productionWorktree.revision + " = " + productionWorktree.version)
print(productionWorktree.newRevision + " = " + productionWorktree.newVersion)
if productionWorktree.revision != productionWorktree.newRevision:
print("-> PRODUCTION (" + productionWorktree.path + ") will be UPDATED")
checkoutWorktree(productionWorktree.path, productionWorktree.newRevision)
cleanWorktree(productionWorktree.path)
print(productionWorktree.path + " is checked out to " + productionWorktree.newRevision + " and cleaned")
updateAppInWorktree(productionWorktree.path)
updateAppVersionInWorktree(productionWorktree.path)
print("MapGuesser is updated in " + productionWorktree.path)
else:
print("-> PRODUCTION (" + productionWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")

35
scripts/install.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -f ${ROOT_DIR}/installed ]; then
echo "MapGuesser is already installed! To force reinstall, delete file 'installed' from the root directory!"
exit 1
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
fi
touch ${ROOT_DIR}/installed

11
scripts/minify.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
find ${ROOT_DIR}/public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find ${ROOT_DIR}/public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find ${ROOT_DIR}/public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;

17
scripts/update-version.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
cd ${ROOT_DIR}
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php

29
scripts/update.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
(cd ${ROOT_DIR} && composer install --no-dev)
else
(cd ${ROOT_DIR} && composer install --dev)
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
fi

View File

@ -14,7 +14,6 @@ class AddUserCommand extends Command
$this->setName('user:add')
->setDescription('Adding of user.')
->addArgument('email', InputArgument::REQUIRED, 'Email of user')
->addArgument('username', InputArgument::REQUIRED, 'Username of user')
->addArgument('password', InputArgument::REQUIRED, 'Password of user')
->addArgument('type', InputArgument::OPTIONAL, 'Type of user');;
}
@ -23,7 +22,6 @@ class AddUserCommand extends Command
{
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setUsername($input->getArgument('username'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());

View File

@ -19,8 +19,6 @@ class MigrateDatabaseCommand extends Command
{
$db = \Container::$dbConnection;
$this->createBaseDb();
$db->startTransaction();
$success = [];
@ -64,8 +62,10 @@ class MigrateDatabaseCommand extends Command
return 0;
}
private function createBaseDb()
private function readDir(string $type): array
{
$done = [];
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
FROM information_schema.tables
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
@ -73,25 +73,16 @@ class MigrateDatabaseCommand extends Command
->fetch(IResultSet::FETCH_NUM)[0];
if ($migrationTableExists != 0) {
return;
}
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
\Container::$dbConnection->multiQuery(file_get_contents(ROOT . '/database/mapguesser.sql'));
}
$result = $select->execute();
private function readDir(string $type): array
{
$done = [];
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
$result = $select->execute();
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
}
}
$path = ROOT . '/database/migrations/' . $type;

View File

@ -1,6 +1,7 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use Faker\Factory;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
@ -11,7 +12,6 @@ use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
@ -190,16 +190,12 @@ class GameController implements IAuthenticationRequired
public function prepareMultiGame(): IContent
{
/**
* @var User|null $user
*/
$user = \Container::$request->user();
if ($user === null)
{
return new JsonContent(['error' => 'anonymous_user']);
}
$roomId = \Container::$request->query('roomId');
$userName = \Container::$request->post('userName');
if (empty($userName)) {
$faker = Factory::create();
$userName = $faker->userName;
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
@ -229,7 +225,7 @@ class GameController implements IAuthenticationRequired
$this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId,
'token' => $token,
'userName' => $user->getDisplayName()
'userName' => $userName
]);
return new JsonContent([

View File

@ -14,7 +14,6 @@ use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Util\UsernameGenerator;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Response\Redirect;
@ -81,20 +80,17 @@ class LoginController
if (\Container::$request->session()->has('tmp_user_data')) {
$tmpUserData = \Container::$request->session()->get('tmp_user_data');
$data = ['email' => $tmpUserData['email']];
} else {
$tmpUserData = [];
$data = [];
}
return new HtmlContent('login/signup', $tmpUserData);
return new HtmlContent('login/signup', $data);
}
public function getSignupSuccess()
public function getSignupSuccess(): IContent
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
return new HtmlContent('login/signup_success');
}
@ -157,14 +153,7 @@ class LoginController
return new JsonContent(['success' => true]);
}
if (
filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false &&
preg_match('/^[a-zA-Z0-9_\-\.]+$/', \Container::$request->post('email')) !== 1
) {
return new JsonContent(['error' => ['errorText' => 'This is not a valid email address or username.']]);
}
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
$user = $this->userRepository->getByEmail(\Container::$request->post('email'));
if ($user === null) {
if (strlen(\Container::$request->post('password')) < 6) {
@ -178,14 +167,10 @@ class LoginController
$tmpUser = new User();
$tmpUser->setPlainPassword(\Container::$request->post('password'));
$tmpUserData = ['password_hashed' => $tmpUser->getPassword()];
if (filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
$tmpUserData['username'] = \Container::$request->post('email');
} else {
$tmpUserData['email'] = \Container::$request->post('email');
}
\Container::$request->session()->set('tmp_user_data', $tmpUserData);
\Container::$request->session()->set('tmp_user_data', [
'email' => \Container::$request->post('email'),
'password_hashed' => $tmpUser->getPassword()
]);
return new JsonContent([
'redirect' => [
@ -199,7 +184,7 @@ class LoginController
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address / username, but the account is not activated. ' .
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
@ -280,144 +265,134 @@ class LoginController
return new JsonContent(['redirect' => ['target' => $this->redirectUrl]]);
}
$newUser = new User();
$user = $this->userRepository->getByEmail(\Container::$request->post('email'));
$googleUserData = \Container::$request->session()->get('google_user_data');
if ($googleUserData !== null) {
$user = $this->userRepository->getByEmail($googleUserData['email']);
if ($user !== null) {
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the email address of this Google account, ' .
'but Google account is not linked to the user. Please <a href="/login?email=' .
urlencode($googleUserData['email']) . '" title="Login">login</a> first to link your Google account!'
]
]);
}
$newUser->setActive(true);
$newUser->setEmail($googleUserData['email']);
$newUser->setGoogleSub($googleUserData['sub']);
} else {
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
if ($user !== null) {
if ($user->getActive()) {
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the given email address / username, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
\Container::$request->setUser($user);
$this->deleteRedirectUrl();
$data = ['redirect' => ['target' => $this->redirectUrl]];
} else {
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address / username. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
if (!\Container::$request->post('g-recaptcha-response')) {
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
}
$captchaValidator = new CaptchaValidator();
$captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
if (!$captchaResponse['success']) {
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
}
}
if (filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if (\Container::$request->session()->has('tmp_user_data')) {
$tmpUserData = \Container::$request->session()->get('tmp_user_data');
$tmpUser = new User();
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword(\Container::$request->post('password'))) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
if (strlen(\Container::$request->post('password')) < 6) {
if ($user !== null) {
if ($user->getActive()) {
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
'errorText' => 'There is a user already registered with the given email address, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
if (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
}
\Container::$request->setUser($user);
$newUser->setActive(false);
$newUser->setEmail(\Container::$request->post('email'));
$newUser->setPlainPassword(\Container::$request->post('password'));
$this->deleteRedirectUrl();
$data = ['redirect' => ['target' => $this->redirectUrl]];
} else {
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (strlen(\Container::$request->post('username')) > 0) {
$username = \Container::$request->post('username');
if (preg_match('/^[a-zA-Z0-9_\-\.]+$/', $username) !== 1) {
return new JsonContent(['error' => ['errorText' => 'Username can contain only english letters, digits, - (hyphen), . (dot), _ (underscore).']]);
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
if (!\Container::$request->post('g-recaptcha-response')) {
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
}
if ($this->userRepository->getByUsername($username) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given username is already taken.']]);
$captchaValidator = new CaptchaValidator();
$captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
if (!$captchaResponse['success']) {
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
}
}
if (filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if (\Container::$request->session()->has('tmp_user_data')) {
$tmpUserData = \Container::$request->session()->get('tmp_user_data');
$tmpUser = new User();
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword(\Container::$request->post('password'))) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
$usernameGenerator = new UsernameGenerator();
do {
$username = $usernameGenerator->generate();
} while ($this->userRepository->getByUsername($username));
if (strlen(\Container::$request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
}
$newUser->setUsername($username);
$newUser->setCreatedDate(new DateTime());
$user = new User();
$user->setEmail(\Container::$request->post('email'));
$user->setPlainPassword(\Container::$request->post('password'));
$user->setCreatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($newUser);
\Container::$persistentDataManager->saveToDb($user);
if ($googleUserData !== null) {
$this->sendWelcomeEmail($newUser->getEmail());
$token = bin2hex(random_bytes(16));
\Container::$request->setUser($newUser);
} else {
$token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
$confirmation = new UserConfirmation();
$confirmation->setUser($newUser);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
\Container::$persistentDataManager->saveToDb($confirmation);
\Container::$persistentDataManager->saveToDb($confirmation);
$this->sendConfirmationEmail($newUser->getEmail(), $token, $newUser->getCreatedDate());
}
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
\Container::$request->session()->delete('tmp_user_data');
\Container::$request->session()->delete('google_user_data');
return new JsonContent(['success' => true]);
}
public function signupWithGoogle(): IContent
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]);
}
$userData = \Container::$request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']);
if ($user === null) {
$sendWelcomeEmail = true;
$user = new User();
$user->setEmail($userData['email']);
$user->setCreatedDate(new DateTime());
} else {
$sendWelcomeEmail = false;
}
$user->setActive(true);
$user->setGoogleSub($userData['sub']);
\Container::$persistentDataManager->saveToDb($user);
if ($sendWelcomeEmail) {
$this->sendWelcomeEmail($user->getEmail());
}
\Container::$request->session()->delete('google_user_data');
\Container::$request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]);
}
public function resetSignup(): IContent
{
\Container::$request->session()->delete('tmp_user_data');
@ -507,19 +482,12 @@ class LoginController
}
}
if (
filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false &&
preg_match('/^[a-zA-Z0-9_\-\.]+$/', \Container::$request->post('email')) !== 1
) {
return new JsonContent(['error' => ['errorText' => 'This is not a valid email address or username.']]);
}
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
$user = $this->userRepository->getByEmail(\Container::$request->post('email'));
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address / username. You can <a href="/signup" title="Sign up">sign up</a>!'
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
]
]);
}
@ -529,7 +497,7 @@ class LoginController
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address / username, but the account is not activated. ' .
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);

View File

@ -8,7 +8,6 @@ use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
@ -20,8 +19,6 @@ use SokoWeb\Util\JwtParser;
class UserController implements IAuthenticationRequired
{
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
@ -34,7 +31,6 @@ class UserController implements IAuthenticationRequired
public function __construct()
{
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
@ -57,130 +53,6 @@ class UserController implements IAuthenticationRequired
return new HtmlContent('account/account', ['user' => $user->toArray()]);
}
public function getGoogleConnectRedirect(): IRedirect
{
/**
* @var User $user
*/
$user = \Container::$request->user();
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
\Container::$request->session()->set('oauth_state', $state);
\Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink(),
$nonce,
$user->getEmail()
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function getGoogleConnectConfirm(): IContent
{
$defaultError = 'Authentication with Google failed. Please <a href="' . \Container::$routeCollection->getRoute('account.googleConnect')->generateLink() . '" title="Connect with Google">try again</a>!';
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
\Container::$request->query('code'),
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$anotherUser = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($anotherUser !== null) {
return new HtmlContent('account/google_connect', [
'success' => false,
'error' => 'This Google account is linked to another account.'
]);
}
\Container::$request->session()->set('google_user_data', $idToken);
/**
* @var User $user
*/
$user = \Container::$request->user();
return new HtmlContent('account/google_connect', [
'success' => true,
'googleAccount' => $idToken['email'],
'userEmail' => $user->getEmail()
]);
}
public function connectGoogle(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong.'
]
]);
}
$googleUserData = \Container::$request->session()->get('google_user_data');
$user->setGoogleSub($googleUserData['sub']);
\Container::$persistentDataManager->saveToDb($user);
return new JsonContent(['success' => true]);
}
public function getGoogleDisconnectConfirm(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
return new HtmlContent('account/google_disconnect', [
'success' => true,
'userEmail' => $user->getEmail()
]);
}
public function disconnectGoogle(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong.'
]
]);
}
$user->setGoogleSub(null);
\Container::$persistentDataManager->saveToDb($user);
return new JsonContent(['success' => true]);
}
public function getGoogleAuthenticateRedirect(): IRedirect
{
/**
@ -276,36 +148,6 @@ class UserController implements IAuthenticationRequired
return new JsonContent(['error' => ['errorText' => $error]]);
}
$newEmail = \Container::$request->post('email');
if ($newEmail !== $user->getEmail()) {
if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if ($this->userRepository->getByEmail($newEmail) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given email address belongs to another account.']]);
}
$user->setEmail($newEmail);
}
$newUsername = \Container::$request->post('username');
if ($newUsername !== $user->getUsername()) {
if (strlen($newUsername) == 0) {
return new JsonContent(['error' => ['errorText' => 'Username cannot be empty.']]);
}
if (preg_match('/^[a-zA-Z0-9_\-\.]+$/', $newUsername) !== 1) {
return new JsonContent(['error' => ['errorText' => 'Username can contain only english letters, digits, - (hyphen), . (dot), _ (underscore).']]);
}
if ($this->userRepository->getByUsername($newUsername) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given username is already taken.']]);
}
$user->setUsername($newUsername);
}
if (strlen(\Container::$request->post('password_new')) > 0) {
if (strlen(\Container::$request->post('password_new')) < 6) {
return new JsonContent([

View File

@ -8,14 +8,12 @@ class User extends Model implements IUser
{
protected static string $table = 'users';
protected static array $fields = ['email', 'username', 'password', 'type', 'active', 'google_sub', 'created'];
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin'];
private string $email = '';
private string $username = '';
private ?string $password = null;
private string $type = 'user';
@ -31,11 +29,6 @@ class User extends Model implements IUser
$this->email = $email;
}
public function setUsername(string $username): void
{
$this->username = $username;
}
public function setPassword(?string $hashedPassword): void
{
$this->password = $hashedPassword;
@ -78,11 +71,6 @@ class User extends Model implements IUser
return $this->email;
}
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): ?string
{
return $this->password;
@ -132,7 +120,7 @@ class User extends Model implements IUser
public function getDisplayName(): string
{
return $this->username;
return $this->email;
}
public function checkPassword(string $password): bool

View File

@ -40,7 +40,7 @@ class ChallengeRepository
$select = new Select(\Container::$dbConnection);
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
$select->where(['user_in_challenge', 'user_id'], '=', $user->getId());
$select->where('user_id', '=', $user->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
@ -50,8 +50,8 @@ class ChallengeRepository
$select = new Select(\Container::$dbConnection);
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
$select->where(['user_in_challenge', 'user_id'], '=', $user->getId());
$select->where(['user_in_challenge', 'is_owner'], '=', true);
$select->where('user_id', '=', $user->getId());
$select->where('is_owner', '=', true);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
@ -60,7 +60,7 @@ class ChallengeRepository
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['challenges', 'id'], '=', ['place_in_challenge', 'challenge_id']);
$select->where(['place_in_challenge', 'place_id'], '=', $place->getId());
$select->where('place_id', '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}

View File

@ -23,7 +23,7 @@ class GuessRepository
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $user->getId());
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->where('challenge_id', '=', $challenge->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
@ -33,8 +33,8 @@ class GuessRepository
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $user->getId());
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->where(['place_in_challenge', 'place_id'], '=', $place->getId());
$select->where('challenge_id', '=', $challenge->getId());
$select->where('place_id', '=', $place->getId());
return \Container::$persistentDataManager->selectFromDb($select, Guess::class);
}
@ -44,8 +44,8 @@ class GuessRepository
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $userId);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['place_in_challenge', 'round']);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
@ -58,8 +58,8 @@ class GuessRepository
}
$select = new Select(\Container::$dbConnection);
$select->where(['guesses__place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['guesses__place_in_challenge', 'round']);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class, true, $withRelations);
}
@ -72,8 +72,8 @@ class GuessRepository
}
$select = new Select(\Container::$dbConnection);
$select->where(['guesses__place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->where(['guesses__place_in_challenge', 'round'], '=', $round);
$select->where('challenge_id', '=', $challenge->getId());
$select->where('round', '=', $round);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class, true, $withRelations);
}
@ -82,7 +82,7 @@ class GuessRepository
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where(['place_in_challenge', 'place_id'], '=', $place->getId());
$select->where('place_id', '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}

View File

@ -22,7 +22,7 @@ class MapRepository
$select = new Select(\Container::$dbConnection);
$select->innerJoin('places', ['maps', 'id'], '=', ['places', 'map_id']);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->where('challenge_id', '=', $challenge->getId());
$select->limit(1);
return \Container::$persistentDataManager->selectFromDb($select, Map::class);

View File

@ -109,8 +109,8 @@ class PlaceRepository
// count the places never visited
$selectUnvisited = new Select(\Container::$dbConnection, 'places');
$selectUnvisited->leftJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectUnvisited->where(['places', 'map_id'], '=', $mapId);
$selectUnvisited->where(['places_by_current_user', 'last_time'], '=', null);
$selectUnvisited->where('map_id', '=', $mapId);
$selectUnvisited->where('last_time', '=', null);
$numberOfUnvisitedPlaces = $selectUnvisited->count();
// look for as many new places as possible but maximum $n
@ -140,11 +140,11 @@ class PlaceRepository
// count places that were visited at least once
$selectOldPlaces = new Select(\Container::$dbConnection, 'places');
$selectOldPlaces->innerJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectOldPlaces->where(['places', 'map_id'], '=', $mapId);
$selectOldPlaces->where('map_id', '=', $mapId);
$numberOfOldPlaces = $selectOldPlaces->count();
// set order by datetime, oldest first
$selectOldPlaces->orderBy(['places_by_current_user', 'last_time']);
$selectOldPlaces->orderBy('last_time');
// selection algorithm with preference (weighting) for older places using Box-Muller transform
$pickGaussianRandomInt = function($numberOfPlaces) {
@ -174,8 +174,8 @@ class PlaceRepository
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['place_in_challenge', 'round']);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
$select->limit(1, $round);
return \Container::$persistentDataManager->selectFromDb($select, Place::class);
@ -185,8 +185,8 @@ class PlaceRepository
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['place_in_challenge', 'round']);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Place::class);
}

View File

@ -57,7 +57,7 @@ class UserInChallengeRepository
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $userId);
$select->where(['user_in_challenge__challenge', 'token'], '=', $token);
$select->where('token', '=', $token);
return \Container::$persistentDataManager->selectFromDb($select, UserInChallenge::class, true, $withRelations);
}

View File

@ -22,23 +22,6 @@ class UserRepository implements IUserRepository
return \Container::$persistentDataManager->selectFromDb($select, User::class);
}
public function getByUsername(string $username): ?User
{
$select = new Select(\Container::$dbConnection);
$select->where('username', '=', $username);
return \Container::$persistentDataManager->selectFromDb($select, User::class);
}
public function getByEmailOrUsername(string $emailOrUsername): ?User
{
if (filter_var($emailOrUsername, FILTER_VALIDATE_EMAIL)) {
return $this->getByEmail($emailOrUsername);
}
return $this->getByUsername($emailOrUsername);
}
public function getByGoogleSub(string $sub): ?User
{
$select = new Select(\Container::$dbConnection);

View File

@ -1,247 +0,0 @@
<?php namespace MapGuesser\Util;
class UsernameGenerator
{
const ADJECTIVES = [
'abundant',
'agile',
'alluring',
'ample',
'adorable',
'angry',
'anxious',
'astonishing',
'beautiful',
'big',
'bitter',
'blissful',
'blue',
'brave',
'bright',
'brilliant',
'busy',
'calm',
'captivating',
'careful',
'charming',
'cheerful',
'clumsy',
'colorful',
'confused',
'cooperative',
'courageous',
'cozy',
'crispy',
'curious',
'dazzling',
'delightful',
'determined',
'eager',
'elegant',
'enchanting',
'enthusiastic',
'exciting',
'exquisite',
'faithful',
'fancy',
'fearless',
'fierce',
'fluffy',
'fresh',
'friendly',
'frigid',
'funny',
'gentle',
'glorious',
'graceful',
'grateful',
'happy',
'harmonious',
'healthy',
'helpful',
'honest',
'hopeful',
'hot',
'humble',
'hungry',
'impressive',
'infamous',
'innocent',
'intense',
'jolly',
'joyful',
'kind',
'lively',
'lonely',
'lovely',
'lucky',
'mysterious',
'naughty',
'nervous',
'nutritious',
'obedient',
'peaceful',
'playful',
'polite',
'powerful',
'precious',
'proud',
'radiant',
'reckless',
'reliable',
'rich',
'romantic',
'rough',
'sad',
'scary',
'sensitive',
'shiny',
'silky',
'sincere',
'sleepy',
'smart',
'sneaky',
'soft',
'sparkling',
'splendid',
'strong',
'stubborn',
'sweet',
'tender',
'thoughtful',
'thrilling',
'timid',
'tranquil',
'trustworthy',
'unique',
'vibrant',
'victorious',
'warm',
'wise',
'witty',
'wonderful',
'worried',
'zealous'
];
const NOUNS = [
'airplane',
'ant',
'apple',
'aquarium',
'backpack',
'banana',
'bear',
'bee',
'camera',
'car',
'cat',
'chocolate',
'desk',
'diamond',
'dog',
'dolphin',
'duck',
'egg',
'eiffeltower',
'elephant',
'fire',
'flower',
'forest',
'fork',
'fox',
'galaxy',
'giraffe',
'globe',
'guitar',
'hammer',
'hamster',
'hat',
'house',
'icecream',
'iguana',
'island',
'jacket',
'jaguar',
'jellyfish',
'jigsaw',
'kangaroo',
'key',
'kite',
'koala',
'lamp',
'lighthouse',
'lightning',
'lion',
'llama',
'moon',
'mountain',
'mouse',
'necklace',
'nest',
'newt',
'notebook',
'ocean',
'octopus',
'orchid',
'owl',
'panda',
'pencil',
'penguin',
'piano',
'queen',
'quilt',
'quokka',
'rabbit',
'rainbow',
'robot',
'ship',
'snake',
'statue',
'sun',
'sunflower',
'table',
'telescope',
'tiger',
'tree',
'turtle',
'uakari',
'umbrella',
'unicorn',
'universe',
'vase',
'violin',
'volcano',
'vulture',
'wallaby',
'waterfall',
'whale',
'xray',
'xylophone',
'yacht',
'yak',
'yarn',
'yeti',
'zebra',
'zeppelin',
'zucchini',
];
function generate(): string
{
$numberOfAdjectives = count(self::ADJECTIVES);
$numberOfNouns = count(self::NOUNS);
$firstAdjective = self::ADJECTIVES[mt_rand(0, $numberOfAdjectives - 1)];
do {
$secondAdjective = self::ADJECTIVES[mt_rand(0, $numberOfAdjectives - 1)];
} while ($firstAdjective === $secondAdjective);
$noun = self::NOUNS[mt_rand(0, $numberOfNouns - 1)];
$firstAdjective = ucfirst($firstAdjective);
$secondAdjective = ucfirst($secondAdjective);
$noun = ucfirst($noun);
return $firstAdjective . $secondAdjective . $noun;
}
}

View File

@ -1,23 +0,0 @@
<?php namespace MapGuesser\Tests\Util;
use MapGuesser\Util\UsernameGenerator;
use PHPUnit\Framework\TestCase;
final class UsernameGeneratorTest extends TestCase
{
public function testCanGenerateRandomUsernameFromComponents(): void
{
$generator = new UsernameGenerator();
$parts = $this->getUsernameParts($generator->generate());
$this->assertEquals(3, count($parts));
$this->assertContains($parts[0], UsernameGenerator::ADJECTIVES);
$this->assertContains($parts[1], UsernameGenerator::ADJECTIVES);
$this->assertContains($parts[2], UsernameGenerator::NOUNS);
}
private function getUsernameParts(string $username): array
{
return explode('-', strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $username)));
}
}

View File

@ -5,11 +5,11 @@
@section(main)
<h2>Account</h2>
<div class="box">
<form id="accountForm" action="/account" method="post" data-reload-on-success="true" data-observe-inputs="email,username,password_new,password_new_confirm">
<form id="accountForm" action="/account" method="post" data-observe-inputs="password_new,password_new_confirm">
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p>
<div class="inputWithButton">
<input type="password" class="text" name="password" placeholder="Current password" autocomplete="current-password" required minlength="6" autofocus><!--
<input type="password" class="text name="password" placeholder="Current password" autocomplete="current-password" required minlength="6" autofocus><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php elseif ($user['password'] !== null): ?>
@ -23,23 +23,16 @@
</div>
<?php endif; ?>
<hr>
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" autocomplete="username" value="<?= $user['email'] ?>">
<input type="username" class="text big fullWidth marginTop" name="username" placeholder="Username" value="<?= $user['username'] ?>">
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?>
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" autocomplete="username" value="<?= $user['email'] ?>" disabled>
<input type="password" class="text big fullWidth marginTop" name="password_new" placeholder="New password" autocomplete="new-password" minlength="6">
<input type="password" class="text big fullWidth marginTop" name="password_new_confirm" placeholder="New password confirmation" autocomplete="new-password" minlength="6">
<p id="accountFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit" name="submit_button" disabled>Save</button>
<button type="submit" name="submit" disabled>Save</button>
</div>
<hr>
<div class="center" style="font-size: 0;">
<?php if ($user['google_sub'] === null): ?>
<a class="button yellow marginRight" href="<?= Container::$routeCollection->getRoute('account.googleConnect')->generateLink() ?>" title="Connect with Google">Connect with Google</a>
<?php else: ?>
<?php if ($user['password'] !== null): ?>
<a class="button yellow marginRight" href="<?= Container::$routeCollection->getRoute('account.googleDisconnect')->generateLink() ?>" title="Disconnect from Google">Disconnect from Google</a>
<?php endif; ?>
<?php endif; ?>
<div class="center">
<a class="button red" href="/account/delete" title="Delete account">Delete account</a>
</div>
</form>

View File

@ -26,7 +26,7 @@
<?php endif; ?>
<p id="deleteAccountFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button class="red marginRight" type="submit" name="submit_button">Delete account</button><!--
<button class="red marginRight" type="submit" name="submit">Delete account</button><!--
--><a class="button gray marginTop" href="/account" title="Back to account">Cancel</a>
</div>
</form>

View File

@ -1,22 +0,0 @@
@extends(templates/layout_normal)
@section(main)
<h2>Connect with Google</h2>
<div class="box compactBox">
<?php if (!$success): ?>
<p class="error justify"><?= $error ?></p>
<?php else: ?>
<form id="connectGoogleForm" action="<?= Container::$routeCollection->getRoute('account.googleConnect-action')->generateLink() ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>">
<p class="justify marginBottom">Your account will be connected with the following Google account: <b><?= $googleAccount ?></b></p>
<input type="email" style="display: none;" name="email" autocomplete="username" value="<?= $userEmail ?>">
<p class="formLabel marginTop">Password</p>
<input type="password" class="text big fullWidth" name="password" autocomplete="current-password" required minlength="6" autofocus>
<p class="formError justify marginTop"></p>
<div class="right marginTop">
<button class="marginRight" type="submit" name="submit"><i class="fa-solid fa-link"></i> Connect</button><!--
--><a class="button gray" href="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>" title="Back to account">Cancel</a>
</div>
</form>
<?php endif; ?>
</div>
@endsection

View File

@ -1,18 +0,0 @@
@extends(templates/layout_normal)
@section(main)
<h2>Disconnect from Google</h2>
<div class="box compactBox">
<form id="connectGoogleForm" action="<?= Container::$routeCollection->getRoute('account.googleDisconnect-action')->generateLink() ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>">
<p class="justify marginBottom">Your account will be disconnected from the currently set Google account.</p>
<input type="email" style="display: none;" name="email" autocomplete="username" value="<?= $userEmail ?>">
<p class="formLabel marginTop">Password</p>
<input type="password" class="text big fullWidth" name="password" autocomplete="current-password" required minlength="6" autofocus>
<p class="formError justify marginTop"></p>
<div class="right marginTop">
<button class="red marginRight" type="submit" name="submit"><i class="fa-solid fa-link-slash"></i> Disconnect</button><!--
--><a class="button gray" href="<?= Container::$routeCollection->getRoute('account')->generateLink() ?>" title="Back to account">Cancel</a>
</div>
</form>
</div>
@endsection

View File

@ -5,13 +5,21 @@
@section(main)
<h2>Sign up</h2>
<div class="box">
<form id="googleSignupForm" action="/signup" method="post" data-redirect-on-success="/signup/success">
<p class="justify">Please confirm your sign up request. Your account will be linked to your Google account.</p>
<form id="googleSignupForm" action="/signup/google" method="post" data-redirect-on-success="<?= $redirectUrl ?>">
<?php if ($found): ?>
<p class="justify">Please confirm that you link your account to your Google account.</p>
<?php else: ?>
<p class="justify">Please confirm your sign up request. Your account will be linked to your Google account.</p>
<?php endif; ?>
<input type="email" class="text big fullWidth marginTop" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<input type="username" class="text big fullWidth marginTop" name="username" placeholder="Username">
<p id="googleSignupFormError" class="formError justify marginTop"></p>
<div class="right">
<button class="marginTop marginRight" type="submit">Sign up</button><!--
<button class="marginTop marginRight" type="submit">
<?php if ($found): ?>
Link
<?php else: ?>
Sign up
<?php endif; ?>
</button><!--
--><button id="cancelGoogleSignupButton" class="gray marginTop" type="button">Cancel</button>
</div>
</form>

View File

@ -4,8 +4,8 @@
<h2>Login</h2>
<div class="box">
<form id="loginForm" action="/login" method="post" data-redirect-on-success="<?= $redirectUrl ?>">
<input type="text" class="text big fullWidth" name="email" placeholder="Email address / username" autocomplete="username" required autofocus>
<input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password" autocomplete="current-password" required minlength="6">
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" autocomplete="username" required autofocus>
<input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password autocomplete="current-password"" required minlength="6">
<p id="loginFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit">Login</button>

View File

@ -6,7 +6,7 @@
<h2>Request password reset</h2>
<div class="box">
<form id="passwordResetForm" action="/password/requestReset" method="post" data-redirect-on-success="/password/requestReset/success">
<input type="text" class="text big fullWidth" name="email" placeholder="Email address / username" autocomplete="username" value="<?= isset($email) ? $email : '' ?>" required autofocus>
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" autocomplete="username" value="<?= isset($email) ? $email : '' ?>" required autofocus>
<?php if (!empty($_ENV['RECAPTCHA_SITEKEY'])): ?>
<div class="marginTop">
<div class="g-recaptcha" data-sitekey="<?= $_ENV['RECAPTCHA_SITEKEY'] ?>"></div>

View File

@ -7,22 +7,12 @@
<h2>Sign up</h2>
<div class="box">
<form id="signupForm" action="/signup" method="post" data-redirect-on-success="/signup/success">
<?php if (isset($email) || isset($username)): ?>
<p class="justify">No user found with the given email address / username. Sign up with one click!</p>
<?php if (isset($email)): ?>
<input type="email" class="text big fullWidth marginTop" name="email" placeholder="Email address" autocomplete="username" value="<?= $email ?>" required>
<?php else: ?>
<input type="email" class="text big fullWidth marginTop" name="email" placeholder="Email address" autocomplete="username" required autofocus>
<?php endif; ?>
<?php if (isset($username)): ?>
<input type="username" class="text big fullWidth marginTop" name="username" placeholder="Username" value="<?= $username ?>">
<?php else: ?>
<input type="username" class="text big fullWidth marginTop" name="username" placeholder="Username" autofocus>
<?php endif; ?>
<input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password confirmation" autocomplete="new-password" required minlength="6">
<?php if (isset($email)): ?>
<p class="justify">No user found with the given email address. Sign up with one click!</p>
<input type="email" class="text big fullWidth marginTop" name="email" placeholder="Email address" autocomplete="username" value="<?= $email ?>" required>
<input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password confirmation" autocomplete="new-password" required minlength="6" autofocus>
<?php else: ?>
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" autocomplete="username" required autofocus>
<input type="username" class="text big fullWidth marginTop" name="username" placeholder="Username">
<input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password" autocomplete="new-password" required minlength="6">
<input type="password" class="text big fullWidth marginTop" name="password_confirm" placeholder="Password confirmation" autocomplete="new-password" minlength="6">
<?php endif; ?>

View File

@ -9,9 +9,9 @@ TODO: condition!
<div id="playMode" class="modal">
<h2>Play map</h2>
<a id="singleButton" class="button fullWidth marginTop" href="" title="Single player">Single player</a>
<p class="bold center marginTop marginBottom">OR</p>
<button id="multiButton" class="fullWidth green" data-map-id="">Multiplayer (beta)</button>
<?php if ($isLoggedIn): ?>
<p class="bold center marginTop marginBottom">OR</p>
<button id="multiButton" class="fullWidth green" data-map-id="">Multiplayer (beta)</button>
<p class="bold center marginTop marginBottom">OR</p>
<button id="challengeButton" class="fullWidth yellow" data-map-id="" data-timer="">Challenge (gamma)</button>
<?php endif; ?>

View File

@ -29,6 +29,6 @@
</main>
<footer>
<p><span class="bold"><?= $_ENV['APP_NAME'] ?></span> <?= str_replace('Release_', '', VERSION) ?></p><!--
--><p>&copy; The <a href="https://git.esoko.eu/esoko/mapguesser" target="_blank">MapGuesser</a> Contributors <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
--><p>&copy; The MapGuesser Contributors <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
</footer>
@endsection

View File

@ -16,8 +16,9 @@ use MapGuesser\Repository\UserRepository;
require 'main.php';
error_reporting(E_ALL);
if (!empty($_ENV['DEV'])) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
ini_set('display_errors', '0');
@ -37,6 +38,7 @@ Container::$routeCollection->group('signup', function (RouteCollection $routeCol
$routeCollection->get('signup', '', [LoginController::class, 'getSignupForm']);
$routeCollection->post('signup-action', '', [LoginController::class, 'signup']);
$routeCollection->get('signup-google', 'google', [LoginController::class, 'getSignupWithGoogleForm']);
$routeCollection->post('signup-google-action', 'google', [LoginController::class, 'signupWithGoogle']);
$routeCollection->post('signup.reset', 'reset', [LoginController::class, 'resetSignup']);
$routeCollection->post('signup-google.reset', 'google/reset', [LoginController::class, 'resetGoogleSignup']);
$routeCollection->get('signup.success', 'success', [LoginController::class, 'getSignupSuccess']);
@ -56,11 +58,6 @@ Container::$routeCollection->group('account', function (RouteCollection $routeCo
$routeCollection->post('account-action', '', [UserController::class, 'saveAccount']);
$routeCollection->get('account.delete', 'delete', [UserController::class, 'getDeleteAccount']);
$routeCollection->post('account.delete-action', 'delete', [UserController::class, 'deleteAccount']);
$routeCollection->get('account.googleConnect', 'googleConnect', [UserController::class, 'getGoogleConnectRedirect']);
$routeCollection->get('account.googleConnect-confirm', 'googleConnect/code', [UserController::class, 'getGoogleConnectConfirm']);
$routeCollection->post('account.googleConnect-action', 'googleConnect', [UserController::class, 'connectGoogle']);
$routeCollection->get('account.googleDisconnect', 'googleDisconnect', [UserController::class, 'getGoogleDisconnectConfirm']);
$routeCollection->post('account.googleDisconnect-action', 'googleDisconnect', [UserController::class, 'disconnectGoogle']);
$routeCollection->get('account.googleAuthenticate', 'googleAuthenticate', [UserController::class, 'getGoogleAuthenticateRedirect']);
$routeCollection->get('account.googleAuthenticate-action', 'googleAuthenticate/code', [UserController::class, 'authenticateWithGoogle']);
});