Skip to content

Commit 9fe6301

Browse files
authored
Finalize user log-in flow (#546)
Closes #489 by revamping the log in screen.
1 parent dbd8aa4 commit 9fe6301

4 files changed

Lines changed: 70 additions & 27 deletions

File tree

pingpong/auth.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ def encode_auth_token(
8282
)
8383

8484

85+
class TimeException(Exception):
86+
def __init__(self, detail: str = "", user_id: str = ""):
87+
self.user_id = user_id
88+
self.detail = detail
89+
90+
8591
def decode_auth_token(token: str, nowfn: NowFn = utcnow) -> AuthToken:
8692
"""Decodes the Auth Token.
8793
@@ -95,7 +101,7 @@ def decode_auth_token(token: str, nowfn: NowFn = utcnow) -> AuthToken:
95101
Raises:
96102
jwt.exceptions.PyJWTError when token is not valid
97103
"""
98-
exc: PyJWTError | None = None
104+
exc: PyJWTError | TimeException | None = None
99105

100106
for secret in config.auth.secret_keys:
101107
try:
@@ -115,15 +121,15 @@ def decode_auth_token(token: str, nowfn: NowFn = utcnow) -> AuthToken:
115121
now = nowfn().timestamp()
116122
nbf = getattr(tok, "nbf", None)
117123
if nbf is not None and now < nbf:
118-
raise PyJWTError("Token not valid yet")
124+
raise TimeException(detail="Token not valid yet", user_id=tok.sub)
119125

120126
exp = getattr(tok, "exp", None)
121127
if exp is not None and now > exp:
122-
raise PyJWTError("Token expired")
128+
raise TimeException(detail="Token expired", user_id=tok.sub)
123129

124130
return tok
125131

126-
except PyJWTError as e:
132+
except (TimeException, PyJWTError) as e:
127133
exc = e
128134
continue
129135

pingpong/server.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import pingpong.metrics as metrics
2929
import pingpong.models as models
3030
import pingpong.schemas as schemas
31-
from .auth import authn_method_for_email
31+
from .auth import TimeException, authn_method_for_email
3232
from .template import email_template as message_template
3333
from .time import convert_seconds
3434
from .saml import get_saml2_client, get_saml2_settings, get_saml2_attrs
@@ -122,8 +122,7 @@ async def parse_session_token(request: Request, call_next):
122122
user_id = int(token.sub)
123123
user = await models.User.get_by_id(request.state.db, user_id)
124124
if not user:
125-
raise ValueError("User does not exist")
126-
125+
raise ValueError("We couldn't locate your account.")
127126
# Modify user state if necessary
128127
if user.state == schemas.UserState.UNVERIFIED:
129128
await user.verify(request.state.db)
@@ -135,6 +134,11 @@ async def parse_session_token(request: Request, call_next):
135134
user=user,
136135
profile=schemas.Profile.from_email(user.email),
137136
)
137+
except TimeException as e:
138+
request.state.session = schemas.SessionState(
139+
status=schemas.SessionStatus.INVALID,
140+
error=e.detail,
141+
)
138142
except PyJWTError as e:
139143
request.state.session = schemas.SessionState(
140144
status=schemas.SessionStatus.INVALID,
@@ -506,6 +510,17 @@ async def auth(request: Request):
506510
auth_token = decode_auth_token(stok, nowfn=nowfn)
507511
except jwt.exceptions.PyJWTError as e:
508512
raise HTTPException(status_code=401, detail=str(e))
513+
except TimeException as e:
514+
user = await models.User.get_by_id(request.state.db, int(e.user_id))
515+
forward = request.query_params.get("redirect", "/")
516+
if user and user.email:
517+
await login_magic(
518+
schemas.MagicLoginRequest(email=user.email, forward=forward), request
519+
)
520+
return RedirectResponse("/login/?new_link=true", status_code=303)
521+
return RedirectResponse(
522+
f"/login/?expired=true&forward={forward}", status_code=303
523+
)
509524
except Exception as e:
510525
raise HTTPException(status_code=500, detail=str(e))
511526

pingpong/test_server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def test_me_with_valid_token_but_missing_user(api, now):
6565
)
6666
assert response.status_code == 200
6767
assert response.json() == {
68-
"error": "User does not exist",
68+
"error": "We couldn't locate your account.",
6969
"profile": None,
7070
"status": "error",
7171
"token": None,
@@ -160,9 +160,9 @@ async def test_auth_with_invalid_token(api):
160160

161161
async def test_auth_with_expired_token(api, now):
162162
expired_token = encode_session_token(123, nowfn=offset(now, seconds=-100_000))
163-
response = api.get(f"/api/v1/auth?token={expired_token}")
164-
assert response.status_code == 401
165-
assert response.json() == {"detail": "Token expired"}
163+
response = api.get(f"/api/v1/auth?token={expired_token}", allow_redirects=False)
164+
assert response.status_code == 303
165+
assert response.headers["location"] == "/login/?expired=true&forward=/"
166166

167167

168168
@with_user(123, "foo@bar.com")

web/pingpong/src/routes/login/+page.svelte

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import PingPongLogo from '$lib/components/PingPongLogo.svelte';
3-
import { Button, P, InputAddon, Input, Helper, Heading, ButtonGroup } from 'flowbite-svelte';
3+
import { Button, InputAddon, Input, Heading, ButtonGroup } from 'flowbite-svelte';
44
import { EnvelopeSolid } from 'flowbite-svelte-icons';
55
import { writable } from 'svelte/store';
66
import { fail } from '@sveltejs/kit';
@@ -10,6 +10,8 @@
1010
1111
export let form;
1212
const forward = $page.url.searchParams.get('forward') || '/';
13+
const expired = $page.url.searchParams.get('expired') === 'true' || false;
14+
const new_link = $page.url.searchParams.get('new_link') === 'true' || false;
1315
const loggingIn = writable(false);
1416
const success = writable(false);
1517
const loginWithMagicLink = async (evt: SubmitEvent) => {
@@ -41,13 +43,43 @@
4143
<header class="bg-blue-dark-40 px-5 md:px-12 py-8">
4244
<Heading tag="h1" class="logo w-full text-center"><PingPongLogo size="full" /></Heading>
4345
</header>
44-
<div class="px-5 md:px-12 py-16 bg-white">
46+
<div class="px-5 md:px-12 pb-16 pt-10 bg-white">
4547
{#if $success}
46-
<div class="text-orange">Success! Follow the link in your email to finish signing in.</div>
48+
<div class="text-4xl text-center font-serif font-bold mt-5 mb-2 text-blue-dark-50">
49+
Success!
50+
</div>
51+
<div class="text-lg text-center">Follow the link in your email to finish signing in.</div>
52+
{:else if new_link}
53+
<div class="text-4xl text-center font-serif font-bold mt-5 mb-4 text-blue-dark-50">
54+
Let's try this again.
55+
</div>
56+
<div class="text-lg text-center">
57+
This log-in link isn't currently valid.<br />We sent a new link to your email.
58+
</div>
4759
{:else}
60+
<div class="mb-6">
61+
{#if expired}
62+
<div class="text-4xl text-center font-serif font-bold mb-2 text-blue-dark-50">
63+
Let's try this again.
64+
</div>
65+
<div class="text-lg text-center">
66+
This log-in link isn't currently valid.<br />Try logging in with your school email
67+
address again.
68+
</div>
69+
{:else}
70+
<div class="text-4xl text-center font-serif font-bold mb-2 text-blue-dark-50">
71+
{form?.error ? 'We could not sign you in.' : 'Welcome to PingPong'}
72+
</div>
73+
<div class="text-lg text-center">
74+
{form?.error
75+
? 'Please make sure you are using the correct email address and try again.'
76+
: 'Use your school email address to log in.'}
77+
</div>
78+
{/if}
79+
</div>
4880
<form on:submit={loginWithMagicLink}>
4981
<ButtonGroup class="w-full rounded-full bg-blue-light-50 shadow-inner p-4">
50-
<InputAddon class="rounded-none border-none bg-transparent text-blue-light-40">
82+
<InputAddon class="rounded-none border-none bg-transparent text-blue-dark-30">
5183
<EnvelopeSolid />
5284
</InputAddon>
5385
<Input
@@ -57,25 +89,15 @@
5789
placeholder="you@school.edu"
5890
name="email"
5991
id="email"
60-
class="bg-transparent border-none"
92+
class="bg-transparent border-none text-md"
6193
></Input>
6294
<Button
6395
pill
64-
class="p-3 px-6 mr-2 rounded-full bg-orange text-white hover:bg-orange-dark"
96+
class="p-3 px-6 mr-2 rounded-full bg-orange-dark hover:bg-orange text-white text-md py-2 px-4"
6597
type="submit"
6698
disabled={$loggingIn}>Login</Button
6799
>
68100
</ButtonGroup>
69-
{#if form?.error}
70-
<div class="p-2">
71-
<P class="text-orange">We could not sign you in.</P>
72-
<P class="text-orange"
73-
>Please make sure you are using the correct email address and try again.
74-
</P>
75-
</div>
76-
{:else}
77-
<Helper class="my-2 text-black text-sm">Log in with your school email address.</Helper>
78-
{/if}
79101
</form>
80102
{/if}
81103
</div>

0 commit comments

Comments
 (0)