DEV Community

Cover image for Setting up authentication in Astro with Prisma and Planetscale
Thomas Ledoux
Thomas Ledoux

Posted on • Originally published at thomasledoux.be

Setting up authentication in Astro with Prisma and Planetscale

I've been wanting to add authentication to my personal website for a while now to see how it works in Astro.
With Prisma and PlanetScale already running for comments on my blogs, I decided to store my account information in PlanetScale.
Because it's just used for my own account, and I'm not storing any other sensitive information in my database, I decided to store the credentials in plain text for now.
I changed my Prisma schema to make this possible:

// schema.prisma

model Account {
  id Int @id @default(autoincrement())
  username String @unique
  password String
}
Enter fullscreen mode Exit fullscreen mode

Once the model is updated in the code, running npx prisma db push propagates the changes to PlanetScale, so the schema is updated in the actual database.

I used an existing package called @astro-auth to handle all the authentication on my site.
For this to work, I needed to add 2 environment variables to my application: ASTROAUTH_URL (the URL my site is hosted on) and ASTROAUTH_SECRET (a self chosen secret key).

Because I stored the credentials in PlanetScale, I needed to use the CredentialProvider to enable logging in with username and password.
There are many other providers available on @astro-auth, go check out the package if you're interested.
The code needed to set this up with @astro-auth looks like this:

// /pages/api/auth/[...astroauth].ts

import AstroAuth from '@astro-auth/core';
import { CredentialProvider } from '@astro-auth/providers';
import { prisma } from '../../../lib/prisma';

export const all = AstroAuth({
  authProviders: [
    CredentialProvider({
      authorize: async properties => {
        const account = await prisma?.account.findFirst({
          where: {
            username: properties.username,
            AND: {
              password: properties.password,
            },
          },
          select: {
            id: true,
          },
        });
        if (account?.id) {
          return properties.username;
        }
        return null;
      },
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Creating a login page was very easy.
I just created a form, calling the signIn() method from @astro-auth on submit, and BOOM: logged in!
The code for the login page:

// /pages/login.astro

<html>
  <head>
    <title>Login</title>
    <script>
      import { signIn } from '@astro-auth/client';

      document.addEventListener('DOMContentLoaded', () => {
        document.querySelector('form')?.addEventListener('submit', async e => {
          e.preventDefault();
          const form = e.target;
          if (form) {
            const formData = new FormData(form as HTMLFormElement);
            const data = Object.fromEntries(formData);
            await signIn({
              provider: 'credential',
              login: data,
            });
            window.location.href = '/';
          }
        });
      });
    </script>
  </head>
  <body>
    <form>
      <label for="username">Name</label>
      <input type="text" name="username" />

      <label for="password">Password</label>
      <input type="password" name="password" />

      <input type="submit" value="Submit" />
    </form>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

After submitting the form, the user's signed in and is redirected to the homepage.
Protecting a page with authentication is easy, just checking the logged in user with the getUser() function from @astro-auth.
Here's an example of a page where I used this check:

// /pages/comment-overview.astro

---
import { getUser } from '@astro-auth/core';
import Layout from '../layouts/Layout.astro';
import { prisma } from '../lib/prisma';
import CommentsOverviewWrapper from '../components/CommentOverviewWrapper';
const user = getUser({ client: Astro });
if (!user) {
  return Astro.redirect('/', 307);
}
const commentsWithPost = await prisma?.comment.findMany({
  include: {
    post: {
      select: {
        url: true,
      },
    },
  },
});
---

<Layout
  description="Overview of comments"
  title="Thomas Ledoux | Comment overview"
>
  <CommentsOverviewWrapper commentsWithPost={commentsWithPost} client:load />
</Layout>
Enter fullscreen mode Exit fullscreen mode

If the user is not logged in, the user will be redirected to the homepage with a 307 status code.
I also have an API route to delete comments on my blog posts, which I want to fence off so only authenticated user can use this API.
It's possible to use the getUser() function from @astro-auth for this too, but this time we're going to pass the request instead of the Astro object.
Example of using this code:

// /pages/api/comments.ts

export const del: APIRoute = async ({ request }) => {
  const user = getUser({ server: request });
  if (user) {
    const body = await request.json();
    const deleteComment = await prisma?.comment.delete({
      where: {
        id: body.id,
      },
    });
    return new Response(
      JSON.stringify({
        message: `Comment with id ${deleteComment?.id} deleted`,
      }),
      { status: 200 }
    );
  }
  return new Response(null, { status: 403 });
};
Enter fullscreen mode Exit fullscreen mode

So when the user is not authenticated, a 403 response will be returned.

Hope this was helpful!
Source code can be found on my Github as always.

Top comments (0)