Add Gitea integration + ESLint setup
Phase 3: Gitea Integration - Migration 012: app_settings table + gitea_commit_sha on endpoint_versions - SettingsService: encrypted token storage (AES-256-GCM from JWT_SECRET) - GiteaService: full REST API client — repo management, file CRUD, branches, commit history, diff/compare, pull requests, sync all - giteaController + routes: 15 API endpoints for settings, branches, commits, PRs, endpoint info, sync - VersionService hooks: auto-sync to Gitea on publish/draft (non-blocking) - Frontend: Gitea tab in Settings (connection, sync status, branch mgmt), Gitea panel in EndpointEditor (file link, last commit SHA) - docker-compose.gitea.yml: optional companion Gitea container - Cache fix: index.html served with no-cache for instant deploys ESLint Setup - Backend: eslint 8 + @typescript-eslint configured - Frontend: eslint 8 + @typescript-eslint + react-hooks + react-refresh - Fixed 15 lint issues: unused imports, require statements, escapes, Function type, empty catch blocks - Added npm run lint / lint:fix scripts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export const getApiKeys = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt, { SignOptions } from 'jsonwebtoken';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mainPool } from '../config/database';
|
||||
import { config } from '../config/environment';
|
||||
|
||||
|
||||
@@ -265,6 +265,7 @@ export const testDatabaseConnection = async (req: AuthRequest, res: Response) =>
|
||||
const dbType = dbResult.rows[0].type;
|
||||
|
||||
if (dbType === 'aql') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { aqlExecutor } = require('../services/AqlExecutor');
|
||||
const result = await aqlExecutor.testConnection(id, env);
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
return res.status(500).json({ error: 'AQL configuration is incomplete' });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { aqlExecutor } = require('../services/AqlExecutor');
|
||||
result = await aqlExecutor.executeAqlQuery(endpoint.database_id, {
|
||||
method: aqlMethod,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { mainPool } from '../config/database';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ExportedEndpoint, ExportedScriptQuery, ScriptExecutionError, Environment } from '../types';
|
||||
import { encryptEndpointData, decryptEndpointData } from '../services/endpointCrypto';
|
||||
import { versionService } from '../services/VersionService';
|
||||
@@ -325,6 +324,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { sqlExecutor } = require('../services/SqlExecutor');
|
||||
const result = await sqlExecutor.executeQuery(database_id, processedQuery, parameters || [], environment);
|
||||
|
||||
@@ -352,6 +352,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { scriptExecutor } = require('../services/ScriptExecutor');
|
||||
const scriptResult = await scriptExecutor.execute(script_language, script_code, {
|
||||
databaseId: database_id,
|
||||
@@ -383,6 +384,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { aqlExecutor } = require('../services/AqlExecutor');
|
||||
const result = await aqlExecutor.executeAqlQuery(database_id, {
|
||||
method: aql_method,
|
||||
@@ -512,8 +514,8 @@ export const exportEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
const encrypted = encryptEndpointData(exportData);
|
||||
|
||||
const safeFileName = endpoint.name.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
||||
const encodedFileName = encodeURIComponent(endpoint.name.replace(/[\/\\:*?"<>|]/g, '_')) + '.kabe';
|
||||
const safeFileName = endpoint.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
const encodedFileName = encodeURIComponent(endpoint.name.replace(/[/\\:*?"<>|]/g, '_')) + '.kabe';
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${safeFileName}.kabe"; filename*=UTF-8''${encodedFileName}`);
|
||||
res.send(encrypted);
|
||||
|
||||
155
backend/src/controllers/giteaController.ts
Normal file
155
backend/src/controllers/giteaController.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import { settingsService } from '../services/SettingsService';
|
||||
import { giteaService } from '../services/GiteaService';
|
||||
|
||||
export const getSettings = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const settings = await settingsService.getGiteaSettings();
|
||||
if (!settings) return res.json({ enabled: false, url: '', token: '', owner: '', repo: '', prod_branch: 'main' });
|
||||
// Mask token
|
||||
const masked = settings.token
|
||||
? '****' + settings.token.slice(-4)
|
||||
: '';
|
||||
res.json({ ...settings, token: masked });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSettings = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { enabled, url, token, owner, repo, prod_branch } = req.body;
|
||||
|
||||
// If token is masked (starts with ****), keep existing
|
||||
let actualToken = token;
|
||||
if (token?.startsWith('****')) {
|
||||
const existing = await settingsService.getGiteaSettings();
|
||||
actualToken = existing?.token || '';
|
||||
}
|
||||
|
||||
await settingsService.setGiteaSettings({
|
||||
enabled: enabled ?? false,
|
||||
url: url || '',
|
||||
token: actualToken || '',
|
||||
owner: owner || '',
|
||||
repo: repo || '',
|
||||
prod_branch: prod_branch || 'main',
|
||||
}, req.user!.id);
|
||||
|
||||
giteaService.invalidateCache();
|
||||
|
||||
res.json({ message: 'Settings saved' });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const testGiteaConnection = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await giteaService.testConnection();
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const syncAll = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await giteaService.initializeRepo();
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const getSyncStatus = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const status = await giteaService.getSyncStatus();
|
||||
res.json(status);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const listBranches = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const branches = await giteaService.listBranches();
|
||||
res.json(branches);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const createBranch = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name, from } = req.body;
|
||||
if (!name) return res.status(400).json({ error: 'Branch name required' });
|
||||
await giteaService.createBranch(name, from);
|
||||
res.json({ message: `Branch ${name} created` });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBranch = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
await giteaService.deleteBranch(name);
|
||||
res.json({ message: `Branch ${name} deleted` });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const compareBranches = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { base, head } = req.query as { base: string; head: string };
|
||||
if (!base || !head) return res.status(400).json({ error: 'base and head required' });
|
||||
const diff = await giteaService.compareBranches(base, head);
|
||||
res.json(diff);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const getCommitHistory = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { path, branch } = req.query as { path?: string; branch?: string };
|
||||
const commits = await giteaService.getCommitHistory(path, branch);
|
||||
res.json(commits);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const getEndpointInfo = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const info = await giteaService.getEndpointGiteaInfo(id);
|
||||
res.json(info || { enabled: false });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const createPR = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { title, head, base, body } = req.body;
|
||||
if (!title || !head) return res.status(400).json({ error: 'title and head branch required' });
|
||||
const pr = await giteaService.createPR(title, head, base, body);
|
||||
res.json(pr);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const mergePR = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { index } = req.params;
|
||||
await giteaService.mergePR(parseInt(index));
|
||||
res.json({ message: 'PR merged' });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user