basic auth for endpoints
This commit is contained in:
parent
346f0a7944
commit
3035d117b5
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user