From 0cd7de59a1990d635f79af9a6f8ad65bfc6655b5 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Thu, 22 Aug 2024 01:32:06 +0800 Subject: [PATCH] add google oauth --- frontend/.env.example | 4 + frontend/bun.lockb | Bin 212702 -> 213066 bytes frontend/drizzle/0016_steady_darkstar.sql | 2 + frontend/drizzle/meta/0016_snapshot.json | 419 ++++++++++++++++++ frontend/drizzle/meta/_journal.json | 7 + frontend/package.json | 133 +++--- .../src/lib/components/icons/google.svelte | 11 + frontend/src/lib/db/schema.ts | 3 +- frontend/src/lib/server/auth.ts | 9 + .../(auth)/login/(components)/form.svelte | 2 +- .../src/routes/(auth)/login/+page.server.ts | 12 + frontend/src/routes/(auth)/login/+page.svelte | 47 +- .../src/routes/(auth)/login/google/+server.ts | 36 ++ .../(auth)/login/google/callback/+server.ts | 127 ++++++ .../src/routes/(auth)/signup/+page.server.ts | 7 + 15 files changed, 743 insertions(+), 76 deletions(-) create mode 100644 frontend/drizzle/0016_steady_darkstar.sql create mode 100644 frontend/drizzle/meta/0016_snapshot.json create mode 100644 frontend/src/lib/components/icons/google.svelte create mode 100644 frontend/src/routes/(auth)/login/google/+server.ts create mode 100644 frontend/src/routes/(auth)/login/google/callback/+server.ts diff --git a/frontend/.env.example b/frontend/.env.example index 7aa65a1..e8a373f 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -24,3 +24,7 @@ PRIVATE_FLYIO_IPV6= # Resend config (if you have a Resend hosting provider) PRIVATE_RESEND_API_KEY= + +# Google OAuth +PRIVATE_GOOGLE_CLIENT_ID= +PRIVATE_GOOGLE_CLIENT_SECRET= \ No newline at end of file diff --git a/frontend/bun.lockb b/frontend/bun.lockb index d4a3e7691b77ef4ea026cf222cc9c3ce2839b4ea..d9aea3b68d839fb071e5724e84814111b761f869 100755 GIT binary patch delta 16438 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 RlyG>tv@GpW$7Okke*=iY;KKj_ delta 16309 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 statement-breakpoint +ALTER TABLE "user" ADD COLUMN "google_id" varchar(255); \ No newline at end of file diff --git a/frontend/drizzle/meta/0016_snapshot.json b/frontend/drizzle/meta/0016_snapshot.json new file mode 100644 index 0000000..f72f870 --- /dev/null +++ b/frontend/drizzle/meta/0016_snapshot.json @@ -0,0 +1,419 @@ +{ + "id": "9ba9c83e-b94c-42c5-84c9-010050810026", + "prevId": "ac377af2-68ea-4c36-a69c-4cc57a04a523", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.email_verification_token": { + "name": "email_verification_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "qr_background": { + "name": "qr_background", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#ffffff'" + }, + "qr_foreground": { + "name": "qr_foreground", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#000000'" + }, + "domain_status": { + "name": "domain_status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'verified'" + }, + "enable_custom_domain": { + "name": "enable_custom_domain", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "custom_ip": { + "name": "custom_ip", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain_id": { + "name": "custom_domain_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.setting": { + "name": "setting", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "qr_background": { + "name": "qr_background", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "qr_foreground": { + "name": "qr_foreground", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.shortener": { + "name": "shortener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "link": { + "name": "link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "ios": { + "name": "ios", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ios_link": { + "name": "ios_link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "android": { + "name": "android", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "android_link": { + "name": "android_link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shortener_code_unique": { + "name": "shortener_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + } + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()" + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.visitor": { + "name": "visitor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "shortener_id": { + "name": "shortener_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "country_code": { + "name": "country_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "device_vendor": { + "name": "device_vendor", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "os": { + "name": "os", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "browser": { + "name": "browser", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/frontend/drizzle/meta/_journal.json b/frontend/drizzle/meta/_journal.json index e0f9ba1..a853288 100644 --- a/frontend/drizzle/meta/_journal.json +++ b/frontend/drizzle/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1723360689804, "tag": "0015_tidy_electro", "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1724250058955, + "tag": "0016_steady_darkstar", + "breakpoints": true } ] } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 09e79ea..89aeda9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,68 +1,69 @@ { - "name": "link-shortener-svelte", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --plugin-search-dir . --check .", - "format": "prettier --write .", - "migrate": "bun ./src/lib/db/migrate.ts", - "db:push": "drizzle-kit push", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate" - }, - "devDependencies": { - "@sveltejs/adapter-node": "^2.0.0", - "@sveltejs/kit": "^2.5.5", - "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@types/pg": "^8.11.6", - "autoprefixer": "^10.4.14", - "bun-types": "^1.0.11", - "drizzle-kit": "^0.23.1", - "lucia": "^3.1.1", - "postcss": "^8.4.24", - "postcss-load-config": "^4.0.1", - "prettier": "^3.1.0", - "prettier-plugin-svelte": "^3.1.0", - "prettier-plugin-tailwindcss": "^0.5.7", - "svelte": "^4.2.17", - "svelte-adapter-bun": "^0.5.1", - "svelte-check": "^3.4.3", - "tailwindcss": "^3.3.2", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^5.2.8" - }, - "type": "module", - "dependencies": { - "@lucia-auth/adapter-drizzle": "^1.0.7", - "@prgm/sveltekit-progress-bar": "^2.0.0", - "@types/he": "^1.2.3", - "apexcharts": "^3.44.0", - "bits-ui": "^0.21.13", - "clsx": "^2.0.0", - "cmdk-sv": "^0.0.13", - "drizzle-orm": "^0.32.1", - "formsnap": "^1.0.0", - "he": "^1.2.0", - "lucide-svelte": "^0.418.0", - "mode-watcher": "^0.1.2", - "nanoid": "^5.0.3", - "node-html-parser": "^6.1.12", - "oslo": "^1.2.0", - "pg": "^8.11.5", - "postgres": "^3.4.3", - "qr-code-styling": "^1.6.0-rc.1", - "resend": "^3.4.0", - "svelte-sonner": "^0.3.10", - "sveltekit-superforms": "^2.12.2", - "tailwind-merge": "^2.0.0", - "tailwind-variants": "^0.1.18", - "vaul-svelte": "^0.3.1", - "zod": "^3.22.4" - } + "name": "link-shortener-svelte", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check .", + "format": "prettier --write .", + "migrate": "bun ./src/lib/db/migrate.ts", + "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^2.0.0", + "@sveltejs/kit": "^2.5.5", + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@types/pg": "^8.11.6", + "autoprefixer": "^10.4.14", + "bun-types": "^1.0.11", + "drizzle-kit": "^0.23.1", + "lucia": "^3.1.1", + "postcss": "^8.4.24", + "postcss-load-config": "^4.0.1", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.1.0", + "prettier-plugin-tailwindcss": "^0.5.7", + "svelte": "^4.2.17", + "svelte-adapter-bun": "^0.5.1", + "svelte-check": "^3.4.3", + "tailwindcss": "^3.3.2", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^5.2.8" + }, + "type": "module", + "dependencies": { + "@lucia-auth/adapter-drizzle": "^1.0.7", + "@prgm/sveltekit-progress-bar": "^2.0.0", + "@types/he": "^1.2.3", + "apexcharts": "^3.44.0", + "arctic": "^1.9.2", + "bits-ui": "^0.21.13", + "clsx": "^2.0.0", + "cmdk-sv": "^0.0.13", + "drizzle-orm": "^0.32.1", + "formsnap": "^1.0.0", + "he": "^1.2.0", + "lucide-svelte": "^0.418.0", + "mode-watcher": "^0.1.2", + "nanoid": "^5.0.3", + "node-html-parser": "^6.1.12", + "oslo": "^1.2.0", + "pg": "^8.11.5", + "postgres": "^3.4.3", + "qr-code-styling": "^1.6.0-rc.1", + "resend": "^3.4.0", + "svelte-sonner": "^0.3.10", + "sveltekit-superforms": "^2.12.2", + "tailwind-merge": "^2.0.0", + "tailwind-variants": "^0.1.18", + "vaul-svelte": "^0.3.1", + "zod": "^3.22.4" + } } diff --git a/frontend/src/lib/components/icons/google.svelte b/frontend/src/lib/components/icons/google.svelte new file mode 100644 index 0000000..580ae4b --- /dev/null +++ b/frontend/src/lib/components/icons/google.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/lib/db/schema.ts b/frontend/src/lib/db/schema.ts index 6ea307b..880209c 100644 --- a/frontend/src/lib/db/schema.ts +++ b/frontend/src/lib/db/schema.ts @@ -14,8 +14,9 @@ export const user = pgTable('user', { uuid: uuid('uuid').defaultRandom(), email_verified: boolean('email_verified').notNull().default(false), email: varchar('email', { length: 255 }).notNull().unique(), + googleId: varchar('google_id', { length: 255 }), username: varchar('username', { length: 255 }), - password: varchar('password', { length: 255 }).notNull(), + password: varchar('password', { length: 255 }), createdAt: timestamp('created_at', { mode: 'string' }) .defaultNow() .notNull(), diff --git a/frontend/src/lib/server/auth.ts b/frontend/src/lib/server/auth.ts index ebf30d5..0994823 100644 --- a/frontend/src/lib/server/auth.ts +++ b/frontend/src/lib/server/auth.ts @@ -3,6 +3,8 @@ import { db } from '$lib/db' import { session, user } from '$lib/db/schema' import { type User } from '$lib/db/types' import { Lucia } from 'lucia' +import { Google } from 'arctic' +import { env } from '$env/dynamic/private' declare module 'lucia' { interface Register { @@ -23,3 +25,10 @@ export const lucia = new Lucia(adapter, { } }, }) + +export const google = new Google( + env.PRIVATE_GOOGLE_CLIENT_ID, + env.PRIVATE_GOOGLE_CLIENT_SECRET, + (env.APP_ENV === 'prod' ? env.ORIGIN : 'http://localhost:5173') + + '/login/google/callback', +) diff --git a/frontend/src/routes/(auth)/login/(components)/form.svelte b/frontend/src/routes/(auth)/login/(components)/form.svelte index 37235fa..b60adc4 100644 --- a/frontend/src/routes/(auth)/login/(components)/form.svelte +++ b/frontend/src/routes/(auth)/login/(components)/form.svelte @@ -56,6 +56,6 @@ {#if $submitting} {/if} - Login + Login with Email diff --git a/frontend/src/routes/(auth)/login/+page.server.ts b/frontend/src/routes/(auth)/login/+page.server.ts index 60ba347..346b148 100644 --- a/frontend/src/routes/(auth)/login/+page.server.ts +++ b/frontend/src/routes/(auth)/login/+page.server.ts @@ -31,6 +31,18 @@ export const actions: Actions = { const user = users[0] + if (user.googleId) { + return setError( + form, + 'email', + 'This email is detected on Google login, please login via Google', + ) + } + + if (!user.password) { + return setError(form, 'email', 'Invalid credentials') + } + const matchPassword = user && (await Bun.password.verify(form.data.password, user.password)) diff --git a/frontend/src/routes/(auth)/login/+page.svelte b/frontend/src/routes/(auth)/login/+page.svelte index 3018a8a..447f498 100644 --- a/frontend/src/routes/(auth)/login/+page.svelte +++ b/frontend/src/routes/(auth)/login/+page.svelte @@ -3,39 +3,70 @@ import Form from './(components)/form.svelte' import type { PageData } from './$types' + import { Button } from '$lib/components/ui/button' + import { LoaderIcon } from 'lucide-svelte' + import Google from '$lib/components/icons/google.svelte' + import { goto } from '$app/navigation' export let data: PageData + + let isLoading = false + + const loginGoogle = async () => { + // isLoading = true + // await fetch('/login/google') + // isLoading = false + window.location.href = '/login/google' + }
-
+ class="container relative h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0"> +
+ class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">

