Commit 8e024e6e authored by Hermann Krumrey's avatar Hermann Krumrey

Merge branch 'develop' into 'master'

Develop

See merge request namibsun/python/bundesliga-tippspiel!26
parents e45fdea2 59c27beb
......@@ -5,4 +5,4 @@ cover/
*.egg-info/
dist/
build/
./secrets.json
\ No newline at end of file
.env
\ No newline at end of file
......@@ -6,7 +6,7 @@ stages:
- release
default:
image: namboy94/ci-docker-environment:0.3.0
image: namboy94/ci-docker-environment:0.8.0
before_script:
- echo "$SERVER_ACCESS_KEY" > ~/.ssh/id_rsa
- chmod 0600 ~/.ssh/id_rsa
......@@ -33,7 +33,15 @@ unittest:
tags: [docker]
only: [master, develop]
script:
- echo "$ENV_FILE" > .env
- python-unittest
- rm .env
type_check:
stage: test
tags: [docker]
script:
- python-static-type-check
gitstats:
stage: stats
......@@ -50,27 +58,29 @@ docgen:
deploy_develop:
stage: deploy
tags: [privileged]
only: [develop]
tags: [hk-tippspiel-develop]
before_script:
- python3 -m venv virtual
- source virtual/bin/activate
- pip install ci-scripts
before_script: []
script:
- export TIPPSPIEL_ENV=develop
- deploy-flask $BUNDESLIGA_TIPPSPIEL_DEVELOP_HOME
- echo "$ENV_FILE" > .env
- export HTTP_PORT=$DEV_HTTP_PORT
- export DEPLOY_MODE=dev
- docker-compose -p bundesliga_tippspiel_dev build
- docker-compose -p bundesliga_tippspiel_dev up -d
- rm .env
deploy_production:
stage: deploy
tags: [privileged]
only: [master]
tags: [hk-tippspiel]
before_script:
- python3 -m venv virtual
- source virtual/bin/activate
- pip install ci-scripts
before_script: []
script:
- export TIPPSPIEL_ENV=production
- deploy-flask $BUNDESLIGA_TIPPSPIEL_HOME
- echo "$ENV_FILE" > .env
- export HTTP_PORT=$PROD_HTTP_PORT
- export DEPLOY_MODE=prod
- docker-compose -p bundesliga_tippspiel_prod build
- docker-compose -p bundesliga_tippspiel_prod up -d
- rm .env
release_upload:
stage: release
......
V 1.2.0:
- Website now runs in docker
V 1.1.1:
- Integrated sentry
V 1.1.0:
- Activated Telegram Reminder
V 1.0.4:
......
FROM ubuntu:18.04
MAINTAINER Hermann Krumrey <hermann@krumreyh.com>
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && \
apt install -y python3 python3-pip python3-mysqldb && \
pip3 install flask
ADD . flask-app
RUN cd flask-app && python3 setup.py install
WORKDIR flask-app
EXPOSE 8000
CMD ["/usr/bin/python3", "server.py"]
......@@ -23,11 +23,42 @@ page
# Deployment notes:
Since this site uses MySQL, `python3-mysql` (Ubuntu) needs to be installed.
You can deploy the website using docker and docker-compose.
To do this run the following commands:
To correctly function, a lot of environment variables must be set and
written to a JSON file using the [generate_secrets.py](generate_secrets.py)
file. Consult that file to see which variables need to be set.
# Builds the docker image
docker build -f docker/Dockerfile -t bundesliga-tippspiel-prod . --no-chache
# Starts the container and the database container
docker-compose -f docker/docker-compose-prod.yml up -d
# If you want to use an updated image
docker-compose -f docker/docker-compose-prod.yml up -d --no-deps bundesliga-tippspiel-prod-app
The .env file must contain the following variables:
* MYSQL_ROOT_PASSWORD
* MYSQL_USER
* MYSQL_PASSWORD
* MYSQL_DATABASE
* FLASK_SECRET
* RECAPTCHA_SITE_KEY
* RECAPTCHA_SECRET_KEY
* SMTP_ADDRESS
* SMTP_PASSWORD
* SMTP_PORT
* SMTP_HOST
* OPENLIGADB_SEASON
* OPENLIGADB_LEAGUE
# Backing up and restoring
All the data is stored in the mysql/mariadb database, so you can backup the
database using the following command:
docker exec bundesliga-tippspiel-prod-db-container mysqldump --user root --password=$MYSQL_ROOT_PASSWORD bundesliga_tippspiel > $BACKUPS_DIR/bundesliga_tippspiel-$(date --iso-8601).db
And restoring can be done like this:
docker exec bundesliga-tippspiel-prod-db-container mysql -u root --password=$MYSQL_ROOT_PASSWORD bundesliga_tippspiel < $BACKUP_FILE
## Further Information
......
"""LICENSE
Copyright 2017 Hermann Krumrey <hermann@krumreyh.com>
This file is part of bundesliga-tippspiel.
bundesliga-tippspiel is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
bundesliga-tippspiel is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with bundesliga-tippspiel. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import os
from typing import Type
from puffotter.flask.Config import Config as BaseConfig
class Config(BaseConfig):
"""
Configuration for the flask application
"""
OPENLIGADB_SEASON: str = os.environ.get("OPENLIGADB_SEASON", "2019")
"""
The openligadb season to use
"""
OPENLIGADB_LEAGUE: str = os.environ.get("OPENLIGADB_LEAGUE", "bl1")
"""
The openligadb league to use
"""
@classmethod
def _load_extras(cls, parent: Type[BaseConfig]):
"""
Loads non-standard configuration variables
:param parent: The base configuration
:return: None
"""
from bundesliga_tippspiel.template_extras import profile_extras
parent.API_VERSION = "2"
parent.STRINGS.update({
"401_message": "Du bist nicht angemeldet. Bitte melde dich an.",
"500_message": "The server encountered an internal error and "
"was unable to complete your request. "
"Either the server is overloaded or there "
"is an error in the application.",
"user_does_not_exist": "Dieser Nutzer existiert nicht",
"user_already_logged_in": "Du bist bereits angemeldet.",
"user_already_confirmed": "Dieser Nutzer ist bereits "
"bestätigt worden.",
"user_is_not_confirmed": "Dieser Nutzer wurde "
"noch nicht bestätigt",
"invalid_password": "Das angegebene Passwort ist inkorrekt.",
"logged_in": "Du hast dich erfolgreich angemeldet",
"logged_out": "Erfolgreich ausgeloggt",
"username_length": "Username zu lang: Der Username muss zwischen "
"{} und {} Zeichen lang sein.",
"passwords_do_not_match": "Die angegebenen Passwörter stimmen "
"nicht miteinander überein.",
"email_already_in_use": "Die gewählte Email-Address wird bereits "
"verwendet.",
"username_already_exists": "Der ausgewählte Username existiert "
"bereits.",
"recaptcha_incorrect": "Bitte löse das ReCaptcha.",
"registration_successful": "Siehe in deiner Email-Inbox nach, "
"um die Registrierung abzuschließen.",
"registration_email_title": "Tippspiel Registrierung",
"confirmation_key_invalid": "Der angegebene Bestätigungsschlüssel "
"ist inkorrekt.",
"user_confirmed_successfully": "Benutzer wurde erfolgreich "
"registriert. "
"Du kannst dich jetzt anmelden.",
"password_reset_email_title": "Password Zurücksetzen",
"password_was_reset": "Passwort erfolgreich zurückgesetzt. "
"Sehe in deinem Email-Postfach nach.",
"password_changed": "Dein Passwort wurde erfolgreich geändert.",
"user_was_deleted": "Dein Account wurde erfolgreich gelöscht"
})
parent.TEMPLATE_EXTRAS.update({
"profile": profile_extras
})
......@@ -18,29 +18,14 @@ along with bundesliga-tippspiel. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
app = Flask(__name__)
"""
The Flask App
"""
db = SQLAlchemy()
sentry_dsn = "https://e91e468e84424758bd74e6908af2c565@sentry.namibsun.net/6"
"""
The SQLAlchemy database connection
The sentry DSN used for exception logging
"""
login_manager = LoginManager(app)
root_path: str = os.path.join(os.path.dirname(os.path.abspath(__file__)))
"""
The Flask-Login Login Manager
The root path of the application
"""
# Config
app.config["TRAP_HTTP_EXCEPTIONS"] = True
login_manager.session_protection = "strong"
if "FLASK_TESTING" in os.environ: # pragma: no cover
app.testing = os.environ["FLASK_TESTING"] == "1"
......@@ -17,13 +17,14 @@ You should have received a copy of the GNU General Public License
along with bundesliga-tippspiel. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Any, Optional, List
from typing import Dict, Any, Optional, List, Type
from flask import abort, redirect, url_for, request
from bundesliga_tippspiel import db
from bundesliga_tippspiel.types.enums import AlertSeverity
from bundesliga_tippspiel.types.exceptions import ActionException
from bundesliga_tippspiel.models.ModelMixin import ModelMixin
from bundesliga_tippspiel.models.match_data.Match import Match
from werkzeug.wrappers import Response
from puffotter.flask.base import db
from puffotter.flask.enums import AlertSeverity
from puffotter.flask.db.ModelMixin import ModelMixin
from bundesliga_tippspiel.exceptions import ActionException
from bundesliga_tippspiel.db.match_data.Match import Match
class Action:
......@@ -105,7 +106,7 @@ class Action:
success_url: str,
success_msg: ActionException,
failure_url: str
) -> str:
) -> Response:
"""
Executes the action and subsequently redirects accordingly
:param success_url: The URL to which to redirect upon success
......@@ -129,7 +130,7 @@ class Action:
return redirect(url_for(failure_url))
@staticmethod
def handle_id_fetch(_id: int, query_cls: type(db.Model)) -> db.Model:
def handle_id_fetch(_id: int, query_cls: Type[db.Model]) -> db.Model:
"""
Handles fetching a single object by using it's ID
Raises an ActionException if an ID does not exist
......@@ -194,12 +195,25 @@ class Action:
:param keyword: The keyword to use, e.g: bet|match|player etc.
:return: The wrapped response dictionary
"""
key = "{}s".format(keyword)
if keyword in ["match"]:
response = {"{}es".format(keyword): result}
else:
response = {"{}s".format(keyword): result}
key = "{}es".format(keyword)
response: Dict[str, Any] = {key: result}
if getattr(self, "id", None) is not None:
response[keyword] = result[0]
return response
# noinspection PyAbstractClass
class GetAction(Action):
"""
Special Action class for 'Get' Actions
"""
def __init__(self, _id: Optional[int]):
"""
:param _id: The ID to get
"""
self.id = None if _id is None else int(_id)
"""LICENSE
Copyright 2017 Hermann Krumrey <hermann@krumreyh.com>
This file is part of bundesliga-tippspiel.
bundesliga-tippspiel is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
bundesliga-tippspiel is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with bundesliga-tippspiel. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Any
from bundesliga_tippspiel import db
from bundesliga_tippspiel.models.auth.ApiKey import ApiKey
from bundesliga_tippspiel.actions.Action import Action
from bundesliga_tippspiel.types.exceptions import ActionException
class ApiKeyDeleteAction(Action):
"""
Action that allows a user to delete an API key
"""
def __init__(self, api_key: str):
"""
Initializes the ApiKeyDeleteAction object
:param api_key: The API key to delete
:raises: ActionException if any problems occur
"""
self.api_key = str(api_key)
def validate_data(self):
"""
Validates user-provided data
:return: None
:raises ActionException: if any data discrepancies are found
"""
api_key = ApiKey.query.get(self.api_key.split(":", 1)[0])
if api_key is None:
raise ActionException(
"API Key does not exist",
"Der API Schlüssel existiert nicht"
)
elif not api_key.verify_key(self.api_key):
raise ActionException(
"API key not valid",
"Der API Schlüssel ist ungültig"
)
def _execute(self) -> Dict[str, Any]:
"""
Confirms a previously registered user
:return: A JSON-compatible dictionary containing the response
:raises ActionException: if anything went wrong
"""
api_key = ApiKey.query.get(self.api_key.split(":", 1)[0])
db.session.delete(api_key)
db.session.commit()
return {}
@classmethod
def _from_dict(cls, data: Dict[str, Any]):
"""
Generates an action from a dictionary
:param data: The dictionary containing the relevant data
:return: The generated Action object
"""
return cls(
data["api_key"]
)
"""LICENSE
Copyright 2017 Hermann Krumrey <hermann@krumreyh.com>
This file is part of bundesliga-tippspiel.
bundesliga-tippspiel is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
bundesliga-tippspiel is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with bundesliga-tippspiel. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Any
from bundesliga_tippspiel import db
from bundesliga_tippspiel.models.auth.User import User
from bundesliga_tippspiel.models.auth.ApiKey import ApiKey
from bundesliga_tippspiel.actions.Action import Action
from bundesliga_tippspiel.types.exceptions import ActionException
from bundesliga_tippspiel.utils.crypto import verify_password, generate_hash, \
generate_random
class ApiKeyGenAction(Action):
"""
Action that allows a user to generate a new API key
"""
def __init__(self, username: str, password: str):
"""
Initializes the ApiKeyGenAction object
:param username: The user's username
:param password: The user's password
:raises: ActionException if any problems occur
"""
self.username = str(username)
self.password = str(password)
def validate_data(self):
"""
Validates user-provided data
:return: None
:raises ActionException: if any data discrepancies are found
"""
user = User.query.filter_by(username=self.username).first()
if user is None:
raise ActionException(
"User does not exist",
"Der spezifizierte Nutzer existiert nicht"
)
elif not user.confirmed:
raise ActionException(
"User is not confirmed",
"Der spezifizierte Nutzer wurde noch nicht bestätigt"
)
elif not verify_password(self.password, user.password_hash):
raise ActionException(
"Invalid Password",
"Das angegebene Passwort ist nicht korrekt"
)
def _execute(self) -> Dict[str, Any]:
"""
Confirms a previously registered user
:return: A JSON-compatible dictionary containing the response
:raises ActionException: if anything went wrong
"""
user = User.query.filter_by(username=self.username).first()
key = generate_random(20)
hashed = generate_hash(key).decode("utf-8")
api_key = ApiKey(user=user, key_hash=hashed)
db.session.add(api_key)
db.session.commit()
return {
"api_key": "{}:{}".format(api_key.id, key.decode("utf-8")),
"expiration": int(api_key.creation_time) + ApiKey.MAX_AGE,
"user": user.__json__(True)
}
@classmethod
def _from_dict(cls, data: Dict[str, Any]):
"""
Generates an action from a dictionary
:param data: The dictionary containing the relevant data
:return: The generated Action object
"""
return cls(
data["username"],
data["password"]
)
"""LICENSE
Copyright 2017 Hermann Krumrey <hermann@krumreyh.com>
This file is part of bundesliga-tippspiel.
bundesliga-tippspiel is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
bundesliga-tippspiel is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with bundesliga-tippspiel. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Any
from flask_login import current_user
from bundesliga_tippspiel import db
from bundesliga_tippspiel.actions.Action import Action
from bundesliga_tippspiel.types.exceptions import ActionException
from bundesliga_tippspiel.utils.crypto import generate_hash
class ChangePasswordAction(Action):
"""
Action that allows a user to change their password
"""
def __init__(
self, old_password: str, new_password: str, password_repeat: str
):
"""
Initializes the ChangePasswordAction object
:param old_password: The old password for verification purposes
:param new_password: The new password
:param password_repeat: The new password repeated
:raises: ActionException if any problems occur
"""
self.old_password = str(old_password)
self.new_password = str(new_password)
self.password_repeat = str(password_repeat)
def validate_data(self):
"""
Validates user-provided data
:return: None
:raises ActionException: if any data discrepancies are found
"""
if not current_user.is_authenticated:
raise ActionException(
"Unauthorized",
"Du musst hierfür angemeldet sein",
401
)
elif not current_user.verify_password(self.old_password):
raise ActionException(
"Invalid Password",
"Das angegebene Passwort ist nicht korrekt"
)
elif self.new_password != self.password_repeat:
raise ActionException(
"Password Mismatch",
"Die angegebenen Passwörter stimmen nicht miteinander überein."
)
def _execute(self) -> Dict[str, Any]:
"""
Confirms a previously registered user
:return: A JSON-compatible dictionary containing the response
:raises ActionException: if anything went wrong
"""
current_user.password_hash = \
generate_hash(self.new_password).decode("utf-8")
db.session.commit()
return {}
@classmethod
def _from_dict(cls, data: Dict[str, Any]):
"""
Generates an action from a dictionary
:param data: The dictionary containing the relevant data
:return: The generated Action object
"""
return cls(
data["old_password"],
data["new_password"],
data["password_repeat"]
)
"""LICENSE
Copyright 2017 Hermann Krumrey <hermann@krumreyh.com>
This file is part of bundesliga-tippspiel.
bundesliga-tippspiel is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
bundesliga-tippspiel is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with bundesliga-tippspiel. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Any
from bundesliga_tippspiel import db
from bundesliga_tippspiel.types.enums import AlertSeverity
from bundesliga_tippspiel.models.auth.User import User
from bundesliga_tippspiel.types.exceptions import ActionException
from bundesliga_tippspiel.utils.db import user_exists
from bundesliga_tippspiel.utils.crypto import verify_password
from bundesliga_tippspiel.actions.Action import Action
class ConfirmAction(Action):
"""
Action that allows the confirmation of previously registered users
"""