parent
eb6d6b388e
commit
95c5882e39
@ -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
|
@ -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
|
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip3 install -r requirements.txt
|
@ -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()
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class MemesConfig(AppConfig):
|
|
||||||
name = 'memes'
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -1,7 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('', views.index, name='index'),
|
|
||||||
]
|
|
@ -1,6 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
from django.http import HttpResponse
|
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
|
||||||
return HttpResponse("hey, testing")
|
|
@ -1,17 +1,4 @@
|
|||||||
asgiref==3.2.10
|
requests
|
||||||
certifi==2020.6.20
|
flask
|
||||||
chardet==3.0.4
|
flask-session
|
||||||
Django==3.0.8
|
peewee
|
||||||
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
|
|
||||||
|
@ -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/<id>")
|
||||||
|
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/<path:filename>")
|
||||||
|
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"""
|
||||||
|
<h3>We are logged in!</h3>
|
||||||
|
<pre>{json.dumps(session["auth"], indent=4, sort_keys=True)}</pre><br>
|
||||||
|
<a href="/logout">Logout</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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()
|
@ -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()
|
|
@ -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
|
@ -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
|
@ -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/'
|
|
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Meme Factory</title>
|
||||||
|
<meta name="author" content="name">
|
||||||
|
<meta name="description" content="description here">
|
||||||
|
<meta name="keywords" content="keywords,here">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/">Home</a><br>
|
||||||
|
<a href="/view">See da meemz</a>
|
||||||
|
<hr>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<a href="{{ url_for('login') }}">Login</a><br>
|
||||||
|
<a href="{{ url_for('submit') }}">Submit A Meme</a><br>
|
||||||
|
<a href="{{ url_for('debug') }}">Visit the secret page!</a>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<title>Upload new File</title>
|
||||||
|
<h1>Upload new File</h1>
|
||||||
|
<form method=post enctype=multipart/form-data>
|
||||||
|
<input type="text" name="title"><br>
|
||||||
|
<input type=file name=file>
|
||||||
|
<input type=submit value=Submit>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>{% block title %}View Meme{% endblock %}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Submitter: {{ session['auth']['preferred_username'] }}</p>
|
||||||
|
<p>ID: {{ meme.id }}</p>
|
||||||
|
<p>Title: {{ meme.title }}</p>
|
||||||
|
<p>Submitted: {{ meme.timestamp }}</p>
|
||||||
|
<p>Image Name: {{ meme.image_name }}</p>
|
||||||
|
<img src="{{ url_for('uploaded_file', filename=meme.image_name) }}" width=400/>
|
||||||
|
{% endblock %}
|
@ -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'))
|
|
||||||
]
|
|
@ -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()
|
|
Reference in new issue