Skip to content

Commit

Permalink
add nuxt tutorials
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowonpaper committed Dec 25, 2023
1 parent 33d2469 commit 2138a05
Show file tree
Hide file tree
Showing 7 changed files with 597 additions and 9 deletions.
3 changes: 1 addition & 2 deletions docs/pages/tutorials/github-oauth/astro.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";

import type { APIContext } from "astro";
import type { DatabaseUser } from "../../../lib/db";

export async function GET(context: APIContext): Promise<Response> {
const code = context.url.searchParams.get("code");
Expand Down Expand Up @@ -210,7 +209,7 @@ const username = user.username;
Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`.

```ts
import { lucia } from "../../auth";
import { lucia } from "@lib/auth";
import type { APIContext } from "astro";

export async function POST(context: APIContext): Promise<Response> {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/tutorials/github-oauth/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ The tutorials go over how to implement a basic GitHub OAuth and covers the basic
- [Astro](/tutorials/github-oauth/astro)
- [Next.js App router](/tutorials/github-oauth/nextjs-app)
- [Next.js Pages router](/tutorials/github-oauth/nextjs-pages)
- [Nuxt](/tutorials/github-oauth/nuxt)
- [SvelteKit](/tutorials/github-oauth/sveltekit)
268 changes: 268 additions & 0 deletions docs/pages/tutorials/github-oauth/nuxt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
---
title: "GitHub OAuth in Nuxt"
---

# Tutorial: GitHub OAuth in Nuxt

Before starting, make sure you've setup your database and middleware as described in the [Getting started](/getting-started/nuxt) page.

An [example project](https://github.com/lucia-auth/examples/tree/v3/nuxt/github-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/examples/tree/v3/nuxt/github-oauth).

```
npx degit https://github.com/lucia-auth/examples/tree/v3/nuxt/github-oauth <directory_name>
```

## Create an OAuth App

[Create a GitHub OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). Set the redirect URI to `http://localhost:3000/login/github/callback`. Copy and paste the client ID and secret to your `.env` file.

```bash
# .env
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
```

## Update database

Add a `github_id` and `username` column to your user table.

| column | type | attributes |
| ----------- | -------- | ---------- |
| `github_id` | `number` | unique |
| `username` | `string` | |

Create a `DatabaseUserAttributes` interface in the module declaration and add your database columns. By default, Lucia will not expose any database columns to the `User` type. To add a `githubId` and `username` field to it, use the `getUserAttributes()` option.

```ts
// server/utils/auth.ts
import { Lucia } from "lucia";

export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: !import.meta.dev
}
},
getUserAttributes: (attributes) => {
return {
// attributes has the type of DatabaseUserAttributes
githubId: attributes.github_id,
username: attributes.username
};
}
});

declare module "lucia" {
interface Register {
Lucia: typeof lucia;
}
interface DatabaseUserAttributes {
github_id: number;
username: string;
}
}
```

## Setup Arctic

We recommend using [Arctic](https://arctic.js.org) for implementing OAuth. It is a lightweight library that provides APIs for creating authorization URLs, validating callbacks, and refreshing access tokens. This is the easiest way to implement OAuth with Lucia and it supports most major providers.

```
npm install arctic
```

Initialize the GitHub provider with the client ID and secret.

```ts
import { GitHub } from "arctic";

export const github = new GitHub(process.env.GITHUB_CLIENT_ID!, process.env.GITHUB_CLIENT_SECRET!);
```

## Sign in page

Create `pages/login/index.vue` and add a basic sign in button, which should be a link to `/login/github`.

```vue
<!-- pages/login/index.vue -->
<template>
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
</template>
```

## Create authorization URL

Create an API route in `server/routes/login/github/index.get.ts`. Generate a new state, create a new authorization URL with createAuthorizationURL(), store the state, and redirect the user to the authorization URL. The user will be prompted to sign in with GitHub.

```ts
// server/routes/login/github/index.get.ts
import { generateState } from "arctic";

export default defineEventHandler(async (event) => {
const state = generateState();
const url = await github.createAuthorizationURL(state);

setCookie(event, "github_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax"
});
return sendRedirect(event, url.toString());
});
```

## Validate callback

Create an API route in `server/routes/login/github/callback.get.ts` to handle the callback. First, get the state from the cookie and the search params and compare them. Validate the authorization code in the search params with `validateAuthorizationCode()`. This will throw a [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError) if the code or credentials are invalid. After validating the code, get the user's profile using the access token. Check if the user is already registered with the GitHub ID and create a new user if not. Finally, create a new session and set the session cookie.

```ts
// server/routes/login/github/callback.get.ts
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";

export default defineEventHandler(async (event) => {
const query = getQuery(event);
const code = query.code?.toString() ?? null;
const state = query.state?.toString() ?? null;
const storedState = getCookie(event, "github_oauth_state") ?? null;
if (!code || !state || !storedState || state !== storedState) {
throw createError({
status: 400
});
}

try {
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`
}
});
const githubUser: GitHubUser = await githubUserResponse.json();
const existingUser = await db.table("user").where("github_id", "=", githubUser.id).get();

if (existingUser) {
const session = await lucia.createSession(existingUser.id, {});
appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize());
return sendRedirect(event, "/");
}

