mirror of https://github.com/TZGyn/shortener
add google oauth
parent
feaf0f3be6
commit
0cd7de59a1
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "user" ALTER COLUMN "password" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ADD COLUMN "google_id" varchar(255);
|
||||||
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { SVGAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
|
type $$Props = SVGAttributes<SVGElement>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg role="img" viewBox="0 0 24 24" {...$$restProps}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
||||||
|
</svg>
|
||||||
@ -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())
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue