very early session and registration support

This commit is contained in:
voussoir 2016-12-18 05:12:14 -08:00
parent 91fcbb7101
commit c843f444e7
14 changed files with 317 additions and 67 deletions

View file

@ -2,33 +2,26 @@ import flask
from flask import request from flask import request
import functools import functools
import time import time
import uuid
import warnings import warnings
def _generate_session_token(): import jsonify
token = str(uuid.uuid4())
#print('MAKE SESSION', token)
return token
def give_session_token(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
# Inject new token so the function doesn't know the difference
token = request.cookies.get('etiquette_session', None)
if not token:
token = _generate_session_token()
request.cookies = dict(request.cookies)
request.cookies['etiquette_session'] = token
ret = function(*args, **kwargs) def required_fields(fields):
'''
# Send the token back to the client Declare that the endpoint requires certain POST body fields. Without them,
if not isinstance(ret, flask.Response): we respond with 400 and a message.
ret = flask.Response(ret) '''
ret.set_cookie('etiquette_session', value=token, max_age=60) def with_required_fields(function):
@functools.wraps(function)
return ret def wrapped(*args, **kwargs):
return wrapped if not all(field in request.form for field in fields):
response = {'error': 'Required fields: %s' % ', '.join(fields)}
response = jsonify.make_json_response(response, status=400)
return response
return function(*args, **kwargs)
return wrapped
return with_required_fields
def not_implemented(function): def not_implemented(function):
''' '''

View file

@ -12,6 +12,7 @@ import exceptions
import helpers import helpers
import jsonify import jsonify
import phototagger import phototagger
import sessions
# pip install # pip install
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip # https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
@ -27,6 +28,7 @@ site.debug = True
P = phototagger.PhotoDB() P = phototagger.PhotoDB()
session_manager = sessions.SessionManager()
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
@ -34,6 +36,9 @@ P = phototagger.PhotoDB()
#################################################################################################### ####################################################################################################
def back_url():
return request.args.get('goto') or request.referrer or '/'
def create_tag(easybake_string): def create_tag(easybake_string):
notes = P.easybake(easybake_string) notes = P.easybake(easybake_string)
notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes]
@ -60,12 +65,6 @@ def delete_synonym(synonym):
master_tag.remove_synonym(synonym) master_tag.remove_synonym(synonym)
return {'action':'delete_synonym', 'synonym': synonym} return {'action':'delete_synonym', 'synonym': synonym}
def make_json_response(j, *args, **kwargs):
dumped = json.dumps(j)
response = flask.Response(dumped, *args, **kwargs)
response.headers['Content-Type'] = 'application/json;charset=utf-8'
return response
def P_album(albumid): def P_album(albumid):
try: try:
return P.get_album(albumid) return P.get_album(albumid)
@ -159,11 +158,82 @@ def send_file(filepath):
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
@site.route('/') @site.route('/')
@decorators.give_session_token @session_manager.give_token
def root(): def root():
motd = random.choice(P.config['motd_strings']) motd = random.choice(P.config['motd_strings'])
return flask.render_template('root.html', motd=motd) session = session_manager.get(request)
return flask.render_template('root.html', motd=motd, session=session)
@site.route('/login', methods=['GET'])
@session_manager.give_token
def get_login():
session = session_manager.get(request)
return flask.render_template('login.html', session=session)
@site.route('/register', methods=['GET'])
def get_register():
return flask.redirect('/login')
@site.route('/login', methods=['POST'])
@session_manager.give_token
@decorators.required_fields(['username', 'password'])
def post_login():
if session_manager.get(request):
flask.abort(403, 'You\'re already signed in.')
username = request.form['username']
password = request.form['password']
user = P.get_user(username=username)
try:
user = P.login(user.id, password)
except exceptions.WrongLogin:
flask.abort(422, 'Wrong login.')
session = sessions.Session(request, user)
session_manager.add(session)
response = flask.Response('redirect', status=302, headers={'Location': '/'})
return response
@site.route('/register', methods=['POST'])
@session_manager.give_token
@decorators.required_fields(['username', 'password_1', 'password_2'])
def post_register():
if session_manager.get(request):
flask.abort(403, 'You\'re already signed in.')
username = request.form['username']
password_1 = request.form['password_1']
password_2 = request.form['password_2']
if password_1 != password_2:
flask.abort(422, 'Passwords do not match.')
try:
user = P.register_user(username, password_1)
except exceptions.UsernameTooShort as e:
flask.abort(422, 'Username shorter than minimum of %d' % P.config['min_username_length'])
except exceptions.UsernameTooLong as e:
flask.abort(422, 'Username longer than maximum of %d' % P.config['max_username_length'])
except exceptions.InvalidUsernameChars as e:
flask.abort(422, 'Username contains invalid characters %s' % e.args[0])
except exceptions.PasswordTooShort as e:
flask.abort(422, 'Password is shorter than minimum of %d' % P.config['min_password_length'])
except exceptions.UserExists as e:
flask.abort(422, 'User %s already exists' % e.args[0])
session = sessions.Session(request, user)
session_manager.add(session)
response = flask.Response('redirect', status=302, headers={'Location': '/'})
return response
@site.route('/logout', methods=['GET', 'POST'])
@session_manager.give_token
def logout():
session_manager.remove(request)
response = flask.Response('redirect', status=302, headers={'Location': back_url()})
return response
@site.route('/favicon.ico') @site.route('/favicon.ico')
@ -182,22 +252,24 @@ def get_album_core(albumid):
return album return album
@site.route('/album/<albumid>') @site.route('/album/<albumid>')
@decorators.give_session_token @session_manager.give_token
def get_album_html(albumid): def get_album_html(albumid):
album = get_album_core(albumid) album = get_album_core(albumid)
session = session_manager.get(request)
response = flask.render_template( response = flask.render_template(
'album.html', 'album.html',
album=album, album=album,
photos=album['photos'], photos=album['photos'],
session=session,
view=request.args.get('view', 'grid'), view=request.args.get('view', 'grid'),
) )
return response return response
@site.route('/album/<albumid>.json') @site.route('/album/<albumid>.json')
@decorators.give_session_token @session_manager.give_token
def get_album_json(albumid): def get_album_json(albumid):
album = get_album_core(albumid) album = get_album_core(albumid)
return make_json_response(album) return jsonify.make_json_response(album)
@site.route('/album/<albumid>.tar') @site.route('/album/<albumid>.tar')
@ -218,21 +290,24 @@ def get_albums_core():
return albums return albums
@site.route('/albums') @site.route('/albums')
@decorators.give_session_token @session_manager.give_token
def get_albums_html(): def get_albums_html():
albums = get_albums_core() albums = get_albums_core()
return flask.render_template('albums.html', albums=albums) session = session_manager.get(request)
return flask.render_template('albums.html', albums=albums, session=session)
@site.route('/albums.json') @site.route('/albums.json')
@decorators.give_session_token @session_manager.give_token
def get_albums_json(): def get_albums_json():
albums = get_albums_core() albums = get_albums_core()
return make_json_response(albums) return jsonify.make_json_response(albums)
@site.route('/bookmarks') @site.route('/bookmarks')
@session_manager.give_token
def get_bookmarks(): def get_bookmarks():
return flask.render_template('bookmarks.html') session = session_manager.get(request)
return flask.render_template('bookmarks.html', session=session)
@site.route('/file/<photoid>') @site.route('/file/<photoid>')
@ -268,20 +343,20 @@ def get_photo_core(photoid):
return photo return photo
@site.route('/photo/<photoid>', methods=['GET']) @site.route('/photo/<photoid>', methods=['GET'])
@decorators.give_session_token @session_manager.give_token
def get_photo_html(photoid): def get_photo_html(photoid):
photo = get_photo_core(photoid) photo = get_photo_core(photoid)
photo['tags'].sort(key=lambda x: x['qualified_name']) photo['tags'].sort(key=lambda x: x['qualified_name'])
return flask.render_template('photo.html', photo=photo) session = session_manager.get(request)
return flask.render_template('photo.html', photo=photo, session=session)
@site.route('/photo/<photoid>.json', methods=['GET']) @site.route('/photo/<photoid>.json', methods=['GET'])
@decorators.give_session_token @session_manager.give_token
def get_photo_json(photoid): def get_photo_json(photoid):
photo = get_photo_core(photoid) photo = get_photo_core(photoid)
photo = make_json_response(photo) photo = jsonify.make_json_response(photo)
return photo return photo
def get_search_core(): def get_search_core():
#print(request.args) #print(request.args)
@ -418,11 +493,12 @@ def get_search_core():
return final_results return final_results
@site.route('/search') @site.route('/search')
@decorators.give_session_token @session_manager.give_token
def get_search_html(): def get_search_html():
search_results = get_search_core() search_results = get_search_core()
search_kwargs = search_results['search_kwargs'] search_kwargs = search_results['search_kwargs']
qualname_map = search_results['qualname_map'] qualname_map = search_results['qualname_map']
session = session_manager.get(request)
response = flask.render_template( response = flask.render_template(
'search.html', 'search.html',
next_page_url=search_results['next_page_url'], next_page_url=search_results['next_page_url'],
@ -430,13 +506,14 @@ def get_search_html():
photos=search_results['photos'], photos=search_results['photos'],
qualname_map=json.dumps(qualname_map), qualname_map=json.dumps(qualname_map),
search_kwargs=search_kwargs, search_kwargs=search_kwargs,
session=session,
total_tags=search_results['total_tags'], total_tags=search_results['total_tags'],
warns=search_results['warns'], warns=search_results['warns'],
) )
return response return response
@site.route('/search.json') @site.route('/search.json')
@decorators.give_session_token @session_manager.give_token
def get_search_json(): def get_search_json():
search_results = get_search_core() search_results = get_search_core()
#search_kwargs = search_results['search_kwargs'] #search_kwargs = search_results['search_kwargs']
@ -445,7 +522,7 @@ def get_search_json():
include_qualname_map = helpers.truthystring(include_qualname_map) include_qualname_map = helpers.truthystring(include_qualname_map)
if not include_qualname_map: if not include_qualname_map:
search_results.pop('qualname_map') search_results.pop('qualname_map')
return make_json_response(search_results) return jsonify.make_json_response(search_results)
@site.route('/static/<filename>') @site.route('/static/<filename>')
@ -468,18 +545,19 @@ def get_tags_core(specific_tag=None):
@site.route('/tags') @site.route('/tags')
@site.route('/tags/<specific_tag>') @site.route('/tags/<specific_tag>')
@decorators.give_session_token @session_manager.give_token
def get_tags_html(specific_tag=None): def get_tags_html(specific_tag=None):
tags = get_tags_core(specific_tag) tags = get_tags_core(specific_tag)
return flask.render_template('tags.html', tags=tags) session = session_manager.get(request)
return flask.render_template('tags.html', tags=tags, session=session)
@site.route('/tags.json') @site.route('/tags.json')
@site.route('/tags/<specific_tag>.json') @site.route('/tags/<specific_tag>.json')
@decorators.give_session_token @session_manager.give_token
def get_tags_json(specific_tag=None): def get_tags_json(specific_tag=None):
tags = get_tags_core(specific_tag) tags = get_tags_core(specific_tag)
tags = [t[0] for t in tags] tags = [t[0] for t in tags]
return make_json_response(tags) return jsonify.make_json_response(tags)
@site.route('/thumbnail/<photoid>') @site.route('/thumbnail/<photoid>')
@ -495,7 +573,7 @@ def get_thumbnail(photoid):
@site.route('/album/<albumid>', methods=['POST']) @site.route('/album/<albumid>', methods=['POST'])
@site.route('/album/<albumid>.json', methods=['POST']) @site.route('/album/<albumid>.json', methods=['POST'])
@decorators.give_session_token @session_manager.give_token
def post_edit_album(albumid): def post_edit_album(albumid):
''' '''
Edit the album's title and description. Edit the album's title and description.
@ -512,18 +590,18 @@ def post_edit_album(albumid):
tag = P_tag(tag) tag = P_tag(tag)
except exceptions.NoSuchTag: except exceptions.NoSuchTag:
response = {'error': 'That tag doesnt exist', 'tagname': tag} response = {'error': 'That tag doesnt exist', 'tagname': tag}
return make_json_response(response, status=404) return jsonify.make_json_response(response, status=404)
recursive = request.form.get('recursive', False) recursive = request.form.get('recursive', False)
recursive = helpers.truthystring(recursive) recursive = helpers.truthystring(recursive)
album.add_tag_to_all(tag, nested_children=recursive) album.add_tag_to_all(tag, nested_children=recursive)
response['action'] = action response['action'] = action
response['tagname'] = tag.name response['tagname'] = tag.name
return make_json_response(response) return jsonify.make_json_response(response)
@site.route('/photo/<photoid>', methods=['POST']) @site.route('/photo/<photoid>', methods=['POST'])
@site.route('/photo/<photoid>.json', methods=['POST']) @site.route('/photo/<photoid>.json', methods=['POST'])
@decorators.give_session_token @session_manager.give_token
def post_edit_photo(photoid): def post_edit_photo(photoid):
''' '''
Add and remove tags from photos. Add and remove tags from photos.
@ -548,22 +626,22 @@ def post_edit_photo(photoid):
tag = P.get_tag(tag) tag = P.get_tag(tag)
except exceptions.NoSuchTag: except exceptions.NoSuchTag:
response = {'error': 'That tag doesnt exist', 'tagname': tag} response = {'error': 'That tag doesnt exist', 'tagname': tag}
return make_json_response(response, status=404) return jsonify.make_json_response(response, status=404)
method(tag) method(tag)
response['action'] = action response['action'] = action
#response['tagid'] = tag.id #response['tagid'] = tag.id
response['tagname'] = tag.name response['tagname'] = tag.name
return make_json_response(response) return jsonify.make_json_response(response)
@site.route('/tags', methods=['POST']) @site.route('/tags', methods=['POST'])
@decorators.give_session_token @session_manager.give_token
def post_edit_tags(): def post_edit_tags():
''' '''
Create and delete tags and synonyms. Create and delete tags and synonyms.
''' '''
print(request.form) #print(request.form)
status = 200 status = 200
if 'create_tag' in request.form: if 'create_tag' in request.form:
action = 'create_tag' action = 'create_tag'
@ -605,6 +683,13 @@ def post_edit_tags():
return response return response
@site.route('/apitest')
@session_manager.give_token
def apitest():
response = flask.Response('testing')
response.set_cookie('etiquette_session', 'don\'t overwrite me')
return response
if __name__ == '__main__': if __name__ == '__main__':
#site.run(threaded=True) #site.run(threaded=True)
pass pass

