From 0466dca275d01b659f977e5471c8ded34cabf9ab Mon Sep 17 00:00:00 2001 From: cyfraeviolae Date: Wed, 3 Apr 2024 13:54:40 -0400 Subject: errors --- app.py | 93 ++++++++++++++++++++++++++++++++-------------------- templates/error.html | 27 +++++++++++++++ 2 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 templates/error.html 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 @@ + + + + Symposium + + + + + + + +
+
+ +
+ +
+

Error.

+ + {{ error }} +
+ + -- cgit v1.2.3