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:
FelixG
2026-02-22 12:21:36 +01:00
parent 8bc43896fb
commit 3802924c6a
44 changed files with 2707 additions and 1 deletions

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Tabs, Typography } from 'antd';
import { CodeOutlined, WindowsOutlined } from '@ant-design/icons';
import { useStore } from '../../store';
import { SshTab } from './SshTab';
import { RdpTab } from './RdpTab';
import { Session } from '../../types';
function tabLabel(session: Session) {
const icon =
session.connection.protocol === 'rdp' ? (
<WindowsOutlined />
) : (
<CodeOutlined />
);
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
{icon}
{session.connection.name}
</span>
);
}
export const SessionTabs: React.FC = () => {
const sessions = useStore((s) => s.sessions);
const activeSessionId = useStore((s) => s.activeSessionId);
const closeSession = useStore((s) => s.closeSession);
const setActiveSession = useStore((s) => s.setActiveSession);
if (sessions.length === 0) {
return (
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: '#bfbfbf',
userSelect: 'none',
}}
>
<Typography.Title level={4} type="secondary" style={{ marginBottom: 8 }}>
No open sessions
</Typography.Title>
<Typography.Text type="secondary">
Double-click a connection in the sidebar to start a session.
</Typography.Text>
</div>
);
}
return (
<Tabs
type="editable-card"
hideAdd
activeKey={activeSessionId ?? undefined}
onChange={setActiveSession}
onEdit={(key, action) => {
if (action === 'remove') closeSession(key as string);
}}
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
tabBarStyle={{ margin: 0, flexShrink: 0 }}
items={sessions.map((session) => ({
key: session.id,
label: tabLabel(session),
closable: true,
style: { height: '100%', padding: 0 },
children: (
<div style={{ height: '100%', overflow: 'hidden' }}>
{session.connection.protocol === 'ssh' ? (
<SshTab session={session} />
) : (
<RdpTab session={session} />
)}
</div>
),
}))}
/>
);
};