title | description | image | author | date | published | slug | ogImage | ||||
---|---|---|---|---|---|---|---|---|---|---|---|
Build authenticated and paywall pages with Stripe and Xata |
Learn how to build protected and paywall pages using using Astro, Stripe, Lucia Auth, Xata and Vercel. |
|
Rishi Raj Jain |
04-10-2024 |
true |
authenticated-paywall-stripe-xata |
In this post, you'll learn how to authenticate users using Xata, create Stripe checkouts and respond to Stripe webhooks in an Astro application.
- Set up Xata
- Create a schema with different column types
- Programmatically create Stripe checkout sessions
- Respond to Stripe webhooks
- Authenticate users with Lucia Auth
You'll need the following:
- Node.js 18 or later
- pnpm package manager
- A Xata account
- A Stripe account
The following technologies are used in this guide:
Technology | Description |
---|---|
Xata | Serverless Postgres database platform for scalable, real-time applications. |
Astro | Framework for building fast, modern websites with serverless backend support. |
Stripe | A platform to accept payments globally. |
Lucia Auth | An authentication library that abstracts away the complexity of handling sessions. |
To complete this guide, you will need to follow these steps:
- Create a new Astro application
- Add Tailwind CSS to the application
- Enable Server Side Rendering in Astro
- Set up a Xata Database
- Create Stripe Checkout Sessions
- Respond to Stripe Webhooks
- Authenticate users with Lucia Auth and Xata
- Deploy to Vercel
Let’s get started by creating a new Astro project. Open your terminal and run the following command:
pnpm create astro@latest protected-and-paid-stripe-xata
pnpm create astro
is the recommended way to scaffold an Astro project quickly.
When prompted, choose:
Empty
when prompted on how to start the new project.Yes
when prompted if you plan to write using Typescript.Strict
when prompted how strict Typescript should be.Yes
when prompted to install dependencies.Yes
when prompted to initialize a git repository.
Once that’s done, you can move into the project directory and start the app:
cd protected-and-paid-stripe-xata
pnpm dev
The app should be running on localhost:4321. Now, let's close the development server as we move on to integrating Tailwind CSS in the application.
For styling the app, you will use Tailwind CSS. Install and set up Tailwind at the root of your project's directory by running:
pnpm astro add tailwind
When prompted, choose:
Yes
when prompted to install the Tailwind dependencies.Yes
when prompted to generate a minimaltailwind.config.mjs
file.Yes
when prompted to make changes to the Astro configuration file.
The command then finishes integrating TailwindCSS into your Astro project. It installed the following dependency:
tailwindcss
: TailwindCSS as a package to scan your project files to generate corresponding styles.@astrojs/tailwind
: The adapter that brings Tailwind's utility CSS classes to every.astro
file and framework component in your project.
Now, let's move on to enabling server side rendering in the application.
To create checkouts using the Stripe API dynamically, you will need to server-side render your Astro application. Enable server side rendering in your Astro project by executing the following command in your terminal:
pnpm add @astrojs/vercel
When prompted, choose:
Yes
when prompted to install the Vercel dependencies.Yes
when prompted to make changes to the Astro configuration file.
The command above installed the following dependency:
@astrojs/vercel
: The adapter that allows you to server-side render your Astro application on Vercel.
Now, let's move on to integrating Xata in the application.
After you've created a Xata account and are logged in, create a database by clicking on + Add database
.
Obtain the Xata initialization command by clicking on a table (say, user
), and then on the Get code snippet
button.
A modal titled Code snippets
is presented which contains a set of commands to integrate Xata into your project, locally.
For interacting with your Xata database using the TypeScript SDK, install the Xata CLI globally by executing the following command in your terminal:
# Installs the CLI globally
npm install -g @xata.io/cli
Then, link your Xata project to your Astro application by executing the following command in your terminal window:
# Initialize your project locally with the Xata CLI
xata init --db https://Rishi-Raj-Jain-s-workspace-80514q.us-east-1.xata.sh/db/astro-stripe-protected-and-paid
Use the following answers to the Xata CLI one-time setup question prompts to integrate Xata with Astro:
Yes
when prompted to add.env
to.gitignore
.src/xata.ts
when prompted to enter the output path for the generated code by the Xata CLI.
Now, let’s move onto creating API endpoints that create checkouts dynamically using Stripe.
A Stripe checkout session, in this case, would refer to a payment link hosted by Stripe where users can pay for a given product.
Let's use the index route to display a buy button that would take users to the Stripe hosted checkout. Create an index route (src/pages/index.astro
) with the following code:
---
// File: src/pages/index.astro
## export const prerender = true
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body class="font-display overflow-x-hidden">
<form class="p-12" method="post" action="/api/stripe/checkout">
<button class="rounded border border-black px-4 py-2 duration-150 hover:scale-105">Get My Product →</button>
</form>
</body>
</html>
The code above creates an index route in the Astro application containing a form that POSTs to /api/stripe/checkout
. To handle the form submission, create a file src/pages/api/stripe/checkout.ts
with the following code:
// File: src/pages/api/stripe/checkout.ts
import Stripe from 'stripe';
import type { APIContext } from 'astro';
export async function POST({ redirect }: APIContext) {
const STRIPE_SECRET_KEY = import.meta.env.STRIPE_SECRET_KEY;
if (!STRIPE_SECRET_KEY) return new Response(null, { status: 500 });
const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2023-10-16' });
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_configuration: 'pmc_1O2qH3SE9voLRYpuz5FLmkvn',
line_items: [
{
quantity: 1,
price_data: {
unit_amount: 0,
currency: 'usd',
product: 'prod_OqWkk7Rz9Yw18f'
}
}
],
success_url: 'http://localhost:4321'
});
return redirect(session.url);
}
The code above does the following:
- Imports the Stripe SDK and the type APIContext by Astro.
- Exports a
POST
HTTP request handler. - Validates the presence of
STRIPE_SECRET_KEY
environment variable. - Creates a new stripe object using the secret key and the latest api version.
- Creates a checkout with a given item adjusted to be for free. Having a checkout containing free items will help you quickly test a purchase flow manually.
- Redirects user to the newly created checkout's URL.
Let's move on to handling the webhook requests triggered by Stripe if there's a successful purchase on the payment link.
Upon a successful purchase, Stripe fires a webhook POST request. To handle the webhook requests in Astro via server-side code, create a src/pages/api/stripe/webhook.ts
file with the following code:
// File: src/pages/api/stripe/webhook.ts
import Stripe from 'stripe';
import { getXataClient } from '@/xata';
import type { APIContext } from 'astro';
// Process rawBody from the request Object
async function getRawBody(request: Request) {
let chunks = [];
let done = false;
const reader = request.body.getReader();
while (!done) {
const { value, done: isDone } = await reader.read();
if (value) {
chunks.push(value);
}
done = isDone;
}
const bodyData = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
let offset = 0;
for (const chunk of chunks) {
bodyData.set(chunk, offset);
offset += chunk.length;
}
return Buffer.from(bodyData);
}
// Stripe API Reference
// https://stripe.com/docs/webhooks#webhook-endpoint-def
export async function POST({ request }: APIContext) {
try {
const STRIPE_SECRET_KEY = import.meta.env.STRIPE_SECRET_KEY;
const STRIPE_WEBHOOK_SIG = import.meta.env.STRIPE_WEBHOOK_SIG;
if (!STRIPE_SECRET_KEY || !STRIPE_WEBHOOK_SIG) return new Response(null, { status: 500 });
const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2023-10-16' });
const rawBody = await getRawBody(request);
let event = JSON.parse(rawBody.toString());
const sig = request.headers.get('stripe-signature');
try {
event = stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SIG);
} catch (err) {
console.log(err.message);
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
if (event.type === 'checkout.session.completed' || event.type === 'payment_intent.succeeded') {
const email = event.data.object?.customer_details?.email;
if (email) {
const xata = getXataClient();
const existingRecord = await xata.db.user.filter({ email }).getFirst();
if (existingRecord) {
await xata.db.user.update(existingRecord.id, { paid: true });
} else {
await xata.db.user.create({ email, paid: true });
}
return new Response('marked the user as paid', { status: 200 });
}
return new Response('no email of the user is found', { status: 200 });
}
return new Response(JSON.stringify(event), { status: 404 });
} catch (e) {
return new Response(e.message || e.toString(), { status: 500 });
}
}
The code above does the following:
- Imports the Stripe class from its SDK,
APIContext
type by Astro andgetXataClient
to access the in-memory Xata instance. - Exports a POST HTTP handler to only respond to incoming POST request(s).
- Validates the presence of
STRIPE_SECRET_KEY
andSTRIPE_WEBHOOK_SIG
environment variables. - Creates a Stripe webhook event from the incoming Stripe webhook request body.
- Checks for an existing record with the email entered during the checkout.
- If a record is found, the
paid
attribute is set totrue
. - Otherwise, it creates a record with the email and
paid
attribute astrue
.
Let's move on to authenticating users with your Xata database and a Lucia library.
To start authenticating users and managing their sessions, install Lucia and Oslo, for various authentication utilities by executing the following command in your terminal:
pnpm add lucia oslo dotenv
The above command installs the packages passed to the install
command. The libraries installed include:
lucia
: An open source authentication library that abstracts away the complexity of handling sessions.dotenv
: A library for handling environment variables.oslo
: A collection of authentication related utilities.
// File: astro.config.mjs
+ import 'dotenv/config'
import node from '@astrojs/node'
import tailwind from '@astrojs/tailwind'
import { defineConfig } from 'astro/config'
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
integrations: [tailwind()],
+ vite: {
+ optimizeDeps: {
+ exclude: ['oslo'],
+ },
+ },
})
The code additions above do the following:
- Imports dotenv which makes all the environment variables accessible via the
process.env
object. - Uses vite's optimizeDeps to exclude the
oslo
library from being pre-bundled, effectively, using the library as is.
Now, create a Lucia directory in the src directory by running the following command:
mkdir src/lucia
Let's now move on to creating a Xata adapter for our Lucia library.
Create a src/lucia/xata.ts
file with the following code:
// File: src/lucia/xata.ts
import { getXataClient } from '@/xata';
import type {
Adapter,
DatabaseSession,
RegisteredDatabaseSessionAttributes,
DatabaseUser,
RegisteredDatabaseUserAttributes,
UserId
} from 'lucia';
The code above imports the getXataClient
utility to retrieve the in-memory Xata instance. It then imports the relevant types from the lucia
module. Append the following code:
// File: src/lucia/xata.ts
// ...
interface UserSchema extends RegisteredDatabaseUserAttributes {
id: string;
}
interface SessionSchema extends RegisteredDatabaseSessionAttributes {
id: string;
user_id: string;
expires_at: Date;
}
The code above defines the following types:
UserSchema
: A type created withRegisteredDatabaseUserAttributes
type definition along withid
attribute as a string.SessionSchema
: A type created withRegisteredDatabaseSessionAttributes
type definition along withid
attribute as a string,user_id
attribute as a string andexpires_at
attribute as a Date.
Append the following code to handle data transformations to Lucia's expected structure:
// File: src/lucia/xata.ts
// ...
function transformIntoDatabaseSession(raw: SessionSchema): DatabaseSession {
const { id, user_id: userId, expires_at: expiresAt, ...attributes } = raw;
return {
id,
userId,
expiresAt,
attributes
};
}
function transformIntoDatabaseUser(raw: UserSchema): DatabaseUser {
const { id, ...attributes } = raw;
return {
id,
attributes
};
}
The code above defines the following methods:
transformIntoDatabaseSession
: returns theSessionSchema
object in the shape ofDatabaseSession
type.transformIntoDatabaseUser
: returns theUserSchema
object in the shape ofDatabaseUser
type.
Append the following code to create the XataAdapter class:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
private controller = getXataClient();
// ...
}
The code above defines a class XataAdapter based on Lucia's Adapter class. Then, it sets a controller
private class variable equivalent to the Xata instance obtained from the getXataClient
method. Define a method to delete a session by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
public async deleteSession(sessionId: string): Promise<void> {
await this.controller.sql`DELETE FROM "session" WHERE id=${sessionId}`;
}
}
The code above defines a deleteSession
method that uses Xata SQL queries to delete a particular session from your database. Define a method to delete all the sessions for a user by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
public async deleteUserSessions(userId: UserId): Promise<void> {
await this.controller.sql`DELETE FROM "session" WHERE user_id=${userId}`;
}
}
The code above defines a deleteUserSessions
method that uses Xata SQL queries to delete all sessions pertaining to a particular user in your database. Define a method to retrieve all the sessions for a user by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
public async getUserSessions(user_id: UserId): Promise<DatabaseSession[]> {
const records = await this.controller.db.session.filter({ user_id }).getAll();
return records.map((val) => {
return transformIntoDatabaseSession(val);
});
}
}
The code above defines a getUserSessions
method that uses Xata SDK methods to retrieve all sessions pertaining to a particular user in your database. Define a method to create a session by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
public async setSession(databaseSession: DatabaseSession): Promise<void> {
await this.controller.db.session.create({
id: databaseSession.id,
user_id: databaseSession.userId,
expires_at: databaseSession.expiresAt,
...databaseSession.attributes
});
}
}
The code above defines a setSession
method that uses Xata SDK methods to insert a record in a session table in your database. Define a method to update a session by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
public async updateSessionExpiration(sessionId: string, expiresAt: Date): Promise<void> {
await this.controller.sql`UPDATE "session" SET expires_at=${expiresAt} WHERE id=${sessionId}`;
}
}
The code above defines an updateSessionExpiration
method that uses a Xata SQL query to update the expires_at
attribute in a particular session in your database. Define a method to delete all expired sessions by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
public async deleteExpiredSessions(): Promise<void> {
await this.controller.sql`DELETE FROM "session" WHERE expires_at <=${new Date()}`;
}
}
The code above defines a deleteExpiredSessions
method that uses a Xata SQL query to delete all the sessions whose expires_at
attribute have expired. Define a method to obtain a session by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
private async getSession(session_id: string): Promise<DatabaseSession | null> {
const records = await this.controller.db.session.filter({ session_id }).getFirst();
return transformIntoDatabaseSession(records);
}
}
The code above defines a getSession
method that uses Xata SDK methods to retrieve a single matching record for a given session from your database. Define a method to obtain a user associated with a session by appending the following code:
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
private async getUserFromSessionId(session_id: string): Promise<DatabaseUser | null> {
const theSession = await this.controller.db.session.filter({ session_id }).getFirst();
if (theSession) {
const { user_id } = theSession;
const getUser = await this.controller.db.user.filter({ user_id }).getFirst();
if (getUser) return transformIntoDatabaseUser(getUser);
}
return null;
}
}
The code above defines a getUserFromSessionId
method that uses Xata SDK methods to retrieve a single matching record for a given session from your database. If a session is found, it further queries to obtain the user associated with the session. Otherwise, it returns null
.
// File: src/lucia/xata.ts
// ...
export class XataAdapter implements Adapter {
// ...
public async getSessionAndUser(
sessionId: string
): Promise<[session: DatabaseSession | null, user: DatabaseUser | null]> {
const [databaseSession, databaseUser] = await Promise.all([
this.getSession(sessionId),
this.getUserFromSessionId(sessionId)
]);
return [databaseSession, databaseUser];
}
}
The code above defines a getSessionAndUser
method that uses the getSession
and getUserFromSessionId
methods to obtain both the user and session information associated with the particular session id.
The XataAdapter class is now ready to be used as Lucia's adapter.
To use Xata with Lucia, create a file index.ts
inside the lucia
directory with the following code:
// File: src/lucia/index.ts
import { Lucia } from 'lucia';
import { XataAdapter } from './xata';
const adapter = new XataAdapter();
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: import.meta.env.PROD
}
},
getUserAttributes: (attributes) => {
return {
paid: attributes.paid,
email: attributes.email
};
}
});
The code above does the following:
- Imports the Lucia and XataAdapter classes.
- Creates a new instance of XataAdapter as
adapter
. - Uses the
adapter
to create a new Lucia instance wherein the session cookie is set toSecure
if the application is running in the production environment. - The
lucia
instance defines thegetUserAttributes
function to only fetchpaid
andemail
attributes from the user record(s).
To create type definitions for the associated user and session, append the following code:
// File: src/lucia/index.ts
// ...
interface DatabaseUserAttributes {
email: string;
paid: boolean | null;
}
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
}
}
The code above defines the following types:
DatabaseUserAttributes
: An object that would contain anemail
attribute as astring
, andpaid
attribute asboolean
ornull
.Lucia
: As the type of the lucia instance created.
To retrieve the authentication status of the session, you will need to obtain the user information associated with the request. Create a file user.ts
with the following code:
// File: src/lucia/user.ts
import { lucia } from '.';
import type { User } from 'lucia';
import type { AstroCookies } from 'astro';
export function getSessionID(cookies: AstroCookies): string | null {
const auth_session = cookies.get('auth_session');
if (!auth_session) return null;
return lucia.readSessionCookie(`auth_session=${auth_session.value}`);
}
export async function getUser(cookies: AstroCookies): Promise<User | null> {
const session_id = getSessionID(cookies);
if (!session_id) return null;
const { user } = await lucia.validateSession(session_id);
return user;
}
The code above does the following:
- Imports the
lucia
instance and theUser
type by Lucia. - Exports a
getSessionID
function which uses Lucia'sreadSessionCookie
utility to obtain the session ID. - Exports a
getUser
function which uses thegetSessionID
function to obtain the session id. Then, it uses Lucia'svalidateSession
utility to obtain the user data information with the session.
Let's move on to creating simple user authentication forms and the API routes to handle the authentication logic.
In this section, you will learn how to create simple forms in Astro, and use the form data in Astro endpoints to sign up, sign out and sign in the users in your application.
To serve responses to a /signup
route in Astro, create a src/pages/signup.astro
file with the following code:
---
// File: src/pages/signup.astro
export const prerender = true
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body class="font-display overflow-x-hidden">
<h1>Sign Up</h1>
<form method="post" action="/api/sign/up">
<label for="email">Email</label>
<input id="email" name="email" />
<label for="password">Password</label>
<input id="password" name="password" />
<button>Continue</button>
</form>
</body>
</html>
The code above creates a sign up route in the Astro application containing a form that POSTs to /api/sign/up
with the user's email and password. Create a src/pages/api/sign/up.ts
file with the following code to handle the form submission:
// File: src/pages/api/sign/up.ts
import { generateId } from 'lucia';
import { lucia } from '@/lucia/index';
import { getXataClient } from '@/xata';
import type { APIContext } from 'astro';
import { Argon2id } from 'oslo/password';
export async function POST({ request, redirect, cookies }: APIContext): Promise<Response> {
const xata = getXataClient();
const user_id = generateId(15);
const formData = await request.formData();
const email = (formData.get('email') as string).trim();
const password = formData.get('password') as string;
const hashed_password = await new Argon2id().hash(password);
const existingRecord = await xata.db.user.filter({ email }).getFirst();
if (existingRecord) {
if (existingRecord.hashed_password !== null) return redirect('/signin');
await xata.db.user.update(existingRecord.id, { user_id, hashed_password });
} else {
await xata.db.user.create({ email, user_id, hashed_password });
}
const session = await lucia.createSession(user_id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect('/');
}
The code above does the following:
- Imports a utility from the Lucia SDK to create random ID(s), the
getXataClient
utility to obtain the in-memory Xata instance, Argon2id class byoslo
to hash given password(s). - Exports a POST HTTP handler to only respond to incoming POST request(s).
- Obtains the email and password from the request's form data.
- Hashes the password using Argon2id's
hash
method. - Retrieves a single matching record using the email from form data.
- If no such record is found, it creates a record in the user table with the email, user id and hashed password.
- Otherwise, if the hashed password is present in the matching record, it redirects to
/signin
route. Otherwise, it updates the existing record with the newly signed up user. - Creates a session using
createSession
helper utility by Lucia and sets the cookie. - Redirects to the index route.
To serve responses to a /signin
route in Astro, create a src/pages/signin.astro
file with the following code:
---
// File: src/pages/signin.astro
export const prerender = true
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body class="font-display overflow-x-hidden">
<h1>Sign In</h1>
<form method="post" action="/api/sign/in">
<label for="email">Email</label>
<input id="email" name="email" />
<label for="password">Password</label>
<input id="password" name="password" />
<button>Continue</button>
</form>
</body>
</html>
The code above creates a sign in route in the Astro application containing a form that POSTs to /api/sign/in
with the user's email and password. Create a src/pages/api/sign/in.ts
file with the following code to handle the form submission:
// File: src/pages/api/sign/in.ts
import { lucia } from '@/lucia/index';
import { getXataClient } from '@/xata';
import type { APIContext } from 'astro';
import { Argon2id } from 'oslo/password';
export async function POST({ request, cookies, redirect }: APIContext): Promise<Response> {
const xata = getXataClient();
const formData = await request.formData();
const email = (formData.get('email') as string).trim();
const password = formData.get('password') as string;
const existingRecord = await xata.db.user.filter({ email }).getFirst();
if (!existingRecord) return new Response('Incorrect email or password', { status: 400 });
const validPassword = await new Argon2id().verify(existingRecord.hashed_password, password);
if (!validPassword) return new Response('Incorrect email or password', { status: 400 });
const session = await lucia.createSession(existingRecord.user_id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect('/');
}
The code above does the following:
- Imports the initialized
lucia
instance andgetXataClient
helper utility. - Exports a POST HTTP handler to only respond to incoming POST request(s).
- Obtains the email and password from the request's form data.
- Looks for an existing record with the email in user table of your database.
- If not found, it returns a 400 Bad Request response.
- Otherwise, it uses
Argon2id
to verify the hashed password. - If the hashed password does not match, it returns a 400 Bad Request response.
- Otherwise, it creates a session using
createSession
helper utility by Lucia and sets the cookie. - Redirects to the index route.
To sign out users in your Astro application, create a src/pages/api/sign/out.ts
file with the following code:
// File: src/pages/api/sign/out.ts
import { lucia } from '@/lucia/index';
import type { APIContext } from 'astro';
import { getSessionID } from '@/lucia/user';
export async function GET({ cookies, redirect }: APIContext): Promise<Response> {
await lucia.invalidateSession(getSessionID(cookies));
const sessionCookie = lucia.createBlankSessionCookie();
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect('/');
}
The code above does the following:
- Imports the initialized
lucia
instance andgetSessionID
helper utility. - Exports a GET HTTP handler to only respond to incoming GET request(s).
- Invalidates the current user session using the
invalidateSession
utility by Lucia. - Creates an empty user session cookie using
createBlankSessionCookie
utility by Lucia. - Sets the blank user session cookie.
- Redirects to the index route.
To restrict access to a page for only paid and authenticated users, you will obtain the user information using the utilities created earlier. Create a file src/pages/protected_and_paid.astro
with the following code:
---
// File: src/pages/protected_and_paid.astro
import { getUser } from '@/lucia/user'
const user = await getUser(Astro.cookies)
if (!user || user.paid !== true) return new Response(null, { status: 403 })
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body class="font-display overflow-x-hidden">
<span> Protected and Paid Content </span>
</body>
</html>
The code above creates a /protected_and_paid
route in the Astro application that does the following:
- Checks if there's a user associated with the session and if they have paid.
- If the above is not true, it returns a 403 Unauthorized response. Otherwise, it returns an HTML page containing the paid and protected information.
To restrict access to a page for only authenticated users, you will obtain the user information using the utilities created earlier. Create a file src/pages/protected.astro
with the following code:
---
import { getUser } from '@/lucia/user'
const user = await getUser(Astro.cookies)
if (!user) return new Response(null, { status: 403 })
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body class="font-display overflow-x-hidden">
<span> Protected Content </span>
</body>
</html>
The code above creates a /protected
route in the Astro application that does the following:
- Checks if there's a user associated with the session.
- If the above is not true, it returns a 403 Unauthorized response. Otherwise, it returns an HTML page containing the protected information.
The repository, is now ready to deploy to Vercel. Use the following steps to deploy:
- Start by creating a GitHub repository containing your app's code.
- Then, navigate to the Vercel Dashboard and create a New Project.
- Link the new project to the GitHub repository you've just created.
- In Settings, update the Environment Variables to match those in your local
.env
file. - Deploy! 🚀
For more detailed insights, explore the references cited in this post.
We'd love to hear from you if you have any feedback on this tutorial, would like to know more about Xata, or if you would like to contribute a community blog or tutorial. Reach out to us on Discord or join us on X | Twitter. Happy building 🦋