basic auth for endpoints

This commit is contained in:
dave 2018-09-22 18:01:33 -07:00
parent 346f0a7944
commit 3035d117b5
4 changed files with 156 additions and 67 deletions

View File

@ -3,7 +3,7 @@ import cherrypy
import logging
from datetime import datetime, timedelta
from photoapp.library import PhotoLibrary
from photoapp.types import Photo, PhotoSet, Tag, TagItem
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus
from jinja2 import Environment, FileSystemLoader, select_autoescape
from sqlalchemy import desc
from sqlalchemy.exc import IntegrityError
@ -15,17 +15,43 @@ APPROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
def auth():
"""
Return the currently authorized username (per request) or None
"""
return cherrypy.session.get('authed', None)
def mime2ext(mime):
return {"image/png": "png",
"image/jpeg": "jpg",
"image/gif": "gif",
"application/octet-stream-xmp": "xmp",
"image/x-canon-cr2": "cr2",
"video/mp4": "mp4",
"video/quicktime": "mov"}[mime]
"""
Given a mime type return the canonical file extension
"""
return {"image/png": "png",
"image/jpeg": "jpg",
"image/gif": "gif",
"application/octet-stream-xmp": "xmp",
"image/x-canon-cr2": "cr2",
"video/mp4": "mp4",
"video/quicktime": "mov"}[mime]
def require_auth(func):
"""
Decorator: raise 403 unless session is authed
"""
def wrapped(*args, **kwargs):
if not auth():
raise cherrypy.HTTPError(403)
return func(*args, **kwargs)
return wrapped
def photo_auth_filter(query):
"""
Sqlalchemy helper: filter the given PhotoSet query to items that match the authorized user's PhotoStatus access
level. Currently, authed users can access ALL photos, and unauthed users can access only PhotoStatus.public
status items.
"""
return query.filter(PhotoSet.status == PhotoStatus.public) if not auth() else query
class PhotosWeb(object):
@ -57,11 +83,23 @@ class PhotosWeb(object):
Return a dict containing variables expected to be on every page
"""
s = self.session()
# all tags / albums with photos visible under the current auth context
tagq = s.query(Tag).join(TagItem).join(PhotoSet)
if not auth():
tagq = tagq.filter(PhotoSet.status == PhotoStatus.public)
tagq = tagq.order_by(Tag.title).all() # pragma: manual auth
albumq = s.query(Tag).join(TagItem).join(PhotoSet)
if not auth():
albumq = albumq.filter(PhotoSet.status == PhotoStatus.public)
albumq = albumq.filter(Tag.is_album == True).order_by(Tag.title).all() # pragma: manual auth
ret = {
"all_tags": s.query(Tag).order_by(Tag.title).all(),
"all_albums": s.query(Tag).filter(Tag.is_album == True).order_by(Tag.title).all(),
"all_tags": tagq,
"all_albums": albumq,
"path": cherrypy.request.path_info,
"auth": auth()
"auth": auth(),
"PhotoStatus": PhotoStatus
}
s.close()
return ret
@ -86,8 +124,9 @@ class PhotosWeb(object):
"""
s = self.session()
page, pgsize = int(page), int(pgsize)
total_sets = s.query(func.count(PhotoSet.id)).first()[0]
images = s.query(PhotoSet).order_by(PhotoSet.date.desc()).offset(pgsize * page).limit(pgsize).all()
total_sets = photo_auth_filter(s.query(func.count(PhotoSet.id))).first()[0]
images = photo_auth_filter(s.query(PhotoSet)).order_by(PhotoSet.date.desc()). \
offset(pgsize * page).limit(pgsize).all()
yield self.render("feed.html", images=[i for i in images], page=page, pgsize=int(pgsize), total_sets=total_sets)
@cherrypy.expose
@ -96,11 +135,11 @@ class PhotosWeb(object):
/stats - show server statistics
"""
s = self.session()
images = s.query(func.count(PhotoSet.uuid),
func.strftime('%Y', PhotoSet.date).label('year'),
func.strftime('%m', PhotoSet.date).label('month')). \
images = photo_auth_filter(s.query(func.count(PhotoSet.uuid),
func.strftime('%Y', PhotoSet.date).label('year'),
func.strftime('%m', PhotoSet.date).label('month'))). \
group_by('year', 'month').order_by(desc('year'), desc('month')).all()
tsize = s.query(func.sum(Photo.size)).scalar()
tsize = photo_auth_filter(s.query(func.sum(Photo.size)).join(PhotoSet)).scalar() # pragma: manual auth
yield self.render("monthly.html", images=images, tsize=tsize)
@cherrypy.expose
@ -110,12 +149,13 @@ class PhotosWeb(object):
TODO using so many coordinates is slow in the browser. dedupe them somehow.
"""
s = self.session()
query = s.query(PhotoSet).filter(PhotoSet.lat != 0, PhotoSet.lon != 0)
query = photo_auth_filter(s.query(PhotoSet)).filter(PhotoSet.lat != 0, PhotoSet.lon != 0)
if i:
query = query.filter(PhotoSet.uuid == i)
yield self.render("map.html", images=query.all(), zoom=int(zoom))
@cherrypy.expose
@require_auth
def create_tags(self, fromdate=None, uuid=None, tag=None, newtag=None, remove=None):
"""
/create_tags - tag multiple items selected by day of photo
@ -178,11 +218,12 @@ class PhotosWeb(object):
raise cherrypy.HTTPRedirect('/feed', 302)
@cherrypy.expose
def sess(self):
def logout(self):
"""
/sess - TODO DELETE ME dump session contents for debugging purposes
/login - enable super features by logging into the app
"""
yield cherrypy.session['authed']
cherrypy.session.clear()
raise cherrypy.HTTPRedirect('/feed', 302)
@cherrypy.popargs('date')
@ -201,20 +242,20 @@ class DateView(object):
pgsize = 100
dt = datetime.strptime(date, "%Y-%m-%d")
dt_end = dt + timedelta(days=1)
total_sets = s.query(func.count(PhotoSet.id)). \
total_sets = photo_auth_filter(s.query(func.count(PhotoSet.id))). \
filter(and_(PhotoSet.date >= dt, PhotoSet.date < dt_end)).first()[0]
images = s.query(PhotoSet).filter(and_(PhotoSet.date >= dt,
PhotoSet.date < dt_end)).order_by(PhotoSet.date). \
images = photo_auth_filter(s.query(PhotoSet)).filter(and_(PhotoSet.date >= dt,
PhotoSet.date < dt_end)).order_by(PhotoSet.date). \
offset(page * pgsize).limit(pgsize).all()
yield self.master.render("date.html", page=page, pgsize=pgsize, total_sets=total_sets,
images=[i for i in images], date=dt)
return
images = s.query(PhotoSet, func.strftime('%Y-%m-%d',
PhotoSet.date).label('gdate'),
func.count('photos.id'),
func.strftime('%Y', PhotoSet.date).label('year'),
func.strftime('%m', PhotoSet.date).label('month'),
func.strftime('%d', PhotoSet.date).label('day')). \
images = photo_auth_filter(s.query(PhotoSet, func.strftime('%Y-%m-%d',
PhotoSet.date).label('gdate'),
func.count('photos.id'),
func.strftime('%Y', PhotoSet.date).label('year'),
func.strftime('%m', PhotoSet.date).label('month'),
func.strftime('%d', PhotoSet.date).label('day'))). \
group_by('gdate').order_by(desc('year'), 'month', 'day').all()
yield self.master.render("dates.html", images=images)
@ -233,10 +274,12 @@ class ThumbnailView(object):
uuid = uuid.split(".")[0]
s = self.master.session()
query = s.query(Photo).filter(Photo.set.has(uuid=uuid)) if item_type == "set" \
else s.query(Photo).filter(Photo.uuid == uuid) if item_type == "one" \
query = photo_auth_filter(s.query(Photo)).filter(Photo.set.has(uuid=uuid)) if item_type == "set" \
else photo_auth_filter(s.query(Photo)).filter(Photo.uuid == uuid) if item_type == "one" \
else None
assert query
# prefer making thumbs from jpeg to avoid loading large raws
first = None
best = None
@ -248,7 +291,7 @@ class ThumbnailView(object):
break
thumb_from = best or first
if not thumb_from:
raise Exception("404") # TODO it right
raise Exception("404")
# TODO some lock around calls to this based on uuid
thumb_path = self.master.library.make_thumb(thumb_from, thumb_size)
if thumb_path:
@ -272,7 +315,7 @@ class DownloadView(object):
s = self.master.session()
query = None if item_type == "set" \
else s.query(Photo).filter(Photo.uuid == uuid) if item_type == "one" \
else photo_auth_filter(s.query(Photo)).filter(Photo.uuid == uuid) if item_type == "one" \
else None # TODO set download query
item = query.first()
@ -296,9 +339,21 @@ class PhotoView(object):
def index(self, uuid):
uuid = uuid.split(".")[0]
s = self.master.session()
photo = s.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
photo = photo_auth_filter(s.query(PhotoSet)).filter(PhotoSet.uuid == uuid).first()
yield self.master.render("photo.html", image=photo)
@cherrypy.expose
@require_auth
def op(self, uuid, op):
s = self.master.session()
photo = s.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
if op == "Make public":
photo.status = PhotoStatus.public
elif op == "Make private":
photo.status = PhotoStatus.private
s.commit()
raise cherrypy.HTTPRedirect('/photo/{}'.format(photo.uuid), 302)
@cherrypy.popargs('uuid')
class TagView(object):
@ -316,19 +371,22 @@ class TagView(object):
s = self.master.session()
if uuid == "untagged":
numphotos = s.query(func.count(PhotoSet.id)).filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).scalar()
photos = s.query(PhotoSet).filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).\
numphotos = photo_auth_filter(s.query(func.count(PhotoSet.id))). \
filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).scalar()
photos = photo_auth_filter(s.query(PhotoSet)).filter(~PhotoSet.id.in_(s.query(TagItem.set_id))).\
offset(page * pgsize).limit(pgsize).all()
yield self.master.render("untagged.html", images=photos, total_items=numphotos, pgsize=pgsize, page=page)
else:
tag = s.query(Tag).filter(Tag.uuid == uuid).first()
numphotos = s.query(func.count(Tag.id)).join(TagItem).join(PhotoSet).filter(Tag.uuid == uuid).scalar()
photos = s.query(PhotoSet).join(TagItem).join(Tag).filter(Tag.uuid == uuid). \
numphotos = photo_auth_filter(s.query(func.count(Tag.id)).join(TagItem).join(PhotoSet)). \
filter(Tag.uuid == uuid).scalar()
photos = photo_auth_filter(s.query(PhotoSet)).join(TagItem).join(Tag).filter(Tag.uuid == uuid). \
order_by(PhotoSet.date.desc()).offset(page * pgsize).limit(pgsize).all()
yield self.master.render("album.html", tag=tag, images=photos,
total_items=numphotos, pgsize=pgsize, page=page)
@cherrypy.expose
@require_auth
def op(self, uuid, op):
s = self.master.session()
tag = s.query(Tag).filter(Tag.uuid == uuid).first()

View File

@ -22,19 +22,29 @@ a {
position: relative;
background: rgb(37, 42, 58);
text-align: center;
}
/* show the "Menu" button on phones */
#nav .nav-menu-button {
display: block;
top: 0.5em;
right: 0.5em;
position: absolute;
.user-status {
padding: 25px 0px;
color: #fff;
span {
font-family: monospace;
}
}
/* show the "Menu" button on phones */
.nav-menu-button {
display: block;
top: 0.5em;
right: 0.5em;
position: absolute;
}
&.active {
height: 80%;
}
}
/* when "Menu" is clicked, the navbar should be 80% height */
#nav.active {
height: 80%;
}
/* don't show the navigation items until the "Menu" button is clicked */
.nav-inner {
display: none;
@ -45,24 +55,28 @@ a {
}
#nav .pure-menu {
background: transparent;
border: none;
text-align: left;
}
#nav .pure-menu-link:hover,
#nav .pure-menu-link:focus {
#nav {
.pure-menu {
background: transparent;
border: none;
text-align: left;
}
.pure-menu-link:hover,
.pure-menu-link:focus {
background: rgb(55, 60, 90);
}
#nav .pure-menu-link {
.pure-menu-link {
color: #fff;
margin-left: 0.5em;
}
#nav .pure-menu-heading {
.pure-menu-heading {
border-bottom: none;
font-size:110%;
color: rgb(75, 113, 151);
}
}
.tag-icon {
width: 15px;

View File

@ -16,7 +16,7 @@
<li class="pure-menu-item"><a href="/" class="pure-menu-link">All photos</a></li>
<li class="pure-menu-item"><a href="/albums" class="pure-menu-link">Albums</a></li>
<li class="pure-menu-item"><a href="/date" class="pure-menu-link">Dates</a></li>
<li class="pure-menu-item"><a href="/monthly" class="pure-menu-link">Stats</a></li>
<li class="pure-menu-item"><a href="/stats" class="pure-menu-link">Stats</a></li>
<li class="pure-menu-item"><a href="/map" class="pure-menu-link">Map</a></li>
<li class="pure-menu-item"><a href="/tag/untagged" class="pure-menu-link">Untagged</a></li>
<li class="pure-menu-item"><a href="/admin/trash" class="pure-menu-link">Trash</a></li>
@ -30,6 +30,15 @@
{% endfor %}
</ul>
</div>
<div class="user-status">
{% if auth %}
<p>Authed as <span>{{ auth }}</span></p>
<p><a href="/logout">Log out</a></p>
{% else %}
<p>Browsing as a guest</p>
<p><a href="/login">Log in</a></p>
{% endif %}
</div>
</div>
</div>
<div id="main" class="pure-u-1">
@ -41,13 +50,15 @@
{{ subtitle }}
</p>
</div>
<div class="email-content-controls pure-u-1-2">
{% block buttons %}
<button class="secondary-button pure-button">420</button>
<button class="secondary-button pure-button">Blaze</button>
<button class="secondary-button pure-button">It</button>
{% endblock %}
</div>
{% if auth %}
<div class="email-content-controls pure-u-1-2">
{% block buttons %}
<button class="secondary-button pure-button">420</button>
<button class="secondary-button pure-button">Blaze</button>
<button class="secondary-button pure-button">It</button>
{% endblock %}
</div>
{% endif %}
</div>
<div class="email-content-body">
{% block body %}default body{% endblock %}

View File

@ -3,7 +3,13 @@
{% set subtitle = image.uuid %}
{% block buttons %}
xxx
<form action="/photo/{{ image.uuid }}/op" method="post">
{% if image.status == PhotoStatus.private %}
<input type="submit" class="secondary-button pure-button" name="op" value="Make public" />
{% else %}
<input type="submit" class="secondary-button pure-button" name="op" value="Make private" />
{% endif %}
</form>
{% endblock %}
{% block body %}