Add relay server for remote HTTPS access
Node.js/TypeScript relay server that enables remote access to MacroPad: - WebSocket-based communication between desktop and relay - Password authentication with bcrypt hashing - Session management with consistent IDs - REST API proxying to desktop app - Web client WebSocket relay - Login page and PWA-ready app page - Designed for cloud-node-container deployment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// Session Manager - handles session storage and authentication
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { config } from '../config';
|
||||
import { generateSessionId } from '../utils/idGenerator';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
passwordHash: string;
|
||||
createdAt: string;
|
||||
lastConnected: string;
|
||||
}
|
||||
|
||||
interface SessionStore {
|
||||
sessions: Record<string, Session>;
|
||||
}
|
||||
|
||||
export class SessionManager {
|
||||
private sessionsFile: string;
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.sessionsFile = path.join(config.dataDir, 'sessions.json');
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
try {
|
||||
if (fs.existsSync(this.sessionsFile)) {
|
||||
const data = JSON.parse(fs.readFileSync(this.sessionsFile, 'utf-8')) as SessionStore;
|
||||
for (const [id, session] of Object.entries(data.sessions || {})) {
|
||||
this.sessions.set(id, session);
|
||||
}
|
||||
logger.info(`Loaded ${this.sessions.size} sessions`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load sessions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
try {
|
||||
const store: SessionStore = {
|
||||
sessions: Object.fromEntries(this.sessions)
|
||||
};
|
||||
fs.mkdirSync(path.dirname(this.sessionsFile), { recursive: true });
|
||||
fs.writeFileSync(this.sessionsFile, JSON.stringify(store, null, 2));
|
||||
} catch (error) {
|
||||
logger.error('Failed to save sessions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async exists(sessionId: string): Promise<boolean> {
|
||||
return this.sessions.has(sessionId);
|
||||
}
|
||||
|
||||
async createSession(password: string): Promise<Session> {
|
||||
// Generate unique session ID
|
||||
let id: string;
|
||||
do {
|
||||
id = generateSessionId(config.sessionIdLength);
|
||||
} while (this.sessions.has(id));
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, config.bcryptRounds);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const session: Session = {
|
||||
id,
|
||||
passwordHash,
|
||||
createdAt: now,
|
||||
lastConnected: now
|
||||
};
|
||||
|
||||
this.sessions.set(id, session);
|
||||
this.save();
|
||||
|
||||
logger.info(`Created new session: ${id}`);
|
||||
return session;
|
||||
}
|
||||
|
||||
async validatePassword(sessionId: string, password: string): Promise<boolean> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, session.passwordHash);
|
||||
|
||||
if (valid) {
|
||||
// Update last connected time
|
||||
session.lastConnected = new Date().toISOString();
|
||||
this.save();
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
getSession(sessionId: string): Session | undefined {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
updateLastConnected(sessionId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.lastConnected = new Date().toISOString();
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
deleteSession(sessionId: string): boolean {
|
||||
const deleted = this.sessions.delete(sessionId);
|
||||
if (deleted) {
|
||||
this.save();
|
||||
logger.info(`Deleted session: ${sessionId}`);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user