diff --git a/.gitignore b/.gitignore index fc78f1a..7e769ef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__ *zip *tar.gz *sql +flask_session +config.py diff --git a/bin/cmd b/bin/cmd new file mode 100755 index 0000000..45dc9bb --- /dev/null +++ b/bin/cmd @@ -0,0 +1,7 @@ +#!/bin/bash + +source .venv/bin/activate +export FLASK_APP=suchwow/app.py +export FLASK_SECRETS=config.py +export FLASK_DEBUG=1 +flask $1 diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..45dbc6e --- /dev/null +++ b/bin/dev @@ -0,0 +1,7 @@ +#!/bin/bash + +source .venv/bin/activate +export FLASK_APP=suchwow/app.py +export FLASK_SECRETS=config.py +export FLASK_DEBUG=1 +flask run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..8bb15f3 --- /dev/null +++ b/bin/setup @@ -0,0 +1,5 @@ +#!/bin/bash + +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt diff --git a/manage.py b/manage.py deleted file mode 100755 index c8332e0..0000000 --- a/manage.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'suchwow.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/memes/__init__.py b/memes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/memes/admin.py b/memes/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/memes/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/memes/apps.py b/memes/apps.py deleted file mode 100644 index 289ba1d..0000000 --- a/memes/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class MemesConfig(AppConfig): - name = 'memes' diff --git a/memes/migrations/__init__.py b/memes/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/memes/models.py b/memes/models.py deleted file mode 100644 index 71a8362..0000000 --- a/memes/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/memes/tests.py b/memes/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/memes/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/memes/urls.py b/memes/urls.py deleted file mode 100644 index d8757d0..0000000 --- a/memes/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path -from . import views - - -urlpatterns = [ - path('', views.index, name='index'), -] diff --git a/memes/views.py b/memes/views.py deleted file mode 100644 index c84d86a..0000000 --- a/memes/views.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.shortcuts import render -from django.http import HttpResponse - - -def index(request): - return HttpResponse("hey, testing") diff --git a/requirements.txt b/requirements.txt index b227c6c..607354e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,4 @@ -asgiref==3.2.10 -certifi==2020.6.20 -chardet==3.0.4 -Django==3.0.8 -django-keycloak==0.1.1 -django-vote==2.2.0 -ecdsa==0.15 -idna==2.10 -pyasn1==0.4.8 -python-jose==3.1.0 -python-keycloak-client==0.2.3 -pytz==2020.1 -requests==2.24.0 -rsa==4.6 -six==1.15.0 -sqlparse==0.3.1 -urllib3==1.25.9 +requests +flask +flask-session +peewee diff --git a/suchwow/app.py b/suchwow/app.py new file mode 100644 index 0000000..15d8da1 --- /dev/null +++ b/suchwow/app.py @@ -0,0 +1,166 @@ +from functools import wraps +import uuid +import os +import json +import requests +from flask import Flask, g, request, redirect, url_for, abort +from flask import jsonify, render_template, flash, session +from flask import send_from_directory, make_response +from flask_session import Session +from werkzeug.utils import secure_filename +from suchwow.models import Meme, db + + +app = Flask(__name__) +app.config.from_envvar("FLASK_SECRETS") +app.secret_key = app.config["SECRET_KEY"] +Session(app) + +OPENID_URL = app.config["OIDC_URL"][0] +OPENID_CLIENT_ID = app.config["OIDC_CLIENT_ID"][0] +OPENID_CLIENT_SECRET = app.config["OIDC_CLIENT_SECRET"][0] +OPENID_REDIRECT_URI = "http://localhost:5000/auth" + + +def allowed_file(filename): + return "." in filename and \ + filename.rsplit(".", 1)[1].lower() in app.config["ALLOWED_EXTENSIONS"] + +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if "auth" not in session or not session["auth"]: + return redirect(url_for("login")) + return f(*args, **kwargs) + return decorated_function + + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/meme/") +def view(id): + if Meme.filter(id=id): + m = Meme.get(Meme.id == id) + return render_template("view.html", meme=m) + else: + return "no meme there brah" + +@app.route("/uploads/") +def uploaded_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + +@app.route("/submit", methods=["GET", "POST"]) +@login_required +def submit(): + if request.method == "POST": + # check if the post request has the file part + if "file" not in request.files: + flash("No file part") + return redirect(request.url) + file = request.files["file"] + # if user does not select file, browser also + # submit an empty part without filename + if file.filename == "": + flash("No selected file") + return redirect(request.url) + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + save_path = os.path.join(app.config["UPLOAD_FOLDER"], filename) + file.save(save_path) + meme = Meme( + title=request.form.get('title'), + submitter=session["auth"]["preferred_username"], + image_name=filename, + ) + meme.save() + return redirect(url_for("view", id=meme.id)) + return render_template("submit.html") + +@app.route("/login") +def login(): + state = uuid.uuid4().hex + session["auth_state"] = state + url = f"{OPENID_URL}/auth?" \ + f"client_id={OPENID_CLIENT_ID}&" \ + f"redirect_uri={OPENID_REDIRECT_URI}&" \ + f"response_type=code&" \ + f"state={state}" + + return redirect(url) + + +@app.route("/auth/") +def auth(): + # todo + assert "state" in request.args + assert "session_state" in request.args + assert "code" in request.args + + # verify state + if not session.get("auth_state"): + return "session error", 500 + if request.args["state"] != session["auth_state"]: + return "attack detected :)", 500 + + # with this authorization code we can fetch an access token + url = f"{OPENID_URL}/token" + data = { + "grant_type": "authorization_code", + "code": request.args["code"], + "redirect_uri": OPENID_REDIRECT_URI, + "client_id": OPENID_CLIENT_ID, + "client_secret": OPENID_CLIENT_SECRET, + "state": request.args["state"] + } + try: + resp = requests.post(url, data=data) + resp.raise_for_status() + except: + return resp.content, 500 + + data = resp.json() + assert "access_token" in data + assert data.get("token_type") == "bearer" + access_token = data["access_token"] + + # fetch user information with the access token + url = f"{OPENID_URL}/userinfo" + + try: + resp = requests.post(url, headers={"Authorization": f"Bearer {access_token}"}) + resp.raise_for_status() + user_profile = resp.json() + except: + return resp.content, 500 + + # user can now visit /secret + session["auth"] = user_profile + return redirect(url_for("index")) + + +@app.route("/debug") +@login_required +def debug(): + return f""" +

