diff --git a/.gitignore b/.gitignore index b6e4761..078cf38 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ dmypy.json # Pyre type checker .pyre/ +.idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b5d20f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.9-slim + +ENV TZ Europe/Budapest +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +WORKDIR /app + +ARG RELEASE_ID +ENV RELEASE_ID ${RELEASE_ID:-""} + +COPY requirements.txt ./ + +RUN pip install --no-cache-dir -r requirements.txt && rm -rf requirements.txt + +COPY ./src . + +EXPOSE 8080 + +ENTRYPOINT ["gunicorn", "-b", "0.0.0.0:8080", "--workers", "4", "--threads", "1", "app:app"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..aaf0737 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +flask +flask-classful +flask-security +flask-admin +gunicorn +sentry-sdk[flask] +py-healthcheck +marshmallow +flask-marshmallow +sqlalchemy +flask-sqlalchemy +marshmallow-sqlalchemy +flask-cors \ No newline at end of file diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..4560415 --- /dev/null +++ b/src/app.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import logging +from flask import Flask +import sentry_sdk +from sentry_sdk.integrations.flask import FlaskIntegration +from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration +from healthcheck import HealthCheck + +from utils.config import SENTRY_DSN, RELEASE_ID, RELEASEMODE, POSTGRES_DB, PORT, POSTGRES_HOSTNAME, POSTGRES_PASSWORD, \ + POSTGRES_USERNAME, DEBUG, SECRET_KEY +from utils import db, ma, health_database_status, security, user_datastore + +""" +Main Flask entrypoint +""" + +__author__ = "@tormakris" +__copyright__ = "Copyright 2020, UnstableVortex Team" +__module_name__ = "app" +__version__text__ = "1" + +if SENTRY_DSN: + sentry_sdk.init( + dsn=SENTRY_DSN, + integrations=[FlaskIntegration(), SqlalchemyIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + release=RELEASE_ID, + environment=RELEASEMODE, + _experiments={"auto_enabling_integrations": True} + ) + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = \ + f"postgresql://{POSTGRES_USERNAME}:{POSTGRES_PASSWORD}@{POSTGRES_HOSTNAME}:5432/{POSTGRES_DB}" +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SECRET_KEY'] = SECRET_KEY + +health = HealthCheck() +db.init_app(app) +ma.init_app(app) +security.init_app(app, user_datastore) + +formatter = logging.Formatter( + fmt="%(asctime)s - %(levelname)s - %(module)s - %(message)s" +) + +handler = logging.StreamHandler() +handler.setFormatter(formatter) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +logger.addHandler(handler) + +# api.add_resource(SampleResource, "/sample") + +health.add_check(health_database_status) +app.add_url_rule("/healthz", "healthcheck", view_func=lambda: health.run()) + + +@app.before_first_request +def init_db(): + db.create_all() + + +if __name__ == "__main__": + app.run( + debug=bool(DEBUG), + host="0.0.0.0", + port=int(PORT), + ) diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..1a1e11d --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1,2 @@ +from .user import User +from .role import Role diff --git a/src/models/role.py b/src/models/role.py new file mode 100644 index 0000000..467ed81 --- /dev/null +++ b/src/models/role.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +from flask_security import RoleMixin + +from utils import db + +""" +Database models +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, UnstableVortex Team" +__module_name__ = "user" +__version__text__ = "1" + + +roles_users = db.Table('roles_users', + db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), + db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) + + +class Role(db.Model, RoleMixin): + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(80), unique=True) + description = db.Column(db.String(255)) diff --git a/src/models/user.py b/src/models/user.py new file mode 100644 index 0000000..3816a3e --- /dev/null +++ b/src/models/user.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +from flask_security import UserMixin + +from .role import roles_users +from utils import db + +""" +Database models +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, UnstableVortex Team" +__module_name__ = "user" +__version__text__ = "1" + + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), unique=True) + password = db.Column(db.String(255)) + active = db.Column(db.Boolean()) + confirmed_at = db.Column(db.DateTime()) + roles = db.relationship('Role', secondary=roles_users, + backref=db.backref('users', lazy='dynamic')) diff --git a/src/templates/base.html b/src/templates/base.html new file mode 100644 index 0000000..ed75cde --- /dev/null +++ b/src/templates/base.html @@ -0,0 +1,45 @@ + + + + + + + + + My Store Web App + + + +
+ {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..2402d5d --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1,25 @@ +{% extends 'webapp/base.html' %} +{% block content %} +{% if images %} +
+ {% for image in images %} +
+
+ + {{image.caption}} + +
+
{{image.creator}}
+

+ {{image.caption}} +

+ Download Now +
+
+
+ {% endfor %} +
+{% else %} +

No images available.

+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/templates/item.html b/src/templates/item.html new file mode 100644 index 0000000..458e105 --- /dev/null +++ b/src/templates/item.html @@ -0,0 +1,73 @@ +{% extends 'webapp/base.html' %} +{% block content %} +
+

Animation by {{ image.creator }}

+ + + + + {{image.caption}} +
+

{{ image.caption }}

+
+
+ + + + + + + + + + + + + + + +
Creator{{ image.creator }}
Number of images{{ image.num_anim }}
Creation date{{ image.creation_date }}
+
+
+ Download +
+ +
+ +{% if current_user.is_authenticated %} +
+
+
+
+ Write a comment +
+ +
+ +
+
+
+
+{% endif %} + +{% if comments %} +{% for comment in comments %} +
+
+

{{ comment.user }}

+
{{ comment.date }}
+

{{ comment.text }}

+
+
+{% endfor %} +{% else %} +
+
+

No comments yet.

+
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/templates/login.html b/src/templates/login.html new file mode 100644 index 0000000..17f5a60 --- /dev/null +++ b/src/templates/login.html @@ -0,0 +1,17 @@ +{% extends 'webapp/base.html' %} + +{% block content %} +
+

Please sign in

+
+
+

+

+

+
+

Dont't have an account? Sign Up here

+ {% if error %} +

Error: {{ error }} + {% endif %} +

+{% endblock %} \ No newline at end of file diff --git a/src/templates/profile.html b/src/templates/profile.html new file mode 100644 index 0000000..5a47d32 --- /dev/null +++ b/src/templates/profile.html @@ -0,0 +1,37 @@ +{% extends 'webapp/base.html' %} + +{% block content %} +{% if current_user.is_authenticated %} +
+

Welcome {{ user.username }}

+ Upload +
+
+{% if images %} +
+ {% for image in images %} +
+
+ + {{image.caption}} + +
+
{{image.creator}}
+

+ {{image.caption}} +

+ Download + Delete +
+
+
+ {% endfor %} +
+ +{% else %} +

No images available.

+{% endif %} +{% else %} +

Log in to view your profile.

+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/templates/register.html b/src/templates/register.html new file mode 100644 index 0000000..6105e71 --- /dev/null +++ b/src/templates/register.html @@ -0,0 +1,18 @@ +{% extends 'webapp/base.html' %} + +{% block content %} +
+

Please sign up

+
+
+

+

+

+

+
+

Already have an account? Sign In here

+ {% if error %} +

Error: {{ error }} + {% endif %} +

+{% endblock %} \ No newline at end of file diff --git a/src/templates/upload.html b/src/templates/upload.html new file mode 100644 index 0000000..00b3544 --- /dev/null +++ b/src/templates/upload.html @@ -0,0 +1,16 @@ +{% extends 'webapp/base.html' %} + +{% block content %} +{% if current_user.is_authenticated %} +
+

File Upload

+
+
+

+

+
+
+{% else %} +

Log in to upload an animation.

+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..fb5d78b --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,4 @@ +from .db import db +from .marshm import ma +from .healthchecks import health_database_status +from .security import security, user_datastore diff --git a/src/utils/config.py b/src/utils/config.py new file mode 100644 index 0000000..97be0c5 --- /dev/null +++ b/src/utils/config.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import os + +""" +Configuration +""" + + +__author__ = "@tormakris" +__copyright__ = "Copyright 2020, Birbnetes Team" +__module_name__ = "config" +__version__text__ = "1" + + +PORT = os.environ.get("PORT", 8080) +DEBUG = os.environ.get("DEBUG", True) + +SENTRY_DSN = os.environ.get("SENTRY_DSN") +RELEASE_ID = os.environ.get("RELEASE_ID", "test") +RELEASEMODE = os.environ.get("RELEASEMODE", "dev") + +POSTGRES_HOSTNAME = os.getenv("POSTGRES_HOSTNAME", "localhost") +POSTGRES_USERNAME = os.getenv("POSTGRES_USERNAME", "webshop") +POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "webshop") +POSTGRES_DB = os.getenv("POSTGRES_DB", "webshop") + +SECRET_KEY = os.getenv("SECRET_KEY") diff --git a/src/utils/db.py b/src/utils/db.py new file mode 100644 index 0000000..9657351 --- /dev/null +++ b/src/utils/db.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from flask_sqlalchemy import SQLAlchemy + +""" +Databse object +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, UnstableVortex Team" +__module_name__ = "db" +__version__text__ = "1" + +db = SQLAlchemy() diff --git a/src/utils/healthchecks.py b/src/utils/healthchecks.py new file mode 100644 index 0000000..6fe0a58 --- /dev/null +++ b/src/utils/healthchecks.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +from .db import db + +""" +Healthchek functions +""" + +__author__ = "@tormakris" +__copyright__ = "Copyright 2020, UnstableVortex Team" +__module_name__ = "healthchecks" +__version__text__ = "1" + + +def health_database_status(): + is_database_working = True + output = 'database is ok' + try: + db.session.execute('SELECT 1') + except Exception as e: + output = str(e) + is_database_working = False + return is_database_working, output diff --git a/src/utils/marshm.py b/src/utils/marshm.py new file mode 100644 index 0000000..146cdb0 --- /dev/null +++ b/src/utils/marshm.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from flask_marshmallow import Marshmallow + +""" +Marshmallow +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, UnstableVortex Team" +__module_name__ = "marshm" +__version__text__ = "1" + +ma = Marshmallow() diff --git a/src/utils/security.py b/src/utils/security.py new file mode 100644 index 0000000..12f4ecc --- /dev/null +++ b/src/utils/security.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +""" +Flask-Security +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, UnstableVortex Team" +__module_name__ = "security" +__version__text__ = "1" + +from flask_security import Security, SQLAlchemyUserDatastore + +from models import User, Role +from utils import db + +user_datastore = SQLAlchemyUserDatastore(db, User, Role) +security = Security()