mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-03 11:07:00 -04:00
203 lines
6.3 KiB
TypeScript
203 lines
6.3 KiB
TypeScript
import fs from 'fs';
|
|
import jwt from 'jsonwebtoken';
|
|
import type { GrantConfig, GrantResponse } from 'grant';
|
|
import { rootDir } from './paths.ts';
|
|
|
|
const getAppleClientSecret = (): string => {
|
|
const headers = {
|
|
alg: 'ES256',
|
|
kid: process.env.APPLE_KEY_ID,
|
|
typ: undefined,
|
|
};
|
|
const claims = {
|
|
iss: process.env.APPLE_TEAM_ID,
|
|
aud: 'https://appleid.apple.com',
|
|
sub: process.env.APPLE_CLIENT_ID,
|
|
};
|
|
const applePrivateKeyFile = `${rootDir}/keys/AuthKey_${process.env.APPLE_KEY_ID}.p8`;
|
|
const privateKey = fs.existsSync(applePrivateKeyFile) ? fs.readFileSync(applePrivateKeyFile).toString('utf-8') : '';
|
|
return jwt.sign(claims, privateKey, {
|
|
algorithm: 'ES256',
|
|
header: headers,
|
|
expiresIn: '180d',
|
|
});
|
|
};
|
|
|
|
function appleIsEnabled(): boolean {
|
|
const conditions = [
|
|
'APPLE_CLIENT_ID',
|
|
'APPLE_TEAM_ID',
|
|
'APPLE_KEY_ID',
|
|
'APPLE_PRIVATE_KEY',
|
|
];
|
|
const unavailable = [];
|
|
for (const condition of conditions) {
|
|
const value = process.env[condition];
|
|
// Checks if the value is not a string or empty
|
|
if (typeof value !== 'string' || value.length < 1 || value.trim().length < 1) {
|
|
unavailable.push(condition);
|
|
}
|
|
}
|
|
if (unavailable.length === 0) {
|
|
return true;
|
|
}
|
|
if (unavailable.length === conditions.length) {
|
|
console.log('Apple authentication is disabled - provide the APPLE_CLIENT_ID, APPLE_TEAM_ID, and APPLE_PRIVATE_KEY to enable these');
|
|
} else {
|
|
console.warn(`Apple authentication is disabled because all required environment values were not provided (missing: ${unavailable.join(', ')})`);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const enableApple = appleIsEnabled();
|
|
|
|
export const config: GrantConfig = {
|
|
defaults: {
|
|
origin: process.env.BASE_URL,
|
|
transport: 'session',
|
|
state: true,
|
|
prefix: '/api/connect',
|
|
scope: ['email'],
|
|
response: ['tokens', 'raw', 'profile'],
|
|
},
|
|
twitter: {
|
|
key: process.env.TWITTER_KEY,
|
|
secret: process.env.TWITTER_SECRET,
|
|
callback: '/api/user/social/twitter',
|
|
profile_url: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
|
|
},
|
|
facebook: {
|
|
key: process.env.FACEBOOK_KEY,
|
|
secret: process.env.FACEBOOK_SECRET,
|
|
callback: '/api/user/social/facebook',
|
|
profile_url: 'https://graph.facebook.com/me?fields=email,name,picture',
|
|
},
|
|
google: {
|
|
key: process.env.GOOGLE_KEY,
|
|
secret: process.env.GOOGLE_SECRET,
|
|
callback: '/api/user/social/google',
|
|
},
|
|
discord: {
|
|
key: process.env.DISCORD_KEY,
|
|
secret: process.env.DISCORD_SECRET,
|
|
callback: '/api/user/social/discord',
|
|
scope: ['identify', 'email'],
|
|
},
|
|
// non-grant, but things break if it's not there
|
|
mastodon: {},
|
|
indieauth: {},
|
|
};
|
|
|
|
if (enableApple) {
|
|
config.apple = {
|
|
key: process.env.APPLE_CLIENT_ID,
|
|
secret: getAppleClientSecret(),
|
|
|
|
callback: '/api/user/social/apple',
|
|
scope: ['openid', 'name', 'email'],
|
|
response: ['raw', 'jwt'],
|
|
nonce: true,
|
|
custom_params: {
|
|
response_type: 'code id_token',
|
|
response_mode: 'form_post',
|
|
},
|
|
};
|
|
}
|
|
|
|
interface SocialProfile {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
username?: string;
|
|
avatar?: string;
|
|
access_token?: string | undefined;
|
|
access_secret?: string | undefined;
|
|
instance?: string;
|
|
}
|
|
|
|
export interface SocialProfilePayload extends SocialProfile {
|
|
avatarCopy?: string | null;
|
|
}
|
|
|
|
export const handlers: Record<string, (r: GrantResponse) => SocialProfile> = {
|
|
twitter(r: GrantResponse): SocialProfile {
|
|
return {
|
|
id: r.profile.id_str,
|
|
email: r.profile.email,
|
|
name: r.profile.screen_name,
|
|
avatar: r.profile.profile_image_url_https.replace('_normal', '_400x400'),
|
|
access_token: r.access_token,
|
|
access_secret: r.access_secret,
|
|
};
|
|
},
|
|
facebook(r: GrantResponse): SocialProfile {
|
|
return {
|
|
id: r.profile.id,
|
|
email: r.profile.email,
|
|
name: r.profile.name,
|
|
avatar: r.profile.picture.data.url,
|
|
access_token: r.access_token,
|
|
access_secret: r.access_secret,
|
|
};
|
|
},
|
|
google(r: GrantResponse): SocialProfile {
|
|
return {
|
|
id: r.profile.sub,
|
|
email: r.profile.email_verified !== false ? r.profile.email : undefined,
|
|
name: r.profile.email,
|
|
avatar: r.profile.picture,
|
|
access_token: r.access_token,
|
|
access_secret: r.access_secret,
|
|
};
|
|
},
|
|
discord(r: GrantResponse): SocialProfile {
|
|
return {
|
|
id: r.profile.id,
|
|
email: r.profile.email,
|
|
name: r.profile.username,
|
|
username: `${r.profile.username}#${r.profile.discriminator}`,
|
|
avatar: `https://cdn.discordapp.com/avatars/${r.profile.id}/${r.profile.avatar}`,
|
|
access_token: r.access_token,
|
|
access_secret: r.access_secret,
|
|
};
|
|
},
|
|
mastodon(r: GrantResponse): SocialProfile {
|
|
const instance = (r as { instance: string }).instance;
|
|
const acct = `${r.profile.username}@${instance}`;
|
|
return {
|
|
id: acct,
|
|
// very possibly not really operated by the user
|
|
email: acct,
|
|
name: acct,
|
|
avatar: r.profile.avatar,
|
|
access_token: r.access_token,
|
|
instance,
|
|
};
|
|
},
|
|
indieauth(r: GrantResponse): SocialProfile {
|
|
return {
|
|
id: r.profile.me,
|
|
email: `indieauth@${r.profile.domain}`,
|
|
name: r.profile.domain,
|
|
instance: (r as { instance: string }).instance,
|
|
};
|
|
},
|
|
apple(r: GrantResponse): SocialProfile {
|
|
const payload = r.jwt!.id_token!.payload;
|
|
return {
|
|
id: payload.email,
|
|
email: payload.email_verified ? payload.email : null,
|
|
name: payload.email,
|
|
};
|
|
},
|
|
};
|
|
|
|
export const lookup: Record<string, ('id' | 'username' | 'name' | 'instance')[]> = {
|
|
discord: ['username', 'id'],
|
|
facebook: ['id'],
|
|
google: ['id'],
|
|
indieauth: ['instance'],
|
|
mastodon: ['name', 'id'],
|
|
twitter: ['name'],
|
|
};
|