211 lines
7.8 KiB
TypeScript
211 lines
7.8 KiB
TypeScript
import { type Tier, type TiersList, tiers } from './patreon'
|
|
import { configs } from '../config/configs';
|
|
|
|
export interface UMAToken {
|
|
upgraded: boolean;
|
|
access_token: string;
|
|
expires_in: number;
|
|
refresh_expires_in: number;
|
|
token_type: string;
|
|
"not-before-policy": number;
|
|
}
|
|
|
|
export interface KeycloakGroup {
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
subGroups: KeycloakGroup[];
|
|
}
|
|
|
|
// Maps patreon tiers to keycloak groups.
|
|
export const patreonToKeycloakMap: Record<string, string> = {
|
|
everyone: 'ccaece3a-bb62-4fc9-be1d-6326ecdec65c',
|
|
free: 'ccaece3a-bb62-4fc9-be1d-6326ecdec65c',
|
|
archiveSupporter: '9e70fe60-5015-44df-ab77-d93b90e86738',
|
|
stealthSupporter: 'fd5358fe-d5e8-43be-85f2-27ddff711bd0',
|
|
tuneItUp: '4127e407-0c09-4c0a-b231-0bc058fb3dbd',
|
|
maxQ: '25b3d3af-2015-49ea-949f-b052fc82c6a3',
|
|
archiveCollector: '9e70fe60-5015-44df-ab77-d93b90e86738',
|
|
advancedArchiveSupporter: '7f875c63-4c74-4ae1-b33c-eefa7904d031',
|
|
quantumSupporter: '13076175-3a6f-4d80-84f2-6a8d5711e5f5',
|
|
sneakyQuantumSupporter: '13076175-3a6f-4d80-84f2-6a8d5711e5f5',
|
|
luberPlusPlus: '205d9c95-68ec-4b20-a6c1-28162af8a8f5'
|
|
};
|
|
|
|
|
|
export function getTierNameFromTierId(tierId: string): string {
|
|
const tierName = Object.entries(tiers).find(([_, id]) => id === tierId)?.[0];
|
|
if (!tierName) throw new Error(`getTierNameFromTierId failed to convert tierId=${tierId} into a tier name.`);
|
|
return tierName
|
|
}
|
|
|
|
export function getKeycloakIdFromTierId(tierId: string): string | undefined {
|
|
// Find the tier name corresponding to the provided tier ID
|
|
const tierName = getTierNameFromTierId(tierId)
|
|
|
|
if (!tierName) {
|
|
console.warn(`Tier ID ${tierId} not found in tiers map.`);
|
|
return undefined; // Return undefined if the tier ID is not found
|
|
}
|
|
|
|
// Use the tier name to get the corresponding Keycloak group ID
|
|
const keycloakId = patreonToKeycloakMap[tierName];
|
|
|
|
if (!keycloakId) {
|
|
console.warn(`Keycloak ID for tier ${tierName} not found in patreonToKeycloakMap.`);
|
|
}
|
|
|
|
return keycloakId;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* This gets us an access_token with our service-account-futureporn user privs.
|
|
* This service account has manage-users role so it can modify their group memebership.
|
|
*/
|
|
export async function getUMAToken(): Promise<UMAToken> {
|
|
const res = await fetch(`${configs.keycloakLocalUrl}/realms/futureporn/protocol/openid-connect/token`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket',
|
|
client_id: configs.keycloakClientId,
|
|
client_secret: configs.keycloakClientSecret,
|
|
audience: 'futureporn'
|
|
}).toString()
|
|
})
|
|
const data = await res.json()
|
|
if (!res.ok) {
|
|
throw new Error(`failed to getUMAToken. body=${JSON.stringify(data, null, 2)}, res.status=${res.status}, res.statusText=${res.statusText}`);
|
|
}
|
|
return data
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getUserGroups(uma: UMAToken, userId: string): Promise<string[]> {
|
|
if (!uma) throw new Error('getUserGroups() requires a UMAToken as second param, but it was undefined.');
|
|
if (!userId) throw new Error('getUserGroups() requires a userId as second param, but it was undefined.');
|
|
const res = await fetch(`${configs.keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${uma.access_token}`
|
|
}
|
|
})
|
|
const data = await res.json() as KeycloakGroup[]
|
|
if (!res.ok) {
|
|
throw new Error(`failed to getUserGroups. body=${JSON.stringify(data, null, 2)}, res.status=${res.status}, res.statusText=${res.statusText}`);
|
|
}
|
|
return data.map((g) => g.id)
|
|
}
|
|
|
|
|
|
|
|
export async function addUserGroup(uma: UMAToken, userId: string, groupId: string): Promise<void> {
|
|
const res = await fetch(`${configs.keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups/${groupId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${uma.access_token}`
|
|
}
|
|
})
|
|
if (!res.ok) {
|
|
throw new Error(`failed to addUserGroup. res.status=${res.status}, res.statusText=${res.statusText}`);
|
|
}
|
|
}
|
|
|
|
|
|
export async function removeUserGroup(uma: UMAToken, userId: string, groupId: string): Promise<void> {
|
|
const res = await fetch(`${configs.keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups/${groupId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${uma.access_token}`
|
|
}
|
|
})
|
|
if (!res.ok) {
|
|
throw new Error(`failed to removeUserGroup. res.status=${res.status}, res.statusText=${res.statusText}`);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Identifies the changes required to sync the actual state with the desired state.
|
|
*
|
|
* @param desired - Array of desired group IDs.
|
|
* @param actual - Array of actual group IDs.
|
|
* @returns An object with:
|
|
* - `toAdd`: Groups in `desired` but not in `actual` (need to be added).
|
|
* - `toRemove`: Groups in `actual` but not in `desired` (need to be removed).
|
|
*/
|
|
export function calculateSyncChanges(desired: string[], actual: string[]) {
|
|
const toAdd = Array.from(new Set(desired.filter(item => !actual.includes(item))));
|
|
const toRemove = Array.from(new Set(actual.filter(item => !desired.includes(item))));
|
|
return { toAdd, toRemove };
|
|
}
|
|
|
|
export async function syncronizeKeycloakRoles(keycloakUserId: string, patreonTiersList: TiersList): Promise<void> {
|
|
|
|
if (!keycloakUserId) throw new Error('syncronizeKeycloakRoles() requires userId as first param, but it was undefined.');
|
|
if (!patreonTiersList) throw new Error('patreonTiersList() requires userId as second param, but it was undefined.');
|
|
|
|
// 1. [-] get Keycloak service account uma token
|
|
// POST https://keycloak.fp.sbtp.xyz/realms/futureporn/protocol/openid-connect/token
|
|
// 2. [-] use the map to determine the groups the user needs to be assigned to
|
|
// 3. [-] get a list of groups the user is currently a member of
|
|
// GET https://keycloak.fp.sbtp.xyz/admin/realms/futureporn/users/fbec8417-e5e3-4282-a98d-ed2fbc4a7e82/groups
|
|
// GET https://keycloak.fp.sbtp.xyz/admin/realms/futureporn/users/fbec8417-e5e3-4282-a98d-ed2fbc4a7e82/role-mappings/realm
|
|
// 4. [ ] add any groups the user should be a member of
|
|
// PUT https://keycloak.fp.sbtp.xyz/admin/realms/futureporn/users/fbec8417-e5e3-4282-a98d-ed2fbc4a7e82/groups/ccaece3a-bb62-4fc9-be1d-6326ecdec65c
|
|
// 5. [ ] remove any groups the user should not be a member of
|
|
// DELETE https://keycloak.fp.sbtp.xyz/admin/realms/futureporn/users/fbec8417-e5e3-4282-a98d-ed2fbc4a7e82/groups/ccaece3a-bb62-4fc9-be1d-6326ecdec65c
|
|
//
|
|
|
|
|
|
const uma = await getUMAToken()
|
|
// console.log('uma token as follows')
|
|
// console.log(uma)
|
|
// console.log('got uma token')
|
|
|
|
|
|
const desiredKeycloakGroupIds = patreonTiersList
|
|
.map((entitledTierId) => getKeycloakIdFromTierId(entitledTierId))
|
|
.filter((groupId): groupId is string => groupId !== undefined);
|
|
// console.log(`got desiredKeycloakGroupIds=${desiredKeycloakGroupIds.join(', ')}`)
|
|
|
|
|
|
if (desiredKeycloakGroupIds.length === 0) {
|
|
throw new Error(`failed to get keycloak group id from the following patreon tiers: ${patreonTiersList.join(',')}`);
|
|
}
|
|
// console.log('desiredKeycloakGroupIds as follows')
|
|
// console.log(desiredKeycloakGroupIds)
|
|
|
|
|
|
const actualKeycloakGroupIds = await getUserGroups(uma, keycloakUserId);
|
|
// console.log(`actualKeyclaokGroupIds as follows`)
|
|
// console.log(actualKeycloakGroupIds)
|
|
|
|
|
|
const { toAdd, toRemove } = calculateSyncChanges(desiredKeycloakGroupIds, actualKeycloakGroupIds);
|
|
|
|
|
|
if (toAdd.length > 0) {
|
|
// console.log(`Groups to add: ${toAdd.join(', ')}`);
|
|
await Promise.all(toAdd.map(groupId => addUserGroup(uma, keycloakUserId, groupId)));
|
|
}
|
|
|
|
if (toRemove.length > 0) {
|
|
// console.log(`Groups to remove: ${toRemove.join(', ')}`);
|
|
await Promise.all(toRemove.map(groupId => removeUserGroup(uma, keycloakUserId, groupId)));
|
|
}
|
|
|
|
|
|
console.log('finished updating keycloak groups.')
|
|
|
|
|
|
}
|
|
|