refactor(backend): move user routes to a module; add packages for cross-spawn types

This commit is contained in:
Haileyesus
2026-03-26 14:12:04 +03:00
parent 1abdb95207
commit 9a8178e9ca
6 changed files with 215 additions and 1 deletions

View File

@@ -1 +0,0 @@

View File

@@ -0,0 +1,137 @@
import express, { type Response } from 'express';
import { userDb } from '@/shared/database/repositories/users.js';
import { authenticateToken } from '@/modules/auth/auth.middleware.js';
import { getSystemGitConfig, spawnAsync } from '@/shared/utils/git-config.js';
import type { AuthenticatedRequest } from '@/shared/types/http.js';
import { logger } from '@/shared/utils/logger.js';
export const userRoutes = express.Router();
/**
* Get the user's git config.
* Falls back to system's global git config if not set in the DB.
*/
userRoutes.get('/git-config', authenticateToken, async (req: AuthenticatedRequest, res: Response): Promise<void> => {
try {
if (!req.user || !req.user.id) {
res.status(401).json({ error: 'User not authenticated' });
return;
}
const userId = Number(req.user.id);
let gitConfig = userDb.getGitConfig(userId);
// If database is empty, try to get from system git config
if (!gitConfig || (!gitConfig.git_name && !gitConfig.git_email)) {
const systemConfig = await getSystemGitConfig();
// If system has values, save them to database for this user
if (systemConfig.git_name || systemConfig.git_email) {
userDb.updateGitConfig(userId, systemConfig.git_name || '', systemConfig.git_email || '');
gitConfig = systemConfig;
logger.info(`Auto-populated git config from system for user ${userId}: ${systemConfig.git_name} <${systemConfig.git_email}>`);
}
}
res.json({
success: true,
gitName: gitConfig?.git_name || null,
gitEmail: gitConfig?.git_email || null
});
} catch (error) {
logger.error('Error getting git config:', { error });
res.status(500).json({ error: 'Failed to get git configuration' });
}
});
/**
* Apply git config globally via git config --global and save to DB
*/
userRoutes.post('/git-config', authenticateToken, async (req: AuthenticatedRequest, res: Response): Promise<void> => {
try {
if (!req.user || !req.user.id) {
res.status(401).json({ error: 'User not authenticated' });
return;
}
const userId = Number(req.user.id);
const { gitName, gitEmail } = req.body;
if (!gitName || !gitEmail) {
res.status(400).json({ error: 'Git name and email are required' });
return;
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(gitEmail)) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
userDb.updateGitConfig(userId, gitName, gitEmail);
try {
await spawnAsync('git', ['config', '--global', 'user.name', String(gitName)]);
await spawnAsync('git', ['config', '--global', 'user.email', String(gitEmail)]);
logger.info(`Applied git config globally: ${gitName} <${gitEmail}>`);
} catch (gitError) {
logger.error('Error applying git config:', { error: gitError });
}
res.json({
success: true,
gitName,
gitEmail
});
} catch (error) {
logger.error('Error updating git config:', { error });
res.status(500).json({ error: 'Failed to update git configuration' });
}
});
/**
* Complete onboarding for the user
*/
userRoutes.post('/complete-onboarding', authenticateToken, async (req: AuthenticatedRequest, res: Response): Promise<void> => {
try {
if (!req.user || !req.user.id) {
res.status(401).json({ error: 'User not authenticated' });
return;
}
const userId = Number(req.user.id);
userDb.completeOnboarding(userId);
res.json({
success: true,
message: 'Onboarding completed successfully'
});
} catch (error) {
logger.error('Error completing onboarding:', { error });
res.status(500).json({ error: 'Failed to complete onboarding' });
}
});
/**
* Get onboarding status for the user
*/
userRoutes.get('/onboarding-status', authenticateToken, async (req: AuthenticatedRequest, res: Response): Promise<void> => {
try {
if (!req.user || !req.user.id) {
res.status(401).json({ error: 'User not authenticated' });
return;
}
const userId = Number(req.user.id);
const hasCompleted = userDb.hasCompletedOnboarding(userId);
res.json({
success: true,
hasCompletedOnboarding: hasCompleted
});
} catch (error) {
logger.error('Error checking onboarding status:', { error });
res.status(500).json({ error: 'Failed to check onboarding status' });
}
});

View File

@@ -12,6 +12,8 @@ import { initializeWatcher } from '@/modules/sessions/sessions.watcher.js';
import { getConnectableHost } from '@/shared/utils/networkHosts.js';
import { logger } from '@/shared/utils/logger.js';
import { authRoutes } from '@/modules/auth/auth.routes.js';
import { userRoutes } from '@/modules/user/user.routes.js';
import { authenticateToken } from '@/modules/auth/auth.middleware.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -58,8 +60,12 @@ app.use((req, res, next) => {
});
// Authentication routes (public)
app.use('/api/auth', authRoutes);
// User API Routes (protected)
app.use('/api/user', authenticateToken, userRoutes);
// This matches files found in the root public folder (like api-docs.html when we run `/api-docs.html`).
// If the file is found, it's automatically sent. If it is not, it passes it to the next route checker.
// This will run in production as well as development URLs.

View File

@@ -0,0 +1,60 @@
import spawn from 'cross-spawn';
import type { SpawnOptionsWithoutStdio } from 'child_process';
type SpawnResult = {
stdout: string;
stderr: string;
};
export function spawnAsync(command: string, args: string[], options: SpawnOptionsWithoutStdio = {}): Promise<SpawnResult> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { ...options, shell: false });
let stdout = '';
let stderr = '';
if (child.stdout) {
child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
}
if (child.stderr) {
child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
}
child.on('error', (error: Error) => { reject(error); });
child.on('close', (code: number | null) => {
if (code === 0) {
resolve({ stdout, stderr });
return;
}
const error = new Error(`Command failed: ${command} ${args.join(' ')}`) as Error & {
code: number | null;
stdout: string;
stderr: string;
};
error.code = code;
error.stdout = stdout;
error.stderr = stderr;
reject(error);
});
});
}
/**
* Read git configuration from system's global git config
*/
export async function getSystemGitConfig(): Promise<{ git_name: string | null; git_email: string | null }> {
try {
const [nameResult, emailResult] = await Promise.all([
spawnAsync('git', ['config', '--global', 'user.name']).catch(() => ({ stdout: '', stderr: '' })),
spawnAsync('git', ['config', '--global', 'user.email']).catch(() => ({ stdout: '', stderr: '' }))
]);
return {
git_name: nameResult.stdout.trim() || null,
git_email: emailResult.stdout.trim() || null
};
} catch (error) {
return { git_name: null, git_email: null };
}
}