Skip to content

Commit

Permalink
feat: ✨ implement google oauth (#27)
Browse files Browse the repository at this point in the history
* feat: ✨ [SEMI-STABLE] implement google oauth

* chore: 🔧 cleanup code

* feat: ✨ use sub prop for id

* style: 🎨 move google button to layout

* fix: 🐛 handle empty name fields

* chore: 🔧 [STABLE] remove unused deps

* chore: 🔧 remove unused fields

* feat: ✨ use email as userId

* fix: 🐛 add console log to try block
  • Loading branch information
KevinWu098 authored Feb 9, 2024
1 parent 2b7f669 commit 9cdb339
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ TBA

If you need credentials for the `.env` file, contact the project lead ([Minh](https://github.com/minhxNguyen7/)).

After changes to the .env file, run `pnpm run check` to update SvelteKit's auto-generated environment variable types.
After changes to the .env file, run `pnpm run check` to update SvelteKit's auto-generated environment variable types.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"dependencies": {
"@iconify/json": "^2.2.160",
"@lucia-auth/adapter-prisma": "3.0.2",
"@lucia-auth/oauth": "^3.5.3",
"@prisma/client": "5.6.0",
"lucia": "2.7.4",
"svelty-picker": "^5.2.1",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified prisma/dev.db
Binary file not shown.
2 changes: 2 additions & 0 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ export const handle: Handle = async ({ event, resolve }) => {
if (event.locals?.auth) {
const session = await event.locals.auth.validate();
const user = session?.user;

if (user) {
event.locals.user = user;
}

if (event.route.id?.startsWith("/(protected)")) {
if (!user) throw redirect(302, "/auth");
// if (!user.verified) throw redirect(302, "/auth/verify/email");
Expand Down
19 changes: 19 additions & 0 deletions src/lib/server/lucia.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { prisma } from "@lucia-auth/adapter-prisma";
import { google } from "@lucia-auth/oauth/providers";
import { lucia } from "lucia";
import { sveltekit } from "lucia/middleware";

import { dev } from "$app/environment";
import {
GOOGLE_OAUTH_CLIENT_ID,
GOOGLE_OAUTH_CLIENT_SECRET,
GOOGLE_OAUTH_REDIRECT_URI,
} from "$env/static/private";
import { prisma as client } from "$lib/server/prisma";

export const auth = lucia({
Expand All @@ -23,8 +29,21 @@ export const auth = lucia({
verified: data.verified,
receiveEmail: data.receiveEmail,
token: data.token,

googleId: data.google_id,
username: data.username,
};
},
});

export const googleAuth = google(auth, {
clientId: GOOGLE_OAUTH_CLIENT_ID!,
clientSecret: GOOGLE_OAUTH_CLIENT_SECRET!,
redirectUri: GOOGLE_OAUTH_REDIRECT_URI!,
scope: [
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
],
});

export type Auth = typeof auth;
9 changes: 9 additions & 0 deletions src/routes/auth/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
</div>
{/if}

<div class="mb-4">
<a
href="/auth/login/google"
class="mx-auto flex w-fit rounded-md bg-blue-500 p-2 font-semibold text-white"
>
Continue with Google
</a>
</div>

<TabGroup justify="justify-center">
<Tab bind:group={tabSet} name="signInTab" value={"signIn"}>Sign In</Tab>
<Tab bind:group={tabSet} name="signUpTab" value={"signUp"}>Sign Up</Tab>
Expand Down
31 changes: 31 additions & 0 deletions src/routes/auth/login/google/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { dev } from "$app/environment";
import { googleAuth } from "$lib/server/lucia";

export const GET = async ({ cookies, locals }) => {
const session = await locals.auth.validate();

if (session) {
return new Response(null, {
status: 302,
headers: {
Location: "/",
},
});
}
const [url, state] = await googleAuth.getAuthorizationUrl();

// Store state.
cookies.set("google_oauth_state", state, {
httpOnly: true,
secure: !dev,
path: "/",
maxAge: 30 * 24 * 60 * 60,
});

return new Response(null, {
status: 302,
headers: {
Location: url.toString(),
},
});
};
114 changes: 114 additions & 0 deletions src/routes/auth/login/google/callback/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { OAuthRequestError } from "@lucia-auth/oauth";
import type { GoogleUser } from "@lucia-auth/oauth/providers";

import { auth, googleAuth } from "$lib/server/lucia";

const getUser = async (googleUser: GoogleUser) => {
if (!googleUser.email) {
return null;
}

try {
const dbUser = await auth.getUser(googleUser.email);
if (dbUser) {
return dbUser;
}
} catch (error) {
/* If a user cannot be found, an error is raised and caught here. */
console.log("User not found in database", error);
}

const token = crypto.randomUUID();
const user = await auth.createUser({
userId: googleUser.email.toLowerCase(),
key: {
providerId: "google",
providerUserId: googleUser.email.toLowerCase(),
password: null,
},
attributes: {
email: googleUser.email.toLowerCase(),
firstName: googleUser.given_name ?? "",
lastName: googleUser.family_name ?? "",
// role: "USER",
verified: false,
receiveEmail: true,
token: token,
},
});

return user;
};

export const GET = async ({ url, cookies, locals }) => {
/**
* Check for a session. if it exists,
* redirect to a page of your liking.
*/
const session = await locals.auth.validate();
if (session) {
return new Response(null, {
status: 302,
headers: {
Location: "/auth",
},
});
}

/**
* Validate state of the request.
*/
const storedState = cookies.get("google_oauth_state") ?? null;
const state = url.searchParams.get("state");
const code = url.searchParams.get("code");
if (!storedState || !state || storedState !== state || !code) {
return new Response(null, {
status: 400,
});
}

try {
const { googleUser } = await googleAuth.validateCallback(code);
const user = await getUser(googleUser);

if (!user) {
/**
* You should probably redirect the user to a page and show a
* message that the account could not be created.
*
* This is a very rare case, but it can happen.
*/
return new Response(null, {
status: 500,
});
}

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

locals.auth.setSession(session);

return new Response(null, {
status: 302,
headers: {
Location: "/auth",
},
});
} catch (e) {
console.log(e);

// Invalid code.
if (e instanceof OAuthRequestError) {
return new Response(null, {
status: 400,
});
}

// All other errors.
return new Response(null, {
status: 500,
});
}
};

0 comments on commit 9cdb339

Please sign in to comment.