254 lines
7.7 KiB
TypeScript
254 lines
7.7 KiB
TypeScript
import { postgrestLocalUrl, patreonVideoAccessBenefitId, giteaUrl } from './constants'
|
|
import { type IPatron } from '@futureporn/types'
|
|
import { PublicPatron } from '@futureporn/utils/patron.ts';
|
|
import { Session } from 'next-auth';
|
|
import { JWT } from 'next-auth/jwt';
|
|
import { type UMAToken, getKeycloakIdFromTierId, getUMAToken, syncronizeKeycloakRoles } from './keycloak';
|
|
import { patreonApiIdentityUrl } from './constants';
|
|
|
|
|
|
export type TiersList = string[]
|
|
interface KeycloakToken {
|
|
account: {
|
|
access_token: string
|
|
}
|
|
}
|
|
interface KeycloakIdpToken {
|
|
access_token: string;
|
|
expires_in: number;
|
|
refresh_expires_in: number;
|
|
refresh_token: string;
|
|
token_type: string;
|
|
"not-before-policy": number;
|
|
scope: string;
|
|
version: string;
|
|
}
|
|
interface PatreonResponse {
|
|
data: UserData;
|
|
included: IncludedItem[];
|
|
links: {
|
|
self: string;
|
|
};
|
|
}
|
|
|
|
interface UserData {
|
|
attributes: UserAttributes;
|
|
id: string;
|
|
relationships: {
|
|
memberships: {
|
|
data: Relationship[];
|
|
};
|
|
};
|
|
type: "user";
|
|
}
|
|
|
|
interface UserAttributes {
|
|
about: string | null;
|
|
created: string; // ISO 8601 date string
|
|
first_name: string;
|
|
full_name: string;
|
|
image_url: string;
|
|
last_name: string;
|
|
thumb_url: string;
|
|
url: string;
|
|
vanity: string | null;
|
|
}
|
|
|
|
interface Relationship {
|
|
id: string;
|
|
type: string;
|
|
}
|
|
|
|
type IncludedItem = Member | Tier | Benefit;
|
|
|
|
interface Member {
|
|
attributes: Record<string, unknown>;
|
|
id: string;
|
|
relationships: {
|
|
currently_entitled_tiers: {
|
|
data: Relationship[];
|
|
};
|
|
};
|
|
type: "member";
|
|
}
|
|
|
|
export interface Tier {
|
|
attributes: Record<string, unknown>;
|
|
id: string;
|
|
relationships: {
|
|
benefits: {
|
|
data: Relationship[];
|
|
};
|
|
};
|
|
type: "tier";
|
|
}
|
|
|
|
interface Benefit {
|
|
attributes: Record<string, unknown>;
|
|
id: string;
|
|
type: "benefit";
|
|
}
|
|
|
|
export interface ICampaign {
|
|
pledgeSum: number;
|
|
patronCount: number;
|
|
}
|
|
|
|
|
|
export interface IMarshalledCampaign {
|
|
data: {
|
|
attributes: {
|
|
pledge_sum: number,
|
|
patron_count: number
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
export const tiers = {
|
|
everyone: '-1',
|
|
free: '10620388',
|
|
archiveSupporter: '8154170',
|
|
stealthSupporter: '9561793',
|
|
tuneItUp: '9184994',
|
|
maxQ: '22529959',
|
|
archiveCollector: '8154171',
|
|
advancedArchiveSupporter: '8686045',
|
|
quantumSupporter: '8694826',
|
|
sneakyQuantumSupporter: '9560538',
|
|
luberPlusPlus: '8686022'
|
|
}
|
|
|
|
|
|
// Maps patreon tiers to keycloak roles
|
|
// @todo this is a dirty hack, this should be removed once we figure out how to sync keycloak roles at login.
|
|
export const tiersToRolesMap: Record<string, string[]> = {
|
|
everyone: ['cdn_2', 'default-roles-futureporn', 'offline_access', 'uma_authorization'],
|
|
free: ['cdn_2', 'default-roles-futureporn', 'offline_access', 'uma_authorization'],
|
|
archiveSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization'],
|
|
stealthSupporter: ['hacker', 'cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization'],
|
|
tuneItUp: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization'],
|
|
maxQ: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'],
|
|
archiveCollector: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'],
|
|
advancedArchiveSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'],
|
|
quantumSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'],
|
|
sneakyQuantumSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'],
|
|
luberPlusPlus: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader']
|
|
};
|
|
|
|
|
|
|
|
export async function updateKeycloakUserPatreonEntitlements(token: JWT): Promise<TiersList> {
|
|
|
|
console.log(`updateKeycloakUserPatreonEntitlements() invoked.`)
|
|
|
|
const userId = token?.sub
|
|
if (!userId) {
|
|
throw new Error(`failed to get userId from token.sub`);
|
|
}
|
|
|
|
const keycloakidpToken = await getKeycloakIdpToken(token.access_token)
|
|
|
|
if (!keycloakidpToken) {
|
|
throw new Error(`failed to get keycloakIdpToken; it was falsy.`)
|
|
}
|
|
|
|
const patreonTiersList = await getPatreonMemberships(keycloakidpToken)
|
|
|
|
|
|
await syncronizeKeycloakRoles(userId, patreonTiersList)
|
|
|
|
return patreonTiersList
|
|
}
|
|
|
|
export async function getKeycloakIdpToken(access_token: string): Promise<KeycloakIdpToken> {
|
|
// console.log(`getKeycloakIdpToken() using access_token=${access_token}`)
|
|
|
|
// @todo check the access_token. if it is expired, use the refresh_token to get a new access_token.
|
|
|
|
const res = await fetch(`https://keycloak.fp.sbtp.xyz/realms/futureporn/broker/patreon/token`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${access_token}`
|
|
}
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const bod = await res.text()
|
|
const msg = `req.status=${res.status} req.statusText=${res.statusText} body=${bod}`
|
|
console.log(msg)
|
|
throw new Error(`Failed to getKeycloakIdpToken. ${msg}`)
|
|
}
|
|
|
|
const idpToken = await res.json()
|
|
return idpToken
|
|
}
|
|
|
|
export function extractCurrentlyEntitledTiers(response: PatreonResponse): Relationship[] {
|
|
return response.included
|
|
.filter((item): item is Member => item.type === "member")
|
|
.flatMap(member => member.relationships.currently_entitled_tiers.data);
|
|
}
|
|
|
|
export async function getPatreonMemberships(token: KeycloakIdpToken): Promise<TiersList> {
|
|
// console.log(`getPatreonMemberships with keycloakidpToken as follows`)
|
|
// console.log(token)
|
|
const query = 'fields%5Buser%5D=about,created,email,first_name,full_name,image_url,last_name,thumb_url,url,vanity&include=memberships,memberships.currently_entitled_tiers,memberships.currently_entitled_tiers.benefits';
|
|
const res = await fetch(`${patreonApiIdentityUrl}?${query}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token.access_token}`
|
|
}
|
|
})
|
|
const data = await res.json() as PatreonResponse
|
|
|
|
// from the currently_entitled_tiers, we only want the id.
|
|
// we filter out any non-futureporn patreon tiers.
|
|
return extractCurrentlyEntitledTiers(data)
|
|
.map((rel) => rel.id)
|
|
.filter((t) => Object.values(tiers).includes(t))
|
|
}
|
|
|
|
|
|
|
|
export function isEntitledToPatronVideoAccess(session: Session): boolean {
|
|
if (!session.user?.patreonBenefits) return false;
|
|
const patreonBenefits = session.user.patreonBenefits
|
|
return (patreonBenefits.includes(patreonVideoAccessBenefitId))
|
|
}
|
|
|
|
|
|
export async function getPatrons(): Promise<PublicPatron[]> {
|
|
let patrons: PublicPatron[] = []
|
|
try {
|
|
const url = `${postgrestLocalUrl}/patrons`
|
|
console.log(`GET requesting ${url}`)
|
|
const res = await fetch(url);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(`failed to get /patrons. res.status=${res.status}, res.statusText=${res.statusText}`);
|
|
if (!data) throw new Error(`no patron data was available. ${JSON.stringify(data)}`);
|
|
patrons = data
|
|
} catch (e) {
|
|
console.error('failed to get patrons~ list')
|
|
console.error(e)
|
|
return [] as PublicPatron[]
|
|
}
|
|
return patrons
|
|
}
|
|
|
|
|
|
export async function getCampaign(): Promise<ICampaign> {
|
|
const res = await fetch('https://www.patreon.com/api/campaigns/8012692', {
|
|
headers: {
|
|
accept: 'application/json'
|
|
},
|
|
next: {
|
|
revalidate: 43200 // 12 hour cache
|
|
}
|
|
})
|
|
const campaignData = await res.json();
|
|
const data = {
|
|
patronCount: campaignData.data.attributes.patron_count,
|
|
pledgeSum: campaignData.data.attributes.campaign_pledge_sum
|
|
}
|
|
return data
|
|
}
|