fix: systematic bug fixes — code-quality parser, env key, config warnings, auth cleanup, deployment safety
- code-quality/repository.ts: fix tsErrorMatch[3] → [4] for type field (group 3 is column, 4 is error|warning) - code-quality/repository.ts: fix ESLint regex to make rule brackets optional (not all formatters include them) - code-quality/repository.ts: fix Vitest test count — parse 'Tests' line (individual tests) instead of 'Test Files' (file count); improve Jest regex to capture pass/fail independently - env/repository.ts: replace raw process.env.ENCRYPTION_KEY with config.ENCRYPTION_KEY so the validated default flows through a single source of truth - config.ts: add startup console.warn when CSRF_SECRET or ENCRYPTION_KEY are using insecure defaults - deployments/orchestrator.ts: refactor runDeploymentScript to use try/catch/finally — deployment record is now always written in the finally block, preventing zombie 'running' states if updateDeployment itself throws - auth.tsx: remove dead 'user &&' guard (user is always truthy after the !user check above); remove debug console.log calls, keep console.error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cdc23696b2
commit
31b414d62b
@ -31,5 +31,13 @@ const envSchema = z.object({
|
|||||||
|
|
||||||
export const config = envSchema.parse(process.env);
|
export const config = envSchema.parse(process.env);
|
||||||
|
|
||||||
|
// Warn loudly when insecure default keys are in use
|
||||||
|
if (config.CSRF_SECRET === 'default-csrf-secret-change-in-production') {
|
||||||
|
console.warn('[config] WARNING: CSRF_SECRET is using the insecure default — set CSRF_SECRET in .env before deploying to production');
|
||||||
|
}
|
||||||
|
if (config.ENCRYPTION_KEY === 'default-encryption-key-change-in-production') {
|
||||||
|
console.warn('[config] WARNING: ENCRYPTION_KEY is using the insecure default — set ENCRYPTION_KEY in .env before deploying to production');
|
||||||
|
}
|
||||||
|
|
||||||
export const productId = productIdentity.productId;
|
export const productId = productIdentity.productId;
|
||||||
export const productName = productIdentity.name;
|
export const productName = productIdentity.name;
|
||||||
|
|||||||
@ -148,7 +148,7 @@ function parseTypeScriptOutput(output: string, projectPath: string): CodeQuality
|
|||||||
if (tsErrorMatch) {
|
if (tsErrorMatch) {
|
||||||
issues.push({
|
issues.push({
|
||||||
id: `ts-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
id: `ts-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||||
type: tsErrorMatch[3] as 'error' | 'warning',
|
type: tsErrorMatch[4] as 'error' | 'warning', // group 4 = type; group 3 = column
|
||||||
category: 'typescript',
|
category: 'typescript',
|
||||||
file: tsErrorMatch[1],
|
file: tsErrorMatch[1],
|
||||||
line: parseInt(tsErrorMatch[2]),
|
line: parseInt(tsErrorMatch[2]),
|
||||||
@ -167,10 +167,12 @@ function parseEslintOutput(output: string, projectPath: string): CodeQualityIssu
|
|||||||
const lines = output.split('\n');
|
const lines = output.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// ESLint format: file:line:col message [rule]
|
// ESLint unix format: file:line:col: message [rule]
|
||||||
const eslintMatch = line.match(/(.+\.tsx?):(\d+):(\d+)\s+(.+?)\s+\[(.+)\]/);
|
// Rule part in brackets may or may not be present depending on formatter
|
||||||
|
const eslintMatch = line.match(/(.+\.tsx?):(\d+):(\d+)[:\s]+(.+?)(?:\s+\[([^\]]+)\])?$/);
|
||||||
if (eslintMatch) {
|
if (eslintMatch) {
|
||||||
const severity = eslintMatch[4].includes('error') ? 'error' : 'warning';
|
const msgAndLevel = eslintMatch[4];
|
||||||
|
const severity = /\berror\b/i.test(msgAndLevel) ? 'error' : 'warning';
|
||||||
issues.push({
|
issues.push({
|
||||||
id: `eslint-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
id: `eslint-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||||
type: severity,
|
type: severity,
|
||||||
@ -178,8 +180,8 @@ function parseEslintOutput(output: string, projectPath: string): CodeQualityIssu
|
|||||||
file: eslintMatch[1],
|
file: eslintMatch[1],
|
||||||
line: parseInt(eslintMatch[2]),
|
line: parseInt(eslintMatch[2]),
|
||||||
column: parseInt(eslintMatch[3]),
|
column: parseInt(eslintMatch[3]),
|
||||||
message: eslintMatch[4],
|
message: msgAndLevel,
|
||||||
rule: eslintMatch[5],
|
rule: eslintMatch[5] ?? 'unknown',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,18 +212,24 @@ function parseTestOutput(output: string): { passed: number; failed: number } {
|
|||||||
let passed = 0;
|
let passed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|
||||||
// Try to parse Vitest output
|
// Try to parse Vitest output — use "Tests" line (individual tests), not "Test Files" line
|
||||||
const vitestMatch = output.match(/Test Files\s+(\d+)\s+\((\d+)\s+failed/);
|
// Format: " Tests 3 failed | 5 passed (8)" or " Tests 8 passed (8)"
|
||||||
if (vitestMatch) {
|
const vitestFailMatch = output.match(/\bTests\b\s+(\d+)\s+failed[^|]*\|\s*(\d+)\s+passed/);
|
||||||
failed = parseInt(vitestMatch[2]);
|
const vitestPassMatch = output.match(/\bTests\b\s+(\d+)\s+passed/);
|
||||||
passed = parseInt(vitestMatch[1]) - failed;
|
if (vitestFailMatch) {
|
||||||
|
failed = parseInt(vitestFailMatch[1]);
|
||||||
|
passed = parseInt(vitestFailMatch[2]);
|
||||||
|
} else if (vitestPassMatch) {
|
||||||
|
passed = parseInt(vitestPassMatch[1]);
|
||||||
|
failed = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse Jest output
|
// Try to parse Jest output: "Tests: 5 passed, 2 failed" or "Tests: 2 failed, 5 passed"
|
||||||
const jestMatch = output.match(/Tests:\s+(\d+)\s+passed,?\s*(\d+)\s+failed/);
|
const jestPassMatch = output.match(/Tests:.*?(\d+)\s+passed/);
|
||||||
if (jestMatch) {
|
const jestFailMatch = output.match(/Tests:.*?(\d+)\s+failed/);
|
||||||
passed = parseInt(jestMatch[1]);
|
if (jestPassMatch || jestFailMatch) {
|
||||||
failed = parseInt(jestMatch[2]);
|
passed = jestPassMatch ? parseInt(jestPassMatch[1]) : 0;
|
||||||
|
failed = jestFailMatch ? parseInt(jestFailMatch[1]) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { passed, failed };
|
return { passed, failed };
|
||||||
|
|||||||
@ -30,6 +30,10 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
|
|||||||
const scriptDir = join(process.cwd(), '../../'); // Go to bytelyst-devops-tools root
|
const scriptDir = join(process.cwd(), '../../'); // Go to bytelyst-devops-tools root
|
||||||
const scriptPath = join(scriptDir, service.scriptPath);
|
const scriptPath = join(scriptDir, service.scriptPath);
|
||||||
|
|
||||||
|
let finalStatus: 'success' | 'failed' = 'failed';
|
||||||
|
let logs = '';
|
||||||
|
let version: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await execAsync(`bash ${scriptPath}`, {
|
const { stdout, stderr } = await execAsync(`bash ${scriptPath}`, {
|
||||||
cwd: scriptDir,
|
cwd: scriptDir,
|
||||||
@ -40,15 +44,9 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
|
logs = `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
|
||||||
|
finalStatus = 'success';
|
||||||
// Update deployment as success
|
version = extractVersion(stdout + stderr) || 'unknown';
|
||||||
await updateDeployment(deploymentId, {
|
|
||||||
status: 'success',
|
|
||||||
logs,
|
|
||||||
completedAt: new Date().toISOString(),
|
|
||||||
version: extractVersion(stdout + stderr) || 'unknown',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update service status
|
// Update service status
|
||||||
const { getServiceById, updateService } = await import('../services/repository.js');
|
const { getServiceById, updateService } = await import('../services/repository.js');
|
||||||
@ -57,21 +55,14 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
|
|||||||
await updateService(service.id, {
|
await updateService(service.id, {
|
||||||
status: 'up',
|
status: 'up',
|
||||||
lastDeployedAt: new Date().toISOString(),
|
lastDeployedAt: new Date().toISOString(),
|
||||||
version: extractVersion(stdout + stderr) || svc.version,
|
version: version || svc.version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const logs = error instanceof Error
|
logs = error instanceof Error
|
||||||
? `ERROR: ${error.message}\n\n${(error as any).stdout ? `STDOUT:\n${(error as any).stdout}\n\n` : ''}${(error as any).stderr ? `STDERR:\n${(error as any).stderr}` : ''}`
|
? `ERROR: ${error.message}\n\n${(error as any).stdout ? `STDOUT:\n${(error as any).stdout}\n\n` : ''}${(error as any).stderr ? `STDERR:\n${(error as any).stderr}` : ''}`
|
||||||
: String(error);
|
: String(error);
|
||||||
|
|
||||||
// Update deployment as failed
|
|
||||||
await updateDeployment(deploymentId, {
|
|
||||||
status: 'failed',
|
|
||||||
logs,
|
|
||||||
completedAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update service status to down
|
// Update service status to down
|
||||||
const { getServiceById, updateService } = await import('../services/repository.js');
|
const { getServiceById, updateService } = await import('../services/repository.js');
|
||||||
const svc = await getServiceById(service.id);
|
const svc = await getServiceById(service.id);
|
||||||
@ -80,6 +71,18 @@ async function runDeploymentScript(service: Service, deploymentId: string) {
|
|||||||
status: 'down',
|
status: 'down',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Always write final status — ensures the deployment never gets stuck in 'running'
|
||||||
|
try {
|
||||||
|
await updateDeployment(deploymentId, {
|
||||||
|
status: finalStatus,
|
||||||
|
logs,
|
||||||
|
completedAt: new Date().toISOString(),
|
||||||
|
...(version ? { version } : {}),
|
||||||
|
});
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error(`Failed to persist final deployment status for ${deploymentId}:`, updateError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,13 +73,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
const token = getAccessTokenFromStorage();
|
const token = getAccessTokenFromStorage();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.log('No token found in storage');
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Checking auth with token...');
|
|
||||||
|
|
||||||
// Add timeout to prevent hanging
|
// Add timeout to prevent hanging
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('Auth check timeout')), 10000)
|
setTimeout(() => reject(new Error('Auth check timeout')), 10000)
|
||||||
@ -90,15 +87,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
timeoutPromise
|
timeoutPromise
|
||||||
]) as MeResponse;
|
]) as MeResponse;
|
||||||
|
|
||||||
console.log('User data received:', userData);
|
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
|
|
||||||
// Simplified admin check - just check global admin role
|
// Simplified admin check - just check global admin role
|
||||||
const globalRole = userData.role;
|
const globalRole = userData.role;
|
||||||
const hasAdminAccess = globalRole === 'admin';
|
const hasAdminAccess = globalRole === 'admin';
|
||||||
setIsAdmin(hasAdminAccess);
|
setIsAdmin(hasAdminAccess);
|
||||||
|
|
||||||
console.log('Admin access:', hasAdminAccess);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth check failed:', error);
|
console.error('Auth check failed:', error);
|
||||||
clearAuthTokens();
|
clearAuthTokens();
|
||||||
@ -109,9 +103,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
async function login(email: string, password: string, productId: string) {
|
async function login(email: string, password: string, productId: string) {
|
||||||
try {
|
try {
|
||||||
console.log('Attempting login for:', email, 'with productId:', productId);
|
|
||||||
const response = await authApi.login({ email, password, productId });
|
const response = await authApi.login({ email, password, productId });
|
||||||
console.log('Login response received:', response);
|
|
||||||
|
|
||||||
setAccessToken(response.accessToken);
|
setAccessToken(response.accessToken);
|
||||||
setRefreshToken(response.refreshToken);
|
setRefreshToken(response.refreshToken);
|
||||||
@ -120,8 +112,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// Check if user has admin access (global admin role)
|
// Check if user has admin access (global admin role)
|
||||||
const hasAdminAccess = response.user.role === 'admin';
|
const hasAdminAccess = response.user.role === 'admin';
|
||||||
setIsAdmin(hasAdminAccess);
|
setIsAdmin(hasAdminAccess);
|
||||||
|
|
||||||
console.log('Login successful, admin access:', hasAdminAccess);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed:', error);
|
console.error('Login failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -148,7 +138,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return <div className="min-h-screen flex items-center justify-center">Redirecting to login...</div>;
|
return <div className="min-h-screen flex items-center justify-center">Redirecting to login...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user && !isAdmin) {
|
if (!isAdmin) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user