Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion docs/guide/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,42 @@ export default defineEventHandler(async (event) => {

- [Password Reset](/guide/password-reset) - Add password reset functionality
- [Components](/components/) - Learn about available Vue components
- [API Reference](/api/) - Explore all available API endpoints
- [API Reference](/api/) - Explore all available API endpoints

## Social Login with Google

This module also supports social login with Google.

### Configuration

First, you need to configure the Google OAuth credentials in your `nuxt.config.ts` file:

```ts
export default defineNuxtConfig({
modules: ['nuxt-users'],
nuxtUsers: {
oauth: {
google: {
clientId: 'your-google-client-id',
clientSecret: 'your-google-client-secret',
redirectUri: 'http://localhost:3000/api/auth/google/callback',
scope: ['email', 'profile'],
},
},
},
})
```

### Usage

The module provides a `GoogleLoginButton` component that you can use in your login page:

```vue
<template>
<LoginForm />
</template>
```

The `LoginForm` component will automatically display the `GoogleLoginButton` if the Google OAuth credentials are configured.

When a user clicks the "Login with Google" button, they will be redirected to Google for authentication. After successful authentication, they will be redirected back to your application at the `redirectUri` you configured. The module will then create a new user if one doesn't exist and create a session for the user.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@lucia-auth/oauth": "^4.0.0",
"@nuxt/kit": "^3.17.6",
"@types/bcrypt": "^5.0.2",
"bcrypt": "^6.0.0",
Expand Down
11 changes: 10 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@ export default defineNuxtConfig({
modules: ['../src/module', '@formkit/nuxt'],
devtools: { enabled: true },
compatibilityDate: '2025-07-08',
nuxtUsers: {},
nuxtUsers: {
oauth: {
google: {
clientId: 'your-google-client-id',
clientSecret: 'your-google-client-secret',
redirectUri: 'http://localhost:3000/api/auth/google/callback',
scope: ['email', 'profile'],
},
},
},
})
20 changes: 20 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { getAppliedMigrations } from './runtime/server/utils/migrate'
import type { ModuleOptions } from './types'

export const defaultOptions: ModuleOptions = {
oauth: {
google: {
clientId: '',
clientSecret: '',
redirectUri: '',
scope: [],
},
},
connector: {
name: 'sqlite',
options: {
Expand Down Expand Up @@ -121,6 +129,12 @@ export default defineNuxtModule<ModuleOptions>({
handler: resolver.resolve('./runtime/server/api/auth/reset-password.post')
})

addServerHandler({
route: '/api/auth/google',
method: 'post',
handler: resolver.resolve('./runtime/server/api/auth/google/login.post')
})

addPlugin(resolver.resolve('./runtime/plugin'))

// Register the LoginForm component
Expand All @@ -140,5 +154,11 @@ export default defineNuxtModule<ModuleOptions>({
name: 'ResetPasswordForm',
filePath: resolver.resolve('./runtime/components/ResetPasswordForm.vue')
})

// Register the GoogleLoginButton component
addComponent({
name: 'GoogleLoginButton',
filePath: resolver.resolve('./runtime/components/GoogleLoginButton.vue')
})
},
})
7 changes: 7 additions & 0 deletions src/runtime/components/GoogleLoginButton.vue
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use FormKit

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<form action="/api/auth/google" method="post">
<button type="submit">
<slot>Login with Google</slot>
</button>
</form>
</template>
5 changes: 5 additions & 0 deletions src/runtime/components/LoginForm.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { navigateTo } from '#app'
import GoogleLoginButton from './GoogleLoginButton.vue'
import type { LoginFormData, LoginFormProps, User } from '~/src/types'

interface Emits {
Expand Down Expand Up @@ -122,6 +123,10 @@ const handleSubmit = async (formData: LoginFormData) => {
</FormKit>
</slot>

<slot name="google-login-button">
<GoogleLoginButton />
</slot>

<!-- Footer slot -->
<slot name="footer">
<div class="login-footer">
Expand Down
42 changes: 42 additions & 0 deletions src/runtime/server/api/auth/google/callback.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { google } from '@lucia-auth/oauth/providers';
import { auth, googleAuth } from '../../utils/lucia';

export default defineEventHandler(async (event) => {
const { code, state } = getQuery(event);
const storedState = getCookie(event, 'google_oauth_state');

if (!code || !state || !storedState || state !== storedState) {
throw new Error('Invalid state');
}

try {
const { getExistingUser, googleUser, createUser } = await googleAuth.validateCallback(code);

const getUser = async () => {
const existingUser = await getExistingUser();
if (existingUser) return existingUser;
const user = await createUser({
attributes: {
email: googleUser.email,
name: googleUser.name,
avatar: googleUser.picture,
},
});
return user;
};

const user = await getUser();
const session = await auth.createSession({
userId: user.userId,
attributes: {},
});
const sessionCookie = auth.createSessionCookie(session);

setCookie(event, sessionCookie.name, sessionCookie.value, sessionCookie.attributes);

return sendRedirect(event, '/');
} catch (e) {
console.error(e);
return sendRedirect(event, '/login');
}
});
14 changes: 14 additions & 0 deletions src/runtime/server/api/auth/google/login.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { googleAuth } from '../../utils/lucia';

export default defineEventHandler(async (event) => {
const [url, state] = await googleAuth.getAuthorizationUrl();

setCookie(event, 'google_oauth_state', state, {
httpOnly: true,
secure: !process.dev,
path: '/',
maxAge: 60 * 60,
});

return sendRedirect(event, url.toString());
});
11 changes: 11 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@ export type DatabaseConfig = {
password?: string
database?: string
}

export interface OauthOptions {
google: {
clientId: string
clientSecret: string
redirectUri: string
scope: string[]
}
}

export interface ModuleOptions {
oauth?: OauthOptions
connector?: {
name: DatabaseType
options: DatabaseConfig
Expand Down
Loading