Initial scaffold: full-stack mRemotify monorepo
Sets up the complete mRemotify project — a browser-based remote connection manager — with a working pnpm workspace monorepo: Frontend (React + TypeScript + Vite + Ant Design 5): - Login page with JWT auth - Resizable sidebar with drag-and-drop connection tree (folders + connections) - Tabbed session area (SSH via xterm.js, RDP via guacamole-common-js) - Connection CRUD modal with SSH/RDP-specific fields - Zustand store for auth, tree data, and open sessions Backend (Fastify + TypeScript + Prisma + PostgreSQL): - JWT authentication (login + /me endpoint) - Full CRUD REST API for folders (self-referencing) and connections - AES-256-CBC password encryption at rest - WebSocket proxy for SSH sessions (ssh2 <-> xterm.js) - WebSocket proxy for RDP sessions (guacd TCP handshake + bidirectional relay) - Admin user seeding on first start Infrastructure: - Docker Compose: postgres (healthcheck) + guacd + backend + frontend/nginx - nginx: serves SPA, proxies /api and /ws (with WebSocket upgrade) to backend - .env.example with all required variables documented Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
85
frontend/src/store/index.ts
Normal file
85
frontend/src/store/index.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { create } from 'zustand';
|
||||
import { Connection, Folder, Session, User } from '../types';
|
||||
|
||||
interface AppState {
|
||||
// Auth
|
||||
token: string | null;
|
||||
user: User | null;
|
||||
setAuth: (token: string, user: User) => void;
|
||||
logout: () => void;
|
||||
|
||||
// Tree data
|
||||
folders: Folder[];
|
||||
connections: Connection[];
|
||||
setFolders: (folders: Folder[]) => void;
|
||||
setConnections: (connections: Connection[]) => void;
|
||||
|
||||
// Open sessions (tabs)
|
||||
sessions: Session[];
|
||||
activeSessionId: string | null;
|
||||
openSession: (connection: Connection) => void;
|
||||
closeSession: (sessionId: string) => void;
|
||||
setActiveSession: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
export const useStore = create<AppState>((set, get) => ({
|
||||
// Auth
|
||||
token: localStorage.getItem('token'),
|
||||
user: (() => {
|
||||
try {
|
||||
const u = localStorage.getItem('user');
|
||||
return u ? (JSON.parse(u) as User) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
|
||||
setAuth: (token, user) => {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
set({ token, user });
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
set({ token: null, user: null, sessions: [], activeSessionId: null });
|
||||
},
|
||||
|
||||
// Tree
|
||||
folders: [],
|
||||
connections: [],
|
||||
setFolders: (folders) => set({ folders }),
|
||||
setConnections: (connections) => set({ connections }),
|
||||
|
||||
// Sessions
|
||||
sessions: [],
|
||||
activeSessionId: null,
|
||||
|
||||
openSession: (connection) => {
|
||||
const existing = get().sessions.find((s) => s.connection.id === connection.id);
|
||||
if (existing) {
|
||||
set({ activeSessionId: existing.id });
|
||||
return;
|
||||
}
|
||||
const id = `session-${Date.now()}-${Math.random()}`;
|
||||
const session: Session = { id, connection };
|
||||
set((state) => ({
|
||||
sessions: [...state.sessions, session],
|
||||
activeSessionId: id,
|
||||
}));
|
||||
},
|
||||
|
||||
closeSession: (sessionId) => {
|
||||
set((state) => {
|
||||
const sessions = state.sessions.filter((s) => s.id !== sessionId);
|
||||
const activeSessionId =
|
||||
state.activeSessionId === sessionId
|
||||
? (sessions[sessions.length - 1]?.id ?? null)
|
||||
: state.activeSessionId;
|
||||
return { sessions, activeSessionId };
|
||||
});
|
||||
},
|
||||
|
||||
setActiveSession: (sessionId) => set({ activeSessionId: sessionId }),
|
||||
}));
|
||||
Reference in New Issue
Block a user