const userId = generateId(15);
await db.table("user").insert({
id: userId,
github_id: githubUser.id,
username: githubUser.login
});
const session = await lucia.createSession(userId, {});
appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize());
return sendRedirect(event, "/");
} catch (e) {
// the specific error message depends on the provider
if (e instanceof OAuth2RequestError) {
// invalid code
throw createError({
status: 400
});
}
throw createError({
status: 500
});
}
});

interface GitHubUser {
id: string;
login: string;
}
```

## Validate requests

You can validate requests by checking `event.context.user`. The field `user.username` is available since we defined the `getUserAttributes()` option. You can protect pages, such as `/`, by redirecting unauthenticated users to the login page.

```ts
export default defineEventHandler((event) => {
if (event.context.user) {
const username = event.context.user.username;
}
// ...
});
```

## Get user in the client

Create an API route in `server/api/user.get.ts`. This will just return the current user.

```ts
// server/api/user.get.ts
export default defineEventHandler((event) => {
return event.context.user;
});
```

Create a composable `useUser()` in `composables/auth.ts`.

```ts
// composables/auth.ts
import type { User } from "lucia";

export const useUser = () => {
const user = useState<User | null>("user", () => null);
return user;
};
```

Then, create a global middleware in `middleware/auth.global.ts` to populate it.

```ts
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware(async () => {
const user = useUser();
user.value = await $fetch("/api/user");
});
```

You can now use `useUser()` client side to get the current user.

```vue
<script lang="ts" setup>
const user = useUser();
</script>
```

## Sign out

Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`.

```ts
// server/api/logout.post.ts
export default eventHandler(async (event) => {
if (!event.context.session) {
throw createError({
statusCode: 403
});
}
await lucia.invalidateSession(event.context.session.id);
appendHeader(event, "Set-Cookie", lucia.createBlankSessionCookie().serialize());
});
```

```vue
<script lang="ts" setup>
async function logout() {
await useFetch("/api/logout", {
method: "POST"
});
navigateTo("/login");
}
</script>
<template>
<form @submit.prevent="logout">
<button>Sign out</button>
</form>
</template>
```
6 changes: 3 additions & 3 deletions docs/pages/tutorials/username-and-password/astro.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Create an API route in `pages/api/signup.ts`. First do a very basic input valida

```ts
// pages/api/signup.ts
import { lucia } from "../../auth";
import { lucia } from "@lib/auth";
import { generateId } from "lucia";
import { Argon2id } from "oslo/password";

Expand Down Expand Up @@ -158,7 +158,7 @@ Create an API route as `pages/api/signup.ts`. First do a very basic input valida

```ts
// pages/api/login.ts
import { lucia } from "../../auth";
import { lucia } from "@lib/auth";
import { Argon2id } from "oslo/password";

import type { APIContext } from "astro";
Expand Down Expand Up @@ -226,7 +226,7 @@ const username = user.username;
Sign out users by invalidating their session with `Lucia.invalidateSession()`. Make sure to remove their session cookie by setting a blank session cookie created with `Lucia.createBlankSessionCookie()`.

```ts
import { lucia } from "../../auth";
import { lucia } from "@lib/auth";
import type { APIContext } from "astro";

export async function POST(context: APIContext): Promise<Response> {
Expand Down
1 change: 1 addition & 0 deletions docs/pages/tutorials/username-and-password/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ The tutorials go over how to implement a basic username and password auth and co
- [Astro](/tutorials/username-and-password/astro)
- [Next.js App router](/tutorials/username-and-password/nextjs-app)
- [Next.js Pages router](/tutorials/username-and-password/nextjs-pages)
- [Nuxt](/tutorials/username-and-password/nuxt)
- [SvelteKit](/tutorials/username-and-password/sveltekit)
12 changes: 8 additions & 4 deletions docs/pages/tutorials/username-and-password/nextjs-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@ export default function Page() {
async function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const formElement = e.target as HTMLFormElement;
await fetch(formElement.action, {
const response = await fetch(formElement.action, {
method: formElement.method,
body: JSON.stringify(Object.fromEntries(new FormData(formElement).entries())),
headers: {
"Content-Type": "application/json"
}
});
router.push("/");
if (response.ok) {
router.push("/");
}
}

return (
Expand Down Expand Up @@ -171,14 +173,16 @@ export default function Page() {
async function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const formElement = e.target as HTMLFormElement;
await fetch(formElement.action, {
const response = await fetch(formElement.action, {
method: formElement.method,
body: JSON.stringify(Object.fromEntries(new FormData(formElement).entries())),
headers: {
"Content-Type": "application/json"
}
});
router.push("/");
if (response.ok) {
router.push("/");
}
}

return (
Expand Down
Loading

0 comments on commit 2138a05

Please sign in to comment.