@ -0,0 +1,13 @@ | |||||
FROM python:3.7 | |||||
LABEL maintainer="Dan Muckerman <danielmuckerman@me.com>" | |||||
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,160 @@ | |||||
import ldap as l | |||||
from ldap3 import Server, Connection, ALL, MODIFY_REPLACE | |||||
from flask import Flask, g, request, session, redirect, url_for, render_template | |||||
from flask_simpleldap import LDAP | |||||
from flask_bootstrap import Bootstrap | |||||
from email_validator import validate_email, EmailNotValidError | |||||
import os | |||||
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) | |||||
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('/') | |||||
@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('profile.j2', user = user) | |||||
@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') | |||||
@ldap.login_required | |||||
@app.route('/update/email', methods=['POST']) | |||||
def update_email(): | |||||
if request.method == 'POST': | |||||
email = request.form['email'] | |||||
dn = request.form['dn'] | |||||
if email != None and len(email) > 0: | |||||
try: | |||||
# Validate. | |||||
valid = validate_email(email) | |||||
# Update with the normalized form. | |||||
conn.modify(dn, {'mail': [(MODIFY_REPLACE, [valid.email])]}) | |||||
return 'Success' | |||||
except EmailNotValidError as e: | |||||
# email is not valid, exception message is human-readable | |||||
print(str(e)) | |||||
return 'Invalid email address' | |||||
return 'Email cannot be empty' | |||||
@ldap.login_required | |||||
@app.route('/update/name', methods=['POST']) | |||||
def update_name(): | |||||
if request.method == 'POST': | |||||
firstName = request.form['firstName'] | |||||
lastName = request.form['lastName'] | |||||
dn = request.form['dn'] | |||||
if (firstName != None and len(firstName) > 0) and (lastName != None and len(lastName) > 0): | |||||
conn.modify(dn, {'givenName': [(MODIFY_REPLACE, [firstName])], | |||||
'sn': [(MODIFY_REPLACE, [lastName])]}) | |||||
return 'Success' | |||||
return 'Name cannot be empty' | |||||
@ldap.login_required | |||||
@app.route('/update/username', methods=['POST']) | |||||
def update_username(): | |||||
if request.method == 'POST': | |||||
userName = request.form['userName'] | |||||
dn = request.form['dn'] | |||||
if userName != None and len(userName) > 0: | |||||
conn.modify(dn, {'uid': [(MODIFY_REPLACE, [userName])]}) | |||||
return 'Success' | |||||
return 'Username cannot be empty' | |||||
@ldap.login_required | |||||
@app.route('/update/password', methods=['POST']) | |||||
def update_password(): | |||||
if request.method == 'POST': | |||||
currentPassword = request.form['currentPassword'] | |||||
newPassword = request.form['newPassword'] | |||||
confirmPassword = request.form['confirmPassword'] | |||||
dn = request.form['dn'] | |||||
if currentPassword == '': | |||||
return 'Please enter your current password' | |||||
if newPassword == '': | |||||
return 'Please enter a new password' | |||||
if confirmPassword == '': | |||||
return 'Please confirm your new password' | |||||
if newPassword != confirmPassword: | |||||
return 'Could not confirm new password, please make sure you typed it correctly' | |||||
test = ldap.bind_user(session['user_id'], currentPassword) | |||||
if test is None: | |||||
return 'Current password is incorrect' | |||||
else: | |||||
conn.extend.standard.modify_password(user=dn, new_password=newPassword) | |||||
return 'Success' | |||||
return 'Error' | |||||
@app.route('/logout') | |||||
def logout(): | |||||
session.pop('user_id', None) | |||||
return redirect(url_for('index')) | |||||
if __name__ == '__main__': | |||||
app.run() |
@ -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 | |||||
email-validator==1.1.1 |
@ -0,0 +1,117 @@ | |||||
@media (max-width: 991.98px) { | |||||
#userNameRow, | |||||
#firstNameRow { | |||||
margin-bottom: 20px; | |||||
} | |||||
#passwordButton { | |||||
margin-bottom: 50px; | |||||
} | |||||
} | |||||
@media (prefers-color-scheme: dark) { | |||||
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; | |||||
} | |||||
} |
@ -0,0 +1,37 @@ | |||||
{% extends "bootstrap/base.html" %} | |||||
{% block title %}Manage your Technical Incompetence account{% 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">Technical Incompetence Account</div> | |||||
</nav> | |||||
{% endblock %} | |||||
{% block content %} | |||||
<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> | |||||
{% endblock %} |
@ -0,0 +1,258 @@ | |||||
{% extends "bootstrap/base.html" %} | |||||
{% block title %}Manage your Technical Incompetence account{% 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">Technical Incompetence Account</div> | |||||
<form class="form-inline ml-auto"> | |||||
{# <div class="custom-control custom-switch"> | |||||
<input type="checkbox" class="custom-control-input" id="darkSwitch" /> | |||||
<label class="custom-control-label" for="darkSwitch">Dark Mode</label> | |||||
</div> | |||||
<div class="navbar-text" style="margin-right: 20px; ">{{ user['userName'] }}</div> #} | |||||
<a class="btn btn-primary" href="/logout" role="button">Logout</a> | |||||
</form> | |||||
</nav> | |||||
{% endblock %} | |||||
{% block content %} | |||||
<div class="container" style="margin-top: 15px"> | |||||
<div id="success-alert" class="alert alert-success" role="alert" style="display: none;"> | |||||
This is a success alert—check it out! | |||||
</div> | |||||
<div id="error-alert" class="alert alert-danger" role="alert" style="display: none;"> | |||||
This is a danger alert—check it out! | |||||
</div> | |||||
<form> | |||||
<div class="row"> | |||||
<div class="col-lg-6" id="firstNameRow"> | |||||
<label for="formGroupExampleInput1">First name</label> | |||||
<input id="firstName" type="text" class="form-control" placeholder="First name" value="{{ user['firstName'] }}"> | |||||
</div> | |||||
<div class="col-lg-6"> | |||||
<label for="formGroupExampleInput2">Last name</label> | |||||
<input id="lastName" type="text" class="form-control" placeholder="Last name" value="{{ user['lastName'] }}"> | |||||
</div> | |||||
</div> | |||||
<br> | |||||
<button type="button" class="btn btn-primary" onclick="updateName();">Change Name</button> | |||||
</form> | |||||
<br><br> | |||||
<form> | |||||
<div class="row"> | |||||
<div class="col-lg-6" id="userNameRow"> | |||||
<label for="formGroupExampleInput3">Username</label> | |||||
<input id="userName" type="text" class="form-control" placeholder="Username" value="{{ user['userName'] }}"> | |||||
<br> | |||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#userNameWarningModal">Change Username</button> | |||||
<br> | |||||
</div> | |||||
<div class="col-lg-6"> | |||||
<label for="formGroupExampleInput4">Email</label> | |||||
<input id="email" type="email" class="form-control" placeholder="Email" value="{{ user['email'] }}"> | |||||
<br> | |||||
<button type="button" class="btn btn-primary" onclick="updateEmail();">Change Email</button> | |||||
<br> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
<br><br> | |||||
<form> | |||||
<button id="passwordButton" type="button" class="btn btn-danger" data-toggle="modal" data-target="#passwordChangeModal">Change Password</button> | |||||
</form> | |||||
</div> | |||||
<div id="userNameWarningModal" class="modal fade" tabindex="-1" role="dialog"> | |||||
<div class="modal-dialog" role="document"> | |||||
<div class="modal-content"> | |||||
<div class="modal-header"> | |||||
<h5 class="modal-title">Warning</h5> | |||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> | |||||
<span aria-hidden="true">×</span> | |||||
</button> | |||||
</div> | |||||
<div class="modal-body"> | |||||
<div id="username-alert" class="alert alert-danger" role="alert" style="display: none;"> | |||||
This is a danger alert—check it out! | |||||
</div> | |||||
<p>If you change your username, you will be logged out here and likely need to re-login in everywhere with your new username.</p> | |||||
</div> | |||||
<div class="modal-footer"> | |||||
<button type="button" class="btn btn-danger" onclick="updateUserName();">Save new username</button> | |||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div id="passwordChangeModal" class="modal fade" tabindex="-1" role="dialog"> | |||||
<div class="modal-dialog" role="document"> | |||||
<div class="modal-content"> | |||||
<div class="modal-header"> | |||||
<h5 class="modal-title">Update Password</h5> | |||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> | |||||
<span aria-hidden="true">×</span> | |||||
</button> | |||||
</div> | |||||
<div class="modal-body"> | |||||
<div id="password-alert" class="alert alert-danger" role="alert" style="display: none;"> | |||||
This is a danger alert—check it out! | |||||
</div> | |||||
<p>If you change your username, you will be logged out here and likely need to re-login in everywhere with your new username.</p> | |||||
<form style="padding: .75rem 1.25rem;"> | |||||
<div class="row"> | |||||
<label for="formGroupExampleInput5">Current Password</label> | |||||
<input id="currentPassword" type="password" class="form-control" placeholder="Current Password"> | |||||
</div> | |||||
<br> | |||||
<div class="row"> | |||||
<label for="formGroupExampleInput6">New Password</label> | |||||
<input id="newPassword" type="password" class="form-control" placeholder="New Password"> | |||||
</div> | |||||
<br> | |||||
<div class="row"> | |||||
<label for="formGroupExampleInput6">Confirm New Password</label> | |||||
<input id="confirmNewPassword" type="password" class="form-control" placeholder="Confirm New Password"> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
<div class="modal-footer"> | |||||
<button type="button" class="btn btn-danger" onclick="updatePassword();">Change Password</button> | |||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{% endblock %} | |||||
{% block scripts %} | |||||
{{ super() }} | |||||
<script> | |||||
function updateEmail() { | |||||
email = $('#email').val(); | |||||
if (email.trim().length === 0) { | |||||
showError('Email is required'); | |||||
return false; | |||||
} | |||||
dn = '{{ user['dn'] }}'; | |||||
$.ajax({ | |||||
url: '/update/email', | |||||
method: 'POST', | |||||
data: { "email": email, | |||||
"dn": dn }, | |||||
success: function(data) { | |||||
if (data === 'Success') | |||||
showSuccess('Email updated!'); | |||||
else | |||||
showError(data); | |||||
} | |||||
}); | |||||
} | |||||
function updateName() { | |||||
first = $('#firstName').val(); | |||||
last = $('#lastName').val(); | |||||
if (first.trim().length === 0 || last.trim().length === 0) { | |||||
showError('Both first and last name are required'); | |||||
return false; | |||||
} | |||||
dn = '{{ user['dn'] }}'; | |||||
$.ajax({ | |||||
url: '/update/name', | |||||
method: 'POST', | |||||
data: { "firstName": first, | |||||
"lastName": last, | |||||
"dn": dn }, | |||||
success: function(data) { | |||||
if (data === 'Success') | |||||
showSuccess('Name updated!'); | |||||
else | |||||
showError(data); | |||||
} | |||||
}); | |||||
} | |||||
function updateUserName() { | |||||
user = $('#userName').val(); | |||||
if (user.trim().length === 0) { | |||||
showUsernameError('Username is required'); | |||||
return false; | |||||
} | |||||
dn = '{{ user['dn'] }}'; | |||||
$.ajax({ | |||||
url: '/update/username', | |||||
method: 'POST', | |||||
data: { "userName": user, | |||||
"dn": dn }, | |||||
success: function(data) { | |||||
if (data === 'Success') | |||||
window.location = window.location.origin + "/logout"; | |||||
else | |||||
showUsernameError(data); | |||||
} | |||||
}); | |||||
} | |||||
function updatePassword() { | |||||
currentPassword = $('#currentPassword').val(); | |||||
newPassword = $('#newPassword').val(); | |||||
confirmPassword = $('#confirmNewPassword').val(); | |||||
dn = '{{ user['dn'] }}'; | |||||
$.ajax({ | |||||
url: '/update/password', | |||||
method: 'POST', | |||||
data: { "currentPassword": currentPassword, | |||||
"newPassword": newPassword, | |||||
"confirmPassword": confirmPassword, | |||||
"dn": dn }, | |||||
success: function(data) { | |||||
if (data === 'Success') | |||||
window.location = window.location.origin + "/logout"; | |||||
else | |||||
showPasswordError(data) | |||||
} | |||||
}); | |||||
} | |||||
function showError(error) { | |||||
hideSuccess(); | |||||
$('#error-alert').text(error); | |||||
$('#error-alert').show(); | |||||
} | |||||
function showPasswordError(error) { | |||||
$('#password-alert').text(error); | |||||
$('#password-alert').show(); | |||||
} | |||||
function showUsernameError(error) { | |||||
$('#username-alert').text(error); | |||||
$('#username-alert').show(); | |||||
} | |||||
function hideError() { | |||||
$('#error-alert').hide(); | |||||
} | |||||
function showSuccess(message) { | |||||
hideError(); | |||||
$('#success-alert').text(message); | |||||
$('#success-alert').show(); | |||||
} | |||||
function hideSuccess() { | |||||
$('#success-alert').hide(); | |||||
} | |||||
</script> | |||||
{% endblock %} |