summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcyfraeviolae <cyfraeviolae>2024-04-03 13:54:40 -0400
committercyfraeviolae <cyfraeviolae>2024-04-03 13:54:40 -0400
commit0466dca275d01b659f977e5471c8ded34cabf9ab (patch)
tree5edd89bf32f24ac1da66352323fc9bf78c04c594
parentaaf5ab84eb155a0f76719a2d5253272c5053d247 (diff)
errors
-rw-r--r--app.py93
-rw-r--r--templates/error.html27
2 files changed, 84 insertions, 36 deletions
diff --git a/app.py b/app.py
index b7a0516..f5d0b85 100644
--- a/app.py
+++ b/app.py
@@ -20,7 +20,7 @@ from sqlalchemy.dialects.postgresql import ARRAY
from litestar import Litestar, get, post, Request
from litestar.contrib.jinja import JinjaTemplateEngine
-from litestar.exceptions import HTTPException
+from litestar.exceptions import HTTPException, ValidationException, NotFoundException, NotAuthorizedException
from litestar.template.config import TemplateConfig
from litestar.response import Template, Redirect
from litestar.response.file import ASGIFileResponse
@@ -33,9 +33,6 @@ from litestar.datastructures import State
import ics
-# use url_fors, timezones?
-# error handling, auth errors, sql errors, input validation
-
class Base(DeclarativeBase):
pass
@@ -107,32 +104,49 @@ parts = [
def gen_iden():
return "-".join(random.choices(parts, k=8))
+def error(msg) -> Template:
+ return Template(template_name="error.html", context=dict(error=msg))
+
+def auth_error() -> Template:
+ return error("Unauthorized.")
+
+def check_auth(password, event):
+ return password and hmac.compare_digest(event.password, password)
+
@get("/")
async def index() -> Template:
return Template(template_name="index.html")
-@get("/event/{iden:str}")
+@get("/event/{iden:str}", name="event")
async def event(state: State, iden: str, password: str = "") -> Template:
async with sessionmaker(bind=state.engine) as session:
async with session.begin():
query = select(Event).where(Event.iden == iden)
result = await session.execute(query)
- event = result.scalar_one()
+ try:
+ event = result.scalar_one()
+ except NoResultFound:
+ return error("Not found.")
manage = False
- if password and hmac.compare_digest(event.password, password):
+ if password:
+ if not check_auth(password, event):
+ return auth_error()
manage = True
context = dict(event=event, manage=manage)
return Template(template_name="event.html", context=context)
-@get("/calendar/{iden:str}")
+@get("/event/{iden:str}/calendar")
async def calendar(state: State, iden: str) -> ASGIStreamingResponse:
async with sessionmaker(bind=state.engine) as session:
async with session.begin():
query = select(Event).where(Event.iden == iden)
result = await session.execute(query)
- event = result.scalar_one()
+ try:
+ event = result.scalar_one()
+ except NoResultFound:
+ return error("Not found.")
ics = event.to_ics()
f = io.StringIO(ics)
return ASGIStreamingResponse(iterator=f, media_type='text/plain', headers={
@@ -147,84 +161,91 @@ class EditRequest:
what: str
@post("/event/create")
-async def create(state: State, data: Annotated[EditRequest, Body(media_type=RequestEncodingType.URL_ENCODED)]) -> Redirect:
+async def create(request: Request, state: State, data: Annotated[EditRequest, Body(media_type=RequestEncodingType.URL_ENCODED)]) -> Redirect:
iden = gen_iden()
password = secrets.token_bytes(16).hex()
event = Event(iden, password, data.title, datetime.datetime.fromisoformat(data.when), data.where, data.what)
async with sessionmaker(bind=state.engine) as session:
- async with session.begin():
- session.add(event)
+ try:
+ async with session.begin():
+ session.add(event)
+ except IntegrityError:
+ return error("Iden already exists.")
- return Redirect(path="/symposium/event/" + iden + "?password=" + event.password)
+ return Redirect("/symposium" + request.app.route_reverse('event', iden=iden) + "?password=" + event.password)
@post("/event/{iden:str}/edit")
-async def edit(state: State, request: Request, iden: str, password: str, data: Annotated[EditRequest, Body(media_type=RequestEncodingType.URL_ENCODED)]) -> Redirect: #-> Template:
+async def edit(state: State, request: Request, iden: str, password: str, data: Annotated[EditRequest, Body(media_type=RequestEncodingType.URL_ENCODED)]) -> Redirect:
async with sessionmaker(bind=state.engine) as session:
async with session.begin():
query = select(Event).where(Event.iden == iden)
result = await session.execute(query)
- event = result.scalar_one()
+ try:
+ event = result.scalar_one()
+ except NoResultFound:
+ return error("Not found.")
- manage = False
- if password and hmac.compare_digest(event.password, password):
- manage = True
- if not manage:
- raise ValueError("no auth")
+ if not check_auth(password, event):
+ return auth_error()
event.title = data.title
event.time = datetime.datetime.fromisoformat(data.when)
event.location = data.where
event.description = data.what
- return Redirect(path="/symposium/event/" + iden + "?password=" + event.password)
+ return Redirect("/symposium" + request.app.route_reverse('event', iden=iden) + "?password=" + event.password)
@dataclass
class RemoveRequest:
name: str
@post("/event/{iden:str}/remove")
-async def remove(state: State, request: Request, iden: str, data: Annotated[RemoveRequest, Body(media_type=RequestEncodingType.URL_ENCODED)], password: str = "") -> Redirect: #-> Template:
+async def remove(state: State, request: Request, iden: str, data: Annotated[RemoveRequest, Body(media_type=RequestEncodingType.URL_ENCODED)], password: str = "") -> Redirect:
async with sessionmaker(bind=state.engine) as session:
async with session.begin():
query = select(Event).where(Event.iden == iden)
result = await session.execute(query)
- event = result.scalar_one()
+ try:
+ event = result.scalar_one()
+ except NoResultFound:
+ return error("Not found.")
- manage = False
- if password and hmac.compare_digest(event.password, password):
- manage = True
- if not manage:
- raise ValueError("no auth")
+ if not check_auth(password, event):
+ return auth_error()
name = data.name
invites = json.loads(event.invites)
if name in invites:
invites.remove(name)
event.invites = json.dumps(invites)
- url = "/symposium/event/" + iden
- if password:
- url += "?password=" + password
- return Redirect(path=url)
+ return Redirect("/symposium" + request.app.route_reverse('event', iden=iden) + "?password=" + event.password)
@dataclass
class JoinRequest:
name: str
@post("/event/{iden:str}/join")
-async def join(state: State, request: Request, iden: str, data: Annotated[JoinRequest, Body(media_type=RequestEncodingType.URL_ENCODED)], password: str = "") -> Redirect: #-> Template:
+async def join(state: State, request: Request, iden: str, data: Annotated[JoinRequest, Body(media_type=RequestEncodingType.URL_ENCODED)], password: str = "") -> Redirect:
async with sessionmaker(bind=state.engine) as session:
async with session.begin():
query = select(Event).where(Event.iden == iden)
result = await session.execute(query)
- event = result.scalar_one()
+ try:
+ event = result.scalar_one()
+ except NoResultFound:
+ return error("Not found.")
+
name = data.name
+ if len(name) > 50:
+ return error("Name too long.")
invites = json.loads(event.invites)
if name in invites:
- raise ValueError("already exists")
+ return error("Name already exists in event.")
invites.append(name)
event.invites = json.dumps(invites)
- url = "/symposium/event/" + iden
+
+ url = "/symposium" + request.app.route_reverse('event', iden=iden)
if password:
url += "?password=" + password
return Redirect(path=url)
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 0000000..0617a20
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Symposium</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" type="text/css" href="/static/styles.css">
+ <link rel="stylesheet" type="text/css" href="/symposium/static/styles.css">
+ <link rel="shortcut icon" type="image/x-icon" href="/symposium/static/favicon.ico">
+ </head>
+ <body>
+ <div class="container">
+ <div>
+ <div class="home">
+ <a href="/symposium" class="home-title">Symposium</a>
+ <span> at </span><a class="site" href="/">cyfraeviolae.org</a>
+ <a class="source" href="/git/symposium">[src]</a>
+ </div>
+ </div>
+
+ <br>
+ <h3>Error.</h3>
+
+ {{ error }}
+ </div>
+ </body>
+</html>