@ -0,0 +1,8 @@ | |||
# All jinja files, anywhere in the repository | |||
**.j2 | |||
# Manifest.json | |||
manifest.json | |||
# Dockerfile | |||
Dockerfile |
@ -0,0 +1,18 @@ | |||
### Python ### | |||
# Byte-compiled / optimized / DLL files | |||
__pycache__/ | |||
*.py[cod] | |||
*$py.class | |||
# Environments | |||
.env | |||
.venv | |||
env/ | |||
venv/ | |||
ENV/ | |||
env.bak/ | |||
venv.bak/ | |||
.envrc | |||
.DS_Store | |||
.vscode/ |
@ -0,0 +1,13 @@ | |||
FROM python:3.7 | |||
LABEL maintainer="$REPO_OWNER" | |||
WORKDIR /project | |||
ADD . /project | |||
RUN rm /project/.envrc | |||
RUN rm -rf /project/env | |||
RUN apt-get update && apt-get install -y python3-dev libldap2-dev libsasl2-dev libssl-dev | |||
RUN pip install -r requirements.txt | |||
CMD ["flask","run","--host=0.0.0.0"] |
@ -0,0 +1,100 @@ | |||
import ldap as l | |||
from ldap3 import Server, Connection, ALL, MODIFY_REPLACE | |||
from flask import Flask, g, request, session, redirect, url_for, render_template, send_from_directory | |||
from flask_simpleldap import LDAP | |||
from flask_bootstrap import Bootstrap | |||
import os | |||
from flask_cache_buster import CacheBuster | |||
app = Flask(__name__) | |||
Bootstrap(app) | |||
app.secret_key = 'asdf' | |||
app.debug = True | |||
# Base | |||
app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication' | |||
app.config['LDAP_HOST'] = os.environ.get('LDAP_HOST') | |||
app.config['LDAP_BASE_DN'] = os.environ.get('LDAP_BASE_DN') | |||
app.config['LDAP_USERNAME'] = os.environ.get('LDAP_USERNAME') | |||
app.config['LDAP_PASSWORD'] = os.environ.get('LDAP_PASSWORD') | |||
# OpenLDAP | |||
app.config['LDAP_OBJECTS_DN'] = 'dn' | |||
app.config['LDAP_OPENLDAP'] = True | |||
app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=posixAccount)(uid=%s))' | |||
ldap = LDAP(app) | |||
config = { | |||
'extensions': ['.js', '.css', '.csv'], | |||
'hash_size': 10 | |||
} | |||
cache_buster = CacheBuster(config=config) | |||
cache_buster.register_cache_buster(app) | |||
server = Server(app.config['LDAP_HOST']) | |||
conn = Connection(server, app.config['LDAP_USERNAME'], app.config['LDAP_PASSWORD'], auto_bind=True) | |||
@app.before_request | |||
def before_request(): | |||
g.user = None | |||
if 'user_id' in session: | |||
# This is where you'd query your database to get the user info. | |||
g.user = {} | |||
@app.route("/manifest.json") | |||
def manifest(): | |||
return send_from_directory('./', 'manifest.json') | |||
@app.route('/') | |||
@ldap.login_required | |||
def index(): | |||
user_dict = ldap.get_object_details(session['user_id']) | |||
if 'user_id' in session: | |||
user = {'dn': 'cn={},cn=usergroup,ou=users,dc=technicalincompetence,dc=club'.format(user_dict['cn'][0].decode('ascii')), | |||
'firstName': user_dict['givenName'][0].decode('ascii'), | |||
'lastName': user_dict['sn'][0].decode('ascii'), | |||
'email': user_dict['mail'][0].decode('ascii'), | |||
'userName': user_dict['uid'][0].decode('ascii'), | |||
} | |||
return render_template('home.j2') | |||
@app.route('/about') | |||
@ldap.login_required | |||
def about(): | |||
return render_template('about.j2') | |||
@app.route('/login', methods=['GET', 'POST']) | |||
def login(): | |||
if g.user: | |||
return redirect(url_for('index')) | |||
if request.method == 'POST': | |||
user = request.form['user'] | |||
passwd = request.form['passwd'] | |||
test = ldap.bind_user(user, passwd) | |||
if test is None or passwd == '': | |||
return render_template('login.j2', error='Invalid credentials') | |||
else: | |||
session['user_id'] = request.form['user'] | |||
session['passwd'] = request.form['passwd'] | |||
return redirect('/') | |||
return render_template('login.j2') | |||
@app.route('/logout') | |||
def logout(): | |||
session.pop('user_id', None) | |||
return redirect(url_for('index')) | |||
if __name__ == '__main__': | |||
app.run() |
@ -0,0 +1,22 @@ | |||
{ | |||
"short_name": "$REPO_NAME", | |||
"name": "$REPO_NAME", | |||
"description": "$REPO_DESCRIPTION", | |||
"icons": [ | |||
{ | |||
"src": "/images/icons-192.png", | |||
"type": "image/png", | |||
"sizes": "192x192" | |||
}, | |||
{ | |||
"src": "/images/icons-512.png", | |||
"type": "image/png", | |||
"sizes": "512x512" | |||
} | |||
], | |||
"start_url": "/", | |||
"background_color": "#343a40", | |||
"theme_color": "#343a40", | |||
"display": "standalone", | |||
"scope": "/" | |||
} |
@ -0,0 +1,7 @@ | |||
Flask==1.1.2 | |||
Flask-Bootstrap4==4.0.2 | |||
Flask-Login==0.5.0 | |||
Flask-SimpleLDAP==1.4.0 | |||
python-ldap==3.2.0 | |||
ldap3==2.7 | |||
Flask-Cache-Buster==1.0.1 |
@ -0,0 +1,313 @@ | |||
/* Scroll fix, try to disable overflow scrolling on mobile to make this feel more native */ | |||
html, body { | |||
width: 100%; | |||
height: 100%; | |||
overflow: hidden; | |||
position: fixed; | |||
padding-bottom: 0; | |||
overscroll-behavior-y: none; | |||
} | |||
/* Hide bootstrap tooltip arrows | |||
I did this so I wouldn't have to deal with toolbar button alignment but you can change that if you want */ | |||
.tooltip .arrow { | |||
display: none; | |||
} | |||
/* Navbar */ | |||
.navbar { | |||
position: absolute; | |||
top: 0; | |||
width: 100%; | |||
} | |||
.toolbar-button { | |||
padding-left: 2rem; | |||
padding-top: .375rem; | |||
line-height: 1.5; | |||
color: white; | |||
-webkit-user-select: none; | |||
-moz-user-select: none; | |||
-ms-user-select: none; | |||
user-select: none; | |||
} | |||
.toolbar-button:hover { | |||
color: #ddd; | |||
text-decoration: none; | |||
} | |||
/* Sidebar */ | |||
#sidebar { | |||
padding-left: 20px; | |||
background-color: #ecedee; | |||
height: 100vh; | |||
padding-top: 20px; | |||
border-right: 1px solid darkgray; | |||
position: fixed; | |||
width: 250px; | |||
} | |||
#sidebar a:hover { | |||
text-decoration: none; | |||
} | |||
#sidebar ul > li { | |||
padding-bottom: 10px; | |||
} | |||
#sidebar ul > li > a { | |||
color: rgba(0,0,0,.5); | |||
} | |||
#sidebar ul > li.active > a { | |||
color: black; | |||
} | |||
#sidebar div > a { | |||
color: rgba(0,0,0,.5); | |||
} | |||
#sidebar div.active > a { | |||
color: black; | |||
} | |||
/* Content area */ | |||
.wrapper { | |||
height: calc(100% - 56px); | |||
overflow: auto; | |||
position: fixed; | |||
top: 56px; | |||
width: 100%; | |||
-webkit-overflow-scrolling: touch; | |||
align-items: stretch; | |||
} | |||
#content { | |||
width: 100%; | |||
padding: 20px; | |||
} | |||
/* Dark theming */ | |||
@media (prefers-color-scheme: dark) { | |||
/* Custom Bootstrap dark theming */ | |||
body { | |||
background-color: #111 !important; | |||
color: #eee; | |||
} | |||
.jumbotron { | |||
background-color: #333 !important; | |||
} | |||
.modal-content { | |||
background-color: #111 !important; | |||
color: #eee; | |||
} | |||
.modal-header { | |||
border-bottom: 1px solid #555 !important; | |||
} | |||
.modal-header .close { | |||
color: #eee !important; | |||
text-shadow: 0 1px 0 #555 !important; | |||
} | |||
.modal-footer { | |||
border-top: 1px solid #555 !important; | |||
} | |||
.bg-light { | |||
background-color: #333 !important; | |||
} | |||
.bg-white { | |||
background-color: #000 !important; | |||
} | |||
.bg-black { | |||
background-color: #eee !important; | |||
} | |||
.form-control { | |||
display: block; | |||
width: 100%; | |||
height: calc(1.5em + 0.75rem + 2px); | |||
padding: 0.375rem 0.75rem; | |||
font-size: 1rem; | |||
font-weight: 400; | |||
line-height: 1.5; | |||
color: #dee2e6; | |||
background-color: #000; | |||
background-clip: padding-box; | |||
border: 1px solid #6c757d; | |||
border-radius: 0.25rem; | |||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | |||
} | |||
@media (prefers-reduced-motion: reduce) { | |||
.form-control { | |||
transition: none; | |||
} | |||
} | |||
.form-control::-ms-expand { | |||
background-color: transparent; | |||
border: 0; | |||
} | |||
.form-control:-moz-focusring { | |||
color: transparent; | |||
text-shadow: 0 0 0 #dee2e6; | |||
} | |||
.form-control:focus { | |||
color: #dee2e6; | |||
background-color: #191d21; | |||
border-color: #b3d7ff; | |||
outline: 0; | |||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); | |||
} | |||
.form-control::-webkit-input-placeholder { | |||
color: #6c757d; | |||
opacity: 1; | |||
} | |||
.form-control::-moz-placeholder { | |||
color: #6c757d; | |||
opacity: 1; | |||
} | |||
.form-control::-ms-input-placeholder { | |||
color: #6c757d; | |||
opacity: 1; | |||
} | |||
.form-control::placeholder { | |||
color: #6c757d; | |||
opacity: 1; | |||
} | |||
.form-control:disabled, | |||
.form-control[readonly] { | |||
background-color: #343a40; | |||
opacity: 1; | |||
} | |||
.card { | |||
background-color: #000; | |||
border: 1px solid #6c757d; | |||
} | |||
/* Sidebar */ | |||
#sidebar { | |||
padding-left: 20px; | |||
background-color: #1a1d20; | |||
height: 100vh; | |||
padding-top: 20px; | |||
border-right: 1px solid black; | |||
} | |||
#sidebar ul > li { | |||
padding-bottom: 10px; | |||
} | |||
#sidebar ul > li > a { | |||
color: rgba(255,255,255,.5); | |||
} | |||
#sidebar ul > li.active > a { | |||
color: white; | |||
} | |||
#sidebar div > a { | |||
color: rgba(255,255,255,.5); | |||
} | |||
#sidebar div.active > a { | |||
color: white; | |||
} | |||
} | |||
/* PWA theming fixes | |||
Tested basically only on iOS, so Android, get fucked maybe */ | |||
@media all and (display-mode: standalone) { | |||
.navbar { | |||
position: absolute; | |||
top: 0; | |||
width: 100%; | |||
padding-top: 44px !important; | |||
} | |||
.wrapper { | |||
height: calc(100vh - 92px); | |||
overflow: auto; | |||
position: fixed; | |||
top: 92px; | |||
width: 100%; | |||
align-items: stretch; | |||
padding-left: env(safe-area-inset-left); | |||
padding-right: env(safe-area-inset-right); | |||
padding-bottom: env(safe-area-inset-bottom); | |||
} | |||
/* Tweaks for size classes above portait phones */ | |||
@media (min-width: 577px) { | |||
.navbar { | |||
position: absolute; | |||
top: 0; | |||
width: 100%; | |||
padding-top: 24px !important; | |||
} | |||
#navbarNavDropdown { | |||
padding-left: env(safe-area-inset-left); | |||
padding-right: env(safe-area-inset-right); | |||
} | |||
.wrapper { | |||
height: calc(100% - 72px); | |||
overflow: auto; | |||
position: fixed; | |||
top: 72px; | |||
width: 100%; | |||
align-items: stretch; | |||
padding-left: env(safe-area-inset-left); | |||
padding-right: env(safe-area-inset-right); | |||
padding-bottom: env(safe-area-insert-bottom); | |||
} | |||
} | |||
} | |||
/* Hide sidebar on phones and portrait tablets */ | |||
@media (max-width: 991.98px) { | |||
#sidebar { | |||
display: none !important; | |||
} | |||
} | |||
/* Display sidebar and hide nav menu for landscape tablets and desktop */ | |||
@media (min-width: 991.98px) { | |||
.navbar-nav { | |||
display: none !important; | |||
} | |||
#sidebar { | |||
min-width: 250px; | |||
max-width: 250px; | |||
display: block !important; | |||
} | |||
#content { | |||
left: 250px; | |||
width: calc(100% - 250px) !important; | |||
position: relative; | |||
} | |||
#sidebar.active { | |||
margin-left: -250px; | |||
} | |||
} |
@ -0,0 +1,64 @@ | |||
{% extends "bootstrap/base.html" %} | |||
{% block title %}$REPO_NAME{% endblock %} | |||
{% block metas %} | |||
{{super()}} | |||
<meta name="apple-mobile-web-app-capable" content="yes"> | |||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> | |||
<link rel="manifest" crossorigin="use-credentials" href="./manifest.json"> | |||
<link rel="apple-touch-icon" href="{{url_for('.static', filename='app-icon.png')}}"> | |||
{% endblock %} | |||
{% block styles %} | |||
{{super()}} | |||
<link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}"> | |||
{% endblock %} | |||
{% block navbar %} | |||
{% include "fragments/navbar.j2" %} | |||
{% endblock %} | |||
{% block content %} | |||
<div class="wrapper"> | |||
<!-- Sidebar --> | |||
<div id="sidebar"> | |||
{% include "fragments/sidebar.j2" %} | |||
</div> | |||
<!-- Page Content --> | |||
<div id="content"> | |||
<div> | |||
Made with <svg | |||
width="24" | |||
height="24" | |||
fill="none" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
style="height: 1rem; vertical-align: middle; margin-bottom: 4px;"> | |||
<use xlink:href="{{url_for('.static', filename='feather-sprite.svg')}}#heart"/></svg> and <svg | |||
width="24" | |||
height="24" | |||
fill="none" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
style="height: 1rem; vertical-align: middle; margin-bottom: 4px;"> | |||
<use xlink:href="{{url_for('.static', filename='feather-sprite.svg')}}#coffee"/></svg> in Outer Austin! | |||
</div> | |||
</div> | |||
</div> | |||
{% endblock %} | |||
{% block scripts %} | |||
{{ super() }} | |||
<script> | |||
// Enable bootstrap tooltips | |||
$(function () { | |||
$('[data-toggle="tooltip"]').tooltip() | |||
}); | |||
</script> | |||
{% endblock %} |
@ -0,0 +1,37 @@ | |||
<div id="navbar" class="navbar navbar-expand-lg sticky-top navbar-dark bg-dark"> | |||
<div class="navbar-brand">$REPO_NAME</div> | |||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" | |||
aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> | |||
<span class="navbar-toggler-icon"></span> | |||
</button> | |||
<div class="collapse navbar-collapse" id="navbarNavDropdown"> | |||
<ul class="navbar-nav"> | |||
{% if request.path == url_for('index') %} | |||
<li class="nav-item active"> | |||
{% else %} | |||
<li class="nav-item"> | |||
{% endif %} | |||
<a class="nav-link" href="{{ url_for('index') }}">Home</a> | |||
</li> | |||
{% if request.path == url_for('about') %} | |||
<li class="nav-item active"> | |||
{% else %} | |||
<li class="nav-item"> | |||
{% endif %} | |||
<a class="nav-link" href="{{ url_for('about') }}">About</a> | |||
</li> | |||
</ul> | |||
<form class="form-inline ml-auto"> | |||
<a class="toolbar-button" href="/logout" style="padding-right: 1rem;" data-toggle="tooltip" data-placement="bottom" title="Logout"><svg | |||
width="24" | |||
height="24" | |||
fill="none" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
style="line-height: 1.5; vertical-align: middle; margin-bottom: 4px;"> | |||
<use xlink:href="{{url_for('.static', filename='feather-sprite.svg')}}#log-out"/></svg></a> | |||
</form> | |||
</div> | |||
</div> |
@ -0,0 +1,39 @@ | |||
<ul class="list-unstyled components"> | |||
{% if request.path == url_for('index') %} | |||
<li class="nav-item active"> | |||
{% else %} | |||
<li class="nav-item"> | |||
{% endif %} | |||
<a href="{{ url_for('index') }}"> | |||
<svg | |||
width="24" | |||
height="24" | |||
fill="none" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
style="height: 1rem; vertical-align: middle; margin-bottom: 4px;"> | |||
<use xlink:href="{{url_for('.static', filename='feather-sprite.svg')}}#home"/> | |||
</svg>Home | |||
</a> | |||
</li> | |||
</ul> | |||
{% if request.path == url_for('about') %} | |||
<div style="position: fixed; bottom: 20px;" class="active"> | |||
{% else %} | |||
<div style="position: fixed; bottom: 20px;"> | |||
{% endif %} | |||
<a href="{{ url_for('about') }}"><svg | |||
width="24" | |||
height="24" | |||
fill="none" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
style="height: 1rem; vertical-align: middle; margin-bottom: 4px;"> | |||
<use xlink:href="{{url_for('.static', filename='feather-sprite.svg')}}#coffee"/> | |||
</svg>About</a> | |||
</div> |
@ -0,0 +1,50 @@ | |||
{% extends "bootstrap/base.html" %} | |||
{% block title %}$REPO_NAME{% endblock %} | |||
{% block metas %} | |||
{{super()}} | |||
<meta name="apple-mobile-web-app-capable" content="yes"> | |||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> | |||
<link rel="manifest" crossorigin="use-credentials" href="./manifest.json"> | |||
<link rel="apple-touch-icon" href="{{url_for('.static', filename='app-icon.png')}}"> | |||
{% endblock %} | |||
{% block styles %} | |||
{{super()}} | |||
<link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}"> | |||
{% endblock %} | |||
{% block navbar %} | |||
{% include "fragments/navbar.j2" %} | |||
{% endblock %} | |||
{% block content %} | |||
<div class="wrapper"> | |||
<!-- Sidebar --> | |||
<div id="sidebar"> | |||
{% include "fragments/sidebar.j2" %} | |||
</div> | |||
<!-- Page Content --> | |||
<div id="content"> | |||
<form> | |||
<div class="row justify-content-center"> | |||
<div class="col-lg-12"> | |||
This is your home page, have fun! | |||
</div> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
{% endblock %} | |||
{% block scripts %} | |||
{{ super() }} | |||
<script> | |||
// Enable bootstrap tooltips | |||
$(function () { | |||
$('[data-toggle="tooltip"]').tooltip() | |||
}); | |||
</script> | |||
{% endblock %} |
@ -0,0 +1,48 @@ | |||
{% extends "bootstrap/base.html" %} | |||
{% block title %}$REPO_NAME{% endblock %} | |||
{% block metas %} | |||
{{super()}} | |||
<meta name="apple-mobile-web-app-capable" content="yes"> | |||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> | |||
<link rel="manifest" crossorigin="use-credentials" href="./manifest.json"> | |||
<link rel="apple-touch-icon" href="{{url_for('.static', filename='app-icon.png')}}"> | |||
{% endblock %} | |||
{% block styles %} | |||
{{super()}} | |||
<link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}"> | |||
{% endblock %} | |||
{% block navbar %} | |||
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-dark"> | |||
<div class="navbar-brand">$REPO_NAME</div> | |||
</nav> | |||
{% endblock %} | |||
{% block content %} | |||
<div class="wrapper"> | |||
<div class="container" style="margin-top: 15px"> | |||
<div class="jumbotron"> | |||
<h1>Sign in for our awesome service</h1> | |||
<p>Forgot your password? Too bad! We don't have emails working yet!</p> | |||
</div> | |||
<div class="col-md-12"> | |||
{% if error is defined %} | |||
<div id="error-alert" class="alert alert-danger" role="alert"> | |||
{{ error }} | |||
</div> | |||
{% endif %} | |||
<form action="" method="post"> | |||
<label for="exampleFormControlInput1">Username</label> | |||
<input name="user" class="form-control"><br> | |||
<label for="exampleFormControlInput2">Password</label> | |||
<input type="password" name="passwd" class="form-control"><br> | |||
<button type="submit" class="btn btn-primary">Log In</button> | |||
</form> | |||
</div> | |||
</div> | |||
{% endblock %} |