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 (215)
......@@ -69,7 +69,7 @@
},
// 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.
// "runServices": [],
......@@ -90,5 +90,6 @@
// "terraform": "latest",
"azure-cli": {
"version": "lts"
}
}
}
......@@ -164,4 +164,6 @@ cython_debug/
_docs/
.env
dotenv
.DS_Store
*.pem
## Stage names in the pipeline.
stages:
- build
- test
- deploy
variables:
## Name for the generated image. Change this if you wish, but watch out
## for special characters and spaces!
DOCKER_IMAGE_NAME: ${DOCKER_REGISTRY}/tjts5901
DOCKER_TAG: latest
DOCKER_IMAGE_NAME: ${DOCKER_REGISTRY}/tjts5901teamfrozen
DOCKER_TAG: $CI_COMMIT_REF_SLUG
## (Optional) More verbose output from pipeline. Enabling it might reveal secrets.
#CI_DEBUG_TRACE: "true"
......@@ -43,3 +45,38 @@ build:
--local dockerfile=. \
--opt build-arg:CI_COMMIT_SHA=${CI_COMMIT_SHA} \
--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
......@@ -49,6 +49,10 @@ RUN pip --disable-pip-version-check install -r /tmp/pip-tmp/requirements.txt &&
## Copy app to WORKDIR folder
COPY . .
## Compile translations before installing app. This is done to make sure that
## translations are compiled before app is installed.
RUN pybabel compile -f -d src/tjts5901/translations/
## Install self as editable (`-e`) module. In a long run it would be recommeded
## to remove `COPY` and only install app as a package.
RUN pip --disable-pip-version-check install -v -e .
......@@ -61,6 +65,10 @@ ENV CI_COMMIT_SHA=${CI_COMMIT_SHA}
## Save build date and time
RUN echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> /app/.env
## Download the currency exchange rates from European Central Bank
RUN flask update-currency-rates
## To prevent vscode constantly re-installing extensions, we need to create a folder for it.
## In devcontainer.json, we mount volume to this folder so that extensions are not lost.
## Create vscode extension folder, and change ownership of it.
......
......@@ -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.
## 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
[python: **.py]
[jinja2: **/templates/**.html]
\ No newline at end of file
......@@ -5,6 +5,13 @@
version: '3'
services:
# MongoDB container
mongodb:
image: mongo:4.2
restart: unless-stopped
ports:
- 27017:27017
tjts5901:
build:
context: .
......@@ -34,10 +41,10 @@ services:
## Don't restart container if it exits. Useful for debugging, not for production.
restart: 'no'
# Example for another service, such as a database.
# mongodb:
# image: mongo:stable
# restart: unless-stopped
## Start a mongodb container and link it to the app container
depends_on:
- mongodb
volumes:
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.
A04:2021-Insecure Design
Made sure that no user information (such as emails) were publicly available to be taken advantage of.
A08:2021 – Software and Data Integrity Failures
Made sure that all of the used libraries and dependencies are from trusted repositories
A05:2021 – Security Misconfiguration
Identified issue where direct error messages would be sent to the end users.
\ No newline at end of file
Weekly Report week 5
Mikael
1. Fixed a bug that prevented using “login here” button on registration page. The link was wrong and it was updated (issue #22).
2. Fixed an issue where login in with wrong credentials would cause internal server error. “User” variable was not defined. Setting it as none before accessing it fixes the issue.
3. Fixed errors in the navigation bar that made it so that the profile page and logout page would not show up (no gitlab issue was made)
4. Edited the visual look for the website and added an background Image
5. Made sure that all of the pages used the base.html code so that they all had unified look
6. Added an multicurrency support to system
7. Made cost calculations for the final report
8. Made privacy issue assessment for the final report
9. Described the functionalities of the system to the final report
10. Evaluated how the system met the customer requirements for the final report
11. Evaluated the situation of half of the OWASP security risks for our system
12. Added localization support for the system with english and finnish languages
13. Solved an issue that would have shown direct error messages to the end user
Arno
1. Fixed the bug, that “access tokens” was displayed as heading on the addItem and listBid page. Probably caused by a copying mistake. Solved through text change.
2. Fixed the bug that adding of items does not work, when other image formats that .jpg are used. Solution: Pre-select.jpg files in image selection window and add instruction text. (Issue 19)
3. Considered bug #20 and #24. They reported that the bidding feature did not work. This is true as it was not implemented at the time the bughunt was conducted. This was also specified in the issue reporting instructions. Now the bidding feature is working.
4. Set the default page to listBid. This solves bug #23, which was reporting that the index start page does not have any functionality. We agree, and removed it because of that.
5. Bug #23 (default page has no functionality and cannot be used for navigation) was solved through the introduction of a navigation bar and the aforementioned setting of the listBid pages as the default page.
6. Removed the now unnecessary email input field from the addItem form. It was necessary to have it before the login feature was implemented to be able to save items.
7. Removed the no longer need /test page.
8. Added feature that a user cannot bid on their own items including an error message.
9. Solved security issues from static testing (e.g. password for testing and random variables are not used in views.py any more, try-catch construct improved). Static security testing does not return issues anymore.
10. Fixed a redirection bug from the register page to the login page. Now users are redirected to a valid URL.
11. Added current price and item image to auction section of profile page.
12. Implemented feature, that upon successful auction completion, the system informs the seller and the successful and unsuccessful buyers. For that the data model of bid and item were changed and _processed and _informed fields introduced.
13. Implemented a feature (frontend and backend) that enables users to enter the auction duration.
14. Created presentation and application demo plan.
15. Evaluated the second half of the OWAPs security risks for our system.
16. Fixed issues with tests for multi language support.
17. Added button on profile page with link token access page.
18. Presented presentation.
Rayan
on vacation
Top 5 security risks considered (from OWASP) https://owasp.org/www-project-top-ten/
Final OWAPs security risks consideration can be found in our final report.
......@@ -14,9 +14,4 @@
# developers or deployments, as you can simply share the .env file rather than hardcoding the
# values into the application itself.
FLASK_APP=tjts5901.app
FLASK_DEBUG=1
# Enable rich logging for more human readable log output. Requires installing
# `rich` and `flask-rich` packages.
#RICH_LOGGING=1
MONGO_URL = "mongodb://localhost:27017/
\ No newline at end of file
nameserver 8.8.8.8
\ No newline at end of file
......@@ -28,6 +28,7 @@ dependencies = {file = ["requirements.txt"]}
[project.optional-dependencies]
test = [
"pytest",
"coverage",
]
docs = [
"mkdocs",
......
......@@ -2,11 +2,23 @@
importlib-metadata
# Framework and libraries
flask
flask==2.2.2
python-dotenv
flask-login
flask_babel
datetime
CurrencyConverter
flask-mongoengine==1.0
Pillow
CurrencyConverter
# Git hooks
pre-commit
# Sentry for error reporting
sentry-sdk[flask]
sentry-sdk[mongoengine]
# More production-ready web server
#gunicorn
......@@ -6,7 +6,14 @@ JYU TJTS5901 Course project
from importlib_metadata import (PackageNotFoundError,
version)
from .app import create_app
try:
__version__ = version(__name__)
except PackageNotFoundError:
__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/
"""
import logging
from os import environ
import os
from typing import Dict, Optional
from dotenv import load_dotenv
......@@ -18,8 +20,14 @@ from flask import (
request,
)
from flask_babel import _
from .utils import get_version
from .db import init_db
from .logging import init_logging
from .i18n import init_babel
logger = logging.getLogger(__name__)
def create_app(config: Optional[Dict] = None) -> Flask:
"""
......@@ -29,15 +37,43 @@ def create_app(config: Optional[Dict] = None) -> Flask:
"""
flask_app = Flask(__name__, instance_relative_config=True)
if config:
flask_app.config.from_mapping(config)
flask_app.config.from_mapping(
SECRET_KEY='dev',
BRAND="The Frozen Auction",
)
# Set flask config variable for "rich" loggin from environment variable.
flask_app.config.from_envvar("RICH_LOGGING", silent=True)
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)
# Register blueprints
from . import views # pylint: disable=import-outside-toplevel
# Initialize logging early, so that we can log the rest of the initialization.
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.')
pass
# Initialize the Flask-Babel extension.
init_babel(flask_app)
# Initialize the database connection.
init_db(flask_app)
# Register blueprints
from . import views
flask_app.register_blueprint(views.bp, url_prefix='')
flask_app.register_blueprint(views.api)
from .auth import init_auth
init_auth(flask_app)
from .currency import init_currency
init_currency(flask_app)
return flask_app
......@@ -49,12 +85,11 @@ load_dotenv()
# Create the Flask application.
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")
# a simple page that says hello
@app.route("/hello")
def hello():
return (_('Hello, World!'))
@app.route("/server-info")
......@@ -65,8 +100,18 @@ def server_info() -> Response:
This is useful for monitoring the server, and for checking that the server is
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 = {
"database_connectable": database_ping,
"version": get_version(),
"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():
"""
Function that handels registration and checks if
everything is inputed properly.
"""
if request.method == 'POST':
print("Registering user...")
email = request.form['email']
password = request.form['password']
password2 = request.form['password2']
error = None
error_message = None
if not email:
error = 'Username is required.'
error_message = ( _("Username is required."))
elif not password:
error = 'Password is required.'
error_message = ( _("Password is required."))
elif password != password2:
error = 'Passwords do not match.'
error_message = ( _("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}"
error_message = ( _("Unexpected error creating user. Please try again later"))
else:
return redirect(url_for("auth.login"))
print("Could not register user:", error)
flash(error_message)
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']
user = None
error = None
error_message = None
message = ( _("Incorrect username or password."))
try:
user = User.objects.get(email=email)
except DoesNotExist:
error = 'Incorrect username.'
error_message = message
if user is None or not check_password_hash(user['password'], password):
error = 'Incorrect username or password.'
error_message = message
if error is None:
remember_me = bool(request.form.get("remember-me", False))
if login_user(user, remember=remember_me):
hello = (_("Hello"))
login = (_("You have been logged in."))
flash(f"{hello} {email}, {login}")
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."
error_message = ( _("Error logging in."))
logger.info("Error logging user in: %r: Error: %s", email, error)
flash(error_message)
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()
deleted = (_("Deleted token "))
flash(f"{deleted}{token.name}")
return redirect(url_for('auth.user_access_tokens', email=token.user.email))
"""
Currency module.
This module contains the currency module, which is used to convert currencies.
Uses the ECB (European Central Bank) as the source of currency conversion rates.
To update the currency conversion rates, run the following command:
$ flask update-currency-rates
"""
from decimal import Decimal
import logging
from pathlib import Path
from zipfile import ZipFile
import urllib.request
import click
from currency_converter import (
SINGLE_DAY_ECB_URL,
CurrencyConverter,
)
from flask_babel import (
get_locale,
format_currency,
)
from babel.numbers import get_territory_currencies, parse_decimal
from flask import (
Flask,
current_app,
render_template,
)
from markupsafe import Markup
from .auth import current_user
REF_CURRENCY = 'EUR'
"Reference currency for the currency converter."
logger = logging.getLogger(__name__)
class CurrencyProxy:
"""
Proxy for the currency converter.
This class is used to proxy the currency converter instance. This is to
ensure that the currency converter is only initialized when it is actually
used, and the used conversion list is the most up-to-date.
"""
def __init__(self, app: Flask):
self._converter = None
self._app = app
self._converter_updated = 0
self._dataset_updated = 0
def get_currency_converter(self) -> CurrencyConverter:
"""
Get a currency converter instance.
Automatically updates the currency converter if the dataset has been
updated.
Exceptions:
RuntimeError: If the currency file is not configured.
FileNotFoundError: If the currency file does not exist.
:return: A currency converter instance.
"""
if not (conversion_file := self._app.config.get('CURRENCY_FILE')):
raise RuntimeError('Currency file not configured.')
# Initialize the currency converter if it has not been initialized yet,
# or if the dataset has been updated.
self._dataset_updated = Path(conversion_file).stat().st_mtime
if self._converter is None or self._dataset_updated > self._converter_updated:
logger.info("Initializing currency converter with file %s.", conversion_file)
self._converter = CurrencyConverter(
currency_file=conversion_file,
ref_currency=REF_CURRENCY,
)
self._converter_updated = self._dataset_updated
return self._converter
def __getattr__(self, name):
"""
Proxy all other attributes to the currency converter.
"""
return getattr(self.get_currency_converter(), name)
def init_currency(app: Flask):
"""
Initialize the currency module.
This function initializes the currency module, and registers the currency
converter as an extension.
:param app: The Flask application.
:return: None
"""
# Set default currency file path
app.config.setdefault('CURRENCY_FILE', app.instance_path + '/currency.csv')
# Register the currency converter as an extension
app.extensions['currency_converter'] = CurrencyProxy(app)
# Register the currency converter as a template filter
app.add_template_filter(format_converted_currency, name='localcurrency')
app.cli.add_command(update_currency_rates)
def format_converted_currency(value, currency=None, **kwargs):
"""
Render a currency value in the preferred currency.
This function renders a currency value in the preferred currency for the
current locale. If the preferred currency is not the reference currency,
the value is converted to the preferred currency.
"""
if currency is None:
currency = get_preferred_currency()
# Convert the value to the preferred currency
local_value = convert_currency(value, currency)
# Format the value
html = render_template("money-tag.html",
base_amount=format_currency(value, currency=REF_CURRENCY, format_type='name', **kwargs),
local_amount=format_currency(local_value, currency=currency, **kwargs))
return Markup(html)
def convert_currency(value, currency=None, from_currency=REF_CURRENCY):
"""
Convert a currency value to the preferred currency.
This function converts a currency value to the preferred currency for the
current locale. If the preferred currency is not the reference currency,
the value is converted to the preferred currency.
"""
if currency != REF_CURRENCY:
return current_app.extensions['currency_converter'].convert(value, from_currency, currency)
return value
def convert_from_currency(value, currency) -> Decimal:
"""
Parses the localized currency value and converts it to the reference currency.
"""
locale = get_locale()
amount = parse_decimal(value, locale=locale)
if currency != REF_CURRENCY:
amount = Decimal(current_app.extensions['currency_converter'].convert(amount, currency, REF_CURRENCY))
return amount
def get_currencies():
"""
Get the list of supported currencies.
"""
return current_app.extensions['currency_converter'].currencies
def get_preferred_currency():
"""
Get the preferred currency.
This function returns the preferred currency for the current locale.
:return: The preferred currency.
"""
if current_user.is_authenticated and current_user.currency:
return str(current_user.currency)
# Fall back to the default currency for the locale
if territory := get_locale().territory:
currency = get_territory_currencies(territory)[0]
if currency in get_currencies():
return currency
else:
logger.warning("Default currency %s is not supported, falling back to %s.", currency, REF_CURRENCY)
return REF_CURRENCY
@click.command()
def update_currency_rates():
"""
Update currency file from the European Central Bank.
This command is meant to be run from the command line, and is not meant to be
used in the application:
$ flask update-currency-rates
:return: None
"""
click.echo('Updating currency file from the European Central Bank...')
fd, _ = urllib.request.urlretrieve(SINGLE_DAY_ECB_URL)
with ZipFile(fd) as zf:
file_name = zf.namelist().pop()
with open(current_app.config['CURRENCY_FILE'], 'wb') as f:
f.write(zf.read(file_name))
click.echo('Done.')