import crypto from 'crypto'; import { env } from '../config/env.js'; interface GiteaUser { id: number; login: string; email: string; full_name: string; avatar_url: string; } interface OAuthTokenResponse { access_token: string; token_type: string; expires_in: number; refresh_token?: string; } export class GiteaService { private giteaUrl: string; private clientId: string; private clientSecret: string; private redirectUri: string; private allowedUsers: Set; constructor() { this.giteaUrl = env.GITEA_URL; this.clientId = env.GITEA_CLIENT_ID; this.clientSecret = env.GITEA_CLIENT_SECRET; this.redirectUri = env.GITEA_REDIRECT_URI; const allowed = env.GITEA_ALLOWED_USERS; this.allowedUsers = new Set(allowed.split(',').map(u => u.trim()).filter(Boolean)); } /** * Generate OAuth authorization URL */ getAuthorizationUrl(state: string): string { const params = new URLSearchParams({ client_id: this.clientId, redirect_uri: this.redirectUri, response_type: 'code', state, scope: 'read:user', }); return `${this.giteaUrl}/login/oauth/authorize?${params.toString()}`; } /** * Exchange authorization code for access token */ async exchangeCodeForToken(code: string): Promise { const response = await fetch(`${this.giteaUrl}/login/oauth/access_token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ client_id: this.clientId, client_secret: this.clientSecret, code, redirect_uri: this.redirectUri, grant_type: 'authorization_code', }), }); if (!response.ok) { throw new Error(`Failed to exchange code: ${response.statusText}`); } return await response.json(); } /** * Fetch user info from Gitea using access token */ async getUserInfo(accessToken: string): Promise { const response = await fetch(`${this.giteaUrl}/api/v1/user`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`Failed to fetch user info: ${response.statusText}`); } return await response.json(); } /** * Check if user is allowed to access the CMS */ isUserAllowed(username: string): boolean { // If no allowed users specified, allow all if (this.allowedUsers.size === 0) { return true; } return this.allowedUsers.has(username); } /** * Generate random state for CSRF protection */ generateState(): string { return crypto.randomBytes(32).toString('hex'); } }