fjtui/src/api.ts

251 lines
7.3 KiB
TypeScript
Raw Normal View History

2026-06-01 00:59:22 +00:00
import axios from 'axios';
2026-06-01 02:02:23 +00:00
import { Config, Issue, Comment, RepoItem } from './types.js';
2026-06-01 00:59:22 +00:00
/**
* Normalizes the Gitea/Forgejo URL by stripping trailing slashes
* and ensuring it has a protocol (defaults to https).
*/
export function normalizeUrl(url: string): string {
let cleaned = url.trim();
if (!cleaned) return '';
if (!/^https?:\/\//i.test(cleaned)) {
cleaned = 'https://' + cleaned;
}
return cleaned.replace(/\/+$/, '');
}
/**
* Creates an Axios instance with standard Gitea/Forgejo headers.
*/
function createAxiosInstance(config: Config) {
const headers: Record<string, string> = {
'Accept': 'application/json',
'Content-Type': 'application/json',
};
if (config.token) {
headers['Authorization'] = `token ${config.token.trim()}`;
}
return axios.create({
baseURL: `${normalizeUrl(config.url)}/api/v1`,
headers,
timeout: 10000,
});
}
/**
* Tests the connection to the Gitea/Forgejo instance and validates that the repository exists.
* Returns the repository full name on success.
*/
export async function validateConnection(config: Config): Promise<string> {
const client = createAxiosInstance(config);
try {
const response = await client.get(`/repos/${config.owner}/${config.repo}`);
return response.data.full_name || `${config.owner}/${config.repo}`;
} catch (error: any) {
if (error.response) {
if (error.response.status === 401) {
throw new Error('Unauthorized: Invalid API Token or authentication failed.');
}
if (error.response.status === 404) {
throw new Error(`Repository "${config.owner}/${config.repo}" not found or is private.`);
}
throw new Error(`API Error: ${error.response.data?.message || error.message}`);
}
throw new Error(`Failed to connect to ${config.url}: ${error.message}`);
}
}
/**
* Fetches a page of issues for a specific repository.
*/
export async function fetchIssues(
config: Config,
options: {
page: number;
limit: number;
state: 'open' | 'closed' | 'all';
type: 'issues' | 'pulls' | 'all';
q: string;
sortField: 'created' | 'updated' | 'comments';
sortOrder: 'asc' | 'desc';
}
): Promise<{ issues: Issue[]; totalCount: number }> {
const client = createAxiosInstance(config);
// Map our internal sort fields to Gitea API sort parameters
let sort = 'newest';
if (options.sortField === 'created') {
sort = options.sortOrder === 'asc' ? 'oldest' : 'newest';
} else if (options.sortField === 'updated') {
sort = options.sortOrder === 'asc' ? 'leastupdate' : 'recentupdate';
} else if (options.sortField === 'comments') {
sort = options.sortOrder === 'asc' ? 'leastcomment' : 'mostcomment';
}
const params: Record<string, any> = {
page: options.page,
limit: options.limit,
state: options.state,
sort: sort,
};
// Filter by type: pulls or issues
if (options.type !== 'all') {
params.type = options.type;
}
// Filter by search query if present
if (options.q.trim()) {
params.q = options.q.trim();
}
try {
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues`, { params });
// Parse x-total-count header
const totalCountHeader = response.headers['x-total-count'];
const totalCount = totalCountHeader ? parseInt(totalCountHeader, 10) : response.data.length;
const issues: Issue[] = response.data.map((item: any) => ({
id: item.id,
number: item.number,
title: item.title,
state: item.state,
body: item.body || '',
user: {
id: item.user?.id || 0,
login: item.user?.login || 'unknown',
full_name: item.user?.full_name || '',
},
created_at: item.created_at,
updated_at: item.updated_at,
comments: item.comments_count || item.comments || 0,
labels: (item.labels || []).map((l: any) => ({
id: l.id,
name: l.name,
color: l.color,
})),
pull_request: item.pull_request,
}));
return { issues, totalCount };
} catch (error: any) {
if (error.response) {
throw new Error(`API Error: ${error.response.data?.message || error.message}`);
}
throw new Error(`Network Error: ${error.message}`);
}
}
/**
* Fetches comments for a specific issue.
*/
export async function fetchIssueComments(
config: Config,
issueNumber: number
): Promise<Comment[]> {
const client = createAxiosInstance(config);
try {
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}/comments`);
const comments: Comment[] = response.data.map((item: any) => ({
id: item.id,
user: {
id: item.user?.id || 0,
login: item.user?.login || 'unknown',
full_name: item.user?.full_name || '',
},
body: item.body || '',
created_at: item.created_at,
updated_at: item.updated_at,
}));
return comments;
} catch (error: any) {
if (error.response) {
throw new Error(`Failed to fetch comments: ${error.response.data?.message || error.message}`);
}
throw new Error(`Network Error: ${error.message}`);
}
}
2026-06-01 02:02:23 +00:00
/**
* Validates connection and fetches all repositories accessible by the user.
*/
export async function authenticateAndFetchRepos(
url: string,
userid: string,
token: string
): Promise<RepoItem[]> {
const normalized = normalizeUrl(url);
const headers: Record<string, string> = {
'Accept': 'application/json',
'Content-Type': 'application/json',
};
if (token.trim()) {
headers['Authorization'] = `token ${token.trim()}`;
}
const client = axios.create({
baseURL: `${normalized}/api/v1`,
headers,
timeout: 10000,
});
// 1. Verify credentials / user existence
try {
if (token.trim()) {
await client.get('/user');
} else if (userid.trim()) {
await client.get(`/users/${userid.trim()}`);
}
} catch (error: any) {
if (error.response) {
if (error.response.status === 401) {
throw new Error('Unauthorized: Invalid API Token.');
}
if (error.response.status === 404) {
throw new Error(`User "${userid}" not found.`);
}
throw new Error(`Authentication failed: ${error.response.data?.message || error.message}`);
}
throw new Error(`Failed to connect to ${normalized}: ${error.message}`);
}
// 2. Fetch repositories
try {
let repos: any[] = [];
if (token.trim()) {
// Fetch authenticated user's repos (lists all they have access to)
const response = await client.get('/user/repos', { params: { limit: 100 } });
repos = response.data;
} else if (userid.trim()) {
// Fetch public repos of the target username
const response = await client.get(`/users/${userid.trim()}/repos`, { params: { limit: 100 } });
repos = response.data;
}
if (!Array.isArray(repos)) {
throw new Error('Invalid response from server (expected list of repositories).');
}
return repos.map((r: any) => ({
id: r.id,
name: r.name,
full_name: r.full_name || `${r.owner?.login}/${r.name}`,
private: !!r.private,
description: r.description || '',
}));
} catch (error: any) {
if (error.response) {
throw new Error(`Failed to fetch repositories: ${error.response.data?.message || error.message}`);
}
throw new Error(`Failed to fetch repositories: ${error.message}`);
}
}