diff --git a/backend/src/index.ts b/backend/src/index.ts index ce201dea..a30e66ed 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -14,9 +14,6 @@ import http from 'http'; const app = express(); const PORT = 3000; -// Environment variables -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; - // ConfigService for persistent auth storage const configService = new ConfigService(); @@ -25,12 +22,19 @@ const fileSystemService = new FileSystemService(); // Cookie settings const COOKIE_NAME = 'sencho_token'; -const COOKIE_OPTIONS = { + +// Helper to determine if request is secure (HTTPS or behind a proxy that terminates SSL) +const isSecureRequest = (req: Request): boolean => { + return req.secure || req.headers['x-forwarded-proto'] === 'https'; +}; + +// Helper to get cookie options dynamically per-request +const getCookieOptions = (req: Request) => ({ httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureRequest(req), sameSite: 'strict' as const, maxAge: 24 * 60 * 60 * 1000, // 24 hours -}; +}); // Middleware app.use(cors({ @@ -48,7 +52,7 @@ declare module 'express' { } // Authentication Middleware -const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { +const authMiddleware = async (req: Request, res: Response, next: NextFunction): Promise => { const token = req.cookies[COOKIE_NAME]; if (!token) { @@ -57,7 +61,8 @@ const authMiddleware = (req: Request, res: Response, next: NextFunction): void = } try { - const decoded = jwt.verify(token, JWT_SECRET) as { username: string }; + const jwtSecret = await configService.getJwtSecret(); + const decoded = jwt.verify(token, jwtSecret) as { username: string }; req.user = { username: decoded.username }; next(); } catch { @@ -112,12 +117,13 @@ app.post('/api/auth/setup', async (req: Request, res: Response): Promise = return; } - // Save credentials + // Save credentials (this also generates the JWT secret) await configService.saveConfig(username, password); // Issue JWT and log user in - const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '24h' }); - res.cookie(COOKIE_NAME, token, COOKIE_OPTIONS); + const jwtSecret = await configService.getJwtSecret(); + const token = jwt.sign({ username }, jwtSecret, { expiresIn: '24h' }); + res.cookie(COOKIE_NAME, token, getCookieOptions(req)); res.json({ success: true, message: 'Setup completed successfully' }); } catch (error) { console.error('Setup error:', error); @@ -138,8 +144,9 @@ app.post('/api/auth/login', async (req: Request, res: Response): Promise = const isValid = await configService.validateCredentials(username, password); if (isValid) { - const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '24h' }); - res.cookie(COOKIE_NAME, token, COOKIE_OPTIONS); + const jwtSecret = await configService.getJwtSecret(); + const token = jwt.sign({ username }, jwtSecret, { expiresIn: '24h' }); + res.cookie(COOKIE_NAME, token, getCookieOptions(req)); res.json({ success: true, message: 'Login successful' }); return; } @@ -154,7 +161,7 @@ app.post('/api/auth/login', async (req: Request, res: Response): Promise = app.post('/api/auth/logout', (req: Request, res: Response): void => { res.clearCookie(COOKIE_NAME, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureRequest(req), sameSite: 'strict', }); res.json({ success: true, message: 'Logged out successfully' }); @@ -183,7 +190,7 @@ const wss = new WebSocket.Server({ noServer: true }); let terminalWs: WebSocket | null = null; // Handle WebSocket upgrade with JWT authentication -server.on('upgrade', (req, socket, head) => { +server.on('upgrade', async (req, socket, head) => { // Parse cookies from the upgrade request const cookieHeader = req.headers.cookie || ''; const cookies = Object.fromEntries( @@ -199,7 +206,8 @@ server.on('upgrade', (req, socket, head) => { } try { - jwt.verify(token, JWT_SECRET); + const jwtSecret = await configService.getJwtSecret(); + jwt.verify(token, jwtSecret); // Authentication successful, proceed with WebSocket connection wss.handleUpgrade(req, socket, head, (ws) => { wss.emit('connection', ws, req); diff --git a/backend/src/services/ConfigService.ts b/backend/src/services/ConfigService.ts index cae56850..192d4510 100644 --- a/backend/src/services/ConfigService.ts +++ b/backend/src/services/ConfigService.ts @@ -1,10 +1,12 @@ import { promises as fs } from 'fs'; import path from 'path'; import bcrypt from 'bcrypt'; +import crypto from 'crypto'; interface AuthConfig { username: string; passwordHash: string; + jwtSecret: string; } export class ConfigService { @@ -12,7 +14,7 @@ export class ConfigService { private configPath: string; constructor() { - this.dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'data'); + this.dataDir = process.env.DATA_DIR || '/app/data'; this.configPath = path.join(this.dataDir, 'sencho.json'); } @@ -46,7 +48,8 @@ export class ConfigService { await this.ensureDataDir(); const saltRounds = 10; const passwordHash = await bcrypt.hash(password, saltRounds); - const config: AuthConfig = { username, passwordHash }; + const jwtSecret = crypto.randomBytes(64).toString('hex'); + const config: AuthConfig = { username, passwordHash, jwtSecret }; await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf-8'); } @@ -58,4 +61,12 @@ export class ConfigService { return await bcrypt.compare(password, config.passwordHash); } + + async getJwtSecret(): Promise { + const config = await this.readConfig(); + if (!config || !config.jwtSecret) { + throw new Error('JWT secret not found - setup may not be complete'); + } + return config.jwtSecret; + } } diff --git a/backend/src/services/FileSystemService.ts b/backend/src/services/FileSystemService.ts index 1bb56dd0..3d7ab6d3 100644 --- a/backend/src/services/FileSystemService.ts +++ b/backend/src/services/FileSystemService.ts @@ -5,7 +5,7 @@ export class FileSystemService { private baseDir: string; constructor() { - this.baseDir = process.env.COMPOSE_DIR || path.join(process.cwd(), '..', 'mock_data', 'docker', 'compose'); + this.baseDir = process.env.COMPOSE_DIR || '/app/compose'; } /** diff --git a/docker-compose.yml b/docker-compose.yml index c1dcaf0f..beb6861d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,19 @@ services: sencho: image: saelix/sencho:latest - build: . container_name: sencho restart: unless-stopped ports: - "3000:3000" volumes: - /var/run/docker.sock:/var/run/docker.sock + + # DATA DIRECTORY + # Left Side: Where Sencho saves your admin login (Change this if you want it elsewhere) + # Right Side: Internal container path (DO NOT CHANGE) - ./data:/app/data - # Stacks Directory - # Left Side = Path on your host server (Change this to your actual folder) - # Right Side = Path inside Sencho (Do NOT change) - - /path/to/your/docker/compose:/app/compose - - environment: - # Tell Sencho's backend where to look inside the container - - COMPOSE_DIR=/app/compose - - DATA_DIR=/app/data - # Generate a secure random string for this in production - - JWT_SECRET=change-me-in-production + # STACKS DIRECTORY + # Left Side: The absolute path to your Docker compose stacks on your server + # Right Side: Internal container path (DO NOT CHANGE) + - SENCHO_STACKS_DIR:/app/compose