mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 09:13:36 +00:00
refactor(backend): move user routes to a module; add packages for cross-spawn types
This commit is contained in:
@@ -1 +0,0 @@
|
||||
|
||||
137
server/src/modules/user/user.routes.ts
Normal file
137
server/src/modules/user/user.routes.ts
Normal 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' });
|
||||
}
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
60
server/src/shared/utils/git-config.ts
Normal file
60
server/src/shared/utils/git-config.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user