diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/LICENSE b/LICENSE index 0e259d4..a4e9dc9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,121 +1,16 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. +MIT No Attribution + +Copyright + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index baaf68b..ee48a62 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# Markdown-Presenter +# flask_boilerplate -Ein kleines Projekt um Markdown Dokumente zu Presenten. \ No newline at end of file +Kleines Startsetup für eine Flask Webapp mit Login und Datenbank \ No newline at end of file diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app.py b/app.py deleted file mode 100644 index 6104bec..0000000 --- a/app.py +++ /dev/null @@ -1,13 +0,0 @@ -from services import (UserService, UserListService) -from config import app, api, docs, CORS - -#*______________ Service Registration ______________ -api.add_resource(UserService, '/api/user/') -docs.register(UserService) -api.add_resource(UserListService, '/api/list/user') -docs.register(UserListService) -#*______________ Application Creation ______________ -if __name__ == '__main__': - app.run(debug=True) - - diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..aa05365 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,43 @@ +import os + +from flask import Flask + + +def create_app(test_config=None): + # create and configure the app + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY='dev', + DATABASE=os.path.join(app.instance_path, 'app.sqlite'), + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile('config.py', silent=True) + else: + # load the test config if passed in + app.config.from_mapping(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # a simple page that says hello + @app.route('/hello') + def hello(): + return 'Hello, World!' + + + from . import db + db.init_app(app) + + from . import auth + app.register_blueprint(auth.bp) + + from . import index + app.register_blueprint(index.bp) + app.add_url_rule('/', endpoint='index') + + return app diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..91af5da --- /dev/null +++ b/app/auth.py @@ -0,0 +1,95 @@ +import functools + +from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for +) +from werkzeug.security import check_password_hash, generate_password_hash + +from app.db import get_db + +bp = Blueprint('auth', __name__, url_prefix='/auth') + + +@bp.route('/register', methods=('GET', 'POST')) +def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + + if not username: + error = 'Username is required.' + elif not password: + error = 'Password is required.' + elif db.execute( + 'SELECT id FROM user WHERE username = ?', (username,) + ).fetchone() is not None: + error = 'User {} is already registered.'.format(username) + + if error is None: + db.execute( + 'INSERT INTO user (username, password) VALUES (?, ?)', + (username, generate_password_hash(password)) + ) + db.commit() + return redirect(url_for('auth.login')) + + flash(error) + + return render_template('auth/register.html') + + +@bp.route('/login', methods=('GET', 'POST')) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + user = db.execute( + 'SELECT * FROM user WHERE username = ?', (username,) + ).fetchone() + + if user is None: + error = 'Incorrect username.' + elif not check_password_hash(user['password'], password): + error = 'Incorrect password.' + + if error is None: + session.clear() + session['user_id'] = user['id'] + return redirect(url_for('index')) + + flash(error) + + return render_template('auth/login.html') + + +@bp.before_app_request +def load_logged_in_user(): + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + g.user = get_db().execute( + 'SELECT * FROM user WHERE id = ?', (user_id,) + ).fetchone() + + +@bp.route('/logout') +def logout(): + session.clear() + return redirect(url_for('index')) + + +def login_required(view): + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('auth.login')) + + return view(**kwargs) + + return wrapped_view \ No newline at end of file diff --git a/app/db.py b/app/db.py new file mode 100644 index 0000000..74dea73 --- /dev/null +++ b/app/db.py @@ -0,0 +1,41 @@ +import sqlite3 + +import click +from flask import current_app, g +from flask.cli import with_appcontext + + +def get_db(): + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + + +def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +def init_db(): + db = get_db() + + with current_app.open_resource('schema.sql') as f: + db.executescript(f.read().decode('utf8')) + + +@click.command('init-db') +@with_appcontext +def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + +def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) \ No newline at end of file diff --git a/app/index.py b/app/index.py new file mode 100644 index 0000000..d27b54d --- /dev/null +++ b/app/index.py @@ -0,0 +1,23 @@ +from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for +) +from werkzeug.exceptions import abort + +from app.auth import login_required +from app.db import get_db + + +bp = Blueprint('index', __name__) + +@bp.route('/') +@login_required +def index(): + db = get_db() + loginEvents = db.execute( + 'SELECT *' + ' FROM loginEvent' + ' ORDER BY created DESC' + ).fetchall() + return render_template('index.html', loginEvents = loginEvents) + + diff --git a/app/schema.sql b/app/schema.sql new file mode 100644 index 0000000..82cc351 --- /dev/null +++ b/app/schema.sql @@ -0,0 +1,15 @@ +DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS post; + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL +); + +CREATE TABLE loginEvent ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + walletId TEXT NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + eventType TEXT NOT NULL +); \ No newline at end of file diff --git a/app/static/bootstrap.css b/app/static/bootstrap.css new file mode 100644 index 0000000..7b79b71 --- /dev/null +++ b/app/static/bootstrap.css @@ -0,0 +1,134 @@ +html { + font-family: sans-serif; + background: #eee; + padding: 1rem; +} + +body { + max-width: 960px; + margin: 0 auto; + background: white; +} + +h1, h2, h3, h4, h5, h6 { + font-family: serif; + color: #377ba8; + margin: 1rem 0; +} + +a { + color: #377ba8; +} + +hr { + border: none; + border-top: 1px solid lightgray; +} + +nav { + background: lightgray; + display: flex; + align-items: center; + padding: 0 0.5rem; +} + +nav h1 { + flex: auto; + margin: 0; +} + +nav h1 a { + text-decoration: none; + padding: 0.25rem 0.5rem; +} + +nav ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; +} + +nav ul li a, nav ul li span, header .action { + display: block; + padding: 0.5rem; +} + +.content { + padding: 0 1rem 1rem; +} + +.content > header { + border-bottom: 1px solid lightgray; + display: flex; + align-items: flex-end; +} + +.content > header h1 { + flex: auto; + margin: 1rem 0 0.25rem 0; +} + +.flash { + margin: 1em 0; + padding: 1em; + background: #cae6f6; + border: 1px solid #377ba8; +} + +.post > header { + display: flex; + align-items: flex-end; + font-size: 0.85em; +} + +.post > header > div:first-of-type { + flex: auto; +} + +.post > header h1 { + font-size: 1.5em; + margin-bottom: 0; +} + +.post .about { + color: slategray; + font-style: italic; +} + +.post .body { + white-space: pre-line; +} + +.content:last-child { + margin-bottom: 0; +} + +.content form { + margin: 1em 0; + display: flex; + flex-direction: column; +} + +.content label { + font-weight: bold; + margin-bottom: 0.5em; +} + +.content input, .content textarea { + margin-bottom: 1em; +} + +.content textarea { + min-height: 12em; + resize: vertical; +} + +input.danger { + color: #cc2f2e; +} + +input[type=submit] { + align-self: start; + min-width: 10em; +} \ No newline at end of file diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 0000000..b7dd5dc --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Log In{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html new file mode 100644 index 0000000..a3c73cc --- /dev/null +++ b/app/templates/auth/register.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Register{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..a258ef6 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,24 @@ + +{% block title %}{% endblock %} - Flaskr + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
\ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..f3acb42 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Login Events{% endblock %}

