Add mailing prototype #4
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