Skip to content

Commit

Permalink
feat(auth): add login with google base
Browse files Browse the repository at this point in the history
  • Loading branch information
TiveCS committed Dec 2, 2024
1 parent 4373674 commit fe61774
Show file tree
Hide file tree
Showing 19 changed files with 2,343 additions and 129 deletions.
93 changes: 93 additions & 0 deletions app/(auth)/login/google/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,93 @@
import { generateSessionToken, createSession } from '@/auth/sessions';
import { google } from '@/auth/oauth';
import { cookies } from 'next/headers';
import { decodeIdToken } from 'arctic';

import type { OAuth2Tokens } from 'arctic';
import { setSessionTokenCookie } from '@/auth/cookies';
import {
COOKIE_GOOGLE_CODE_VERIFIER,
COOKIE_GOOGLE_OAUTH_STATE,
} from '@/auth/constants';
import { createUser, findUserByGoogleId } from '@/db/repo/users';

export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const cookieStore = await cookies();
const storedState = cookieStore.get(COOKIE_GOOGLE_OAUTH_STATE)?.value ?? null;
const codeVerifier =
cookieStore.get(COOKIE_GOOGLE_CODE_VERIFIER)?.value ?? null;

if (
code === null ||
state === null ||
storedState === null ||
codeVerifier === null
) {
return new Response(null, {
status: 400,
});
}

if (state !== storedState) {
return new Response(null, {
status: 400,
});
}

let tokens: OAuth2Tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400,
});
}

const claims = decodeIdToken(tokens.idToken()) as {
sub: string;
name: string;
email: string;
picture: string;
};

const googleUserId = claims.sub;
const name = claims.name;
const email = claims.email;
const picture = claims.picture;

const existingUser = await findUserByGoogleId(googleUserId);

if (existingUser) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
}

const user = await createUser({
email,
googleId: googleUserId,
name,
picture,
});

const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user.id);
await setSessionTokenCookie(sessionToken, session.expiresAt);

return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
}
41 changes: 41 additions & 0 deletions app/(auth)/login/google/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,41 @@
// app/login/google/route.ts
import { generateState, generateCodeVerifier } from 'arctic';
import { google } from '@/auth/oauth';
import { cookies } from 'next/headers';
import {
COOKIE_GOOGLE_CODE_VERIFIER,
COOKIE_GOOGLE_OAUTH_STATE,
} from '@/auth/constants';

export async function GET(): Promise<Response> {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, [
'openid',
'profile',
'email',
]);

const cookieStore = await cookies();
cookieStore.set(COOKIE_GOOGLE_OAUTH_STATE, state, {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
cookieStore.set(COOKIE_GOOGLE_CODE_VERIFIER, codeVerifier, {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});

return new Response(null, {
status: 302,
headers: {
Location: url.toString(),
},
});
}
8 changes: 8 additions & 0 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 1,8 @@
export default function LoginPage() {
return (
<>
login page
<a href="/login/google">Login with Google</a>
</>
);
}
27 changes: 14 additions & 13 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,21 1,22 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import { AntdRegistry } from '@ant-design/nextjs-registry';
import './globals.css';

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
src: './fonts/GeistVF.woff',
variable: '--font-geist-sans',
weight: '100 900',
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
src: './fonts/GeistMonoVF.woff',
variable: '--font-geist-mono',
weight: '100 900',
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Create Next App',
description: 'Generated by create next app',
};

export default function RootLayout({
Expand All @@ -26,9 27,9 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased font-[family-name:var(--font-geist-sans)]`}
>
{children}
<AntdRegistry>{children}</AntdRegistry>
</body>
</html>
);
Expand Down
109 changes: 12 additions & 97 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,101 1,16 @@
import Image from "next/image";
import { getCurrentSession } from '@/auth/sessions';
import { redirect } from 'next/navigation';

export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
export default async function Home() {
const { user } = await getCurrentSession();
if (user === null) {
return redirect('/login');
}

<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
return (
<>
Hello page
{user.name}
</>
);
}
19 changes: 19 additions & 0 deletions auth/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,19 @@
'use server';

import { redirect } from 'next/navigation';
import { deleteSessionTokenCookie } from './cookies';
import { getCurrentSession, invalidateSession } from './sessions';

async function signOut() {
const { session } = await getCurrentSession();
if (!session) {
return {
error: 'Unauthorized',
};
}

await invalidateSession(session.id);
await deleteSessionTokenCookie();

return redirect('/login');
}
4 changes: 4 additions & 0 deletions auth/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,4 @@
export const COOKIE_SESSION = 'session';

export const COOKIE_GOOGLE_OAUTH_STATE = 'google_oauth_state';
export const COOKIE_GOOGLE_CODE_VERIFIER = 'google_code_verifier';
27 changes: 27 additions & 0 deletions auth/cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,27 @@
import { cookies } from 'next/headers';
import { COOKIE_SESSION } from './constants';

export async function setSessionTokenCookie(
token: string,
expiresAt: Date
): Promise<void> {
const cookieStore = await cookies();
cookieStore.set(COOKIE_SESSION, token, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
expires: expiresAt,
path: '/',
});
}

export async function deleteSessionTokenCookie(): Promise<void> {
const cookieStore = await cookies();
cookieStore.set(COOKIE_SESSION, '', {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: 0,
path: '/',
});
}
8 changes: 8 additions & 0 deletions auth/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,8 @@
import { env } from '@/env';
import { Google } from 'arctic';

export const google = new Google(
env.GOOGLE_CLIENT_ID,
env.GOOGLE_CLIENT_SECRET,
'http://localhost:3000/login/google/callback'
);
Loading

0 comments on commit fe61774

Please sign in to comment.