Skip to content

Instantly share code, notes, and snippets.

@angezanetti
Created May 27, 2022 16:31
Show Gist options
  • Save angezanetti/c5e67900974bb840c1b74d37abc59e09 to your computer and use it in GitHub Desktop.
Save angezanetti/c5e67900974bb840c1b74d37abc59e09 to your computer and use it in GitHub Desktop.
Twitter auth with sveltekit & supabase
import supabase from '$lib/db';
import * as cookie from 'cookie';
import { TwitterApi } from 'twitter-api-v2';
const twitterClient = new TwitterApi({
appKey: 'xxx',
appSecret: 'xxxxx'
});
// GET REQUEST
export async function get({ params, url }) {
const path = params.auth;
switch (path) {
case 'auth/twitter/login':
return await handleTwitterLogin();
case 'auth/twitter/callback':
return await handleTwitterCallback(params, url);
case 'auth/logout':
return await handleTwitterLogout(url);
default:
return { status: 404 };
}
}
function twitterUserClient(accessToken, accessSecret) {
return new TwitterApi({
appKey: 'xxxx',
appSecret: 'xxxxx',
accessToken,
accessSecret
});
}
/**
* Generates Twitter authentication URL and temporarily stores `oauth_token` in database.
*/
async function handleTwitterLogin() {
let authLink;
try {
authLink = await twitterClient.generateAuthLink(
`https://xxxx.app/api/auth/twitter/callback`
);
} catch (e) {
console.error(e);
return {
status: 303,
headers: {
location: '/'
}
};
}
if (!authLink?.url || !authLink?.oauth_token || !authLink?.oauth_token_secret) {
return { status: 500 };
}
const { data, error } = await supabase
.from('tokens')
.insert({
redirect: authLink.url,
oauth_token: authLink.oauth_token,
oauth_token_secret: authLink.oauth_token_secret
})
.limit(1)
.single();
if (error || !data?.id) {
return { status: 500 };
}
return {
status: 303,
headers: {
location: authLink.url
}
};
}
/**
* Handles Twitters authentication callback, fetches permanent access-tokens,
* and saves them to the database.
*/
async function handleTwitterCallback(params, url) {
const oauth_token = url.searchParams.get('oauth_token');
const oauth_verifier = url.searchParams.get('oauth_verifier');
if (!oauth_token || !oauth_verifier) {
console.error("No 'oauth_token' in callback request");
return {
status: 303,
headers: {
location: '/'
}
};
}
const { data: token, error } = await supabase
.from('tokens')
.select()
.eq('oauth_token', oauth_token)
.limit(1)
.single();
const oauth_token_secret = token['oauth_token_secret'];
if (error || !oauth_token_secret) {
console.error("Error while accessing database or 'oauth_token_secret' not found", error);
return {
status: 303,
headers: {
location: '/'
}
};
}
try {
// Create twitter user-client from temporary tokens
const twitterClient = twitterUserClient(oauth_token, oauth_token_secret);
const { accessToken: access_token, accessSecret: access_secret } = await twitterClient.login(
oauth_verifier
);
// Delete all existing with duplicate tokens
await supabase
.from('tokens')
.delete()
.neq('id', token.id)
.match({ access_token, access_secret });
// Sync User-Profile with authorized twitter-user client
const profile = await syncTwitterProfileDetails(access_token, access_secret);
// Update with permanent token, delete temporary tokens
const { data: newToken, error } = await supabase
.from('tokens')
.update({
user_id: profile['id'],
access_token,
access_secret,
oauth_token: null,
oauth_token_secret: null,
twitter_id: profile['twitter_user_id']
})
.eq('oauth_token', oauth_token)
.limit(1)
.single();
if (error || !newToken?.id) {
console.error('Error while updating access-tokens', error);
return {
status: 303,
headers: {
location: '/'
}
};
}
// Successfully authenticated, redirecting…
return {
status: 303,
headers: {
'Set-Cookie': cookie.serialize('user_id', profile['id'], {
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
path: '/'
}),
location: '/'
}
};
} catch (e) {
console.error('Error while logging in with twitter user-client', e);
}
}
/**
* Handles logout of twitter user and removes token from the database and cookie
*/
export const handleTwitterLogout = async (url) => {
const user = url.searchParams.get('user');
console.log(user);
const { error } = await supabase.from('tokens').delete().eq('user_id', user);
if (error) console.error(`Error while deleting token with id '${user}' on logout`, error);
return {
status: 303,
headers: {
'set-cookie': 'user_id=""; path=/; HttpOnly; expires=Thu, 01 Jan 1970 00:00:00 GMT',
location: '/'
}
};
};
/**
* If User has successfully authenticated twitter user, it's
* Twitter-related profile information is updated automatically.
*/
export const syncTwitterProfileDetails = async (twitterAccessToken, twitterAccessSecret) => {
if (!twitterAccessToken || !twitterAccessSecret) return;
const twitterClient = await twitterUserClient(twitterAccessToken, twitterAccessSecret);
const twitterUser = await twitterClient.v1.verifyCredentials({ include_email: true });
const attributesExist =
twitterUser?.id_str &&
twitterUser?.email &&
twitterUser?.screen_name &&
twitterUser?.name &&
twitterUser?.followers_count &&
twitterUser?.profile_image_url_https;
if (!attributesExist) return;
await supabase.from('user_profile').delete().eq('twitter_user_id', twitterUser.id_str);
const { data: profile, error } = await supabase.from('user_profile').insert({
twitter_user_id: twitterUser.id_str,
full_name: twitterUser.name,
email: twitterUser.email,
user_name: twitterUser.screen_name,
followers_count: twitterUser.followers_count,
avatar_url: twitterUser.profile_image_url_https
});
if (error || !profile) {
console.error('Error while syncing profile with twitter data', error);
}
return profile[0];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment