Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • startuplab/courses/tjts5901-continuous-software-engineering/TJTS5901-K23_template
  • planet-of-the-apes/tjts-5901-apeuction
  • uunot-yliopiston-leivissa/tjts-5901-uunot
  • contain-the-cry/tjts-5901-auction-system
  • avengers/avengers
  • cse6/cse-6
  • 13th/13-sins-of-gitlab
  • fast-and-furious/fast-and-furious
  • back-to-the-future/delorean-auction
  • monty-pythons-the-meaning-of-life/the-meaning-of-life
  • team-atlantis/the-empire
  • code-with-the-wind/auction-project
  • the-pirates/the-pirates
  • do-the-right-thing/do-the-right-thing
  • inception/inception
  • the-social-network-syndicate/the-social-auction-network
  • team-the-hunt-for-red-october/tjts-5901-k-23-red-october
  • good-on-paper/good-paper-project
  • desperados/desperados
19 results
Show changes
Commits on Source (163)
Showing with 922 additions and 26 deletions
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
}, },
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [ 5001 ], // "forwardPorts": [ 5001 ],
// Uncomment the next line if you want start specific services in your Docker Compose config. // Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [], // "runServices": [],
...@@ -90,5 +90,6 @@ ...@@ -90,5 +90,6 @@
// "terraform": "latest", // "terraform": "latest",
"azure-cli": { "azure-cli": {
"version": "lts" "version": "lts"
}
} }
} }
...@@ -164,4 +164,6 @@ cython_debug/ ...@@ -164,4 +164,6 @@ cython_debug/
_docs/ _docs/
.env .env
dotenv
.DS_Store .DS_Store
*.pem
## Stage names in the pipeline. ## Stage names in the pipeline.
stages: stages:
- build - build
- test
- deploy
variables: variables:
## Name for the generated image. Change this if you wish, but watch out ## Name for the generated image. Change this if you wish, but watch out
## for special characters and spaces! ## for special characters and spaces!
DOCKER_IMAGE_NAME: ${DOCKER_REGISTRY}/tjts5901 DOCKER_IMAGE_NAME: ${DOCKER_REGISTRY}/tjts5901teamfrozen
DOCKER_TAG: latest DOCKER_TAG: $CI_COMMIT_REF_SLUG
## (Optional) More verbose output from pipeline. Enabling it might reveal secrets. ## (Optional) More verbose output from pipeline. Enabling it might reveal secrets.
#CI_DEBUG_TRACE: "true" #CI_DEBUG_TRACE: "true"
...@@ -43,3 +45,38 @@ build: ...@@ -43,3 +45,38 @@ build:
--local dockerfile=. \ --local dockerfile=. \
--opt build-arg:CI_COMMIT_SHA=${CI_COMMIT_SHA} \ --opt build-arg:CI_COMMIT_SHA=${CI_COMMIT_SHA} \
--output type=image,name=${DOCKER_IMAGE_NAME}:${DOCKER_TAG},push=true --output type=image,name=${DOCKER_IMAGE_NAME}:${DOCKER_TAG},push=true
## Run the tests. If any of the tests fails, pipeline is rejected.
test:
## Optional: include stage and environment name
stage: test
# environment: testing
image: ${DOCKER_IMAGE_NAME}:${DOCKER_TAG}
variables:
## Setup variable pointin to mongo service
MONGO_URL: mongodb://frozen-mdb:9Llvg8WY1I8bLeBo6t8vZbWMnL6g7pOqJ9OBtj6N6XPlWe0UWt9q8phaiaSdfnrlkgAu2KOPiW3IACDbrIm5Eg==@frozen-mdb.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@frozen-mdb@
## When job is started, also start these things
services:
- name: mongo:4.2 # update to reflect same version used on production
alias: mongo
script:
- pip install --disable-pip-version-check -e .[test]
- pytest -v
- echo "Test were run succesfully!"
include:
- template: Jobs/SAST.gitlab-ci.yml
## Deploy latest image to the production
deploy:
stage: deploy
environment: production
image: docker:20.10.23
only:
- main
script:
## Copy credentials to container
- mkdir -p ${HOME}/.docker && echo "${DOCKER_AUTH_CONFIG}" > "${HOME}/.docker/config.json"
## Add the `latest` tag to the image we have build.
- docker buildx imagetools create ${DOCKER_IMAGE_NAME}:${DOCKER_TAG} --tag ${DOCKER_IMAGE_NAME}:latest
...@@ -21,3 +21,31 @@ docker run -it -p 5001:5001 -e "FLASK_DEBUG=1" -v "${PWD}:/app" tjts5901 ...@@ -21,3 +21,31 @@ docker run -it -p 5001:5001 -e "FLASK_DEBUG=1" -v "${PWD}:/app" tjts5901
``` ```
Please see the `docs/tjts5901` folder for more complete documentation. Please see the `docs/tjts5901` folder for more complete documentation.
## Application address
```
https://frozen-app.azurewebsites.net/
```
## Reporting guideline
```
Check out the website and report any issues that you notice. Mark them as issues in the gitlab. Use the bug report template (found in the issues).
Please remember to add a date and a time to your report. If there are no issues, please report that as well in form of a short free from gitlab issue.
We would like you to check out at least the following features (lack on nav bar is known issue)
Registeration https://frozen-app.azurewebsites.net/auth/register
Login https://frozen-app.azurewebsites.net/auth/
Add item for auction https://frozen-app.azurewebsites.net/addItem
Check items at (we know it's ugly) https://frozen-app.azurewebsites.net/listBid
view your profile page (we know it's ugly) https://frozen-app.azurewebsites.net/auth/profile
logout https://frozen-app.azurewebsites.net/auth/logout
Generate token https://frozen-app.azurewebsited.net/auth/profile/yourmail/token (remember to use mail you used for registration)
```
\ No newline at end of file
...@@ -5,6 +5,13 @@ ...@@ -5,6 +5,13 @@
version: '3' version: '3'
services: services:
# MongoDB container
mongodb:
image: mongo:4.2
restart: unless-stopped
ports:
- 27017:27017
tjts5901: tjts5901:
build: build:
context: . context: .
...@@ -34,10 +41,10 @@ services: ...@@ -34,10 +41,10 @@ services:
## Don't restart container if it exits. Useful for debugging, not for production. ## Don't restart container if it exits. Useful for debugging, not for production.
restart: 'no' restart: 'no'
# Example for another service, such as a database. ## Start a mongodb container and link it to the app container
# mongodb: depends_on:
# image: mongo:stable - mongodb
# restart: unless-stopped
volumes: volumes:
tjts5901-vscode-extensions: tjts5901-vscode-extensions:
Weekly report week 1
Everyone: set their local development environment
Mikael: Created a document for way of working and wrote 1 user story
Veera: Wrote 2 user stories
Rayan: Wrote 2 user stories
Arno: started setting up the project in GitLab and in Azure
Top 5 security risks considered (Not much was yet done)
A01:2021-Broken Access Control
Different roles and their access levels to the system were defined. Access control was discussed.
A07:2021-Identification and Authentication Failures
Authentication was decided to be left to third party platform. It was discussed that no default or unsecure credentials should be deployed to the final product.
A09:2021-Security Logging and Monitoring Failures
Design of the monitoring view was discussed
A04:2021-Insecure Design
Security taken into account in early design. Security related user stories were discussed.
A05:2021-Security Misconfiguration
It was discussed that no default account should be ever left on the finished product
\ No newline at end of file
Weekly Report week 2
Mikael
1. wrote 6 user stories
2. Identified the 5 most concerning security risks for the weeks tasks
3. Setted up the testing up
4. Started the Login/registration feature
Arno
1. Completed setting up the Azure infrastructure and the Gitlab-runner.
2. Added application address https://frozen-app.azurewebsites.net/ to README.md
3. Setup the database on azure
4. Initial setup of flask application (pages “/”, “/hello” and “/server-info”)
5. Added initial schemas for user and items into the database
6. Added test for saving of objects (user and item) into the database (page “/test”)
7. Considered A02:2021 and A10:2021.
Rayan
1. Finish local environment
2. Web page for adding items
Top 5 security risks considered (from OWASP) https://owasp.org/www-project-top-ten/
A10:2021-Server-Side Request Forgery
Data are currently not accessible via internet
A02:2021-Cryptographic Failures
Passwords should be stored encrypted
A05:2021-Security Misconfiguration
Automated test process for build, unit tests and deployment was setted up to help spot mistakes
A07:2021-Identification and Authentication Failures
No weak usernames or admin password will be implemented
A01:2021-Broken Access Control
The roles and their access rights have been discussed
Weekly Report week 3
Mikael:
1. Wrote 6 more user stories and added them to gitlab
2. Added frontend solution for login and registration pages with bootstrap 4 style sheet
3. Identified the most relevant security issues for the week
4. Added static testing (SAST)
Arno:
1. Backend for saving of offers
2. Backend for loading of offers
3. Adding image saving feature to add_items page and backend support
4. Fixing deployment pipeline in gitlab
5. Code refactoring views.py for compliance with recommendations
6. Setup logging and monitoring scripts
7. Setup logging via sentry
8. Created database unittest
9. Considered OWASP A09:2021, A01:2021 and A05:2021.
Rayan:
1. Frontend page for list of items on offer
Top 5 security risks considered (from OWASP) https://owasp.org/www-project-top-ten/
A09:2021-Security Logging and Monitoring Failures
Setting up logging with sentry, including email notifications on failures
A04:2021-Insecure Design
Need for security testing was identified. User stories were written on that base.
A01:2021-Broken Access Control
Added access control this week. Access control cannot be bypassed by modifying the URL anymore (use of login_required feature of flask_login)
A05:2021-Security Misconfiguration
No default accounts are used. Unnecessary testing features should be removed before final submission of the project.
A08:2021-Software and Data Integrity Failures
Added static testing to minimise the chance that malicious or vulnerable code gets into the final product.
Weekly Report week 4
Mikael
1. Played Privacy Problems tool (game)
2. Created a instructions for bug hunt
3. Created a bug report template for bug hunt
4. Created multiple bug reports for other teams
5. Added a navigation bar to the site
6. Added the weekly report to repository
Arno
1. Added image resizing feature in addItems
2. Added image showing feature to items list
3. Backend for saving of users
4. Added image feature to db unittest
5. Added logout function and made sure, that pages are only accessible after login
6. Reviewed and updated existing user stories; wrote 4 new user stories as issues in GitLab
7. Played Privacy Problems tool (game)
8. Updated db test to take into consideration, that user must now be logged-in
9. Implemented filter for items in view of items (only open auctions are shown)
10 Added token generation feature (frontend and backend, including db model)
11. Added Bid db model
12. Implemented and tested bidding on items via REST api
13. Implemented backend support for bidding on items
14. Adding image feature to item detail page
15. Shown prices for items now update when someone bids on them (current price instead of starting price)
Rayan
1. Played Privacy Problems tool (game)
2. Added biding page
3. Identified the most notable security risks for the week.
Top 5 security risks considered (from OWASP) https://owasp.org/www-project-top-ten/
A01 Broken Access Control
Added check for login. Users have now log into the site in order to access any of the pages and can logout
A02 Cryptographic failures
User information is now saved to the database. Passwords are encrypted properly.
A07 identification and Authentication failures
Implemented a third party authentication to make the authentication stronger. You now must have a token to make a API request
A04:2021-Insecure Design
Made sure that no user information (such as emails) were publicly available to be taken advantage of.
A06:2021 – Vulnerable and Outdated Components
Made sure that all of the used libraries and dependencies are from trusted repositories
...@@ -14,9 +14,4 @@ ...@@ -14,9 +14,4 @@
# developers or deployments, as you can simply share the .env file rather than hardcoding the # developers or deployments, as you can simply share the .env file rather than hardcoding the
# values into the application itself. # values into the application itself.
FLASK_APP=tjts5901.app MONGO_URL = "mongodb://localhost:27017/
FLASK_DEBUG=1 \ No newline at end of file
# Enable rich logging for more human readable log output. Requires installing
# `rich` and `flask-rich` packages.
#RICH_LOGGING=1
nameserver 8.8.8.8
\ No newline at end of file
...@@ -28,6 +28,7 @@ dependencies = {file = ["requirements.txt"]} ...@@ -28,6 +28,7 @@ dependencies = {file = ["requirements.txt"]}
[project.optional-dependencies] [project.optional-dependencies]
test = [ test = [
"pytest", "pytest",
"coverage",
] ]
docs = [ docs = [
"mkdocs", "mkdocs",
......
...@@ -2,11 +2,20 @@ ...@@ -2,11 +2,20 @@
importlib-metadata importlib-metadata
# Framework and libraries # Framework and libraries
flask flask==2.2.2
python-dotenv python-dotenv
flask-login
flask_babel
flask-mongoengine==1.0
Pillow
# Git hooks # Git hooks
pre-commit pre-commit
# Sentry for error reporting
sentry-sdk[flask]
sentry-sdk[mongoengine]
# More production-ready web server # More production-ready web server
#gunicorn #gunicorn
...@@ -6,7 +6,14 @@ JYU TJTS5901 Course project ...@@ -6,7 +6,14 @@ JYU TJTS5901 Course project
from importlib_metadata import (PackageNotFoundError, from importlib_metadata import (PackageNotFoundError,
version) version)
from .app import create_app
try: try:
__version__ = version(__name__) __version__ = version(__name__)
except PackageNotFoundError: except PackageNotFoundError:
__version__ = "unknown" __version__ = "unknown"
__all__ = [
"create_app",
"__version__",
]
\ No newline at end of file
...@@ -7,7 +7,9 @@ Flask tutorial: https://flask.palletsprojects.com/en/2.2.x/tutorial/ ...@@ -7,7 +7,9 @@ Flask tutorial: https://flask.palletsprojects.com/en/2.2.x/tutorial/
""" """
import logging
from os import environ from os import environ
import os
from typing import Dict, Optional from typing import Dict, Optional
from dotenv import load_dotenv from dotenv import load_dotenv
...@@ -19,7 +21,10 @@ from flask import ( ...@@ -19,7 +21,10 @@ from flask import (
) )
from .utils import get_version from .utils import get_version
from .db import init_db
from .logging import init_logging
logger = logging.getLogger(__name__)
def create_app(config: Optional[Dict] = None) -> Flask: def create_app(config: Optional[Dict] = None) -> Flask:
""" """
...@@ -29,15 +34,38 @@ def create_app(config: Optional[Dict] = None) -> Flask: ...@@ -29,15 +34,38 @@ def create_app(config: Optional[Dict] = None) -> Flask:
""" """
flask_app = Flask(__name__, instance_relative_config=True) flask_app = Flask(__name__, instance_relative_config=True)
if config: flask_app.config.from_mapping(
SECRET_KEY='dev',
BRAND="The Frozen Auction",
)
init_logging(flask_app)
# load the instance config, if it exists, when not testing
if config is None:
flask_app.config.from_pyfile('config.py', silent=True)
else:
flask_app.config.from_mapping(config) flask_app.config.from_mapping(config)
# Set flask config variable for "rich" loggin from environment variable. # Initialize logging early, so that we can log the rest of the initialization.
flask_app.config.from_envvar("RICH_LOGGING", silent=True) init_logging(flask_app)
# ensure the instance folder exists
try:
os.makedirs(flask_app.instance_path)
except OSError:
print('OsError when trying to open flask_app folder. Instance_path not existing.')
# Register blueprints # Initialize the database connection.
from . import views # pylint: disable=import-outside-toplevel init_db(flask_app)
# Register blueprints
from . import views
flask_app.register_blueprint(views.bp, url_prefix='') flask_app.register_blueprint(views.bp, url_prefix='')
flask_app.register_blueprint(views.api)
from .auth import init_auth
init_auth(flask_app)
return flask_app return flask_app
...@@ -49,13 +77,6 @@ load_dotenv() ...@@ -49,13 +77,6 @@ load_dotenv()
# Create the Flask application. # Create the Flask application.
app = create_app() app = create_app()
# Initialize "rich" output if enabled. It produces more human readable logs.
# You need to install `flask-rich` to use this.
if app.config.get("RICH_LOGGING"):
from flask_rich import RichApplication
RichApplication(app)
app.logger.info("Using [blue]rich[/blue] interface for logging")
@app.route("/server-info") @app.route("/server-info")
def server_info() -> Response: def server_info() -> Response:
...@@ -65,8 +86,18 @@ def server_info() -> Response: ...@@ -65,8 +86,18 @@ def server_info() -> Response:
This is useful for monitoring the server, and for checking that the server is This is useful for monitoring the server, and for checking that the server is
running correctly. running correctly.
""" """
# Test for database connection
database_ping = False
try:
from .db import db # pylint: disable=import-outside-toplevel
database_ping = db.connection.admin.command('ping').get("ok", False) and True
except Exception as exc: # pylint: disable=broad-except
logger.warning("Error querying mongodb server: %r", exc,
exc_info=True,
extra=flask_app.config.get_namespace("MONGODB"))
response = { response = {
"database_connectable": database_ping,
"version": get_version(), "version": get_version(),
"build_date": environ.get("BUILD_DATE", None) "build_date": environ.get("BUILD_DATE", None)
} }
......
from datetime import datetime
import functools
import logging
import base64
from flask import (
Blueprint, flash, redirect, render_template, request, session, url_for, abort
)
from flask_login import (
LoginManager,
login_user,
logout_user,
login_required,
current_user,
)
from flask_babel import _
from werkzeug.security import check_password_hash, generate_password_hash
from sentry_sdk import set_user
from .models import AccessToken, User, Item
from mongoengine import DoesNotExist
from mongoengine.queryset.visitor import Q
bp = Blueprint('auth', __name__, url_prefix='/auth')
logger = logging.getLogger(__name__)
def init_auth(app):
"""
Integrate authentication into the application.
"""
app.register_blueprint(bp)
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.user_loader(load_logged_in_user)
app.config['AUTH_HEADER_NAME'] = 'Authorization'
login_manager.request_loader(load_user_from_request)
login_manager.init_app(app)
logger.debug("Initialized authentication")
def load_user_from_request(request):
"""
Load a user from the request.
This function is used by Flask-Login to load a user from the request.
"""
api_key = request.headers.get("Authorization")
if api_key:
api_key = api_key.replace("Bearer ", "", 1)
try:
token = AccessToken.objects.get(token=api_key)
if token.expires and token.expires < datetime.utcnow():
logger.warning("Token expired: %s", api_key)
return None
# User is authenticated
token.last_used_at = datetime.utcnow()
token.save()
logger.debug("User authenticated via token: %r", token.user.email, extra={
"user": token.user.email,
"user_id": str(token.user.id),
"token": token.token,
})
return token.user
except DoesNotExist:
logger.error("Token not found: %s", api_key)
return None
def load_logged_in_user(user_id):
"""
Load a user from the database, given the user's id.
"""
try:
user = User.objects.get(id=user_id)
set_user({"id": str(user.id), "email": user.email})
except DoesNotExist:
logger.error("User not found: %s", user_id)
return None
return user
def get_user_by_email(email: str) -> User:
"""
Get a user from the database, given the user's email.
If the email is 'me', then the current user is returned.
:param email: The email of the user to get.
"""
if email is None:
abort(404)
if email == "me" and current_user.is_authenticated:
email = current_user.email
try:
user = User.objects.get_or_404(email=email)
except DoesNotExist:
logger.error("User not found: %s", email)
abort(404)
return user
@bp.route('/register', methods=('GET', 'POST'))
def register():
if request.method == 'POST':
print("Registering user...")
email = request.form['email']
password = request.form['password']
password2 = request.form['password2']
error = None
if not email:
error = 'Username is required.'
elif not password:
error = 'Password is required.'
elif password != password2:
error = 'Passwords do not match.'
if error is None:
try:
user = User(
email=email,
password=generate_password_hash(password)
)
user.save()
flash("You have been registered. Please log in.")
except Exception as exc:
error = f"Error creating user: {exc!s}"
else:
return redirect(url_for("auth.login"))
print("Could not register user:", error)
flash(error)
return render_template('auth/register.html')
@bp.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
error = None
try:
user = User.objects.get(email=email)
except DoesNotExist:
error = 'Incorrect username.'
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user['password'], password):
error = 'Incorrect password.'
if error is None:
remember_me = bool(request.form.get("remember-me", False))
if login_user(user, remember=remember_me):
flash(f"Hello {email}, You have been logged in.")
next = request.args.get('next')
# Better check that the user actually clicked on a relative link
# or else they could redirect you to a malicious website!
if next is None or not next.startswith('/'):
next = url_for('views.list_bid')
return redirect(next)
else:
error = "Error logging in."
logger.info("Error logging user in: %r: Error: %s", email, error)
flash(error)
return render_template('auth/login.html')
@bp.route('/logout')
@login_required
def logout():
"""
Log out the current user.
Also removes the "remember me" cookie.
"""
logout_user()
flash("You have been logged out.")
return redirect(url_for('auth.login'))
@bp.route('/profile', defaults={'email': 'me'})
@bp.route('/profile/<email>')
@login_required
def profile(email):
"""
Show the user's profile page for the given email.
If the email is 'me', then the current user's profile is shown.
"""
from .views import get_item_price
user: User = get_user_by_email(email)
# List the items user has created
items = Item.objects(seller=user).all()
for item in items:
item.current_price = get_item_price(item)
item.created_at = item.created_at.strftime("%Y-%m-%d %H:%M:%S")
item.closes_at = item.closes_at.strftime("%Y-%m-%d %H:%M:%S")
item.image_base64 = base64.b64encode(item.image.read()).decode('utf-8')
return render_template('auth/profile.html', user=user, items=items)
@bp.route('/profile/<email>/token', methods=('GET', 'POST'))
@login_required
def user_access_tokens(email):
"""
Show the user's tokens page for the given email.
"""
user: User = get_user_by_email(email)
# Fetch all the user tokens that are active or have no expire date
tokens = AccessToken.objects(Q(expires__gte=datetime.now()) | Q(expires=None), user=user).all()
token = None
if request.method == 'POST':
try:
name = request.form['name']
if expires := request.form.get('expires'):
expires = datetime.fromisoformat(expires)
else:
expires = None
token = AccessToken(
user=user,
name=name,
expires=expires,
)
token.save()
except KeyError as exc:
logger.debug("Missing required field: %s", exc)
flash(("Required field missing"))
except Exception as exc:
logger.exception("Error creating token: %s", exc)
flash(("Error creating token: %s") % exc)
else:
flash(("Created token: %s") % token.name)
return render_template('auth/tokens.html', user=user, tokens=tokens, token=token)
@bp.route('/profile/<email>/token/<id>', methods=('POST',))
def delete_user_access_token(email, id):
"""
Delete an access token.
"""
user = get_user_by_email(email)
token = AccessToken.objects.get_or_404(id=id)
if token.user != user:
logger.warning("User %s tried to delete token %s", user.email, token.name, extra={
"user": user.email,
"token": str(token.id),
"token_user": token.user.email,
})
abort(403)
token.delete()
flash(f"Deleted token {token.name}")
return redirect(url_for('auth.user_access_tokens', email=token.user.email))
import logging
from os import environ
from flask_mongoengine import MongoEngine
db = MongoEngine()
logger = logging.getLogger(__name__)
def init_db(app):
"""
Initialize the database connection.
Fetches the database connection string from the environment variable `MONGO_URL`
and, if present, sets the `MONGODB_SETTINGS` configuration variable to use it.
"""
# To keep secrets private, we use environment variables to store the database connection string.
# `MONGO_URL` is expected to be a valid MongoDB connection string
mongodb_url = environ.get("MONGO_URL")
if mongodb_url is not None:
app.config["MONGODB_SETTINGS"] = {
"host": mongodb_url,
}
logger.info("Database connection string found, using it.",
# You can use the `extra` parameter to add extra information to the log message.
# This is useful for debugging, but should be removed in production.
extra={"MONGO_URL": mongodb_url} if app.debug else {})
else:
logger.warning("No database connection string found in env, using defaults.",
extra={"MONGODB_SETTINGS": app.config.get("MONGODB_SETTINGS")} if app.debug else {})
db.init_app(app)
"""
==============
Logging module
==============
In this module we'll create a new :class:`~Logger` interface, using pythons inbuild :module:`logging` module.
By default flask sends messages to stdout.
To use this module, import it into your application and call :func:`~init_logging` function:
>>> from tjts5901.logging import init_logging
>>> init_logging(app)
To use logging in your application, import the logger instance, and use it as follows:
>>> import logging
>>> logger = logging.getLogger(__name__)
>>> logger.info("Hello world!")
"""
import logging
from os import environ
import sentry_sdk
from flask import Flask
from flask.logging import default_handler as flask_handler
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.pymongo import PyMongoIntegration
from .utils import get_version
def init_logging(app: Flask):
"""
Integrate our own logging interface into application.
To bind logger into your application instance use::
>>> init_logging(app)
:param app: :class:`~Flask` instance to use as logging basis.
"""
# Setup own logger instance. Usually you'll see something like
# >>> logger = logging.getLogger(__name__)
# where `__name__` reflects the package name, which is usually `"__main__"`,
# or in this exact case `tjts5901.logging`. I'll rather define static name.
# To get access to your logger in outside of module scope you can then
# use the same syntax as follows.
logger = logging.getLogger("tjts5901")
# If flask is running in debug mode, set our own handler to log also debug
# messages.
if app.config.get("DEBUG"):
logger.setLevel(level=logging.DEBUG)
# Add flask default logging handler as one of our target handlers.
# When changes to flask logging handler is made, our logging handler
# adapts automatically. Logging pipeline:
# our appcode -> our logger -> flask handler -> ????
logger.addHandler(flask_handler)
logger.debug("TJTS5901 Logger initialised.")
# Try to get enviroment name from different sources
if enviroment := environ.get('CI_ENVIRONMENT_NAME'):
enviroment = enviroment.lower()
elif app.testing:
enviroment = "testing"
elif app.debug:
enviroment = "development"
# Populate config with environment variables for sentry logging
app.config.setdefault('SENTRY_DSN', environ.get('SENTRY_DSN'))
app.config.setdefault('SENTRY_ENVIRONMENT', enviroment)
app.config.setdefault('CI_COMMIT_SHA', environ.get('CI_COMMIT_SHA'))
# Setup sentry logging
sentry_dsn = app.config.get("SENTRY_DSN")
release = app.config.get("CI_COMMIT_SHA", get_version() or "dev")
enviroment = app.config.get("CI_ENVIRONMENT_NAME")
if sentry_dsn:
sentry = sentry_sdk.init(
dsn=sentry_dsn,
integrations=[
# Flask integration
FlaskIntegration(),
# Mongo integration. Mongoengine uses pymongo, so we need to
# integrate pymongo.
PyMongoIntegration(),
# Sentry will automatically pick up the logging module.
#LoggingIntegration(level=logging.INFO, event_level=logging.ERROR),
],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
# Set sentry debug mode to true if flask is running in debug mode.
#debug=bool(app.debug),
# By default the SDK will try to use the SENTRY_RELEASE
# environment variable, or infer a git commit
# SHA as release, however you may want to set
# something more human-readable.
release=release,
environment=enviroment,
)
app.config.setdefault("SENTRY_RELEASE", sentry._client.options["release"])
logger.info("Sentry logging enabled.", extra={"SENTRY_DSN": sentry_dsn})
else:
logger.warning("Sentry DSN not found. Sentry logging disabled.")
from PIL import Image
from io import BytesIO
from datetime import datetime
from secrets import token_urlsafe
from .db import db
from mongoengine import (
StringField,
IntField,
ReferenceField,
DateTimeField,
EmailField,
FileField,
BooleanField,
)
from flask_login import UserMixin
from bson import ObjectId
class User(UserMixin, db.Document):
"""
Model representing a user of the auction site.
"""
id: ObjectId
email = EmailField(required=True, unique=True)
password = StringField(required=True)
created_at = DateTimeField(required=True, default=datetime.utcnow)
is_disabled = BooleanField(default=False)
"Whether the user is disabled."
@property
def is_active(self) -> bool:
"""
Return whether the user is active.
This is used by Flask-Login to determine whether the user is
allowed to log in.
"""
return not self.is_disabled
def get_id(self) -> str:
"""
Return the user's id as a string.
"""
return str(self.id)
class Item(db.Document):
"""
A model for items that are listed on the auction site.
"""
# Create index for sorting items by closing date
meta = {"indexes": [
{"fields": [
"closes_at",
"closed_processed",
"seller_informed_about_result"
]}
]}
title = StringField(max_length=100, required=True)
description = StringField(max_length=2000, required=True)
starting_bid = IntField(required=True, min_value=0)
seller = ReferenceField(User, required=True)
created_at = DateTimeField(required=True, default=datetime.utcnow)
closes_at = DateTimeField()
closed_processed = BooleanField(required=True, default=False)
"Flag indicating if closing of auction was already processed"
seller_informed_about_result = BooleanField(required=True, default=False)
"Flag indicating if seller has already been informed about the result of the auction"
image = FileField()
def save(self, *args, **kwargs):
# Resize image before saving
if self.image:
image = Image.open(BytesIO(self.image.read()))
image.thumbnail((200, 200))
output = BytesIO()
image.save(output, format='JPEG')
self.image.replace(output.getvalue(), content_type='image/jpeg')
super().save(*args, **kwargs)
@property
def is_open(self) -> bool:
"""
Return whether the item is open for bidding.
"""
return self.closes_at > datetime.utcnow()
class Bid(db.Document):
"""
A model for bids on items.
"""
meta = {"indexes": [
{"fields": [
"amount",
"item",
"created_at",
"auction_end_processed",
"winning_bid",
"bidder_informed"
]}
]}
amount = IntField(required=True, min_value=0)
"Indicates the value of the bid."
bidder = ReferenceField(User, required=True)
"User who placed the bid."
item = ReferenceField(Item, required=True)
"Item that the bid is for."
created_at = DateTimeField(required=True, default=datetime.utcnow)
"Date and time that the bid was placed."
auction_end_processed = BooleanField(required=True, default=False)
"Flag indicating, that closed auction has been processes"
winning_bid = BooleanField(required=True, default=False)
"Flag indicating, whether the bid was the winning bid of the auction"
bidder_informed = BooleanField(required=True, default=False)
"Flag indicating, whether the bidder has been informed about the result of the auction"
class AccessToken(db.Document):
"""
Access token for a user.
This is used to authenticate API requests.
"""
meta = {"indexes": [
{"fields": [
"token",
"user",
"expires",
]}
]}
name = StringField(max_length=100, required=True)
"Human-readable name for the token."
user = ReferenceField(User, required=True)
"User that the token is for."
token = StringField(required=True, unique=True, default=token_urlsafe)
"The token string."
last_used_at = DateTimeField(required=False)
"Date and time that the token was last used."
created_at = DateTimeField(required=True, default=datetime.utcnow)
"Date and time that the token was created."
expires = DateTimeField(required=False)
"Date and time that the token expires."
Image diff could not be displayed: it is too large. Options to address this: view the blob.