hugex-gh / app /lib /account-linking.server.ts
drbh
fix: improve token rention
c202a37
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { dirname } from 'path';
export interface AccountLink {
githubUserId: string;
githubLogin: string;
huggingfaceUsername: string;
huggingfaceAccessToken?: string;
linkedAt: string;
lastUpdated: string;
}
export interface AccountLinksData {
links: AccountLink[];
metadata: {
version: string;
createdAt: string;
lastModified: string;
};
}
export class AccountLinkingService {
private filePath: string;
constructor() {
this.filePath = process.env.ACCOUNT_LINKS_FILE || '/tmp/account-links.json';
this.ensureFileExists();
}
private ensureFileExists(): void {
const dir = dirname(this.filePath);
// Create directory if it doesn't exist
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
// Create file if it doesn't exist
if (!existsSync(this.filePath)) {
const initialData: AccountLinksData = {
links: [],
metadata: {
version: '1.0.0',
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
},
};
this.writeData(initialData);
}
}
private readData(): AccountLinksData {
try {
const content = readFileSync(this.filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error('Error reading account links file:', error);
// Return default structure if file is corrupted
return {
links: [],
metadata: {
version: '1.0.0',
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
},
};
}
}
private writeData(data: AccountLinksData): void {
try {
data.metadata.lastModified = new Date().toISOString();
writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
} catch (error) {
console.error('Error writing account links file:', error);
throw new Error('Failed to save account links');
}
}
/**
* Create a new account link
*/
createLink(githubUserId: string, githubLogin: string, huggingfaceUsername: string, huggingfaceAccessToken?: string): AccountLink {
const data = this.readData();
const now = new Date().toISOString();
// Check for existing links
const existingGithubLink = data.links.find(link => link.githubUserId === githubUserId);
const existingHfLink = data.links.find(link => link.huggingfaceUsername === huggingfaceUsername);
if (existingGithubLink) {
throw new Error(`GitHub user ${githubLogin} is already linked to HuggingFace user ${existingGithubLink.huggingfaceUsername}`);
}
if (existingHfLink) {
throw new Error(`HuggingFace user ${huggingfaceUsername} is already linked to GitHub user ${existingHfLink.githubLogin}`);
}
const newLink: AccountLink = {
githubUserId,
githubLogin,
huggingfaceUsername,
huggingfaceAccessToken,
linkedAt: now,
lastUpdated: now,
};
data.links.push(newLink);
this.writeData(data);
console.log(`βœ… Created account link: ${githubLogin} ↔ ${huggingfaceUsername}`);
return newLink;
}
/**
* Find account link by GitHub user
*/
findByGitHubUser(githubUserId: string): AccountLink | null {
const data = this.readData();
return data.links.find(link => link.githubUserId === githubUserId) || null;
}
/**
* Find account link by HuggingFace user
*/
findByHuggingFaceUser(huggingfaceUsername: string): AccountLink | null {
const data = this.readData();
return data.links.find(link => link.huggingfaceUsername === huggingfaceUsername) || null;
}
/**
* Update an existing account link
*/
updateLink(githubUserId: string, updates: Partial<Omit<AccountLink, 'githubUserId' | 'linkedAt'>>): AccountLink {
const data = this.readData();
const linkIndex = data.links.findIndex(link => link.githubUserId === githubUserId);
if (linkIndex === -1) {
throw new Error(`No account link found for GitHub user ID: ${githubUserId}`);
}
const existingLink = data.links[linkIndex];
const updatedLink: AccountLink = {
...existingLink,
...updates,
lastUpdated: new Date().toISOString(),
};
data.links[linkIndex] = updatedLink;
this.writeData(data);
console.log(`βœ… Updated account link for GitHub user: ${existingLink.githubLogin}`);
return updatedLink;
}
/**
* Update HuggingFace access token for an existing account link
*/
updateAccessToken(githubUserId: string, accessToken: string): AccountLink {
return this.updateLink(githubUserId, { huggingfaceAccessToken: accessToken });
}
/**
* Remove an account link
*/
removeLink(githubUserId: string): boolean {
const data = this.readData();
const initialLength = data.links.length;
data.links = data.links.filter(link => link.githubUserId !== githubUserId);
if (data.links.length < initialLength) {
this.writeData(data);
console.log(`βœ… Removed account link for GitHub user ID: ${githubUserId}`);
return true;
}
return false;
}
/**
* Get all account links
*/
getAllLinks(): AccountLink[] {
const data = this.readData();
return data.links;
}
/**
* Get linking statistics
*/
getStats(): { totalLinks: number; lastModified: string } {
const data = this.readData();
return {
totalLinks: data.links.length,
lastModified: data.metadata.lastModified,
};
}
/**
* Check if accounts can be linked (no conflicts)
*/
canLink(githubUserId: string, huggingfaceUsername: string): { canLink: boolean; reason?: string } {
const data = this.readData();
const existingGithubLink = data.links.find(link => link.githubUserId === githubUserId);
if (existingGithubLink) {
return {
canLink: false,
reason: `GitHub account is already linked to HuggingFace user: ${existingGithubLink.huggingfaceUsername}`,
};
}
const existingHfLink = data.links.find(link => link.huggingfaceUsername === huggingfaceUsername);
if (existingHfLink) {
return {
canLink: false,
reason: `HuggingFace account is already linked to GitHub user: ${existingHfLink.githubLogin}`,
};
}
return { canLink: true };
}
}
// Singleton instance
export const accountLinkingService = new AccountLinkingService();