122 lines
3.0 KiB
TypeScript
122 lines
3.0 KiB
TypeScript
|
|
// 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;
|
||
|
|
}
|
||
|
|
}
|