View file

@ -2,10 +2,10 @@ import datetime
import math import math
import mimetypes import mimetypes
import os import os
import warnings
import constants import constants
import exceptions import exceptions
import warnings
from voussoirkit import bytestring from voussoirkit import bytestring

View file

@ -1,4 +1,12 @@
import flask
import helpers import helpers
import json
def make_json_response(j, *args, **kwargs):
dumped = json.dumps(j)
response = flask.Response(dumped, *args, **kwargs)
response.headers['Content-Type'] = 'application/json;charset=utf-8'
return response
def album(a, minimal=False): def album(a, minimal=False):
j = { j = {

79
sessions.py Normal file
View file

@ -0,0 +1,79 @@
import flask
from flask import request
import functools
import helpers
import uuid
def _generate_token():
token = str(uuid.uuid4())
#print('MAKE SESSION', token)
return token
def _normalize_token(token):
if isinstance(token, flask.Request):
token = token.cookies.get('etiquette_session', None)
class SessionManager:
def __init__(self):
self.sessions = {}
def add(self, session):
self.sessions[session.token] = session
def get(self, token):
token = _normalize_token(token)
return self.sessions.get(token, None)
def give_token(self, function):
'''
This decorator ensures that the user has an `etiquette_session` cookie
before reaching the request handler.
If the user does not have the cookie, they are given one.
If they do, its lifespan is reset.
'''
@functools.wraps(function)
def wrapped(*args, **kwargs):
# Inject new token so the function doesn't know the difference
token = request.cookies.get('etiquette_session', None)
if not token:
token = _generate_token()
request.cookies = dict(request.cookies)
request.cookies['etiquette_session'] = token
response = function(*args, **kwargs)
if not isinstance(response, flask.Response):
response = flask.Response(response)
# Send the token back to the client
# but only if the endpoint didn't manually set the cookie.
for (headerkey, value) in response.headers:
if headerkey == 'Set-Cookie' and value.startswith('etiquette_session='):
break
else:
response.set_cookie('etiquette_session', value=token, max_age=86400)
self.maintain(token)
return response
return wrapped
def maintain(self, token):
session = self.get(token)
if session:
session.maintain()
def remove(self, token):
token = _normalize_token(token)
if token in self.sessions:
self.sessions.pop(token)
class Session:
def __init__(self, request, user):
self.token = _normalize_token(request)
self.user = user
self.ip_address = request.remote_addr
self.user_agent = request.headers.get('User-Agent', '')
self.last_activity = int(helpers.now())
def maintain(self):
self.last_activity = int(helpers.now())

View file

@ -22,7 +22,7 @@ p
<body> <body>
{{header.make_header()}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body">
<h2>{{album["title"]}}</h2> <h2>{{album["title"]}}</h2>
<p>{{album["description"]}}</p> <p>{{album["description"]}}</p>

View file

@ -16,7 +16,7 @@
<body> <body>
{{header.make_header()}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body">
{% for album in albums %} {% for album in albums %}
{% if album["title"] %} {% if album["title"] %}

View file

@ -13,7 +13,7 @@
<body> <body>
{{header.make_header()}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body">
<a href="/search?has_tags=no&orderby=random-desc&mimetype=image">Needs tagging</a> <a href="/search?has_tags=no&orderby=random-desc&mimetype=image">Needs tagging</a>
</div> </div>

View file

@ -1,7 +1,13 @@
{% macro make_header() %} {% macro make_header(session) %}
<div id="header"> <div id="header">
<a class="header_element" href="/">Etiquette</a> <a class="header_element" href="/">Etiquette</a>
<a class="header_element" href="/search">Search</a> <a class="header_element" href="/search">Search</a>
<a class="header_element" href="/tags">Tags</a> <a class="header_element" href="/tags">Tags</a>
{% if session %}
<a class="header_element" href="/user/{{session.user.username}}">{{session.user.username}}</a>
<a class="header_element" href="/logout" style="flex:0">Logout</a>
{% else %}
<a class="header_element" href="/login">Log in</a>
{% endif %}
</div> </div>
{% endmacro %} {% endmacro %}

74
templates/login.html Normal file
View file

@ -0,0 +1,74 @@
<!DOCTYPE html5>
<html>
<head>
{% import "header.html" as header %}
<title>Login/Register</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
<script src="/static/common.js"></script>
<style>
#content_body
{
justify-content: center;
align-content: center;
flex: 1;
}
#login_register_box
{
margin: auto;
display: flex;
flex-direction: row;
}
form
{
flex: 1;
display: flex;
flex-direction: column;
padding: 25px;
margin: 25px;
border: 1px black solid;
border-radius: 6px;
}
form > *
{
margin-top: 5px;
margin-bottom: 5px;
}
input
{
width: 300px;
}
button
{
width: 80px;
}
</style>
</head>
<body>
{{header.make_header(session=session)}}
<div id="content_body">
<div id="login_register_box">
<form id="login_form" action="/login" method="post">
<span>Log in</span>
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit">Log in</button>
</form>
<form id="register_form" action="/register" method="post">
<span>Register</span>
<input type="text" name="username" placeholder="username">
<input type="password" name="password_1" placeholder="password">
<input type="password" name="password_2" placeholder="password again">
<button type="submit">Register</button>
</form>
</div>
</div>
</body>
<script type="text/javascript">
</script>
</html>

View file

@ -87,7 +87,7 @@
<body> <body>
{{header.make_header()}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body">
<div id="left"> <div id="left">
<div id="editor_area"> <div id="editor_area">

View file

@ -34,6 +34,11 @@ a:hover
<a href="/tags">Browse tags</a> <a href="/tags">Browse tags</a>
<a href="/albums">Browse albums</a> <a href="/albums">Browse albums</a>
<a href="/bookmarks">Bookmarks</a> <a href="/bookmarks">Bookmarks</a>
{% if session %}
<a href="/user/{{session.user.username}}">{{session.user.username}}</a>
{% else %}
<a href="/login">Log in</a>
{% endif %}
</body> </body>

View file

@ -119,7 +119,7 @@ form
<body> <body>
{{header.make_header()}} {{header.make_header(session=session)}}
<div id="error_message_area"> <div id="error_message_area">
{% for warn in warns %} {% for warn in warns %}
<span class="search_warning">{{warn}}</span> <span class="search_warning">{{warn}}</span>

View file

@ -59,7 +59,7 @@ body
<body> <body>
{{header.make_header()}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body">
<div id="left"> <div id="left">
<ul> <ul>