Login to your account

-

+

Enter your email below to login to your account

+ +
+
+ +
+
+ + Or continue with + +
+
-

+

Don't Have An Account? Signup{' '} + class="hover:text-primary underline underline-offset-4"> Here

diff --git a/frontend/src/routes/(auth)/login/google/+server.ts b/frontend/src/routes/(auth)/login/google/+server.ts new file mode 100644 index 0000000..2b33d55 --- /dev/null +++ b/frontend/src/routes/(auth)/login/google/+server.ts @@ -0,0 +1,36 @@ +import { google } from '$lib/server/auth' +import { generateState, generateCodeVerifier } from 'arctic' +import { serializeCookie } from 'oslo/cookie' +import { env } from '$env/dynamic/private' +import { redirect } from '@sveltejs/kit' + +export const GET = async (event) => { + const state = generateState() + const codeVerifier = generateCodeVerifier() + + const url = await google.createAuthorizationURL( + state, + codeVerifier, + { + scopes: ['email', 'profile'], + }, + ) + + event.cookies.set('google_oauth_state', state, { + httpOnly: true, + secure: env.APP_ENV === 'prod', + maxAge: 60 * 10, // 10 minutes + path: '/', + sameSite: 'lax', + }) + + event.cookies.set('google_oauth_code_verifier', codeVerifier, { + httpOnly: true, + secure: env.APP_ENV === 'prod', + maxAge: 60 * 10, // 10 minutes + path: '/', + sameSite: 'lax', + }) + + return redirect(302, url.toString()) +} diff --git a/frontend/src/routes/(auth)/login/google/callback/+server.ts b/frontend/src/routes/(auth)/login/google/callback/+server.ts new file mode 100644 index 0000000..85ff945 --- /dev/null +++ b/frontend/src/routes/(auth)/login/google/callback/+server.ts @@ -0,0 +1,127 @@ +import { OAuth2RequestError } from 'arctic' +import { google, lucia } from '$lib/server/auth' +import { db } from '$lib/db' +import { user } from '$lib/db/schema' +import { eq } from 'drizzle-orm' + +interface GoogleUser { + sub: string // Unique identifier for the user + name: string // Full name of the user + email: string // Email address of the user +} + +export async function GET(event) { + const code = event.url.searchParams.get('code') + const state = event.url.searchParams.get('state') + const codeVerifier = event.cookies.get('google_oauth_code_verifier') + const storedState = event.cookies.get('google_oauth_state') ?? null + + if ( + !code || + !state || + !storedState || + !codeVerifier || + state !== storedState + ) { + return new Response(null, { + status: 400, + }) + } + + try { + const tokens = await google.validateAuthorizationCode( + code, + codeVerifier, + ) + const googleUserResponse = await fetch( + 'https://www.googleapis.com/oauth2/v3/userinfo', + { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }, + ) + const googleUser: GoogleUser = await googleUserResponse.json() + + const existingGoogleUser = await db.query.user.findFirst({ + where: (user, { eq }) => eq(user.googleId, googleUser.sub), + }) + + if (existingGoogleUser) { + const session = await lucia.createSession( + existingGoogleUser.id, + {}, + ) + const sessionCookie = lucia.createSessionCookie(session.id) + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes, + }) + + return new Response(null, { + status: 302, + headers: { + Location: '/dashboard', + }, + }) + } + + const existingUser = await db.query.user.findFirst({ + where: (user, { eq }) => eq(user.email, googleUser.email), + }) + + if (existingUser) { + const updateUser = await db + .update(user) + .set({ + email_verified: true, + password: null, + username: googleUser.name, + }) + .where(eq(user.id, existingUser.id)) + .returning() + + const newUser = updateUser[0] + + const session = await lucia.createSession(newUser.id, {}) + const sessionCookie = lucia.createSessionCookie(session.id) + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes, + }) + } else { + const insertUser = await db + .insert(user) + .values({ + email: googleUser.email, // Using email as username + email_verified: true, + googleId: googleUser.sub, + username: googleUser.name, // Name field may not always be present, handle accordingly + }) + .returning() + + const newUser = insertUser[0] + + const session = await lucia.createSession(newUser.id, {}) + const sessionCookie = lucia.createSessionCookie(session.id) + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes, + }) + } + + return new Response(null, { + status: 302, + headers: { + Location: '/dashboard', + }, + }) + } catch (e) { + return new Response(null, { + status: 302, + headers: { + Location: '/login', + }, + }) + } +} diff --git a/frontend/src/routes/(auth)/signup/+page.server.ts b/frontend/src/routes/(auth)/signup/+page.server.ts index 62181ad..4179452 100644 --- a/frontend/src/routes/(auth)/signup/+page.server.ts +++ b/frontend/src/routes/(auth)/signup/+page.server.ts @@ -37,6 +37,13 @@ export const actions: Actions = { const user = users[0] if (user) { + if (user.googleId) { + return setError( + form, + 'email', + 'This email is detected on Google login, please login via Google', + ) + } return setError(form, 'email', 'Email Already Exist') }