mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-06-25 08:45:22 -04:00
feat: add Sign In with Google
Adds Google OAuth alongside existing Apple and email/password auth. Follows the same patterns as Apple Sign-In: state cookie for CSRF, anonymousId migration, and user linking by email. Key differences: Google callback is a GET redirect (sameSite: lax) and uses a static client secret instead of a signed JWT. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,23 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/auth/google">
|
||||
<input type="hidden" name="anonymousId" value={anonymousId} />
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-white text-black rounded-md hover:bg-gray-100 transition-colors font-medium mt-3"
|
||||
data-umami-event="Sign in with Google"
|
||||
>
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="flex items-center my-4">
|
||||
<div class="flex-1 border-t border-white/20"></div>
|
||||
<span class="px-3 text-sm text-white/60">or</span>
|
||||
|
||||
@@ -99,6 +99,7 @@ export async function createUser(anonymousId: string, email: string, passwordHas
|
||||
email,
|
||||
passwordHash,
|
||||
appleId: null,
|
||||
googleId: null,
|
||||
firstName: firstName || null,
|
||||
lastName: lastName || null,
|
||||
isPrivate: false
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function validateSessionToken(token: string) {
|
||||
const [result] = await db
|
||||
.select({
|
||||
// Adjust user table here to tweak returned data
|
||||
user: { id: table.user.id, email: table.user.email, firstName: table.user.firstName, lastName: table.user.lastName, appleId: table.user.appleId },
|
||||
user: { id: table.user.id, email: table.user.email, firstName: table.user.firstName, lastName: table.user.lastName, appleId: table.user.appleId, googleId: table.user.googleId },
|
||||
session: table.session
|
||||
})
|
||||
.from(table.session)
|
||||
@@ -99,6 +99,7 @@ export async function createUser(anonymousId: string, email: string, passwordHas
|
||||
email,
|
||||
passwordHash,
|
||||
appleId: null,
|
||||
googleId: null,
|
||||
firstName: firstName || null,
|
||||
lastName: lastName || null,
|
||||
isPrivate: false
|
||||
@@ -117,6 +118,11 @@ export async function getUserByAppleId(appleId: string) {
|
||||
return user || null;
|
||||
}
|
||||
|
||||
export async function getUserByGoogleId(googleId: string) {
|
||||
const [user] = await db.select().from(table.user).where(eq(table.user.googleId, googleId));
|
||||
return user || null;
|
||||
}
|
||||
|
||||
export async function migrateAnonymousStats(anonymousId: string | undefined, userId: string) {
|
||||
if (!anonymousId || anonymousId === userId) return;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export const user = sqliteTable('user', {
|
||||
email: text('email').unique(),
|
||||
passwordHash: text('password_hash'),
|
||||
appleId: text('apple_id').unique(),
|
||||
googleId: text('google_id').unique(),
|
||||
isPrivate: integer('is_private', { mode: 'boolean' }).default(false)
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
export function getGoogleAuthUrl(state: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: Bun.env.GOOGLE_CLIENT_ID!,
|
||||
redirect_uri: `${Bun.env.PUBLIC_SITE_URL}/auth/google/callback`,
|
||||
response_type: 'code',
|
||||
scope: 'openid email profile',
|
||||
state,
|
||||
access_type: 'online',
|
||||
prompt: 'select_account'
|
||||
});
|
||||
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export async function exchangeGoogleCode(
|
||||
code: string,
|
||||
redirectUri: string
|
||||
): Promise<{
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
id_token: string;
|
||||
scope: string;
|
||||
}> {
|
||||
const params = new URLSearchParams({
|
||||
client_id: Bun.env.GOOGLE_CLIENT_ID!,
|
||||
client_secret: Bun.env.GOOGLE_CLIENT_SECRET!,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri
|
||||
});
|
||||
|
||||
const response = await fetch(GOOGLE_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: params.toString()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
throw new Error(`Google token exchange failed: ${errText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Google's id_token JWT payload without signature verification.
|
||||
* Safe because the token is received directly from Google's token endpoint over TLS.
|
||||
*/
|
||||
export function decodeGoogleIdToken(idToken: string): {
|
||||
sub: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
name?: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
} {
|
||||
const [, payloadB64] = idToken.split('.');
|
||||
const padded = payloadB64 + '='.repeat((4 - (payloadB64.length % 4)) % 4);
|
||||
const payload = JSON.parse(atob(padded.replace(/-/g, '+').replace(/_/g, '/')));
|
||||
return payload;
|
||||
}
|
||||
Reference in New Issue
Block a user