Skip to content

Commit f92c3c0

Browse files
feat: add Google OAuth login alongside GitHub
Create unified /login page with GitHub and Google sign-in options. Refactor auth callbacks into provider-specific routes (/api/auth/callback/{github,google}). Update all login redirects to use new /login page with return_to parameter. Add state validation and CSRF protection via auth_state cookie. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 7b7859e commit f92c3c0

File tree

9 files changed

+345
-15
lines changed

9 files changed

+345
-15
lines changed

src/routes/+page.svelte

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,12 @@
251251
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" /><rect x="14" y="14" width="7" height="7" /><rect x="3" y="14" width="7" height="7" /></svg>
252252
Dashboard
253253
</a>
254-
{:else}
255-
<a href="/api/auth/login">
256-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
257-
Custom Configs
258-
</a>
259-
{/if}
254+
{:else}
255+
<a href="/login">
256+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
257+
Custom Configs
258+
</a>
259+
{/if}
260260
<a href="/docs">
261261
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
262262
Docs

src/routes/[username]/[slug]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
try {
5858
const authCheck = await fetch('/api/user');
5959
if (!authCheck.ok) {
60-
window.location.href = '/api/auth/login';
60+
window.location.href = `/login?return_to=${encodeURIComponent(window.location.pathname)}`;
6161
return;
6262
}
6363

src/routes/api/auth/callback/+server.ts renamed to src/routes/api/auth/callback/github/+server.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@ export const GET: RequestHandler = async ({ url, platform, cookies }) => {
1010
if (!env) throw new Error('Platform env not available');
1111

1212
const code = url.searchParams.get('code');
13+
const state = url.searchParams.get('state');
14+
const savedState = cookies.get('auth_state');
15+
1316
if (!code) {
14-
redirect(302, `${env.APP_URL}?error=no_code`);
17+
redirect(302, '/login?error=no_code');
18+
}
19+
20+
if (state !== savedState) {
21+
redirect(302, '/login?error=invalid_state');
1522
}
1623

1724
const tokenResponse = await fetch(GITHUB_TOKEN_URL, {
@@ -94,5 +101,9 @@ export const GET: RequestHandler = async ({ url, platform, cookies }) => {
94101
maxAge: thirtyDays
95102
});
96103

97-
redirect(302, '/dashboard');
104+
const returnTo = cookies.get('auth_return_to') || '/dashboard';
105+
cookies.delete('auth_state', { path: '/' });
106+
cookies.delete('auth_return_to', { path: '/' });
107+
108+
redirect(302, returnTo);
98109
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { redirect } from '@sveltejs/kit';
2+
import type { RequestHandler } from './$types';
3+
import { signToken, generateId, slugify } from '$lib/server/auth';
4+
5+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
6+
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
7+
8+
export const GET: RequestHandler = async ({ url, platform, cookies }) => {
9+
const env = platform?.env;
10+
if (!env) throw new Error('Platform env not available');
11+
12+
const code = url.searchParams.get('code');
13+
const state = url.searchParams.get('state');
14+
const savedState = cookies.get('auth_state');
15+
16+
if (!code) {
17+
redirect(302, '/login?error=no_code');
18+
}
19+
20+
if (state !== savedState) {
21+
redirect(302, '/login?error=invalid_state');
22+
}
23+
24+
const tokenResponse = await fetch(GOOGLE_TOKEN_URL, {
25+
method: 'POST',
26+
headers: { 'Content-Type': 'application/json' },
27+
body: JSON.stringify({
28+
client_id: env.GOOGLE_CLIENT_ID,
29+
client_secret: env.GOOGLE_CLIENT_SECRET,
30+
code,
31+
redirect_uri: `${env.APP_URL}/api/auth/callback/google`,
32+
grant_type: 'authorization_code'
33+
})
34+
});
35+
36+
const tokenData = await tokenResponse.json();
37+
if (tokenData.error || !tokenData.access_token) {
38+
redirect(302, '/login?error=token_failed');
39+
}
40+
41+
const userResponse = await fetch(GOOGLE_USERINFO_URL, {
42+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
43+
});
44+
45+
const googleUser = await userResponse.json();
46+
if (!googleUser.id || !googleUser.email) {
47+
redirect(302, '/login?error=user_failed');
48+
}
49+
50+
const userId = `google_${googleUser.id}`;
51+
const reservedUsernames = ['openboot', 'admin', 'api', 'dashboard', 'install', 'login', 'logout', 'settings', 'help', 'support', 'docs', 'blog'];
52+
53+
let username = slugify(googleUser.email.split('@')[0]);
54+
if (reservedUsernames.includes(username.toLowerCase()) || username.length < 3) {
55+
username = `user-${googleUser.id.slice(-8)}`;
56+
}
57+
58+
const existingUser = await env.DB.prepare('SELECT username FROM users WHERE id = ?').bind(userId).first();
59+
if (existingUser) {
60+
username = (existingUser as { username: string }).username;
61+
}
62+
63+
await env.DB.prepare(
64+
`
65+
INSERT INTO users (id, username, email, avatar_url, updated_at)
66+
VALUES (?, ?, ?, ?, datetime('now'))
67+
ON CONFLICT(id) DO UPDATE SET
68+
email = excluded.email,
69+
avatar_url = excluded.avatar_url,
70+
updated_at = datetime('now')
71+
`
72+
)
73+
.bind(userId, username, googleUser.email, googleUser.picture || '')
74+
.run();
75+
76+
const existingConfig = await env.DB.prepare('SELECT id FROM configs WHERE user_id = ? AND slug = ?').bind(userId, 'default').first();
77+
78+
if (!existingConfig) {
79+
await env.DB.prepare(
80+
`
81+
INSERT INTO configs (id, user_id, slug, name, description, base_preset, packages)
82+
VALUES (?, ?, 'default', 'Default', 'My default configuration', 'developer', '[]')
83+
`
84+
)
85+
.bind(generateId(), userId)
86+
.run();
87+
}
88+
89+
const thirtyDays = 30 * 24 * 60 * 60;
90+
const token = await signToken({ userId, username, exp: Date.now() + thirtyDays * 1000 }, env.JWT_SECRET);
91+
92+
cookies.set('session', token, {
93+
path: '/',
94+
httpOnly: true,
95+
secure: true,
96+
sameSite: 'lax',
97+
maxAge: thirtyDays
98+
});
99+
100+
const returnTo = cookies.get('auth_return_to') || '/dashboard';
101+
cookies.delete('auth_state', { path: '/' });
102+
cookies.delete('auth_return_to', { path: '/' });
103+
104+
redirect(302, returnTo);
105+
};

src/routes/api/auth/login/+server.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,45 @@ import { redirect } from '@sveltejs/kit';
22
import type { RequestHandler } from './$types';
33

44
const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize';
5+
const GOOGLE_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
56

6-
export const GET: RequestHandler = async ({ platform }) => {
7+
export const GET: RequestHandler = async ({ url, platform, cookies }) => {
78
const env = platform?.env;
89
if (!env) throw new Error('Platform env not available');
910

11+
const provider = url.searchParams.get('provider') || 'github';
12+
const returnTo = url.searchParams.get('return_to') || '/dashboard';
13+
const state = crypto.randomUUID();
14+
15+
const cookieOptions = {
16+
path: '/',
17+
httpOnly: true,
18+
secure: true,
19+
sameSite: 'lax' as const,
20+
maxAge: 600
21+
};
22+
23+
cookies.set('auth_return_to', returnTo, cookieOptions);
24+
cookies.set('auth_state', state, cookieOptions);
25+
26+
if (provider === 'google') {
27+
const params = new URLSearchParams({
28+
client_id: env.GOOGLE_CLIENT_ID,
29+
redirect_uri: `${env.APP_URL}/api/auth/callback/google`,
30+
response_type: 'code',
31+
scope: 'openid email profile',
32+
state,
33+
access_type: 'online',
34+
prompt: 'select_account'
35+
});
36+
redirect(302, `${GOOGLE_AUTHORIZE_URL}?${params}`);
37+
}
38+
1039
const params = new URLSearchParams({
1140
client_id: env.GITHUB_CLIENT_ID,
12-
redirect_uri: `${env.APP_URL}/api/auth/callback`,
41+
redirect_uri: `${env.APP_URL}/api/auth/callback/github`,
1342
scope: 'read:user user:email',
14-
state: crypto.randomUUID()
43+
state
1544
});
1645

1746
redirect(302, `${GITHUB_AUTHORIZE_URL}?${params}`);

src/routes/cli-auth/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
2525
if (!$auth.user && !$auth.loading) {
2626
const returnTo = encodeURIComponent(`/cli-auth?code=${urlCode}`);
27-
window.location.href = `/api/auth/login?return_to=${returnTo}`;
27+
window.location.href = `/login?return_to=${returnTo}`;
2828
return;
2929
}
3030

src/routes/dashboard/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@
210210
onMount(async () => {
211211
await auth.check();
212212
if (!$auth.user && !$auth.loading) {
213-
goto('/api/auth/login');
213+
goto('/login?return_to=/dashboard');
214214
return;
215215
}
216216
await loadConfigs();

src/routes/docs/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
Dashboard
9898
</Button>
9999
{:else}
100-
<Button href="/api/auth/login" variant="secondary">
100+
<Button href="/login" variant="secondary">
101101
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
102102
<path
103103
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"

0 commit comments

Comments
 (0)