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 (49)
Showing with 621 additions and 27 deletions
......@@ -164,4 +164,5 @@ cython_debug/
_docs/
.env
dotenv
.DS_Store
## 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,36 @@ 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!"
## 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,6 @@ 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/
\ 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
wrote 6 user stories
Identified the 5 most concerning security risks for the weeks tasks
Setted up the testing up
Started the Login/registration feature
Arno
Completed setting up the Azure infrastructure and the Gitlab-runner.
Added application address https://frozen-app.azurewebsites.net/ to README.md
Setup the database on azure
Initial setup of flask application (pages “/”, “/hello” and “/server-info”)
Added initial schemas for user and items into the database
Added test for saving of objects (user and item) into the database (page “/test”)
Considered A02:2021 and A10:2021.
Rayan
Finish local environment
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
......@@ -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
......@@ -28,6 +28,7 @@ dependencies = {file = ["requirements.txt"]}
[project.optional-dependencies]
test = [
"pytest",
"coverage",
]
docs = [
"mkdocs",
......
......@@ -2,11 +2,18 @@
importlib-metadata
# Framework and libraries
flask
flask==2.2.2
python-dotenv
flask-mongoengine==1.0
Pillow
# 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
......@@ -8,6 +8,7 @@ Flask tutorial: https://flask.palletsprojects.com/en/2.2.x/tutorial/
"""
from os import environ
import os
from typing import Dict, Optional
from dotenv import load_dotenv
......@@ -19,6 +20,8 @@ from flask import (
)
from .utils import get_version
from .db import init_db
from .logging import init_logging
def create_app(config: Optional[Dict] = None) -> Flask:
......@@ -29,16 +32,42 @@ def create_app(config: Optional[Dict] = None) -> Flask:
"""
flask_app = Flask(__name__, instance_relative_config=True)
if config:
flask_app.config.from_mapping(
SECRET_KEY='dev',
BRAND="Hill Valley DMC dealership",
)
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)
# Set flask config variable for "rich" loggin from environment variable.
flask_app.config.from_envvar("RICH_LOGGING", silent=True)
# 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:
pass
# Register blueprints
from . import views # pylint: disable=import-outside-toplevel
# Initialize the database connection.
init_db(flask_app)
# Register blueprints
from . import views
flask_app.register_blueprint(views.bp, url_prefix='')
# a simple page that says hello
@flask_app.route('/hello')
def hello():
return 'Hello, World!'
return flask_app
......@@ -49,13 +78,6 @@ 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")
@app.route("/server-info")
def server_info() -> Response:
......
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, see: blah blah blah
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 datetime import datetime
from .db import db
from mongoengine import (
StringField,
IntField,
ReferenceField,
DateTimeField,
EmailField,
FileField,
)
class User(db.Document):
"""
Model representing a user of the auction site.
"""
email = EmailField(required=True, unique=True)
password = StringField(required=True)
created_at = DateTimeField(required=True, default=datetime.utcnow)
class Item(db.Document):
"""
A model for items that are listed on the auction site.
"""
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()
image = FileField()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Add item</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
</head>
<body>
<header>
</header>
<main>
<form class="container-fluid" action="{{ url_for('views.add_item') }}" method="POST" enctype="multipart/form-data">
<div class="container">
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="nItem" class="form-label">Item's name</label>
<input type="text" class="form-control" id="nItem" name="nItem">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" maxlength="700"></textarea>
</div>
<div class="mb-3">
<label for="sPrice" class="form-label">Starting price</label>
<input type="number" class="form-control" id="sPrice" name="sPrice" min="0" max="100000000">
</div>
<div class="mb-3">
<label for="image" class="form-label">Image</label>
<input type="file" class="form-control" id="image" name="image">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</main>
<footer>
</footer>
</body>
</html>
\ No newline at end of file
......@@ -10,5 +10,8 @@
</head>
<body>
<h1>Hello, world!</h1>
<h2>Hallo, Welt!</h2>
<h2>Bonjour le monde!</h2>
<h2>Hei maailma!</h2>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>View items</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<script>
function bid(){
const form = document.querySelector("#bid");
const button = document.querySelector("button");
if (form.className=="open"){
form.className="";
form.type="hidden";
button.innerHTML = "Bid";
}
else{
form.className="open";
form.type="number";
button.innerHTML = "Cancel";
}
}
</script>
</head>
<body>
<header>
<h2> Overview over items currently on sale </h2>
<br>
</header>
<main>
<div class="list-group container">
{%for item in items %}
<div class="d-flex w-100 justify-content-between mb-4">
<img class=" col-md-2 col-xs-2" src="Gaming_5000x3125.jpg"{{ item.image }} alt="Image" />
<a href="#" class="list-group-item list-group-item-action" aria-current="true">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ item.title }}</h5>
<small>{{ item.created_at }}</small>
</div>
<p class="mb-1">{{ item.description }}</p>
<small>{{ item.starting_bid }}</small>
<div class="row"><button class="col-md-2" onclick="bid()">Bid</button><form method="post" action="#" class="col-md-4"><input id="bid" type="hidden" min="{{ item.starting_bid }}" /></form></div>
</a>
</div>
{% endfor %}
</div>
</main>
<footer>
</footer>
</body>
</html>
\ No newline at end of file
......@@ -3,7 +3,10 @@ Basic views for Application
===========================
"""
from flask import Blueprint, render_template, send_from_directory
import random
from datetime import datetime, timedelta
from flask import Blueprint, render_template, send_from_directory, request, flash, url_for, redirect
from .models import Item, User
# Main blueprint.
bp = Blueprint('views', __name__)
......@@ -20,5 +23,104 @@ def index() -> str:
# Render template file. Template file is using Jinja2 syntax, and can be passed an arbitrary number
# of arguments to be used in the template. The template file is located in the templates directory.
html = render_template("index.html.j2", title="TJTS5901 Example. I should be changed.")
html = render_template("index.html.j2", title="TJTS5901 Example. It was changed by Arno.")
return html
@bp.route("/test")
def test_item_adding():
"""
Test item is added
"""
# for test purpose create random number (unique mail address required)
ran_int = str(random.randint(0,10000))
# Create a new user
user = User()
user.email = "test" + ran_int + "@gmail.com"
user.password = "placeholder"
user.save()
# Create a new item
item = Item()
item.title = "Test title"
item.description = "This is a test description"
item.starting_bid = 100
item.seller = user
item.save()
return "OK"
@bp.route("/addItem", methods=('GET', 'POST'))
def add_item():
"""
AddItem page.
"""
if request.method == 'POST':
nItem = request.form.get('nItem')
description = request.form.get('description')
starting_price = int(request.form.get('sPrice'))
image = request.files['image']
error = None
if not nItem:
error = 'Item name is required.'
if not starting_price or starting_price < 1:
error = 'Starting price must be greater than 0.'
if error is None:
# Placeholder for user as long as login function is not defined
ran_int = str(random.randint(0,10000))
user = User()
user.email = "test" + ran_int + "@gmail.com"
user.password = "placeholder"
user.save()
try:
item = Item(
title=nItem,
description=description,
starting_bid=starting_price,
seller=user,
closes_at=datetime.utcnow() + timedelta(days=1),
image=image
)
item.save()
except Exception as exc:
error = f"Error creating item: {exc!s}"
print('error')
else:
return redirect(url_for('views.adding_successful'))
flash(error)
item = render_template("addItem.html")
return item
@bp.route("/adding_successful")
def adding_successful():
return "Item added successfully"
@bp.route("/listBid")
def list_bid():
"""
page that lists all items currently on auction
"""
# for test reasons all items are shown and no selection on only on sale items
items = Item.objects.all()
for item in items:
print(type(item.image))
html = render_template("listBid.html", items=items)
return html
@bp.route("/bid")
def bid(id):
"""
method that saves bid on object
"""
return "ok"
import pytest
from flask import Flask
from tjts5901.app import create_app
@pytest.fixture
def app():
"""
Application fixture.
Every test that requires `app` as parameter can use this fixture.
Example:
>>> def test_mytest(app: Flask):
>>> ...
"""
flask_app = create_app({
'TESTING': True,
'DEBUG': False,
# We need to set SERVER_NAME and PREFERRED_URL_SCHEME for testing.
'SERVER_NAME': 'localhost.localdomain',
'PREFERRED_URL_SCHEME': 'http',
})
# If you have done ties4080 course and have used Flask-WTF, you might
# have noticed that CSRF protection is enabled by default. This is
# problematic for testing, because we don't have a browser to generate
# CSRF tokens. We can disable CSRF protection for testing, but we need
# to make sure that we don't have CSRF protection enabled in production.
# flask_app.config['WTF_CSRF_ENABLED'] = False
# flask_app.config['WTF_CSRF_METHODS'] = []
# flask_app.config['WTF_CSRF_CHECK_DEFAULT'] = False
flask_app.testing = True
yield flask_app
# Do some cleanup here if needed.
...
@pytest.fixture
def client(app):
return app.test_client()
from tjts5901.views import test_item_adding
def test_db():
"""
Testing the db connection via previously defined test method
"""
#assert init_db({'TESTING': True}).testing
assert test_item_adding()