import { env } from '$env/dynamic/private'; import { encodeBase32LowerCaseNoPadding } from '@oslojs/encoding'; const baseURL = env.GITEA_BASE_URL ?? 'https://gitea.wbd-rd.nl'; const clientId = env.GITEA_CLIENT_ID ?? ''; const clientSecret = env.GITEA_CLIENT_SECRET ?? ''; const redirectURI = env.GITEA_REDIRECT_URI ?? 'http://localhost:3000/auth/gitea/callback'; const allowedOrg = env.GITEA_ALLOWED_ORG ?? ''; export const GITEA_BASE_URL = baseURL; export const GITEA_ALLOWED_ORG = allowedOrg; export interface GiteaUser { id: number; login: string; full_name?: string; email?: string; avatar_url?: string; } interface GiteaOrg { username?: string; name?: string; } export function generateState(): string { const bytes = new Uint8Array(20); crypto.getRandomValues(bytes); return encodeBase32LowerCaseNoPadding(bytes); } export function buildAuthorizationURL(state: string): string { if (!clientId) throw new Error('GITEA_CLIENT_ID is not configured'); const u = new URL(`${baseURL}/login/oauth/authorize`); u.searchParams.set('client_id', clientId); u.searchParams.set('redirect_uri', redirectURI); u.searchParams.set('response_type', 'code'); u.searchParams.set('state', state); // Gitea recognises space-separated scopes. read:user + read:organization // are sufficient for identity and org-membership checks. u.searchParams.set('scope', 'read:user read:organization'); return u.toString(); } export async function exchangeCode(code: string): Promise { if (!clientSecret) throw new Error('GITEA_CLIENT_SECRET is not configured'); const res = await fetch(`${baseURL}/login/oauth/access_token`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code, grant_type: 'authorization_code', redirect_uri: redirectURI }) }); if (!res.ok) { const text = await res.text(); throw new Error(`Gitea token exchange failed (${res.status}): ${text}`); } const data = (await res.json()) as { access_token?: string }; if (!data.access_token) throw new Error('Gitea response missing access_token'); return data.access_token; } export async function fetchGiteaUser(accessToken: string): Promise { const res = await fetch(`${baseURL}/api/v1/user`, { headers: { Authorization: `Bearer ${accessToken}` } }); if (!res.ok) throw new Error(`Failed to fetch Gitea user: ${res.status}`); return (await res.json()) as GiteaUser; } export async function isUserInOrg( accessToken: string, login: string, org: string ): Promise { if (!org) return true; const res = await fetch(`${baseURL}/api/v1/users/${encodeURIComponent(login)}/orgs`, { headers: { Authorization: `Bearer ${accessToken}` } }); if (!res.ok) return false; const orgs = (await res.json()) as GiteaOrg[]; return orgs.some((o) => o.username === org || o.name === org); }