aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/app.py4
-rw-r--r--app/app_account.py39
-rw-r--r--app/app_sessions.py27
-rw-r--r--app/app_templating.py51
-rw-r--r--app/app_wallet.py46
-rw-r--r--flake.nix2
-rw-r--r--readme.md10
-rw-r--r--static/images/favicon.svg93
-rw-r--r--static/stylesheets/grids-responsive-min.css7
-rw-r--r--static/stylesheets/laundry.css139
-rw-r--r--static/stylesheets/main.css195
-rw-r--r--static/stylesheets/pepal.css40
-rw-r--r--static/stylesheets/pure-min.css11
-rw-r--r--templates/_base.html.jinja71
-rw-r--r--templates/_fragments.html.jinja34
-rw-r--r--templates/homepage.html.jinja95
-rw-r--r--templates/launder.html.jinja38
-rw-r--r--templates/wallet.html.jinja140
18 files changed, 1016 insertions, 26 deletions
diff --git a/app/app.py b/app/app.py
index 3bf0337..b22b3cd 100644
--- a/app/app.py
+++ b/app/app.py
@@ -3,6 +3,7 @@
3# Licence: EUPL-1.2 3# Licence: EUPL-1.2
4 4
5from fastapi import FastAPI, status 5from fastapi import FastAPI, status
6from fastapi.staticfiles import StaticFiles
6import app_sessions 7import app_sessions
7import app_account 8import app_account
8import app_wallet 9import app_wallet
@@ -15,3 +16,6 @@ main.add_middleware(app_sessions.SessionManager)
15# Register our request handlers 16# Register our request handlers
16main.include_router(app_account.router) 17main.include_router(app_account.router)
17main.include_router(app_wallet.router) 18main.include_router(app_wallet.router)
19
20# Handler for static resource files (CSS, JS, ...)
21main.mount('/', StaticFiles(directory='./static/'), name='static')
diff --git a/app/app_account.py b/app/app_account.py
index 3f4869d..e3d6433 100644
--- a/app/app_account.py
+++ b/app/app_account.py
@@ -3,6 +3,7 @@
3# Licence: EUPL-1.2 3# Licence: EUPL-1.2
4 4
5from fastapi import APIRouter, Depends, Request, Form, status 5from fastapi import APIRouter, Depends, Request, Form, status
6from fastapi.responses import RedirectResponse, HTMLResponse
6 7
7from passlib.context import CryptContext 8from passlib.context import CryptContext
8import re 9import re
@@ -10,8 +11,9 @@ import re
10from embrace.exceptions import IntegrityError 11from embrace.exceptions import IntegrityError
11from psycopg2.errors import UniqueViolation 12from psycopg2.errors import UniqueViolation
12 13
13from app_sessions import UserSession 14from app_sessions import UserSession, FlashMessageQueue
14from app_database import db_transaction 15from app_database import db_transaction
16from app_templating import TemplateRenderer
15 17
16 18
17# Password hashing context. 19# Password hashing context.
@@ -20,42 +22,51 @@ password_ctx = CryptContext(schemes=['bcrypt'], deprecated='auto')
20 22
21username_pattern = re.compile(r'^[a-zA-Z0-9-_]{4,16}$') 23username_pattern = re.compile(r'^[a-zA-Z0-9-_]{4,16}$')
22 24
25to_homepage = RedirectResponse('/', status_code=status.HTTP_303_SEE_OTHER)
26to_wallet = RedirectResponse('/wallet', status_code=status.HTTP_303_SEE_OTHER)
27
23router = APIRouter() 28router = APIRouter()
24 29
25 30
26@router.get('/') 31@router.get('/', response_class=HTMLResponse)
27def homepage( 32def homepage(
28 session: UserSession=Depends(UserSession), 33 session: UserSession=Depends(UserSession),
34 render: TemplateRenderer=Depends(TemplateRenderer),
29): 35):
30 if session.is_logged_in(): 36 if session.is_logged_in():
31 return 'Welcome!' 37 return to_wallet
32 38
33 return 'Homepage here.' 39 return render('homepage.html.jinja')
34 40
35 41
36@router.post('/account/register') 42@router.post('/account/register')
37def account_register( 43def account_register(
38 session: UserSession=Depends(UserSession), 44 session: UserSession=Depends(UserSession),
45 messages: FlashMessageQueue=Depends(FlashMessageQueue),
39 username: str=Form(...), 46 username: str=Form(...),
40 password: str=Form(...), 47 password: str=Form(...),
41): 48):
42 try: 49 try:
43 if username_pattern.match(username) is None: 50 if username_pattern.match(username) is None:
44 return 'error: Invalid username format.' 51 messages.add('error', 'Invalid username format.')
52 return to_homepage
45 53
46 if not 4 <= len(password) <= 32: 54 if not 4 <= len(password) <= 32:
47 return 'error: Invalid password length.' 55 messages.add('error', 'Invalid password length.')
56 return to_homepage
48 57
49 hash = password_ctx.hash(password) 58 hash = password_ctx.hash(password)
50 with db_transaction() as tx: 59 with db_transaction() as tx:
51 user = tx.create_account(username=username, password_hash=hash) 60 user = tx.create_account(username=username, password_hash=hash)
52 61
53 session.login(user.id) 62 session.login(user.id)
54 return 'Account succesfully created. Welcome!' 63 messages.add('success', 'Account succesfully created. Welcome!')
64 return to_wallet
55 65
56 except IntegrityError as exception: 66 except IntegrityError as exception:
57 if isinstance(exception.__cause__, UniqueViolation): 67 if isinstance(exception.__cause__, UniqueViolation):
58 return 'error: This username is already taken.' 68 messages.add('error', 'This username is already taken.')
69 return to_homepage
59 else: 70 else:
60 raise exception 71 raise exception
61 72
@@ -63,6 +74,7 @@ def account_register(
63@router.post('/account/login') 74@router.post('/account/login')
64def session_login( 75def session_login(
65 session: UserSession=Depends(UserSession), 76 session: UserSession=Depends(UserSession),
77 messages: FlashMessageQueue=Depends(FlashMessageQueue),
66 username: str=Form(...), 78 username: str=Form(...),
67 password: str=Form(...), 79 password: str=Form(...),
68): 80):
@@ -71,17 +83,20 @@ def session_login(
71 83
72 if user is not None and password_ctx.verify(password, user.password_hash): 84 if user is not None and password_ctx.verify(password, user.password_hash):
73 session.login(user.id) 85 session.login(user.id)
74 return 'Welcome back!' 86 messages.add('info', 'Welcome back!')
87 return to_wallet
75 else: 88 else:
76 return 'error: Invalid credentials.' 89 messages.add('error', 'Invalid credentials.')
90 return to_homepage
77 91
78 92
79@router.post('/account/logout') 93@router.post('/account/logout')
80def session_logout( 94def session_logout(
81 session: UserSession=Depends(UserSession), 95 session: UserSession=Depends(UserSession),
96 messages: FlashMessageQueue=Depends(FlashMessageQueue),
82): 97):
83 if session.is_logged_in(): 98 if session.is_logged_in():
84 session.logout() 99 session.logout()
85 return 'You have been successfully logged out.' 100 messages.add('info', 'You have been successfully logged out.')
86 101
87 return 'Nothing to do' 102 return to_homepage
diff --git a/app/app_sessions.py b/app/app_sessions.py
index 89521fb..7a931d5 100644
--- a/app/app_sessions.py
+++ b/app/app_sessions.py
@@ -15,6 +15,33 @@ cookie_key = environ['COOKIE_SECRET_KEY']
15SessionManager = partial(SessionMiddleware, secret_key=cookie_key) 15SessionManager = partial(SessionMiddleware, secret_key=cookie_key)
16 16
17 17
18class FlashMessageQueue:
19 """
20 Session decorator for managing session flash messages to be displayed to
21 the user from one page to another. This suits confirmation and error
22 messages. Messages are stored in the session cookie, which is limited in
23 size to about 4kb.
24 """
25
26 def __init__(self, request: Request):
27 if 'messages' not in request.session:
28 request.session['messages'] = []
29
30 self._messages = request.session['messages']
31
32 def add(self, class_: str, message: str):
33 self._messages.append((class_, message))
34
35 def __iter__(self):
36 return self
37
38 def __next__(self):
39 if not self._messages:
40 raise StopIteration
41
42 return self._messages.pop(0)
43
44
18class UserSession: 45class UserSession:
19 """ 46 """
20 Session decorator for managing user login sessions. 47 Session decorator for managing user login sessions.
diff --git a/app/app_templating.py b/app/app_templating.py
new file mode 100644
index 0000000..427170b
--- /dev/null
+++ b/app/app_templating.py
@@ -0,0 +1,51 @@
1# UGE / L2 / Intro to relational databases / Python project prototype
2# Author: Pacien TRAN-GIRARD
3# Licence: EUPL-1.2
4
5from typing import Optional, NamedTuple
6from decimal import Decimal
7
8from fastapi import Depends, Request
9from fastapi.templating import Jinja2Templates
10
11from app_sessions import UserSession, FlashMessageQueue