refactor: enhance security by dynamically retrieving JWT secret and updating cookie options; adjust directory paths for deployment

This commit is contained in:
SaelixCode
2026-02-20 20:40:16 -05:00
parent 312ad6a4c4
commit b022b1c879
4 changed files with 46 additions and 31 deletions
+24 -16
View File
@@ -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<void> => {
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<void> =
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<void> =
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<void> =
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);
+13 -2
View File
@@ -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<string> {
const config = await this.readConfig();
if (!config || !config.jwtSecret) {
throw new Error('JWT secret not found - setup may not be complete');
}
return config.jwtSecret;
}
}
+1 -1
View File
@@ -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';
}
/**
+8 -12
View File
@@ -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