We are going to go over some steps to put Login functionality into our Astro website.
Pieces needed that we will cover:
auth.html
to grab user information from Discord/logout
astro pageYou can create a new Discord App by logging into discord.com/developers and going to the Applications section.
I created app Astronaut
Go into the OAuth2
> General
section
You will need to enter 2 urls that Discord will verify as white-listed
These are urls verified by Discord so that only you can use this mechanism from allowed sites.
In our case we are setting up 2 so that we can use it from development (locally) and once deployed.
This is all you need for the Discord App.
auth.html
is a static html page that takes a successful discord login and stores information about the user into the browser’s localStorage
. This information can then be used by our site to know who our visitor is.
As you may have noticed in the app redirects, I am pointing to /auth.html
, we will create this file in /public/auth.html
Here is its content:
/* /public/auth.html */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<title>Discord Auth</title>
</head>
<body>
</body>
<script type="text/javascript">
// dependencies: axios moment
// grab url hash coming back from Discord Authentication
var rt = document.location.hash;
var token = false;
var expires = -99;
// split it and look for access_token, and expires_in
var parts = rt.split('&');
parts.forEach(function (p) {
var vals = p.split('=');
if (vals.length === 2 && vals[0] === 'access_token') {
token = vals[1];
}
if (vals.length === 2 && vals[0] === 'expires_in') {
expires = parseInt(vals[1]);
}
});
if (token && expires > 0) {
// exchange access_token for user_info
var m = moment().add(2, 'days');
axios.get('https://discordapp.com/api/users/@me', {
headers: {
'Authorization': 'Bearer ' + token
}
})
.then((response) => { // store info client-side
localStorage.setItem('discord_user', JSON.stringify(response.data));
localStorage.setItem('expires', m.format());
var redirTo = localStorage.getItem('afterLogin');
// redirect to afterLogin value or homepage
if (!redirTo) redirTo = '/';
document.location = redirTo;
});
}
</script>
</html>
This page does not have body markup and its purpose is to catch the user-action after they have authenticated to discord. Discord will redirect here and pass to our site an access_token
that we can use to grab some information about the user.
Hopefully there is sufficient /*commentary*/
to follow the logic.
After a successful login, this leaves us with a localStorage
like:
Before taking a look at the component code, we will setup env variables
to not lose developer experience. This is because when developing locally, we will need to use the local redirect url, and use the other one when building for deploy.
Astro can handle this in an elegant way by leveraging what Vite does. Here is Astro’s docs.
So we will create 2 files in our project root:
Be sure to use your Discord App’s Client Id
With these 2 files, Astro will know which one to use depending if it is using the dev server, or if it is building the site.
/* /src/components/AuthUser.astro */
---
// grab values from .env file
const { DISCORD_CLIENT_ID, DISCORD_REDIRECT } = import.meta.env;
// build discord auth url
const discord_oauth_url =
'https://discordapp.com/oauth2/authorize?client_id='
+ DISCORD_CLIENT_ID
+ '&redirect_uri='
+ DISCORD_REDIRECT
+ '&scope=identify&response_type=token';
let logoutPage = Astro.props.logoutPage || '/logout';
---
<div id="user-logged-out" style="display:none;">
<a href={discord_oauth_url}>Login <i class="fab fa-discord"></i></a>
</div>
<div id="user-logged-in" style="display:none;">
<div class="dropdown">
<span class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown">
<img id="user-avatar" height="28" style="margin:0 0 0 -6px;" />
<span id="username"></span>
</span>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" id="action-logout" style="cursor:pointer">Logout</a></li>
</ul>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js" type="text/javascript"></script>
<script type="text/javascript" define:vars={{logoutPage}}>
var expires = localStorage.getItem('expires') || 0;
var m_expires = moment(expires);
var now = moment();
if (now.isBefore(m_expires)) {
document.getElementById('user-logged-in').style.display = '';
var user = JSON.parse(localStorage.getItem('discord_user'));
// document.getElementById('username').innerText = user.username;
document.getElementById('user-avatar').src =
'https://cdn.discordapp.com/avatars/' + user.id + '/' + user.avatar + '.png';
} else {
document.getElementById('user-logged-out').style.display = '';
}
document.getElementById('action-logout').addEventListener('click', function () {
localStorage.setItem('discord_user', false);
localStorage.setItem('expires', '0');
window.location = logoutPage;
});
</script>
<style scoped>
#user-logged-out a, #user-logged-out a:visited { color:#ffa; text-decoration:none; }
</style>
We are building the url to send the user to discord to authenticate using our Discord App. Our app’s id
and redirect url
are taken from the proper environment variables file.
We are also setting our logout page
, could be specified in the component’s props, or it defaults to /logout
The html section contains two main divs that are hidden.
div#user-logged-out
will be toggled to show depending on what the javascript
finds in the browser’s localStorage
.
Same with div#user-logged-in
, it’ll be toggled on depending on the localStorage
state. This div has a bootstrap dropdown with an option to logout
.
The javascript will look into localStorage
and depending on what it finds, it will show the respective div
.
It checks if the expiration time has elasped. If it has expired, then the div#user-logged-out
is shown, else div#user-logged-in
is shown.
The localStorage
state has been set from auth.html
after a successful discord authentication.
¯\_(ツ)_/¯
Now that we have AuthUser.astro
, we can use it in our Layout
---
import AuthUser from './../components/AuthUser.astro';
---
<!-- Somewhere in your Layout -->
<div class="float-end text-light">
<AuthUser logoutPage="/logout" />
</div>
The logout.astro
page will just serve to tell the user that we no longer know who they are.
/* /src/pages/logout.astro */
---
import BaseLayout from './../layouts/BaseLayout.astro';
---
<BaseLayout title="See you later :eyes:">
<h1>Who are you o_O</h1>
</BaseLayout>
AuthUser.astro
component uses Bootstrap 5.1 classesAuthUser.astro
component uses Font-Awesome 5.15 iconsNow with this on your site, tell us what you plan on doing in the comments… Just kidding, no commenting here yet.