We are logged in!

+
{json.dumps(session["auth"], indent=4, sort_keys=True)}

+ Logout + """ + +@app.route("/logout") +def logout(): + session["auth"] = None + return redirect(url_for("index")) + +@app.errorhandler(404) +def not_found(error): + return make_response(jsonify({"error": "Page not found"}), 404) + +@app.cli.command('dbinit') +def dbinit(): + db.create_tables([Meme]) + +if __name__ == "__main__": + app.run() diff --git a/suchwow/asgi.py b/suchwow/asgi.py deleted file mode 100644 index 63e9104..0000000 --- a/suchwow/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for suchwow project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'suchwow.settings') - -application = get_asgi_application() diff --git a/suchwow/config.example.py b/suchwow/config.example.py new file mode 100644 index 0000000..b7bae28 --- /dev/null +++ b/suchwow/config.example.py @@ -0,0 +1,9 @@ +OIDC_URL = 'https://login.wownero.com/auth/realms/master/protocol/openid-connect', +OIDC_CLIENT_ID = 'suchwowxxx', +OIDC_CLIENT_SECRET = 'xxxxxxxxxx', +OIDC_REDIRECT_URL = 'http://localhost:5000/auth' +SECRET = 'yyyyyyyyyyyyy', +SESSION_TYPE = 'filesystem' +UPLOAD_FOLDER = '/path/to/the/uploads' +ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) +MAX_CONTENT_LENGTH = 16 * 1024 * 1024 diff --git a/suchwow/models.py b/suchwow/models.py new file mode 100644 index 0000000..7245471 --- /dev/null +++ b/suchwow/models.py @@ -0,0 +1,15 @@ +from peewee import * +from datetime import datetime + + +db = SqliteDatabase('data/sqlite.db') + +class Meme(Model): + id = AutoField() + title = CharField() + submitter = CharField() + image_name = CharField() + timestamp = DateTimeField(default=datetime.now) + + class Meta: + database = db diff --git a/suchwow/settings.py b/suchwow/settings.py deleted file mode 100644 index 6ccf088..0000000 --- a/suchwow/settings.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Django settings for suchwow project. - -Generated by 'django-admin startproject' using Django 3.0.8. - -For more information on this file, see -https://docs.djangoproject.com/en/3.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.0/ref/settings/ -""" - -import os - - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SECRET_KEY = os.environ['SECRET_KEY'] -DEBUG = os.environ['DEBUG'] -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_keycloak.apps.KeycloakAppConfig' -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - # 'django_keycloak.middleware.BaseKeycloakMiddleware' -] - -ROOT_URLCONF = 'suchwow.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'suchwow.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/3.0/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'data/db.sqlite3'), - } -} - - -# Password validation -# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -# Authentication -# AUTHENTICATION_BACKENDS = [ -# 'django_keycloak.auth.backends.KeycloakAuthorizationCodeBackend', -# ] -# LOGIN_URL = 'keycloak_login' -KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.OpenIdConnectProfile' - -# Internationalization -# https://docs.djangoproject.com/en/3.0/topics/i18n/ - -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' -USE_I18N = True -USE_L10N = True -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.0/howto/static-files/ -STATIC_URL = '/static/' diff --git a/suchwow/templates/base.html b/suchwow/templates/base.html new file mode 100644 index 0000000..7380181 --- /dev/null +++ b/suchwow/templates/base.html @@ -0,0 +1,17 @@ + + + + + + Meme Factory + + + + + + Home
+ See da meemz +
+ {% block content %}{% endblock %} + + diff --git a/suchwow/templates/index.html b/suchwow/templates/index.html new file mode 100644 index 0000000..6903dc8 --- /dev/null +++ b/suchwow/templates/index.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} + + {% block content %} + Login
+ Submit A Meme
+ Visit the secret page! + {% endblock %} diff --git a/suchwow/templates/submit.html b/suchwow/templates/submit.html new file mode 100644 index 0000000..503abd5 --- /dev/null +++ b/suchwow/templates/submit.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} +Upload new File +

Upload new File

+
+
+ + +
+{% endblock %} diff --git a/suchwow/templates/view.html b/suchwow/templates/view.html new file mode 100644 index 0000000..6b28e0a --- /dev/null +++ b/suchwow/templates/view.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}View Meme{% endblock %}

+{% endblock %} + +{% block content %} +

Submitter: {{ session['auth']['preferred_username'] }}

+

ID: {{ meme.id }}

+

Title: {{ meme.title }}

+

Submitted: {{ meme.timestamp }}

+

Image Name: {{ meme.image_name }}

+ +{% endblock %} diff --git a/suchwow/urls.py b/suchwow/urls.py deleted file mode 100644 index 4642bc8..0000000 --- a/suchwow/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -"""suchwow URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path, include -import memes - -urlpatterns = [ - path('admin/', admin.site.urls), - path('keycloak/', include('django_keycloak.urls')), - path('', include('memes.urls')) -] diff --git a/suchwow/wsgi.py b/suchwow/wsgi.py deleted file mode 100644 index 7eb39ca..0000000 --- a/suchwow/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for suchwow project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'suchwow.settings') - -application = get_wsgi_application()