To see the code we wrote in the previous article in action, we'll implement complementary frontend pages in this article. Most of the code in this article is similar to what we did in registration and token validation.
Source code
The source code for this series is hosted on GitHub via:
As usual, this is just mostly HTML with Svelte's syntactic sugar. If you view this page in the browser, it looks just like this:
It also has a corresponding +page.server.js which handles the form submission:
// routes/auth/login/+page.server.jsimport{BASE_API_URI}from'$lib/utils/constants';import{formatError}from'$lib/utils/helpers';import{fail,redirect}from'@sveltejs/kit';/** @type {import('./$types').PageServerLoad} */exportasyncfunctionload({locals}){// redirect user if logged inif (locals.user){throwredirect(302,'/');}}/** @type {import('./$types').Actions} */exportconstactions={/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @returns Error data or redirects user to the home page or the previous page
*/login:async ({request,fetch,cookies})=>{constdata=awaitrequest.formData();constemail=String(data.get('email'));constpassword=String(data.get('password'));constnext=String(data.get('next'));/** @type {RequestInit} */constrequestInitOptions={method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:email,password:password})};constres=awaitfetch(`${BASE_API_URI}/users/login/`,requestInitOptions);if (!res.ok){constresponse=awaitres.json();consterrors=formatError(response.error);returnfail(400,{errors:errors});}if (res.headers.has('Set-Cookie')){constsessionID=Object.fromEntries(res.headers)['set-cookie'].split(';')[0].split(/=(.*)/s)[1];constpath=Object.fromEntries(res.headers)['set-cookie'].split(';')[1].split('=')[1];constmaxAge=Number(Object.fromEntries(res.headers)['set-cookie'].split(';')[2].split('=')[1]);cookies.set('go-auth-sessionid',sessionID,{httpOnly:true,sameSite:'lax',path:path,secure:true,maxAge:maxAge});}throwredirect(303,next||'/');}};
In the load() function, we bar authenticated users from accessing the page since they are already logged in. Then, the named form action. It's pretty similar to what we did before. The major difference is how we extracted the session token from the response header and set the cookie in the browser:
This saved cookie is then used in frontend/src/hooks.server.js to fetch the currently logged user:
// frontend/src/hooks.server.jsimport{BASE_API_URI}from'$lib/utils/constants';/** @type {import('@sveltejs/kit').Handle} */exportasyncfunctionhandle({event,resolve}){if (event.locals.user){// if there is already a user in session load page as normalreturnawaitresolve(event);}// get cookies from browserconstsession=event.cookies.get('go-auth-sessionid');if (!session){// if there is no session load page as normalreturnawaitresolve(event);}// find the user based on the sessionconstres=awaitevent.fetch(`${BASE_API_URI}/users/current-user/`,{credentials:'include',headers:{Cookie:`sessionid=${session}`}});if (!res.ok){// if there is no session load page as normalreturnawaitresolve(event);}// if `user` exists set `events.local`constresponse=awaitres.json();event.locals.user=response;if (event.locals.user.profile.birth_date){event.locals.user.profile.birth_date=response['profile']['birth_date'].split('T')[0];}// load page as normalreturnawaitresolve(event);}
handle hook is useful in achieving this thereby ensuring that our application's users remain logged in as long as their cookies are still valid. Immediately such cookies expire or get invalidated, users must re-login. A user whose cookie is valid gets access to data via the /users/current-user/ endpoint, one of the handlers implemented in the last article. Also, note that you need to send the cookie with the request for it to succeed.
Having retrieved the logged-in user, we need to make sure all other parts of our application have access it to. To do that, we will create a +layour.server.js and from there propagate the data:
Now, every page can access the data via the data.user object of the page store. We will use it next.
Step 2: Logout logic
To log users out, we will also stick to web standards by using the form tag for such an action. Our frontend/src/lib/components/Header.svelte should now look like this:
If you check the action, we are referencing a route, /auth/logout. The logout route. This route is non-existent yet. We will create it. However, this route will not have a +page.svelte file because we don't have what to show there. However, it will have a +page.server.js because that is what will send our data to the backend. Since we used the route as our action, then the form action will be unnamed or default. If the action were /auth/logout?/logout, then the form action wouldn't be the default:
// frontend/src/routes/auth/logout/+page.server.jsimport{BASE_API_URI}from'$lib/utils/constants';import{fail,redirect}from'@sveltejs/kit';/** @type {import('./$types').PageServerLoad} */exportasyncfunctionload({locals}){// redirect user if not logged inif (!locals.user){throwredirect(302,`/auth/login?next=/auth/logout`);}}/** @type {import('./$types').Actions} */exportconstactions={default:async ({fetch,cookies})=>{/** @type {RequestInit} */constrequestInitOptions={method:'POST',credentials:'include',headers:{'Content-Type':'application/json',Cookie:`sessionid=${cookies.get('go-auth-sessionid')}`}};constres=awaitfetch(`${BASE_API_URI}/users/logout/`,requestInitOptions);if (!res.ok){constresponse=awaitres.json();consterrors=[];errors.push({error:response.error,id:0});returnfail(400,{errors:errors});}// eat the cookiecookies.delete('go-auth-sessionid',{path:'/'});// redirect the userthrowredirect(302,'/auth/login');}};
Here, we only sent the cookie to the server. With that, the server knows who wants to log out. Then, it deletes such a cookie from the server and on a successful response, we also delete it in the browser.
With that, we will end it here. The next article will be about token regeneration and password reset functionalities. See you!
Outro
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, health care, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn: LinkedIn and Twitter: Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!