Gmail working
This commit is contained in:
58
src/lib/google.ts
Normal file
58
src/lib/google.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { google } from 'googleapis';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
|
const {
|
||||||
|
GOOGLE_CLIENT_ID,
|
||||||
|
GOOGLE_CLIENT_SECRET
|
||||||
|
} = env;
|
||||||
|
|
||||||
|
/* NEW REDIRECT — must match Google Cloud OAuth settings */
|
||||||
|
const REDIRECT_URI = 'http://localhost:5173/private/api/gmail';
|
||||||
|
|
||||||
|
export const scopes = ['https://www.googleapis.com/auth/gmail.send'];
|
||||||
|
|
||||||
|
export function getOAuthClient() {
|
||||||
|
return new google.auth.OAuth2(
|
||||||
|
GOOGLE_CLIENT_ID,
|
||||||
|
GOOGLE_CLIENT_SECRET,
|
||||||
|
REDIRECT_URI
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAuthUrl() {
|
||||||
|
return getOAuthClient().generateAuthUrl({
|
||||||
|
access_type: 'offline',
|
||||||
|
prompt: 'consent',
|
||||||
|
scope: scopes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exchangeCodeForTokens(code: string) {
|
||||||
|
const { tokens } = await getOAuthClient().getToken(code);
|
||||||
|
if (!tokens.refresh_token) throw new Error('No refresh_token returned');
|
||||||
|
return tokens.refresh_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendGmail(
|
||||||
|
refreshToken: string,
|
||||||
|
{ to, subject, text }: { to: string; subject: string; text: string }
|
||||||
|
) {
|
||||||
|
const oauth = getOAuthClient();
|
||||||
|
oauth.setCredentials({ refresh_token: refreshToken });
|
||||||
|
|
||||||
|
const gmail = google.gmail({ version: 'v1', auth: oauth });
|
||||||
|
const raw = Buffer
|
||||||
|
.from(
|
||||||
|
[`To: ${to}`,
|
||||||
|
'Content-Type: text/plain; charset="UTF-8"',
|
||||||
|
'Content-Transfer-Encoding: 7bit',
|
||||||
|
`Subject: ${subject}`,
|
||||||
|
'',
|
||||||
|
text].join('\n'))
|
||||||
|
.toString('base64url');
|
||||||
|
|
||||||
|
await gmail.users.messages.send({
|
||||||
|
userId: 'me',
|
||||||
|
requestBody: { raw }
|
||||||
|
});
|
||||||
|
}
|
||||||
64
src/routes/private/api/gmail/+server.ts
Normal file
64
src/routes/private/api/gmail/+server.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { json, redirect } from '@sveltejs/kit';
|
||||||
|
import {
|
||||||
|
createAuthUrl,
|
||||||
|
exchangeCodeForTokens,
|
||||||
|
sendGmail,
|
||||||
|
getOAuthClient
|
||||||
|
} from '$lib/google';
|
||||||
|
|
||||||
|
/* ───────────── GET ───────────── */
|
||||||
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
|
/* 1. /private/api/gmail?action=auth → 302 to Google */
|
||||||
|
if (url.searchParams.get('action') === 'auth') {
|
||||||
|
throw redirect(302, createAuthUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Google callback /private/api/gmail?code=XXXX */
|
||||||
|
const code = url.searchParams.get('code');
|
||||||
|
if (code) {
|
||||||
|
try {
|
||||||
|
const refreshToken = await exchangeCodeForTokens(code);
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<script>
|
||||||
|
localStorage.setItem('gmail_refresh_token', ${JSON.stringify(refreshToken)});
|
||||||
|
location = '/private/creator';
|
||||||
|
</script>`;
|
||||||
|
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
|
||||||
|
} catch (err) {
|
||||||
|
return new Response((err as Error).message, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Bad request', { status: 400 });
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ───────────── POST ───────────── */
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
const { action, refreshToken, to, subject, text } = await request.json();
|
||||||
|
|
||||||
|
/* send e-mail */
|
||||||
|
if (action === 'send') {
|
||||||
|
if (!refreshToken) return new Response('Missing token', { status: 401 });
|
||||||
|
try {
|
||||||
|
await sendGmail(refreshToken, { to, subject, text });
|
||||||
|
return json({ ok: true });
|
||||||
|
} catch (err) {
|
||||||
|
return new Response((err as Error).message, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* revoke token */
|
||||||
|
if (action === 'revoke') {
|
||||||
|
if (!refreshToken) return new Response('Missing token', { status: 401 });
|
||||||
|
try {
|
||||||
|
await getOAuthClient().revokeToken(refreshToken);
|
||||||
|
return json({ ok: true });
|
||||||
|
} catch (err) {
|
||||||
|
return new Response((err as Error).message, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Bad request', { status: 400 });
|
||||||
|
};
|
||||||
63
src/routes/private/creator/+page.svelte
Normal file
63
src/routes/private/creator/+page.svelte
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
let authorized = false;
|
||||||
|
let refreshToken = '';
|
||||||
|
|
||||||
|
let to = '';
|
||||||
|
let subject = '';
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
refreshToken = localStorage.getItem('gmail_refresh_token') ?? '';
|
||||||
|
authorized = !!refreshToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ⇢ redirects straight to Google via server 302 */
|
||||||
|
const connect = () => goto('/private/api/gmail?action=auth');
|
||||||
|
|
||||||
|
async function sendEmail() {
|
||||||
|
const r = await fetch('/private/api/gmail', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'send',
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
text: body,
|
||||||
|
refreshToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
r.ok ? alert('Sent!') : alert(await r.text());
|
||||||
|
to = subject = body = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnect() {
|
||||||
|
if (!confirm('Disconnect Google account?')) return;
|
||||||
|
await fetch('/private/api/gmail', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: 'revoke', refreshToken })
|
||||||
|
});
|
||||||
|
localStorage.removeItem('gmail_refresh_token');
|
||||||
|
refreshToken = '';
|
||||||
|
authorized = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !authorized}
|
||||||
|
<section class="space-y-4">
|
||||||
|
<p>You haven’t connected your Google account yet.</p>
|
||||||
|
<button class="btn" on:click={connect}>Connect Google</button>
|
||||||
|
</section>
|
||||||
|
{:else}
|
||||||
|
<button class="btn-secondary mb-4" on:click={disconnect}>Disconnect Google</button>
|
||||||
|
|
||||||
|
<form class="space-y-4" on:submit|preventDefault={sendEmail}>
|
||||||
|
<input class="input w-full" type="email" placeholder="recipient@example.com" bind:value={to} required />
|
||||||
|
<input class="input w-full" placeholder="Subject" bind:value={subject} required />
|
||||||
|
<textarea class="textarea w-full" rows="6" placeholder="Message" bind:value={body} required />
|
||||||
|
<button class="btn-primary">Send</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user