+{% endblock %} + +{% block content %} + + Hello World + +{% endblock %} \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100644 index 4d75670..0000000 --- a/config.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import Flask -from flask_restful import Api -from apispec import APISpec -from apispec.ext.marshmallow import MarshmallowPlugin -from flask_apispec.extension import FlaskApiSpec -from flask_marshmallow import Marshmallow -from flask_sqlalchemy import SQLAlchemy -from flask_cors import CORS -#!______________ App Setup _____________ _ -app = Flask(__name__, static_url_path='/static') -api_v1_cors_config = { - "origins": ["http://localhost:5000"] -} - -#!______________ CORS Setup _____________ _ -CORS(app, resources={"/api/*": api_v1_cors_config}) - -#!______________ DB Setup ______________ -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///markdown.db' -app.config['SECRET_KEY'] = 'InputSecretKeyHere' -db = SQLAlchemy(app) - -#!______________ Marshmallow Setup ______________ -ma = Marshmallow(app) - -#!______________ API & Swagger Setup ______________ -api = Api(app) -app.config.update({ - 'APISPEC_SPEC': APISpec( - title='Markdown Presenter', - version='v0.0.1', - plugins=[MarshmallowPlugin()], - openapi_version='2.0.0' - ), - 'APISPEC_SWAGGER_URL': '/swagger/', - 'APISPEC_SWAGGER_UI_URL': '/swagger-ui/' -}) - -#!______________ Docs Setup ______________ -docs = FlaskApiSpec(app) \ No newline at end of file diff --git a/models.py b/models.py deleted file mode 100644 index 5ac9c7b..0000000 --- a/models.py +++ /dev/null @@ -1,10 +0,0 @@ -from config import db - -#!______________ DB Models ______________ -class User(db.Model): - __tablename__ = "User" - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(50)) - image = db.Column(db.String()) - description = db.Column(db.String()) - diff --git a/rebuild_db.py b/rebuild_db.py deleted file mode 100644 index 2c56232..0000000 --- a/rebuild_db.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -from config import db -from termcolor import colored -from config import app - -def RebuildDatabase(): - if os.path.exists('acceth.db'): - print(colored('Removing existing DB', 'blue')) - os.remove('acceth.db') - - with app.app_context(): - db.create_all() - db.session.commit() - - print(colored('New DB created.', 'green')) - -if __name__ == '__main__': - RebuildDatabase() \ No newline at end of file diff --git a/schemes.py b/schemes.py deleted file mode 100644 index 7f180fd..0000000 --- a/schemes.py +++ /dev/null @@ -1,25 +0,0 @@ -from marshmallow_sqlalchemy import SQLAlchemyAutoSchema -from config import db -from models import User -from marshmallow import fields - -#*______________ Base Schema ______________ -class BaseScheme(SQLAlchemyAutoSchema): - def __str__(self): - return str(self.__class__) + ": " + str(self.__dict__) - class Meta: - ordered = True - sqla_session = db.session - include_fk = True - load_instance = True - -#*______________ User Schemes ______________ -class UserSchema(BaseScheme): - class Meta(BaseScheme.Meta): - model = User - id = fields.Int() - name = fields.Str() -class UserInsertSchema(UserSchema): - user_id = fields.Int() -class UserResponseSchema(UserSchema): - name = fields.Str() \ No newline at end of file diff --git a/services.py b/services.py deleted file mode 100644 index 70568ed..0000000 --- a/services.py +++ /dev/null @@ -1,46 +0,0 @@ -from flask_apispec import marshal_with, doc, use_kwargs -from flask_apispec.views import MethodResource -from flask_restful import Resource -from schemes import (UserSchema, UserResponseSchema) -from config import db -from models import User - -#!______________ User ______________ -class UserService(MethodResource, Resource): - @doc(description='Get User by User_id', tags=['User']) - @marshal_with(UserResponseSchema) - def get(self, user_id): - quser = db.session.query(User).get(user_id) - return UserSchema().dump(quser) - - @doc(description='Add new User', tags=['User']) - @use_kwargs(UserSchema, location=('json')) - @marshal_with(UserResponseSchema()) - def post(self, user, user_id): - db.session.add(user) - db.session.commit() - return UserSchema().dump(user) - - @doc(description='Update User with PUT', tags=['User']) - @use_kwargs(UserSchema, location=('json')) - @marshal_with(UserResponseSchema()) - def put(self, user, user_id): - db.session.add(user) - db.session.commit() - return UserSchema().dump(user) - - @doc(description='Delete existing User', tags=['User']) - @use_kwargs(UserSchema, location=('json')) - @marshal_with(UserResponseSchema()) - def delete(self, user, user_id): - user = db.session.query(User).get(user_id) - db.session.delete(user) - db.session.commit() - return UserSchema().dump(user) - -class UserListService(MethodResource, Resource): - @doc(description='Get a List of all User', tags=['List']) - @marshal_with(UserResponseSchema(many=True)) - def get(self): - users = db.session.query(User).all() - return UserSchema(many=True).dump(users) \ No newline at end of file diff --git a/wsgi.py b/wsgi.py deleted file mode 100644 index 0ad1c54..0000000 --- a/wsgi.py +++ /dev/null @@ -1,5 +0,0 @@ -from config import app - -if __name__ == "__main__": - app.run() -