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
import functools
import time
import uuid
import warnings
def _generate_session_token():
token = str(uuid.uuid4())
#print('MAKE SESSION', token)
return token
import jsonify
def give_session_token(function):
def required_fields(fields):
'''
Declare that the endpoint requires certain POST body fields. Without them,
we respond with 400 and a message.
'''
def with_required_fields(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)
# Send the token back to the client
if not isinstance(ret, flask.Response):
ret = flask.Response(ret)
ret.set_cookie('etiquette_session', value=token, max_age=60)
return ret
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):
'''

View file

@ -12,6 +12,7 @@ import exceptions
import helpers
import jsonify
import phototagger
import sessions
# pip install
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
@ -27,6 +28,7 @@ site.debug = True
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):
notes = P.easybake(easybake_string)
notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes]
@ -60,12 +65,6 @@ def delete_synonym(synonym):
master_tag.remove_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):
try:
return P.get_album(albumid)
@ -159,11 +158,82 @@ def send_file(filepath):
####################################################################################################
####################################################################################################
@site.route('/')
@decorators.give_session_token
@session_manager.give_token
def root():
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')
@ -182,22 +252,24 @@ def get_album_core(albumid):
return album
@site.route('/album/<albumid>')
@decorators.give_session_token
@session_manager.give_token
def get_album_html(albumid):
album = get_album_core(albumid)
session = session_manager.get(request)
response = flask.render_template(
'album.html',
album=album,
photos=album['photos'],
session=session,
view=request.args.get('view', 'grid'),
)
return response
@site.route('/album/<albumid>.json')
@decorators.give_session_token
@session_manager.give_token
def get_album_json(albumid):
album = get_album_core(albumid)
return make_json_response(album)
return jsonify.make_json_response(album)
@site.route('/album/<albumid>.tar')
@ -218,21 +290,24 @@ def get_albums_core():
return albums
@site.route('/albums')
@decorators.give_session_token
@session_manager.give_token
def get_albums_html():
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')
@decorators.give_session_token
@session_manager.give_token
def get_albums_json():
albums = get_albums_core()
return make_json_response(albums)
return jsonify.make_json_response(albums)
@site.route('/bookmarks')
@session_manager.give_token
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>')
@ -268,20 +343,20 @@ def get_photo_core(photoid):
return photo
@site.route('/photo/<photoid>', methods=['GET'])
@decorators.give_session_token
@session_manager.give_token
def get_photo_html(photoid):
photo = get_photo_core(photoid)
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'])
@decorators.give_session_token
@session_manager.give_token
def get_photo_json(photoid):
photo = get_photo_core(photoid)
photo = make_json_response(photo)
photo = jsonify.make_json_response(photo)
return photo
def get_search_core():
#print(request.args)
@ -418,11 +493,12 @@ def get_search_core():
return final_results
@site.route('/search')
@decorators.give_session_token
@session_manager.give_token
def get_search_html():
search_results = get_search_core()
search_kwargs = search_results['search_kwargs']
qualname_map = search_results['qualname_map']
session = session_manager.get(request)
response = flask.render_template(
'search.html',
next_page_url=search_results['next_page_url'],
@ -430,13 +506,14 @@ def get_search_html():
photos=search_results['photos'],
qualname_map=json.dumps(qualname_map),
search_kwargs=search_kwargs,
session=session,
total_tags=search_results['total_tags'],
warns=search_results['warns'],
)
return response
@site.route('/search.json')
@decorators.give_session_token
@session_manager.give_token
def get_search_json():
search_results = get_search_core()
#search_kwargs = search_results['search_kwargs']
@ -445,7 +522,7 @@ def get_search_json():
include_qualname_map = helpers.truthystring(include_qualname_map)
if not include_qualname_map:
search_results.pop('qualname_map')
return make_json_response(search_results)
return jsonify.make_json_response(search_results)
@site.route('/static/<filename>')
@ -468,18 +545,19 @@ def get_tags_core(specific_tag=None):
@site.route('/tags')
@site.route('/tags/<specific_tag>')
@decorators.give_session_token
@session_manager.give_token
def get_tags_html(specific_tag=None):
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/<specific_tag>.json')
@decorators.give_session_token
@session_manager.give_token
def get_tags_json(specific_tag=None):
tags = get_tags_core(specific_tag)
tags = [t[0] for t in tags]
return make_json_response(tags)
return jsonify.make_json_response(tags)
@site.route('/thumbnail/<photoid>')
@ -495,7 +573,7 @@ def get_thumbnail(photoid):
@site.route('/album/<albumid>', methods=['POST'])
@site.route('/album/<albumid>.json', methods=['POST'])
@decorators.give_session_token
@session_manager.give_token
def post_edit_album(albumid):
'''
Edit the album's title and description.
@ -512,18 +590,18 @@ def post_edit_album(albumid):
tag = P_tag(tag)
except exceptions.NoSuchTag:
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 = helpers.truthystring(recursive)
album.add_tag_to_all(tag, nested_children=recursive)
response['action'] = action
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>.json', methods=['POST'])
@decorators.give_session_token
@session_manager.give_token
def post_edit_photo(photoid):
'''
Add and remove tags from photos.
@ -548,22 +626,22 @@ def post_edit_photo(photoid):
tag = P.get_tag(tag)
except exceptions.NoSuchTag:
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)
response['action'] = action
#response['tagid'] = tag.id
response['tagname'] = tag.name
return make_json_response(response)
return jsonify.make_json_response(response)
@site.route('/tags', methods=['POST'])
@decorators.give_session_token
@session_manager.give_token
def post_edit_tags():
'''
Create and delete tags and synonyms.
'''
print(request.form)
#print(request.form)
status = 200
if 'create_tag' in request.form:
action = 'create_tag'
@ -605,6 +683,13 @@ def post_edit_tags():
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__':
#site.run(threaded=True)
pass

View file

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

View file

@ -1,4 +1,12 @@
import flask
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):
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>
{{header.make_header()}}
{{header.make_header(session=session)}}
<div id="content_body">
<h2>{{album["title"]}}</h2>
<p>{{album["description"]}}</p>

View file

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

View file

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

View file

@ -1,7 +1,13 @@
{% macro make_header() %}
{% macro make_header(session) %}
<div id="header">
<a class="header_element" href="/">Etiquette</a>
<a class="header_element" href="/search">Search</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>
{% 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>
{{header.make_header()}}
{{header.make_header(session=session)}}
<div id="content_body">
<div id="left">
<div id="editor_area">

View file

@ -34,6 +34,11 @@ a:hover
<a href="/tags">Browse tags</a>
<a href="/albums">Browse albums</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>

View file

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

View file

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