mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 02:12:59 +00:00
refactor: enhance security by dynamically retrieving JWT secret and updating cookie options; adjust directory paths for deployment
This commit is contained in:
+24
-16
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user