Last active
July 22, 2024 02:26
-
-
Save wilsonowilson/1ddc5407d8466a150bb283fad9f741ae to your computer and use it in GitHub Desktop.
Setting up ConvertKit Plugin Oauth with Typescript + Firebase
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { page } from '$app/stores'; | |
import { getInitialUser } from '$lib/Identity/api/auth'; | |
import AuthPageLayout from '$lib/Identity/components/AuthPageLayout.svelte'; | |
import InlineLoader from '@senja/shared/components/InlineLoader.svelte'; | |
import TertiaryButton from '@senja/shared/components/TertiaryButton.svelte'; | |
import { ConvertkitIcon } from '@senja/shared/components/icons/integrations'; | |
import { optimizeImage } from '@senja/shared/utils/cdn'; | |
// This is the first URL ConvertKit will call. It'll contain a redirect URL, a <state> parameter and a client_id. | |
// Once you've gained the user's consent, redirect them to Convertkit's redirect_uri. | |
// You can use any JS framework of your choice, but in this example I'm using Svelte. | |
let loading = false; | |
let signUpLink = | |
'https://app.senja.io/signup?utm_campaign=integrations&utm_medium=partner&utm_source=convertkit&ref=convertkit-integration'; | |
async function completeOauth() { | |
loading = true; | |
const currentUser = await getInitialUser(); | |
if (!currentUser) { | |
window.location.href = signUpLink; | |
return; | |
} | |
window.location.href = | |
$page.url.searchParams.get('redirect_uri') + | |
`?state=${$page.url.searchParams.get('state')}&code=${ | |
currentUser.uid | |
}&client_id=${$page.url.searchParams.get('client_id')}`; | |
} | |
</script> | |
<AuthPageLayout integrationIcon={optimizeImage(ConvertkitIcon.src, { format: 'webp', width: 200 })}> | |
<h1 slot="header" class="text-2xl mt-4 font-medium"> | |
Connect your Senja account to Convertkit | |
</h1> | |
<p slot="description" class="text-gray-500"> | |
Senja helps you start collecting, managing and sharing your testimonials in minutes, not | |
days. <br /> <br /> | |
Connect your ConvertKit account to inject your testimonials into your emails. | |
</p> | |
<div slot="form"> | |
<TertiaryButton class="w-full" on:click={completeOauth}> | |
<InlineLoader {loading}>Connect your account</InlineLoader> | |
</TertiaryButton> | |
<div class="text-sm mt-4 text-gray-500"> | |
Don't have an account? <a | |
data-sveltekit-reload | |
href={signUpLink} | |
class="text-primary font-medium" | |
> | |
Sign up | |
</a> | |
</div> | |
</div> | |
</AuthPageLayout> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { RequestHandler, Router } from 'express'; | |
import { firebaseAdmin } from '../../services'; | |
import { refreshIdToken, signInWithCustomToken } from '../utilities/sign-in-with-token'; | |
export const convertkitRouter = Router(); | |
const convertkitSecret = process.env.CONVERTKIT_CLIENT_SECRET as string; | |
// Make sure requests are coming from Convertkit. | |
// You'll be able to set your client_secret in the Convertkit dashboard | |
const verificationMiddleware: RequestHandler = (req, res, next) => { | |
const secret = req.body.client_secret; | |
if (secret !== convertkitSecret) { | |
return res.status(401).json({ error: 'Unauthorized' }); | |
} | |
next(); | |
}; | |
// Create a JWT for the user and return it to Convertkit. | |
// Convertkit will call this endpoint with a code that you provide. | |
// In my case, I'm using the Firebase user's UID as the code and exchanging it for | |
// a custom token using the Firebase Admin SDK. Once I have the custom token, I can | |
// sign in the user and return the access token and refresh token to Convertkit. | |
convertkitRouter.post('/convertkit/auth/callback', verificationMiddleware, async (req, res) => { | |
const uid = req.body.code; | |
const token = await firebaseAdmin.auth().createCustomToken(uid); | |
const result = await signInWithCustomToken(token); | |
if (result.isErr()) { | |
return res.status(400).json({ | |
error: result.error, | |
}); | |
} | |
return res.json({ | |
access_token: result.value.idToken, | |
refresh_token: result.value.refreshToken, | |
expires_in: result.value.expiresIn, | |
created_at: Date.now(), | |
}); | |
}); | |
// Convertkit will call this endpoint to refresh the access token. | |
convertkitRouter.post('/convertkit/auth/refresh', async (req, res) => { | |
const { refresh_token } = req.body; | |
const response = await refreshIdToken(refresh_token); | |
if (response.isErr()) { | |
return res.status(400).json({ | |
error: response.error, | |
}); | |
} | |
const payload = { | |
access_token: response.value.idToken, | |
refresh_token: response.value.refreshToken, | |
expires_in: response.value.expiresIn, | |
created_at: Date.now(), | |
}; | |
return res.json(payload); | |
}); | |
// revoke your access token here. In firebase, you'd have to log out the user | |
// from all sessions using the admin SDK. | |
convertkitRouter.post('/convertkit/auth/revoke', verificationMiddleware, async (req, res) => { | |
return res.json({ message: 'success' }); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { err, ok } from '@senja/shared/types/result'; | |
const API_KEY = '<YOUR-FIREBASE-KEY>' | |
export async function signInWithCustomToken(token: string) { | |
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${API_KEY}`; | |
const body = { | |
token: token, | |
returnSecureToken: true, | |
}; | |
const response = await fetch(url, { | |
method: 'POST', | |
body: JSON.stringify(body), | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
}); | |
const data = await response.json(); | |
if (!response.ok) { | |
return err(data.error.message); | |
} | |
return ok({ | |
idToken: data.idToken, | |
refreshToken: data.refreshToken, | |
expiresIn: parseInt(data.expiresIn), | |
}); | |
} | |
export async function refreshIdToken(refreshToken: string) { | |
const url = `https://securetoken.googleapis.com/v1/token?key=${API_KEY}`; | |
const body = { | |
grant_type: 'refresh_token', | |
refresh_token: refreshToken, | |
}; | |
const response = await fetch(url, { | |
method: 'POST', | |
body: JSON.stringify(body), | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
}); | |
const data = await response.json(); | |
if (!response.ok) { | |
return err(data.error.message); | |
} | |
const idToken = data.id_token; | |
const newRefreshToken = data.refresh_token; | |
return ok({ | |
idToken, | |
refreshToken: newRefreshToken, | |
expiresIn: parseInt(data.expires_in), | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment