The file C:\Users\MikeSwanson\.claude\plans\velvet-tumbling-tide.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
     1→# GuruConnect User Management Plan
     2→
     3→## Overview
     4→
     5→Implement user authentication and authorization to secure the GuruConnect dashboard. Users can only be created by admins, with granular role-based permissions including per-client machine access control.
     6→
     7→## Requirements (User-Specified)
     8→
     9→- **User Creation:** Admin-only (no self-registration)
    10→- **Roles/Permissions:** Granular (view-only, control, admin, per-client access)
    11→- **Auth Source:** Local users only (no external SSO)
    12→
    13→## Architecture
    14→
    15→```
    16→┌─────────────────────────────────────────────────────────────┐
    17→│  Browser (Dashboard)                                        │
    18→│  - Login form → POST /api/auth/login                        │
    19→│  - JWT stored in localStorage/cookie                        │
    20→│  - JWT sent with all API requests                           │
    21→└─────────────────────────────────────────────────────────────┘
    22→                              │
    23→                              ▼
    24→┌─────────────────────────────────────────────────────────────┐
    25→│  Server API                                                 │
    26→│  - JWT validation middleware                                │
    27→│  - Role/permission checks per endpoint                      │
    28→│  - User management endpoints (admin only)                   │
    29→└─────────────────────────────────────────────────────────────┘
    30→                              │
    31→                              ▼
    32→┌─────────────────────────────────────────────────────────────┐
    33→│  PostgreSQL Database                                        │
    34→│  - users table (credentials, roles)                         │
    35→│  - user_permissions table (granular permissions)            │
    36→│  - user_client_access table (per-client access)             │
    37→└─────────────────────────────────────────────────────────────┘
    38→```
    39→
    40→## Database Schema
    41→
    42→### New Migration: `002_user_management.sql`
    43→
    44→```sql
    45→-- Users table
    46→CREATE TABLE users (
    47→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    48→    username VARCHAR(64) UNIQUE NOT NULL,
    49→    password_hash VARCHAR(255) NOT NULL,
    50→    email VARCHAR(255),
    51→    role VARCHAR(32) NOT NULL DEFAULT 'viewer',  -- 'admin', 'operator', 'viewer'
    52→    enabled BOOLEAN NOT NULL DEFAULT true,
    53→    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    54→    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    55→    last_login TIMESTAMPTZ
    56→);
    57→
    58→-- Granular permissions (what actions a user can perform)
    59→CREATE TABLE user_permissions (
    60→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    61→    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    62→    permission VARCHAR(64) NOT NULL,  -- 'view', 'control', 'transfer', 'manage_users', etc.
    63→    UNIQUE(user_id, permission)
    64→);
    65→
    66→-- Per-client access (which machines a user can access)
    67→-- NULL = access to all clients (for admins)
    68→CREATE TABLE user_client_access (
    69→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    70→    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    71→    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    72→    UNIQUE(user_id, client_id)
    73→);
    74→
    75→-- Indexes
    76→CREATE INDEX idx_users_username ON users(username);
    77→CREATE INDEX idx_user_permissions_user ON user_permissions(user_id);
    78→CREATE INDEX idx_user_client_access_user ON user_client_access(user_id);
    79→```
    80→
    81→### Role Definitions
    82→
    83→| Role | Description | Default Permissions |
    84→|------|-------------|---------------------|
    85→| admin | Full access, manage users | All permissions, all clients |
    86→| operator | Can control machines | view, control, transfer |
    87→| viewer | View-only access | view |
    88→
    89→### Permission Types
    90→
    91→| Permission | Description |
    92→|------------|-------------|
    93→| view | See dashboard, client list, status |
    94→| control | Take control of machines (keyboard/mouse) |
    95→| transfer | File transfer access |
    96→| manage_users | Create/edit/delete users (admin only) |
    97→| manage_clients | Delete clients, edit client metadata |
    98→
    99→## API Endpoints
   100→
   101→### Authentication
   102→
   103→| Method | Endpoint | Description | Auth |
   104→|--------|----------|-------------|------|
   105→| POST | `/api/auth/login` | Login, returns JWT | None |
   106→| POST | `/api/auth/logout` | Invalidate token | JWT |
   107→| GET | `/api/auth/me` | Get current user info | JWT |
   108→| POST | `/api/auth/change-password` | Change own password | JWT |
   109→
   110→### User Management (Admin Only)
   111→
   112→| Method | Endpoint | Description |
   113→|--------|----------|-------------|
   114→| GET | `/api/users` | List all users |
   115→| POST | `/api/users` | Create new user |
   116→| GET | `/api/users/:id` | Get user details |
   117→| PUT | `/api/users/:id` | Update user |
   118→| DELETE | `/api/users/:id` | Delete user |
   119→| PUT | `/api/users/:id/permissions` | Set user permissions |
   120→| PUT | `/api/users/:id/clients` | Set client access |
   121→
   122→## JWT Token Structure
   123→
   124→```json
   125→{
   126→  "sub": "user-uuid",
   127→  "username": "admin",
   128→  "role": "admin",
   129→  "permissions": ["view", "control", "transfer", "manage_users"],
   130→  "exp": 1234567890,
   131→  "iat": 1234567890
   132→}
   133→```
   134→
   135→## Files to Modify/Create
   136→
   137→### Server Changes
   138→
   139→| File | Action | Purpose |
   140→|------|--------|---------|
   141→| `server/migrations/002_user_management.sql` | CREATE | Database schema |
   142→| `server/src/auth/mod.rs` | REWRITE | Real JWT auth implementation |
   143→| `server/src/auth/jwt.rs` | CREATE | JWT creation/validation |
   144→| `server/src/auth/password.rs` | CREATE | Argon2 password hashing |
   145→| `server/src/models/user.rs` | CREATE | User model and DB queries |
   146→| `server/src/api/auth.rs` | CREATE | Auth API endpoints |
   147→| `server/src/api/users.rs` | CREATE | User management endpoints |
   148→| `server/src/api/mod.rs` | MODIFY | Add new route modules |
   149→| `server/src/main.rs` | MODIFY | Add auth middleware |
   150→
   151→### Dashboard Changes
   152→
   153→| File | Action | Purpose |
   154→|------|--------|---------|
   155→| `server/static/login.html` | CREATE | Login page |
   156→| `server/static/dashboard.html` | MODIFY | Add auth checks, user menu |
   157→| `server/static/users.html` | CREATE | User management page (admin) |
   158→
   159→## Implementation Phases
   160→
   161→### Phase 1: Database & Models
   162→1. Create migration `002_user_management.sql`
   163→2. Create `models/user.rs` with User struct and DB operations
   164→3. Add initial admin user creation (first-run setup)
   165→
   166→### Phase 2: Authentication
   167→1. Implement `auth/password.rs` - Argon2 hashing
   168→2. Implement `auth/jwt.rs` - Token create/validate
   169→3. Rewrite `auth/mod.rs` - Real middleware
   170→4. Create `api/auth.rs` - Login/logout endpoints
   171→
   172→### Phase 3: Authorization
   173→1. Add permission checking to existing endpoints
   174→2. Add client access filtering to client list
   175→3. Protect WebSocket connections with JWT
   176→
   177→### Phase 4: User Management API
   178→1. Create `api/users.rs` - CRUD endpoints
   179→2. Add permission/client access management
   180→3. Admin-only route protection
   181→
   182→### Phase 5: Dashboard UI
   183→1. Create login page with redirect
   184→2. Add logout button and user menu
   185→3. Create user management page
   186→4. Add permission-based UI hiding
   187→
   188→## Initial Admin Setup
   189→
   190→On first run (no users in DB):
   191→1. Server detects empty users table
   192→2. Generates random admin password
   193→3. Creates default admin user
   194→4. Prints credentials to console/logs
   195→5. Forces password change on first login (optional)
   196→
   197→Alternative: Environment variable for initial admin password.
   198→
   199→## Security Considerations
   200→
   201→1. **Password Storage:** Argon2id with recommended parameters
   202→2. **JWT Secret:** Random 256-bit key, stored in config/env
   203→3. **Token Expiry:** 24h default, configurable
   204→4. **HTTPS Only:** Cookies marked Secure in production
   205→5. **Rate Limiting:** Consider adding for login endpoint
   206→6. **Audit Logging:** Log auth events (login, logout, failed attempts)
   207→
   208→## Existing Dependencies (Already in Cargo.toml)
   209→
   210→- `jsonwebtoken` - JWT handling
   211→- `argon2` - Password hashing
   212→- `sqlx` - Database queries
   213→
   214→## Critical Files to Review During Implementation
   215→
   216→- `server/src/auth/mod.rs` - Current placeholder auth (lines 1-50)
   217→- `server/src/api/mod.rs` - Current API routes
   218→- `server/src/ws.rs` - WebSocket auth (needs JWT validation)
   219→- `server/src/db.rs` - Database connection handling
   220→

# GuruConnect User Management Plan

## Overview

Implement user authentication and authorization to secure the GuruConnect dashboard. Users can only be created by admins, with granular role-based permissions including per-client machine access control.

## Requirements (User-Specified)

- **User Creation:** Admin-only (no self-registration)
- **Roles/Permissions:** Granular (view-only, control, admin, per-client access)
- **Auth Source:** Local users only (no external SSO)

## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│  Browser (Dashboard)                                        │
│  - Login form → POST /api/auth/login                        │
│  - JWT stored in localStorage/cookie                        │
│  - JWT sent with all API requests                           │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Server API                                                 │
│  - JWT validation middleware                                │
│  - Role/permission checks per endpoint                      │
│  - User management endpoints (admin only)                   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  PostgreSQL Database                                        │
│  - users table (credentials, roles)                         │
│  - user_permissions table (granular permissions)            │
│  - user_client_access table (per-client access)             │
└─────────────────────────────────────────────────────────────┘
```

## Database Schema

### New Migration: `002_user_management.sql`

```sql
-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username VARCHAR(64) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(255),
    role VARCHAR(32) NOT NULL DEFAULT 'viewer',  -- 'admin', 'operator', 'viewer'
    enabled BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login TIMESTAMPTZ
);

-- Granular permissions (what actions a user can perform)
CREATE TABLE user_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    permission VARCHAR(64) NOT NULL,  -- 'view', 'control', 'transfer', 'manage_users', etc.
    UNIQUE(user_id, permission)
);

-- Per-client access (which machines a user can access)
-- NULL = access to all clients (for admins)
CREATE TABLE user_client_access (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    UNIQUE(user_id, client_id)
);

-- Indexes
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX idx_user_client_access_user ON user_client_access(user_id);
```

### Role Definitions

| Role | Description | Default Permissions |
|------|-------------|---------------------|
| admin | Full access, manage users | All permissions, all clients |
| operator | Can control machines | view, control, transfer |
| viewer | View-only access | view |

### Permission Types

| Permission | Description |
|------------|-------------|
| view | See dashboard, client list, status |
| control | Take control of machines (keyboard/mouse) |
| transfer | File transfer access |
| manage_users | Create/edit/delete users (admin only) |
| manage_clients | Delete clients, edit client metadata |

## API Endpoints

### Authentication

| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| POST | `/api/auth/login` | Login, returns JWT | None |
| POST | `/api/auth/logout` | Invalidate token | JWT |
| GET | `/api/auth/me` | Get current user info | JWT |
| POST | `/api/auth/change-password` | Change own password | JWT |

### User Management (Admin Only)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/users` | List all users |
| POST | `/api/users` | Create new user |
| GET | `/api/users/:id` | Get user details |
| PUT | `/api/users/:id` | Update user |
| DELETE | `/api/users/:id` | Delete user |
| PUT | `/api/users/:id/permissions` | Set user permissions |
| PUT | `/api/users/:id/clients` | Set client access |

## JWT Token Structure

```json
{
  "sub": "user-uuid",
  "username": "admin",
  "role": "admin",
  "permissions": ["view", "control", "transfer", "manage_users"],
  "exp": 1234567890,
  "iat": 1234567890
}
```

## Files to Modify/Create

### Server Changes

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/002_user_management.sql` | CREATE | Database schema |
| `server/src/auth/mod.rs` | REWRITE | Real JWT auth implementation |
| `server/src/auth/jwt.rs` | CREATE | JWT creation/validation |
| `server/src/auth/password.rs` | CREATE | Argon2 password hashing |
| `server/src/models/user.rs` | CREATE | User model and DB queries |
| `server/src/api/auth.rs` | CREATE | Auth API endpoints |
| `server/src/api/users.rs` | CREATE | User management endpoints |
| `server/src/api/mod.rs` | MODIFY | Add new route modules |
| `server/src/main.rs` | MODIFY | Add auth middleware |

### Dashboard Changes

| File | Action | Purpose |
|------|--------|---------|
| `server/static/login.html` | CREATE | Login page |
| `server/static/dashboard.html` | MODIFY | Add auth checks, user menu |
| `server/static/users.html` | CREATE | User management page (admin) |

## Implementation Phases

### Phase 1: Database & Models
1. Create migration `002_user_management.sql`
2. Create `models/user.rs` with User struct and DB operations
3. Add initial admin user creation (first-run setup)

### Phase 2: Authentication
1. Implement `auth/password.rs` - Argon2 hashing
2. Implement `auth/jwt.rs` - Token create/validate
3. Rewrite `auth/mod.rs` - Real middleware
4. Create `api/auth.rs` - Login/logout endpoints

### Phase 3: Authorization
1. Add permission checking to existing endpoints
2. Add client access filtering to client list
3. Protect WebSocket connections with JWT

### Phase 4: User Management API
1. Create `api/users.rs` - CRUD endpoints
2. Add permission/client access management
3. Admin-only route protection

### Phase 5: Dashboard UI
1. Create login page with redirect
2. Add logout button and user menu
3. Create user management page
4. Add permission-based UI hiding

## Initial Admin Setup

On first run (no users in DB):
1. Server detects empty users table
2. Generates random admin password
3. Creates default admin user
4. Prints credentials to console/logs
5. Forces password change on first login (optional)

Alternative: Environment variable for initial admin password.

## Security Considerations

1. **Password Storage:** Argon2id with recommended parameters
2. **JWT Secret:** Random 256-bit key, stored in config/env
3. **Token Expiry:** 24h default, configurable
4. **HTTPS Only:** Cookies marked Secure in production
5. **Rate Limiting:** Consider adding for login endpoint
6. **Audit Logging:** Log auth events (login, logout, failed attempts)

## Existing Dependencies (Already in Cargo.toml)

- `jsonwebtoken` - JWT handling
- `argon2` - Password hashing
- `sqlx` - Database queries

## Critical Files to Review During Implementation

- `server/src/auth/mod.rs` - Current placeholder auth (lines 1-50)
- `server/src/api/mod.rs` - Current API routes
- `server/src/ws.rs` - WebSocket auth (needs JWT validation)
- `server/src/db.rs` - Database connection handling

-Create a Windows Service running as SYSTEM to handle Ctrl+Alt+Del (Secure Attention Sequence) requests. The agent communicates with the service via named pipe IPC.

+Implement user authentication and authorization to secure the GuruConnect dashboard. Users can only be created by admins, with granular role-based permissions including per-client machine access control.

-- `SendSAS()` API requires SYSTEM privileges or `SeTcbPrivilege`

-- Running as Administrator is NOT sufficient

-- Windows blocks Ctrl+Alt+Del key injection for security

-- All professional remote desktop tools (ScreenConnect, TeamViewer) use this pattern

+- **User Creation:** Admin-only (no self-registration)

+- **Roles/Permissions:** Granular (view-only, control, admin, per-client access)

+- **Auth Source:** Local users only (no external SSO)

-│  GuruConnect Agent (runs as logged-in user)                 │

-│  - Receives SpecialKey message from server                  │

-│  - Connects to named pipe: \\.\pipe\guruconnect-sas         │

-│  - Sends "sas" command                                      │

+│  Browser (Dashboard)                                        │

+│  - Login form → POST /api/auth/login                        │

+│  - JWT stored in localStorage/cookie                        │

+│  - JWT sent with all API requests                           │

-│  GuruConnect SAS Service (runs as SYSTEM)                   │

-│  - Windows Service: "GuruConnectSAS"                        │

-│  - Listens on named pipe                                    │

-│  - Calls SendSAS(0) via sas.dll                             │

-│  - Returns success/error                                    │

+│  Server API                                                 │

+│  - JWT validation middleware                                │

+│  - Role/permission checks per endpoint                      │

+│  - User management endpoints (admin only)                   │

+│  PostgreSQL Database                                        │

+│  - users table (credentials, roles)                         │

+│  - user_permissions table (granular permissions)            │

+│  - user_client_access table (per-client access)             │

-### New Binary: `guruconnect-sas-service`

+### New Migration: `002_user_management.sql`

-**Location:** `agent/src/bin/sas_service.rs` (or separate crate)

+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

+    username VARCHAR(64) UNIQUE NOT NULL,

+    password_hash VARCHAR(255) NOT NULL,

+    role VARCHAR(32) NOT NULL DEFAULT 'viewer',  -- 'admin', 'operator', 'viewer'

+    enabled BOOLEAN NOT NULL DEFAULT true,

+    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

+    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-1. Windows Service using `windows-service` crate (already in Cargo.toml)

-2. Named pipe server at `\\.\pipe\guruconnect-sas`

-3. Accepts commands: `sas` (send Ctrl+Alt+Del)

-4. Loads `sas.dll` and calls `SendSAS(0)`

+-- Granular permissions (what actions a user can perform)

+    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,

+    permission VARCHAR(64) NOT NULL,  -- 'view', 'control', 'transfer', 'manage_users', etc.

+-- Per-client access (which machines a user can access)

+-- NULL = access to all clients (for admins)

+    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,

-const SERVICE_NAME: &str = "GuruConnectSAS";

-const SERVICE_DISPLAY: &str = "GuruConnect SAS Service";

-const SERVICE_DESC: &str = "Handles Secure Attention Sequence for GuruConnect remote sessions";

-const PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas";

-const INSTALL_DIR: &str = r"C:\Program Files\GuruConnect";

+CREATE INDEX idx_users_username ON users(username);

+CREATE INDEX idx_user_permissions_user ON user_permissions(user_id);

+CREATE INDEX idx_user_client_access_user ON user_client_access(user_id);

-| `agent/src/bin/sas_service.rs` | CREATE | Windows Service binary |

-| `agent/src/sas_client.rs` | CREATE | Named pipe client for agent |

-| `agent/src/input/keyboard.rs` | MODIFY | Use sas_client instead of direct call |

-| `agent/src/startup.rs` | MODIFY | Install/uninstall service |

-| `agent/Cargo.toml` | MODIFY | Add binary target |

+| Role | Description | Default Permissions |

+| admin | Full access, manage users | All permissions, all clients |

+| operator | Can control machines | view, control, transfer |

-#### Step 1: Create SAS Service Binary (`sas_service.rs`)

+| view | See dashboard, client list, status |

+| control | Take control of machines (keyboard/mouse) |

+| manage_users | Create/edit/delete users (admin only) |

+| manage_clients | Delete clients, edit client metadata |

-// 1. Windows service boilerplate (define_windows_service!, service_dispatcher)

-    match std::env::args().nth(1).as_deref() {

-        Some("install") => install_service(),

-        Some("uninstall") => uninstall_service(),

-        Some("run") => run_service(),  // Called by SCM

+| Method | Endpoint | Description | Auth |

+| POST | `/api/auth/login` | Login, returns JWT | None |

+| POST | `/api/auth/logout` | Invalidate token | JWT |

+| GET | `/api/auth/me` | Get current user info | JWT |

+| POST | `/api/auth/change-password` | Change own password | JWT |

-// Create pipe with SYSTEM security (allow all users to connect)

-    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,

-    Some(&security_attributes),  // Allow non-admin connections

+| GET | `/api/users` | List all users |

+| POST | `/api/users` | Create new user |

+| GET | `/api/users/:id` | Get user details |

+| PUT | `/api/users/:id` | Update user |

+| DELETE | `/api/users/:id` | Delete user |

+| PUT | `/api/users/:id/permissions` | Set user permissions |

+| PUT | `/api/users/:id/clients` | Set client access |

-        let lib = LoadLibraryW(w!("sas.dll"))?;

-        let proc = GetProcAddress(lib, s!("SendSAS"))

-            .ok_or_else(|| anyhow!("SendSAS not found"))?;

-        let send_sas: extern "system" fn(i32) = std::mem::transmute(proc);

-        send_sas(0);  // 0 = Ctrl+Alt+Del

+  "permissions": ["view", "control", "transfer", "manage_users"],

-#### Step 4: Agent SAS Client (`sas_client.rs`)

-        .open(r"\\.\pipe\guruconnect-sas")?;

+| `server/migrations/002_user_management.sql` | CREATE | Database schema |

+| `server/src/auth/mod.rs` | REWRITE | Real JWT auth implementation |

+| `server/src/auth/jwt.rs` | CREATE | JWT creation/validation |

+| `server/src/auth/password.rs` | CREATE | Argon2 password hashing |

+| `server/src/models/user.rs` | CREATE | User model and DB queries |

+| `server/src/api/auth.rs` | CREATE | Auth API endpoints |

+| `server/src/api/users.rs` | CREATE | User management endpoints |

+| `server/src/api/mod.rs` | MODIFY | Add new route modules |

+| `server/src/main.rs` | MODIFY | Add auth middleware |

-        Err(anyhow!("SAS service error"))

+| `server/static/login.html` | CREATE | Login page |

+| `server/static/dashboard.html` | MODIFY | Add auth checks, user menu |

+| `server/static/users.html` | CREATE | User management page (admin) |

-pub fn send_sas(&mut self) -> Result<()> {

-    if let Ok(()) = crate::sas_client::request_sas() {

+1. Create migration `002_user_management.sql`

+2. Create `models/user.rs` with User struct and DB operations

+3. Add initial admin user creation (first-run setup)

-    // Fallback to direct call (for elevated sessions)

+1. Implement `auth/password.rs` - Argon2 hashing

+2. Implement `auth/jwt.rs` - Token create/validate

+3. Rewrite `auth/mod.rs` - Real middleware

+4. Create `api/auth.rs` - Login/logout endpoints

-#### Step 6: Service Installation (startup.rs)

+1. Add permission checking to existing endpoints

+2. Add client access filtering to client list

+3. Protect WebSocket connections with JWT

-pub fn install_sas_service() -> Result<()> {

-    // Copy sas_service.exe to C:\Program Files\GuruConnect\

-    // Run: sc create GuruConnectSAS binPath= "..." start= auto

+1. Create `api/users.rs` - CRUD endpoints

+2. Add permission/client access management

-pub fn uninstall_sas_service() -> Result<()> {

+5. Forces password change on first login (optional)

-windows = { version = "0.58", features = [...] }

+Alternative: Environment variable for initial admin password.

-1. **Pipe Security:** Create pipe with DACL allowing authenticated users

-2. **Service Account:** Runs as LocalSystem (has SeTcbPrivilege)

-3. **Command Validation:** Only accept "sas" command, nothing else

-4. **No Sensitive Data:** Pipe only carries simple commands

+1. **Password Storage:** Argon2id with recommended parameters

+2. **JWT Secret:** Random 256-bit key, stored in config/env

+3. **Token Expiry:** 24h default, configurable

+4. **HTTPS Only:** Cookies marked Secure in production

+5. **Rate Limiting:** Consider adding for login endpoint

+6. **Audit Logging:** Log auth events (login, logout, failed attempts)

+## Existing Dependencies (Already in Cargo.toml)

-3. If not, copies service exe and installs via `sc.exe`

-5. Future Ctrl+Alt+Del requests go through pipe

+## Critical Files to Review During Implementation

-2. Install service: `guruconnect-sas-service.exe install`

-5. Verify Windows security screen appears

-- GuruRMM service pattern: `gururmm/agent/src/service.rs`

-- Current keyboard: `guru-connect/agent/src/input/keyboard.rs`

-- Named pipe examples: Windows API documentation

+- `server/src/auth/mod.rs` - Current placeholder auth (lines 1-50)

+- `server/src/api/mod.rs` - Current API routes

+- `server/src/ws.rs` - WebSocket auth (needs JWT validation)

+- `server/src/db.rs` - Database connection handling

# GuruConnect SAS Service Plan

## Overview

Create a Windows Service running as SYSTEM to handle Ctrl+Alt+Del (Secure Attention Sequence) requests. The agent communicates with the service via named pipe IPC.

## Why a Service is Required

- `SendSAS()` API requires SYSTEM privileges or `SeTcbPrivilege`
- Running as Administrator is NOT sufficient
- Windows blocks Ctrl+Alt+Del key injection for security
- All professional remote desktop tools (ScreenConnect, TeamViewer) use this pattern

## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│  GuruConnect Agent (runs as logged-in user)                 │
│  - Receives SpecialKey message from server                  │
│  - Connects to named pipe: \\.\pipe\guruconnect-sas         │
│  - Sends "sas" command                                      │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  GuruConnect SAS Service (runs as SYSTEM)                   │
│  - Windows Service: "GuruConnectSAS"                        │
│  - Listens on named pipe                                    │
│  - Calls SendSAS(0) via sas.dll                             │
│  - Returns success/error                                    │
└─────────────────────────────────────────────────────────────┘
```

## Implementation

### New Binary: `guruconnect-sas-service`

**Location:** `agent/src/bin/sas_service.rs` (or separate crate)

**Functionality:**
1. Windows Service using `windows-service` crate (already in Cargo.toml)
2. Named pipe server at `\\.\pipe\guruconnect-sas`
3. Accepts commands: `sas` (send Ctrl+Alt+Del)
4. Loads `sas.dll` and calls `SendSAS(0)`

### Service Details

```rust
// Service constants
const SERVICE_NAME: &str = "GuruConnectSAS";
const SERVICE_DISPLAY: &str = "GuruConnect SAS Service";
const SERVICE_DESC: &str = "Handles Secure Attention Sequence for GuruConnect remote sessions";
const PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas";

// Install location
const INSTALL_DIR: &str = r"C:\Program Files\GuruConnect";
```

### Files to Create/Modify

| File | Action | Purpose |
|------|--------|---------|
| `agent/src/bin/sas_service.rs` | CREATE | Windows Service binary |
| `agent/src/sas_client.rs` | CREATE | Named pipe client for agent |
| `agent/src/input/keyboard.rs` | MODIFY | Use sas_client instead of direct call |
| `agent/src/startup.rs` | MODIFY | Install/uninstall service |
| `agent/Cargo.toml` | MODIFY | Add binary target |

### Step-by-Step Implementation

#### Step 1: Create SAS Service Binary (`sas_service.rs`)

```rust
// Key components:
// 1. Windows service boilerplate (define_windows_service!, service_dispatcher)
// 2. Named pipe server loop
// 3. SendSAS implementation
// 4. Install/uninstall subcommands

fn main() {
    match std::env::args().nth(1).as_deref() {
        Some("install") => install_service(),
        Some("uninstall") => uninstall_service(),
        Some("run") => run_service(),  // Called by SCM
        _ => print_usage(),
    }
}
```

#### Step 2: Named Pipe Server

```rust
// Create pipe with SYSTEM security (allow all users to connect)
let pipe = CreateNamedPipeW(
    PIPE_NAME,
    PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
    1,  // Max instances
    512, 512,  // Buffer sizes
    0,  // Default timeout
    Some(&security_attributes),  // Allow non-admin connections
);

loop {
    ConnectNamedPipe(pipe, None);
    // Read command, execute, respond
    DisconnectNamedPipe(pipe);
}
```

#### Step 3: SendSAS Implementation

```rust
fn send_sas() -> Result<()> {
    unsafe {
        let lib = LoadLibraryW(w!("sas.dll"))?;
        let proc = GetProcAddress(lib, s!("SendSAS"))
            .ok_or_else(|| anyhow!("SendSAS not found"))?;
        let send_sas: extern "system" fn(i32) = std::mem::transmute(proc);
        send_sas(0);  // 0 = Ctrl+Alt+Del
        Ok(())
    }
}
```

#### Step 4: Agent SAS Client (`sas_client.rs`)

```rust
pub fn request_sas() -> Result<()> {
    let mut pipe = File::options()
        .read(true)
        .write(true)
        .open(r"\\.\pipe\guruconnect-sas")?;

    pipe.write_all(b"sas\n")?;

    let mut response = [0u8; 64];
    let n = pipe.read(&mut response)?;

    if &response[..n] == b"ok\n" {
        Ok(())
    } else {
        Err(anyhow!("SAS service error"))
    }
}
```

#### Step 5: Update keyboard.rs

```rust
pub fn send_sas(&mut self) -> Result<()> {
    // Try service first
    if let Ok(()) = crate::sas_client::request_sas() {
        return Ok(());
    }

    // Fallback to direct call (for elevated sessions)
    // ... existing code ...
}
```

#### Step 6: Service Installation (startup.rs)

```rust
pub fn install_sas_service() -> Result<()> {
    // Copy sas_service.exe to C:\Program Files\GuruConnect\
    // Run: sc create GuruConnectSAS binPath= "..." start= auto
    // Run: sc start GuruConnectSAS
}

pub fn uninstall_sas_service() -> Result<()> {
    // Run: sc stop GuruConnectSAS
    // Run: sc delete GuruConnectSAS
    // Remove exe file
}
```

### Cargo.toml Changes

```toml
[[bin]]
name = "guruconnect-sas-service"
path = "src/bin/sas_service.rs"

[dependencies]
# Already present:
windows-service = "0.7"
windows = { version = "0.58", features = [...] }
```

### Security Considerations

1. **Pipe Security:** Create pipe with DACL allowing authenticated users
2. **Service Account:** Runs as LocalSystem (has SeTcbPrivilege)
3. **Command Validation:** Only accept "sas" command, nothing else
4. **No Sensitive Data:** Pipe only carries simple commands

### Installation Flow

1. Agent starts (persistent mode)
2. Checks if SAS service installed
3. If not, copies service exe and installs via `sc.exe`
4. Starts service
5. Future Ctrl+Alt+Del requests go through pipe

### Testing

1. Build both binaries
2. Install service: `guruconnect-sas-service.exe install`
3. Connect agent, join viewer
4. Click Ctrl+Alt+Del button
5. Verify Windows security screen appears

### Reference Files

- GuruRMM service pattern: `gururmm/agent/src/service.rs`
- Current keyboard: `guru-connect/agent/src/input/keyboard.rs`
- Named pipe examples: Windows API documentation


The plan is complete. Now I should exit plan mode to present this to the user for approval.


Plan complete. Here's the summary:

**GuruConnect User Management Plan**

- **Database:** 3 new tables (users, user_permissions, user_client_access)
- **Roles:** admin, operator, viewer
- **Permissions:** view, control, transfer, manage_users, manage_clients
- **Per-client access:** Restrict which machines non-admin users can see/control

**Implementation Phases:**
1. Database schema & user models
2. JWT authentication (login/logout endpoints)
3. Authorization middleware (permission checks)
4. User management API (admin CRUD)
5. Dashboard UI (login page, user management)

**Initial Admin:** Auto-generated on first run, credentials printed to console.

**Files to create:** 8 new files (migrations, auth modules, API endpoints, UI pages)


ExitPlanMode

# GuruConnect User Management Plan

## Overview

Implement user authentication and authorization to secure the GuruConnect dashboard. Users can only be created by admins, with granular role-based permissions including per-client machine access control.

## Requirements (User-Specified)

- **User Creation:** Admin-only (no self-registration)
- **Roles/Permissions:** Granular (view-only, control, admin, per-client access)
- **Auth Source:** Local users only (no external SSO)

## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│  Browser (Dashboard)                                        │
│  - Login form → POST /api/auth/login                        │
│  - JWT stored in localStorage/cookie                        │
│  - JWT sent with all API requests                           │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Server API                                                 │
│  - JWT validation middleware                                │
│  - Role/permission checks per endpoint                      │
│  - User management endpoints (admin only)                   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  PostgreSQL Database                                        │
│  - users table (credentials, roles)                         │
│  - user_permissions table (granular permissions)            │
│  - user_client_access table (per-client access)             │
└─────────────────────────────────────────────────────────────┘
```

## Database Schema

### New Migration: `002_user_management.sql`

```sql
-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username VARCHAR(64) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(255),
    role VARCHAR(32) NOT NULL DEFAULT 'viewer',  -- 'admin', 'operator', 'viewer'
    enabled BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login TIMESTAMPTZ
);

-- Granular permissions (what actions a user can perform)
CREATE TABLE user_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    permission VARCHAR(64) NOT NULL,  -- 'view', 'control', 'transfer', 'manage_users', etc.
    UNIQUE(user_id, permission)
);

-- Per-client access (which machines a user can access)
-- NULL = access to all clients (for admins)
CREATE TABLE user_client_access (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    UNIQUE(user_id, client_id)
);

-- Indexes
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX idx_user_client_access_user ON user_client_access(user_id);
```

### Role Definitions

| Role | Description | Default Permissions |
|------|-------------|---------------------|
| admin | Full access, manage users | All permissions, all clients |
| operator | Can control machines | view, control, transfer |
| viewer | View-only access | view |

### Permission Types

| Permission | Description |
|------------|-------------|
| view | See dashboard, client list, status |
| control | Take control of machines (keyboard/mouse) |
| transfer | File transfer access |
| manage_users | Create/edit/delete users (admin only) |
| manage_clients | Delete clients, edit client metadata |

## API Endpoints

### Authentication

| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| POST | `/api/auth/login` | Login, returns JWT | None |
| POST | `/api/auth/logout` | Invalidate token | JWT |
| GET | `/api/auth/me` | Get current user info | JWT |
| POST | `/api/auth/change-password` | Change own password | JWT |

### User Management (Admin Only)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/users` | List all users |
| POST | `/api/users` | Create new user |
| GET | `/api/users/:id` | Get user details |
| PUT | `/api/users/:id` | Update user |
| DELETE | `/api/users/:id` | Delete user |
| PUT | `/api/users/:id/permissions` | Set user permissions |
| PUT | `/api/users/:id/clients` | Set client access |

## JWT Token Structure

```json
{
  "sub": "user-uuid",
  "username": "admin",
  "role": "admin",
  "permissions": ["view", "control", "transfer", "manage_users"],
  "exp": 1234567890,
  "iat": 1234567890
}
```

## Files to Modify/Create

### Server Changes

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/002_user_management.sql` | CREATE | Database schema |
| `server/src/auth/mod.rs` | REWRITE | Real JWT auth implementation |
| `server/src/auth/jwt.rs` | CREATE | JWT creation/validation |
| `server/src/auth/password.rs` | CREATE | Argon2 password hashing |
| `server/src/models/user.rs` | CREATE | User model and DB queries |
| `server/src/api/auth.rs` | CREATE | Auth API endpoints |
| `server/src/api/users.rs` | CREATE | User management endpoints |
| `server/src/api/mod.rs` | MODIFY | Add new route modules |
| `server/src/main.rs` | MODIFY | Add auth middleware |

### Dashboard Changes

| File | Action | Purpose |
|------|--------|---------|
| `server/static/login.html` | CREATE | Login page |
| `server/static/dashboard.html` | MODIFY | Add auth checks, user menu |
| `server/static/users.html` | CREATE | User management page (admin) |

## Implementation Phases

### Phase 1: Database & Models
1. Create migration `002_user_management.sql`
2. Create `models/user.rs` with User struct and DB operations
3. Add initial admin user creation (first-run setup)

### Phase 2: Authentication
1. Implement `auth/password.rs` - Argon2 hashing
2. Implement `auth/jwt.rs` - Token create/validate
3. Rewrite `auth/mod.rs` - Real middleware
4. Create `api/auth.rs` - Login/logout endpoints

### Phase 3: Authorization
1. Add permission checking to existing endpoints
2. Add client access filtering to client list
3. Protect WebSocket connections with JWT

### Phase 4: User Management API
1. Create `api/users.rs` - CRUD endpoints
2. Add permission/client access management
3. Admin-only route protection

### Phase 5: Dashboard UI
1. Create login page with redirect
2. Add logout button and user menu
3. Create user management page
4. Add permission-based UI hiding

## Initial Admin Setup

On first run (no users in DB):
1. Server detects empty users table
2. Generates random admin password
3. Creates default admin user
4. Prints credentials to console/logs
5. Forces password change on first login (optional)

Alternative: Environment variable for initial admin password.

## Security Considerations

1. **Password Storage:** Argon2id with recommended parameters
2. **JWT Secret:** Random 256-bit key, stored in config/env
3. **Token Expiry:** 24h default, configurable
4. **HTTPS Only:** Cookies marked Secure in production
5. **Rate Limiting:** Consider adding for login endpoint
6. **Audit Logging:** Log auth events (login, logout, failed attempts)

## Existing Dependencies (Already in Cargo.toml)

- `jsonwebtoken` - JWT handling
- `argon2` - Password hashing
- `sqlx` - Database queries

## Critical Files to Review During Implementation

- `server/src/auth/mod.rs` - Current placeholder auth (lines 1-50)
- `server/src/api/mod.rs` - Current API routes
- `server/src/ws.rs` - WebSocket auth (needs JWT validation)
- `server/src/db.rs` - Database connection handling


User has approved your plan. You can now start coding. Start with updating your todo list if applicable

Your plan has been saved to: C:\Users\MikeSwanson\.claude\plans\velvet-tumbling-tide.md
You can refer back to it if needed during implementation.

## Approved Plan:
# GuruConnect User Management Plan

## Overview

Implement user authentication and authorization to secure the GuruConnect dashboard. Users can only be created by admins, with granular role-based permissions including per-client machine access control.

## Requirements (User-Specified)

- **User Creation:** Admin-only (no self-registration)
- **Roles/Permissions:** Granular (view-only, control, admin, per-client access)
- **Auth Source:** Local users only (no external SSO)

## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│  Browser (Dashboard)                                        │
│  - Login form → POST /api/auth/login                        │
│  - JWT stored in localStorage/cookie                        │
│  - JWT sent with all API requests                           │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  Server API                                                 │
│  - JWT validation middleware                                │
│  - Role/permission checks per endpoint                      │
│  - User management endpoints (admin only)                   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  PostgreSQL Database                                        │
│  - users table (credentials, roles)                         │
│  - user_permissions table (granular permissions)            │
│  - user_client_access table (per-client access)             │
└─────────────────────────────────────────────────────────────┘
```

## Database Schema

### New Migration: `002_user_management.sql`

```sql
-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username VARCHAR(64) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(255),
    role VARCHAR(32) NOT NULL DEFAULT 'viewer',  -- 'admin', 'operator', 'viewer'
    enabled BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login TIMESTAMPTZ
);

-- Granular permissions (what actions a user can perform)
CREATE TABLE user_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    permission VARCHAR(64) NOT NULL,  -- 'view', 'control', 'transfer', 'manage_users', etc.
    UNIQUE(user_id, permission)
);

-- Per-client access (which machines a user can access)
-- NULL = access to all clients (for admins)
CREATE TABLE user_client_access (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    UNIQUE(user_id, client_id)
);

-- Indexes
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX idx_user_client_access_user ON user_client_access(user_id);
```

### Role Definitions

| Role | Description | Default Permissions |
|------|-------------|---------------------|
| admin | Full access, manage users | All permissions, all clients |
| operator | Can control machines | view, control, transfer |
| viewer | View-only access | view |

### Permission Types

| Permission | Description |
|------------|-------------|
| view | See dashboard, client list, status |
| control | Take control of machines (keyboard/mouse) |
| transfer | File transfer access |
| manage_users | Create/edit/delete users (admin only) |
| manage_clients | Delete clients, edit client metadata |

## API Endpoints

### Authentication

| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| POST | `/api/auth/login` | Login, returns JWT | None |
| POST | `/api/auth/logout` | Invalidate token | JWT |
| GET | `/api/auth/me` | Get current user info | JWT |
| POST | `/api/auth/change-password` | Change own password | JWT |

### User Management (Admin Only)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/users` | List all users |
| POST | `/api/users` | Create new user |
| GET | `/api/users/:id` | Get user details |
| PUT | `/api/users/:id` | Update user |
| DELETE | `/api/users/:id` | Delete user |
| PUT | `/api/users/:id/permissions` | Set user permissions |
| PUT | `/api/users/:id/clients` | Set client access |

## JWT Token Structure

```json
{
  "sub": "user-uuid",
  "username": "admin",
  "role": "admin",
  "permissions": ["view", "control", "transfer", "manage_users"],
  "exp": 1234567890,
  "iat": 1234567890
}
```

## Files to Modify/Create

### Server Changes

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/002_user_management.sql` | CREATE | Database schema |
| `server/src/auth/mod.rs` | REWRITE | Real JWT auth implementation |
| `server/src/auth/jwt.rs` | CREATE | JWT creation/validation |
| `server/src/auth/password.rs` | CREATE | Argon2 password hashing |
| `server/src/models/user.rs` | CREATE | User model and DB queries |
| `server/src/api/auth.rs` | CREATE | Auth API endpoints |
| `server/src/api/users.rs` | CREATE | User management endpoints |
| `server/src/api/mod.rs` | MODIFY | Add new route modules |
| `server/src/main.rs` | MODIFY | Add auth middleware |

### Dashboard Changes

| File | Action | Purpose |
|------|--------|---------|
| `server/static/login.html` | CREATE | Login page |
| `server/static/dashboard.html` | MODIFY | Add auth checks, user menu |
| `server/static/users.html` | CREATE | User management page (admin) |

## Implementation Phases

### Phase 1: Database & Models
1. Create migration `002_user_management.sql`
2. Create `models/user.rs` with User struct and DB operations
3. Add initial admin user creation (first-run setup)

### Phase 2: Authentication
1. Implement `auth/password.rs` - Argon2 hashing
2. Implement `auth/jwt.rs` - Token create/validate
3. Rewrite `auth/mod.rs` - Real middleware
4. Create `api/auth.rs` - Login/logout endpoints

### Phase 3: Authorization
1. Add permission checking to existing endpoints
2. Add client access filtering to client list
3. Protect WebSocket connections with JWT

### Phase 4: User Management API
1. Create `api/users.rs` - CRUD endpoints
2. Add permission/client access management
3. Admin-only route protection

### Phase 5: Dashboard UI
1. Create login page with redirect
2. Add logout button and user menu
3. Create user management page
4. Add permission-based UI hiding

## Initial Admin Setup

On first run (no users in DB):
1. Server detects empty users table
2. Generates random admin password
3. Creates default admin user
4. Prints credentials to console/logs
5. Forces password change on first login (optional)

Alternative: Environment variable for initial admin password.

## Security Considerations

1. **Password Storage:** Argon2id with recommended parameters
2. **JWT Secret:** Random 256-bit key, stored in config/env
3. **Token Expiry:** 24h default, configurable
4. **HTTPS Only:** Cookies marked Secure in production
5. **Rate Limiting:** Consider adding for login endpoint
6. **Audit Logging:** Log auth events (login, logout, failed attempts)

## Existing Dependencies (Already in Cargo.toml)

- `jsonwebtoken` - JWT handling
- `argon2` - Password hashing
- `sqlx` - Database queries

## Critical Files to Review During Implementation

- `server/src/auth/mod.rs` - Current placeholder auth (lines 1-50)
- `server/src/api/mod.rs` - Current API routes
- `server/src/ws.rs` - WebSocket auth (needs JWT validation)
- `server/src/db.rs` - Database connection handling


The plan is approved. Now I need to implement it. Let me start by creating the todo list and then work through the implementation phases.

First, I need to read the existing files to understand the current state before making changes.


Excellent! Plan approved. Let me set up the todo list and start implementing.


TodoWrite

Create database migration 002_user_management.sql

Create models/user.rs with User struct and DB operations

Implement auth/password.rs - Argon2 hashing

Implement auth/jwt.rs - Token create/validate

Rewrite auth/mod.rs - Real middleware

Create api/auth.rs - Login/logout endpoints

Create api/users.rs - User management endpoints

Update api/mod.rs and main.rs for new routes

Create login.html page

Update dashboard.html with auth checks

Create users.html management page

Run migration and test


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create database migration 002_user_management.sql

Create models/user.rs with User struct and DB operations

Implement auth/password.rs - Argon2 hashing

Implement auth/jwt.rs - Token create/validate

Rewrite auth/mod.rs - Real middleware

Create api/auth.rs - Login/logout endpoints

Create api/users.rs - User management endpoints

Update api/mod.rs and main.rs for new routes

Create login.html page

Update dashboard.html with auth checks

Create users.html management page

Run migration and test


Let me first read the existing files I need to understand, particularly:
1. The current auth/mod.rs
2. The existing migrations folder path
3. The current api structure

Let me start by reading those files.


     1→//! Authentication module
     2→//!
     3→//! Handles JWT validation for dashboard users and API key
     4→//! validation for agents.
     5→
     6→use axum::{
     7→    extract::FromRequestParts,
     8→    http::{request::Parts, StatusCode},
     9→};
    10→
    11→/// Authenticated user from JWT
    12→#[derive(Debug, Clone)]
    13→pub struct AuthenticatedUser {
    14→    pub user_id: String,
    15→    pub email: String,
    16→    pub roles: Vec<String>,
    17→}
    18→
    19→/// Authenticated agent from API key
    20→#[derive(Debug, Clone)]
    21→pub struct AuthenticatedAgent {
    22→    pub agent_id: String,
    23→    pub org_id: String,
    24→}
    25→
    26→/// Extract authenticated user from request (placeholder for MVP)
    27→#[axum::async_trait]
    28→impl<S> FromRequestParts<S> for AuthenticatedUser
    29→where
    30→    S: Send + Sync,
    31→{
    32→    type Rejection = (StatusCode, &'static str);
    33→
    34→    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
    35→        // TODO: Implement JWT validation
    36→        // For MVP, accept any request
    37→
    38→        // Look for Authorization header
    39→        let _auth_header = parts
    40→            .headers
    41→            .get("Authorization")
    42→            .and_then(|v| v.to_str().ok());
    43→
    44→        // Placeholder - in production, validate JWT
    45→        Ok(AuthenticatedUser {
    46→            user_id: "mvp-user".to_string(),
    47→            email: "mvp@example.com".to_string(),
    48→            roles: vec!["admin".to_string()],
    49→        })
    50→    }
    51→}
    52→
    53→/// Validate an agent API key (placeholder for MVP)
    54→pub fn validate_agent_key(_api_key: &str) -> Option<AuthenticatedAgent> {
    55→    // TODO: Implement actual API key validation
    56→    // For MVP, accept any key
    57→    Some(AuthenticatedAgent {
    58→        agent_id: "mvp-agent".to_string(),
    59→        org_id: "mvp-org".to_string(),
    60→    })
    61→}
    62→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Authentication module
//!
//! Handles JWT validation for dashboard users and API key
//! validation for agents.

use axum::{
    extract::FromRequestParts,
    http::{request::Parts, StatusCode},
};

/// Authenticated user from JWT
#[derive(Debug, Clone)]
pub struct AuthenticatedUser {
    pub user_id: String,
    pub email: String,
    pub roles: Vec<String>,
}

/// Authenticated agent from API key
#[derive(Debug, Clone)]
pub struct AuthenticatedAgent {
    pub agent_id: String,
    pub org_id: String,
}

/// Extract authenticated user from request (placeholder for MVP)
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthenticatedUser
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, &'static str);

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        // TODO: Implement JWT validation
        // For MVP, accept any request

        // Look for Authorization header
        let _auth_header = parts
            .headers
            .get("Authorization")
            .and_then(|v| v.to_str().ok());

        // Placeholder - in production, validate JWT
        Ok(AuthenticatedUser {
            user_id: "mvp-user".to_string(),
            email: "mvp@example.com".to_string(),
            roles: vec!["admin".to_string()],
        })
    }
}

/// Validate an agent API key (placeholder for MVP)
pub fn validate_agent_key(_api_key: &str) -> Option<AuthenticatedAgent> {
    // TODO: Implement actual API key validation
    // For MVP, accept any key
    Some(AuthenticatedAgent {
        agent_id: "mvp-agent".to_string(),
        org_id: "mvp-org".to_string(),
    })
}


Now I have a good understanding of the current state. Let me read the existing migration and the api/mod.rs to understand the current structure.


     1→-- GuruConnect Initial Schema
     2→-- Machine persistence, session audit logging, and support codes
     3→
     4→-- Enable UUID generation
     5→CREATE EXTENSION IF NOT EXISTS "pgcrypto";
     6→
     7→-- Machines table - persistent agent records that survive server restarts
     8→CREATE TABLE connect_machines (
     9→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    10→    agent_id VARCHAR(255) UNIQUE NOT NULL,
    11→    hostname VARCHAR(255) NOT NULL,
    12→    os_version VARCHAR(255),
    13→    is_elevated BOOLEAN DEFAULT FALSE,
    14→    is_persistent BOOLEAN DEFAULT TRUE,
    15→    first_seen TIMESTAMPTZ DEFAULT NOW(),
    16→    last_seen TIMESTAMPTZ DEFAULT NOW(),
    17→    last_session_id UUID,
    18→    status VARCHAR(20) DEFAULT 'offline',
    19→    created_at TIMESTAMPTZ DEFAULT NOW(),
    20→    updated_at TIMESTAMPTZ DEFAULT NOW()
    21→);
    22→
    23→CREATE INDEX idx_connect_machines_agent_id ON connect_machines(agent_id);
    24→CREATE INDEX idx_connect_machines_status ON connect_machines(status);
    25→
    26→-- Sessions table - connection history
    27→CREATE TABLE connect_sessions (
    28→    id UUID PRIMARY KEY,
    29→    machine_id UUID REFERENCES connect_machines(id) ON DELETE CASCADE,
    30→    started_at TIMESTAMPTZ DEFAULT NOW(),
    31→    ended_at TIMESTAMPTZ,
    32→    duration_secs INTEGER,
    33→    is_support_session BOOLEAN DEFAULT FALSE,
    34→    support_code VARCHAR(10),
    35→    status VARCHAR(20) DEFAULT 'active'
    36→);
    37→
    38→CREATE INDEX idx_connect_sessions_machine ON connect_sessions(machine_id);
    39→CREATE INDEX idx_connect_sessions_started ON connect_sessions(started_at DESC);
    40→CREATE INDEX idx_connect_sessions_support_code ON connect_sessions(support_code);
    41→
    42→-- Session events - comprehensive audit log
    43→CREATE TABLE connect_session_events (
    44→    id BIGSERIAL PRIMARY KEY,
    45→    session_id UUID REFERENCES connect_sessions(id) ON DELETE CASCADE,
    46→    event_type VARCHAR(50) NOT NULL,
    47→    timestamp TIMESTAMPTZ DEFAULT NOW(),
    48→    viewer_id VARCHAR(255),
    49→    viewer_name VARCHAR(255),
    50→    details JSONB,
    51→    ip_address INET
    52→);
    53→
    54→CREATE INDEX idx_connect_events_session ON connect_session_events(session_id);
    55→CREATE INDEX idx_connect_events_time ON connect_session_events(timestamp DESC);
    56→CREATE INDEX idx_connect_events_type ON connect_session_events(event_type);
    57→
    58→-- Support codes - persistent across restarts
    59→CREATE TABLE connect_support_codes (
    60→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    61→    code VARCHAR(10) UNIQUE NOT NULL,
    62→    session_id UUID,
    63→    created_by VARCHAR(255) NOT NULL,
    64→    created_at TIMESTAMPTZ DEFAULT NOW(),
    65→    expires_at TIMESTAMPTZ,
    66→    status VARCHAR(20) DEFAULT 'pending',
    67→    client_name VARCHAR(255),
    68→    client_machine VARCHAR(255),
    69→    connected_at TIMESTAMPTZ
    70→);
    71→
    72→CREATE INDEX idx_support_codes_code ON connect_support_codes(code);
    73→CREATE INDEX idx_support_codes_status ON connect_support_codes(status);
    74→CREATE INDEX idx_support_codes_session ON connect_support_codes(session_id);
    75→
    76→-- Trigger to auto-update updated_at on machines
    77→CREATE OR REPLACE FUNCTION update_connect_updated_at()
    78→RETURNS TRIGGER AS $$
    79→BEGIN
    80→    NEW.updated_at = NOW();
    81→    RETURN NEW;
    82→END;
    83→$$ LANGUAGE plpgsql;
    84→
    85→CREATE TRIGGER update_connect_machines_updated_at
    86→    BEFORE UPDATE ON connect_machines
    87→    FOR EACH ROW
    88→    EXECUTE FUNCTION update_connect_updated_at();
    89→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

-- GuruConnect Initial Schema
-- Machine persistence, session audit logging, and support codes

-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Machines table - persistent agent records that survive server restarts
CREATE TABLE connect_machines (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id VARCHAR(255) UNIQUE NOT NULL,
    hostname VARCHAR(255) NOT NULL,
    os_version VARCHAR(255),
    is_elevated BOOLEAN DEFAULT FALSE,
    is_persistent BOOLEAN DEFAULT TRUE,
    first_seen TIMESTAMPTZ DEFAULT NOW(),
    last_seen TIMESTAMPTZ DEFAULT NOW(),
    last_session_id UUID,
    status VARCHAR(20) DEFAULT 'offline',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_connect_machines_agent_id ON connect_machines(agent_id);
CREATE INDEX idx_connect_machines_status ON connect_machines(status);

-- Sessions table - connection history
CREATE TABLE connect_sessions (
    id UUID PRIMARY KEY,
    machine_id UUID REFERENCES connect_machines(id) ON DELETE CASCADE,
    started_at TIMESTAMPTZ DEFAULT NOW(),
    ended_at TIMESTAMPTZ,
    duration_secs INTEGER,
    is_support_session BOOLEAN DEFAULT FALSE,
    support_code VARCHAR(10),
    status VARCHAR(20) DEFAULT 'active'
);

CREATE INDEX idx_connect_sessions_machine ON connect_sessions(machine_id);
CREATE INDEX idx_connect_sessions_started ON connect_sessions(started_at DESC);
CREATE INDEX idx_connect_sessions_support_code ON connect_sessions(support_code);

-- Session events - comprehensive audit log
CREATE TABLE connect_session_events (
    id BIGSERIAL PRIMARY KEY,
    session_id UUID REFERENCES connect_sessions(id) ON DELETE CASCADE,
    event_type VARCHAR(50) NOT NULL,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    viewer_id VARCHAR(255),
    viewer_name VARCHAR(255),
    details JSONB,
    ip_address INET
);

CREATE INDEX idx_connect_events_session ON connect_session_events(session_id);
CREATE INDEX idx_connect_events_time ON connect_session_events(timestamp DESC);
CREATE INDEX idx_connect_events_type ON connect_session_events(event_type);

-- Support codes - persistent across restarts
CREATE TABLE connect_support_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(10) UNIQUE NOT NULL,
    session_id UUID,
    created_by VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    expires_at TIMESTAMPTZ,
    status VARCHAR(20) DEFAULT 'pending',
    client_name VARCHAR(255),
    client_machine VARCHAR(255),
    connected_at TIMESTAMPTZ
);

CREATE INDEX idx_support_codes_code ON connect_support_codes(code);
CREATE INDEX idx_support_codes_status ON connect_support_codes(status);
CREATE INDEX idx_support_codes_session ON connect_support_codes(session_id);

-- Trigger to auto-update updated_at on machines
CREATE OR REPLACE FUNCTION update_connect_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_connect_machines_updated_at
    BEFORE UPDATE ON connect_machines
    FOR EACH ROW
    EXECUTE FUNCTION update_connect_updated_at();


     1→//! REST API endpoints
     2→
     3→use axum::{
     4→    extract::{Path, State},
     5→    Json,
     6→};
     7→use serde::Serialize;
     8→use uuid::Uuid;
     9→
    10→use crate::session::SessionManager;
    11→
    12→/// Viewer info returned by API
    13→#[derive(Debug, Serialize)]
    14→pub struct ViewerInfoApi {
    15→    pub id: String,
    16→    pub name: String,
    17→    pub connected_at: String,
    18→}
    19→
    20→impl From<crate::session::ViewerInfo> for ViewerInfoApi {
    21→    fn from(v: crate::session::ViewerInfo) -> Self {
    22→        Self {
    23→            id: v.id,
    24→            name: v.name,
    25→            connected_at: v.connected_at.to_rfc3339(),
    26→        }
    27→    }
    28→}
    29→
    30→/// Session info returned by API
    31→#[derive(Debug, Serialize)]
    32→pub struct SessionInfo {
    33→    pub id: String,
    34→    pub agent_id: String,
    35→    pub agent_name: String,
    36→    pub started_at: String,
    37→    pub viewer_count: usize,
    38→    pub viewers: Vec<ViewerInfoApi>,
    39→    pub is_streaming: bool,
    40→    pub is_online: bool,
    41→    pub is_persistent: bool,
    42→    pub last_heartbeat: String,
    43→    pub os_version: Option<String>,
    44→    pub is_elevated: bool,
    45→    pub uptime_secs: i64,
    46→    pub display_count: i32,
    47→}
    48→
    49→impl From<crate::session::Session> for SessionInfo {
    50→    fn from(s: crate::session::Session) -> Self {
    51→        Self {
    52→            id: s.id.to_string(),
    53→            agent_id: s.agent_id,
    54→            agent_name: s.agent_name,
    55→            started_at: s.started_at.to_rfc3339(),
    56→            viewer_count: s.viewer_count,
    57→            viewers: s.viewers.into_iter().map(ViewerInfoApi::from).collect(),
    58→            is_streaming: s.is_streaming,
    59→            is_online: s.is_online,
    60→            is_persistent: s.is_persistent,
    61→            last_heartbeat: s.last_heartbeat.to_rfc3339(),
    62→            os_version: s.os_version,
    63→            is_elevated: s.is_elevated,
    64→            uptime_secs: s.uptime_secs,
    65→            display_count: s.display_count,
    66→        }
    67→    }
    68→}
    69→
    70→/// List all active sessions
    71→pub async fn list_sessions(
    72→    State(sessions): State<SessionManager>,
    73→) -> Json<Vec<SessionInfo>> {
    74→    let sessions = sessions.list_sessions().await;
    75→    Json(sessions.into_iter().map(SessionInfo::from).collect())
    76→}
    77→
    78→/// Get a specific session by ID
    79→pub async fn get_session(
    80→    State(sessions): State<SessionManager>,
    81→    Path(id): Path<String>,
    82→) -> Result<Json<SessionInfo>, (axum::http::StatusCode, &'static str)> {
    83→    let session_id = Uuid::parse_str(&id)
    84→        .map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Invalid session ID"))?;
    85→
    86→    let session = sessions.get_session(session_id).await
    87→        .ok_or((axum::http::StatusCode::NOT_FOUND, "Session not found"))?;
    88→
    89→    Ok(Json(SessionInfo::from(session)))
    90→}
    91→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! REST API endpoints

use axum::{
    extract::{Path, State},
    Json,
};
use serde::Serialize;
use uuid::Uuid;

use crate::session::SessionManager;

/// Viewer info returned by API
#[derive(Debug, Serialize)]
pub struct ViewerInfoApi {
    pub id: String,
    pub name: String,
    pub connected_at: String,
}

impl From<crate::session::ViewerInfo> for ViewerInfoApi {
    fn from(v: crate::session::ViewerInfo) -> Self {
        Self {
            id: v.id,
            name: v.name,
            connected_at: v.connected_at.to_rfc3339(),
        }
    }
}

/// Session info returned by API
#[derive(Debug, Serialize)]
pub struct SessionInfo {
    pub id: String,
    pub agent_id: String,
    pub agent_name: String,
    pub started_at: String,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfoApi>,
    pub is_streaming: bool,
    pub is_online: bool,
    pub is_persistent: bool,
    pub last_heartbeat: String,
    pub os_version: Option<String>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
}

impl From<crate::session::Session> for SessionInfo {
    fn from(s: crate::session::Session) -> Self {
        Self {
            id: s.id.to_string(),
            agent_id: s.agent_id,
            agent_name: s.agent_name,
            started_at: s.started_at.to_rfc3339(),
            viewer_count: s.viewer_count,
            viewers: s.viewers.into_iter().map(ViewerInfoApi::from).collect(),
            is_streaming: s.is_streaming,
            is_online: s.is_online,
            is_persistent: s.is_persistent,
            last_heartbeat: s.last_heartbeat.to_rfc3339(),
            os_version: s.os_version,
            is_elevated: s.is_elevated,
            uptime_secs: s.uptime_secs,
            display_count: s.display_count,
        }
    }
}

/// List all active sessions
pub async fn list_sessions(
    State(sessions): State<SessionManager>,
) -> Json<Vec<SessionInfo>> {
    let sessions = sessions.list_sessions().await;
    Json(sessions.into_iter().map(SessionInfo::from).collect())
}

/// Get a specific session by ID
pub async fn get_session(
    State(sessions): State<SessionManager>,
    Path(id): Path<String>,
) -> Result<Json<SessionInfo>, (axum::http::StatusCode, &'static str)> {
    let session_id = Uuid::parse_str(&id)
        .map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Invalid session ID"))?;

    let session = sessions.get_session(session_id).await
        .ok_or((axum::http::StatusCode::NOT_FOUND, "Session not found"))?;

    Ok(Json(SessionInfo::from(session)))
}


     1→//! GuruConnect Server - WebSocket Relay Server
     2→//!
     3→//! Handles connections from both agents and dashboard viewers,
     4→//! relaying video frames and input events between them.
     5→
     6→mod config;
     7→mod relay;
     8→mod session;
     9→mod auth;
    10→mod api;
    11→mod db;
    12→mod support_codes;
    13→
    14→pub mod proto {
    15→    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
    16→}
    17→
    18→use anyhow::Result;
    19→use axum::{
    20→    Router,
    21→    routing::{get, post},
    22→    extract::{Path, State, Json},
    23→    response::{Html, IntoResponse},
    24→    http::StatusCode,
    25→};
    26→use std::net::SocketAddr;
    27→use tower_http::cors::{Any, CorsLayer};
    28→use tower_http::trace::TraceLayer;
    29→use tower_http::services::ServeDir;
    30→use tracing::{info, Level};
    31→use tracing_subscriber::FmtSubscriber;
    32→use serde::Deserialize;
    33→
    34→use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
    35→
    36→/// Application state
    37→#[derive(Clone)]
    38→pub struct AppState {
    39→    sessions: session::SessionManager,
    40→    support_codes: SupportCodeManager,
    41→    db: Option<db::Database>,
    42→}
    43→
    44→#[tokio::main]
    45→async fn main() -> Result<()> {
    46→    // Initialize logging
    47→    let _subscriber = FmtSubscriber::builder()
    48→        .with_max_level(Level::INFO)
    49→        .with_target(true)
    50→        .init();
    51→
    52→    info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION"));
    53→
    54→    // Load configuration
    55→    let config = config::Config::load()?;
    56→
    57→    // Use port 3002 for GuruConnect
    58→    let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
    59→    info!("Loaded configuration, listening on {}", listen_addr);
    60→
    61→    // Initialize database if configured
    62→    let database = if let Some(ref db_url) = config.database_url {
    63→        match db::Database::connect(db_url, config.database_max_connections).await {
    64→            Ok(db) => {
    65→                // Run migrations
    66→                if let Err(e) = db.migrate().await {
    67→                    tracing::error!("Failed to run migrations: {}", e);
    68→                    return Err(e);
    69→                }
    70→                Some(db)
    71→            }
    72→            Err(e) => {
    73→                tracing::warn!("Failed to connect to database: {}. Running without persistence.", e);
    74→                None
    75→            }
    76→        }
    77→    } else {
    78→        info!("No DATABASE_URL set, running without persistence");
    79→        None
    80→    };
    81→
    82→    // Create session manager
    83→    let sessions = session::SessionManager::new();
    84→
    85→    // Restore persistent machines from database
    86→    if let Some(ref db) = database {
    87→        match db::machines::get_all_machines(db.pool()).await {
    88→            Ok(machines) => {
    89→                info!("Restoring {} persistent machines from database", machines.len());
    90→                for machine in machines {
    91→                    sessions.restore_offline_machine(&machine.agent_id, &machine.hostname).await;
    92→                }
    93→            }
    94→            Err(e) => {
    95→                tracing::warn!("Failed to restore machines: {}", e);
    96→            }
    97→        }
    98→    }
    99→
   100→    // Create application state
   101→    let state = AppState {
   102→        sessions,
   103→        support_codes: SupportCodeManager::new(),
   104→        db: database,
   105→    };
   106→
   107→    // Build router
   108→    let app = Router::new()
   109→        // Health check
   110→        .route("/health", get(health))
   111→
   112→        // Portal API - Support codes
   113→        .route("/api/codes", post(create_code))
   114→        .route("/api/codes", get(list_codes))
   115→        .route("/api/codes/:code/validate", get(validate_code))
   116→        .route("/api/codes/:code/cancel", post(cancel_code))
   117→
   118→        // WebSocket endpoints
   119→        .route("/ws/agent", get(relay::agent_ws_handler))
   120→        .route("/ws/viewer", get(relay::viewer_ws_handler))
   121→
   122→        // REST API - Sessions
   123→        .route("/api/sessions", get(list_sessions))
   124→        .route("/api/sessions/:id", get(get_session))
   125→        .route("/api/sessions/:id", axum::routing::delete(disconnect_session))
   126→
   127→        // HTML page routes (clean URLs)
   128→        .route("/login", get(serve_login))
   129→        .route("/dashboard", get(serve_dashboard))
   130→
   131→        // State
   132→        .with_state(state)
   133→
   134→        // Serve static files for portal (fallback)
   135→        .fallback_service(ServeDir::new("static").append_index_html_on_directories(true))
   136→
   137→        // Middleware
   138→        .layer(TraceLayer::new_for_http())
   139→        .layer(
   140→            CorsLayer::new()
   141→                .allow_origin(Any)
   142→                .allow_methods(Any)
   143→                .allow_headers(Any),
   144→        );
   145→
   146→    // Start server
   147→    let addr: SocketAddr = listen_addr.parse()?;
   148→    let listener = tokio::net::TcpListener::bind(addr).await?;
   149→
   150→    info!("Server listening on {}", addr);
   151→
   152→    axum::serve(listener, app).await?;
   153→
   154→    Ok(())
   155→}
   156→
   157→async fn health() -> &'static str {
   158→    "OK"
   159→}
   160→
   161→// Support code API handlers
   162→
   163→async fn create_code(
   164→    State(state): State<AppState>,
   165→    Json(request): Json<CreateCodeRequest>,
   166→) -> Json<SupportCode> {
   167→    let code = state.support_codes.create_code(request).await;
   168→    info!("Created support code: {}", code.code);
   169→    Json(code)
   170→}
   171→
   172→async fn list_codes(
   173→    State(state): State<AppState>,
   174→) -> Json<Vec<SupportCode>> {
   175→    Json(state.support_codes.list_active_codes().await)
   176→}
   177→
   178→#[derive(Deserialize)]
   179→struct ValidateParams {
   180→    code: String,
   181→}
   182→
   183→async fn validate_code(
   184→    State(state): State<AppState>,
   185→    Path(code): Path<String>,
   186→) -> Json<CodeValidation> {
   187→    Json(state.support_codes.validate_code(&code).await)
   188→}
   189→
   190→async fn cancel_code(
   191→    State(state): State<AppState>,
   192→    Path(code): Path<String>,
   193→) -> impl IntoResponse {
   194→    if state.support_codes.cancel_code(&code).await {
   195→        (StatusCode::OK, "Code cancelled")
   196→    } else {
   197→        (StatusCode::BAD_REQUEST, "Cannot cancel code")
   198→    }
   199→}
   200→
   201→// Session API handlers (updated to use AppState)
   202→
   203→async fn list_sessions(
   204→    State(state): State<AppState>,
   205→) -> Json<Vec<api::SessionInfo>> {
   206→    let sessions = state.sessions.list_sessions().await;
   207→    Json(sessions.into_iter().map(api::SessionInfo::from).collect())
   208→}
   209→
   210→async fn get_session(
   211→    State(state): State<AppState>,
   212→    Path(id): Path<String>,
   213→) -> Result<Json<api::SessionInfo>, (StatusCode, &'static str)> {
   214→    let session_id = uuid::Uuid::parse_str(&id)
   215→        .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?;
   216→
   217→    let session = state.sessions.get_session(session_id).await
   218→        .ok_or((StatusCode::NOT_FOUND, "Session not found"))?;
   219→
   220→    Ok(Json(api::SessionInfo::from(session)))
   221→}
   222→
   223→async fn disconnect_session(
   224→    State(state): State<AppState>,
   225→    Path(id): Path<String>,
   226→) -> impl IntoResponse {
   227→    let session_id = match uuid::Uuid::parse_str(&id) {
   228→        Ok(id) => id,
   229→        Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
   230→    };
   231→
   232→    if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
   233→        info!("Session {} disconnected by admin", session_id);
   234→        (StatusCode::OK, "Session disconnected")
   235→    } else {
   236→        (StatusCode::NOT_FOUND, "Session not found")
   237→    }
   238→}
   239→
   240→// Static page handlers
   241→async fn serve_login() -> impl IntoResponse {
   242→    match tokio::fs::read_to_string("static/login.html").await {
   243→        Ok(content) => Html(content).into_response(),
   244→        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
   245→    }
   246→}
   247→
   248→async fn serve_dashboard() -> impl IntoResponse {
   249→    match tokio::fs::read_to_string("static/dashboard.html").await {
   250→        Ok(content) => Html(content).into_response(),
   251→        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
   252→    }
   253→}
   254→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! GuruConnect Server - WebSocket Relay Server
//!
//! Handles connections from both agents and dashboard viewers,
//! relaying video frames and input events between them.

mod config;
mod relay;
mod session;
mod auth;
mod api;
mod db;
mod support_codes;

pub mod proto {
    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
}

use anyhow::Result;
use axum::{
    Router,
    routing::{get, post},
    extract::{Path, State, Json},
    response::{Html, IntoResponse},
    http::StatusCode,
};
use std::net::SocketAddr;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tower_http::services::ServeDir;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
use serde::Deserialize;

use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};

/// Application state
#[derive(Clone)]
pub struct AppState {
    sessions: session::SessionManager,
    support_codes: SupportCodeManager,
    db: Option<db::Database>,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    let _subscriber = FmtSubscriber::builder()
        .with_max_level(Level::INFO)
        .with_target(true)
        .init();

    info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION"));

    // Load configuration
    let config = config::Config::load()?;

    // Use port 3002 for GuruConnect
    let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
    info!("Loaded configuration, listening on {}", listen_addr);

    // Initialize database if configured
    let database = if let Some(ref db_url) = config.database_url {
        match db::Database::connect(db_url, config.database_max_connections).await {
            Ok(db) => {
                // Run migrations
                if let Err(e) = db.migrate().await {
                    tracing::error!("Failed to run migrations: {}", e);
                    return Err(e);
                }
                Some(db)
            }
            Err(e) => {
                tracing::warn!("Failed to connect to database: {}. Running without persistence.", e);
                None
            }
        }
    } else {
        info!("No DATABASE_URL set, running without persistence");
        None
    };

    // Create session manager
    let sessions = session::SessionManager::new();

    // Restore persistent machines from database
    if let Some(ref db) = database {
        match db::machines::get_all_machines(db.pool()).await {
            Ok(machines) => {
                info!("Restoring {} persistent machines from database", machines.len());
                for machine in machines {
                    sessions.restore_offline_machine(&machine.agent_id, &machine.hostname).await;
                }
            }
            Err(e) => {
                tracing::warn!("Failed to restore machines: {}", e);
            }
        }
    }

    // Create application state
    let state = AppState {
        sessions,
        support_codes: SupportCodeManager::new(),
        db: database,
    };

    // Build router
    let app = Router::new()
        // Health check
        .route("/health", get(health))

        // Portal API - Support codes
        .route("/api/codes", post(create_code))
        .route("/api/codes", get(list_codes))
        .route("/api/codes/:code/validate", get(validate_code))
        .route("/api/codes/:code/cancel", post(cancel_code))

        // WebSocket endpoints
        .route("/ws/agent", get(relay::agent_ws_handler))
        .route("/ws/viewer", get(relay::viewer_ws_handler))

        // REST API - Sessions
        .route("/api/sessions", get(list_sessions))
        .route("/api/sessions/:id", get(get_session))
        .route("/api/sessions/:id", axum::routing::delete(disconnect_session))

        // HTML page routes (clean URLs)
        .route("/login", get(serve_login))
        .route("/dashboard", get(serve_dashboard))

        // State
        .with_state(state)

        // Serve static files for portal (fallback)
        .fallback_service(ServeDir::new("static").append_index_html_on_directories(true))

        // Middleware
        .layer(TraceLayer::new_for_http())
        .layer(
            CorsLayer::new()
                .allow_origin(Any)
                .allow_methods(Any)
                .allow_headers(Any),
        );

    // Start server
    let addr: SocketAddr = listen_addr.parse()?;
    let listener = tokio::net::TcpListener::bind(addr).await?;

    info!("Server listening on {}", addr);

    axum::serve(listener, app).await?;

    Ok(())
}

async fn health() -> &'static str {
    "OK"
}

// Support code API handlers

async fn create_code(
    State(state): State<AppState>,
    Json(request): Json<CreateCodeRequest>,
) -> Json<SupportCode> {
    let code = state.support_codes.create_code(request).await;
    info!("Created support code: {}", code.code);
    Json(code)
}

async fn list_codes(
    State(state): State<AppState>,
) -> Json<Vec<SupportCode>> {
    Json(state.support_codes.list_active_codes().await)
}

#[derive(Deserialize)]
struct ValidateParams {
    code: String,
}

async fn validate_code(
    State(state): State<AppState>,
    Path(code): Path<String>,
) -> Json<CodeValidation> {
    Json(state.support_codes.validate_code(&code).await)
}

async fn cancel_code(
    State(state): State<AppState>,
    Path(code): Path<String>,
) -> impl IntoResponse {
    if state.support_codes.cancel_code(&code).await {
        (StatusCode::OK, "Code cancelled")
    } else {
        (StatusCode::BAD_REQUEST, "Cannot cancel code")
    }
}

// Session API handlers (updated to use AppState)

async fn list_sessions(
    State(state): State<AppState>,
) -> Json<Vec<api::SessionInfo>> {
    let sessions = state.sessions.list_sessions().await;
    Json(sessions.into_iter().map(api::SessionInfo::from).collect())
}

async fn get_session(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Json<api::SessionInfo>, (StatusCode, &'static str)> {
    let session_id = uuid::Uuid::parse_str(&id)
        .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?;

    let session = state.sessions.get_session(session_id).await
        .ok_or((StatusCode::NOT_FOUND, "Session not found"))?;

    Ok(Json(api::SessionInfo::from(session)))
}

async fn disconnect_session(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> impl IntoResponse {
    let session_id = match uuid::Uuid::parse_str(&id) {
        Ok(id) => id,
        Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
    };

    if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
        info!("Session {} disconnected by admin", session_id);
        (StatusCode::OK, "Session disconnected")
    } else {
        (StatusCode::NOT_FOUND, "Session not found")
    }
}

// Static page handlers
async fn serve_login() -> impl IntoResponse {
    match tokio::fs::read_to_string("static/login.html").await {
        Ok(content) => Html(content).into_response(),
        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
    }
}

async fn serve_dashboard() -> impl IntoResponse {
    match tokio::fs::read_to_string("static/dashboard.html").await {
        Ok(content) => Html(content).into_response(),
        Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
    }
}


Now I have a clear picture. Let me also check the Cargo.toml to make sure the dependencies are there, then I'll start creating files.


     1→[package]
     2→name = "guruconnect-server"
     3→version = "0.1.0"
     4→edition = "2021"
     5→authors = ["AZ Computer Guru"]
     6→description = "GuruConnect Remote Desktop Relay Server"
     7→
     8→[dependencies]
     9→# Async runtime
    10→tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }
    11→
    12→# Web framework
    13→axum = { version = "0.7", features = ["ws", "macros"] }
    14→tower = "0.5"
    15→tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip", "fs"] }
    16→
    17→# WebSocket
    18→futures-util = "0.3"
    19→
    20→# Database
    21→sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "json"] }
    22→
    23→# Protocol (protobuf)
    24→prost = "0.13"
    25→prost-types = "0.13"
    26→bytes = "1"
    27→
    28→# Serialization
    29→serde = { version = "1", features = ["derive"] }
    30→serde_json = "1"
    31→
    32→# Logging
    33→tracing = "0.1"
    34→tracing-subscriber = { version = "0.3", features = ["env-filter"] }
    35→
    36→# Error handling
    37→anyhow = "1"
    38→thiserror = "1"
    39→
    40→# Configuration
    41→toml = "0.8"
    42→
    43→# Auth
    44→jsonwebtoken = "9"
    45→argon2 = "0.5"
    46→
    47→# Crypto
    48→ring = "0.17"
    49→
    50→# UUID
    51→uuid = { version = "1", features = ["v4", "serde"] }
    52→
    53→# Time
    54→chrono = { version = "0.4", features = ["serde"] }
    55→rand = "0.8"
    56→
    57→[build-dependencies]
    58→prost-build = "0.13"
    59→
    60→[profile.release]
    61→lto = true
    62→codegen-units = 1
    63→strip = true
    64→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

[package]
name = "guruconnect-server"
version = "0.1.0"
edition = "2021"
authors = ["AZ Computer Guru"]
description = "GuruConnect Remote Desktop Relay Server"

[dependencies]
# Async runtime
tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }

# Web framework
axum = { version = "0.7", features = ["ws", "macros"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip", "fs"] }

# WebSocket
futures-util = "0.3"

# Database
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "json"] }

# Protocol (protobuf)
prost = "0.13"
prost-types = "0.13"
bytes = "1"

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Error handling
anyhow = "1"
thiserror = "1"

# Configuration
toml = "0.8"

# Auth
jsonwebtoken = "9"
argon2 = "0.5"

# Crypto
ring = "0.17"

# UUID
uuid = { version = "1", features = ["v4", "serde"] }

# Time
chrono = { version = "0.4", features = ["serde"] }
rand = "0.8"

[build-dependencies]
prost-build = "0.13"

[profile.release]
lto = true
codegen-units = 1
strip = true


All the dependencies I need are already present:- jsonwebtoken = "9"
- argon2 = "0.5"
- sqlx with postgres, uuid, chrono, json features
- rand for random password generation
- chrono for timestamps

Now let me start creating the files. I'll start with the migration.


All dependencies are already in place. Let me start implementing. First, the database migration:


Write

-- GuruConnect User Management Schema
-- User authentication, roles, and per-client access control

-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username VARCHAR(64) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(255),
    role VARCHAR(32) NOT NULL DEFAULT 'viewer',
    enabled BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login TIMESTAMPTZ
);

-- Granular permissions (what actions a user can perform)
CREATE TABLE user_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    permission VARCHAR(64) NOT NULL,
    UNIQUE(user_id, permission)
);

-- Per-client access (which machines a user can access)
-- No entries = access to all clients (for admins)
CREATE TABLE user_client_access (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES connect_machines(id) ON DELETE CASCADE,
    UNIQUE(user_id, client_id)
);

-- Indexes
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_enabled ON users(enabled);
CREATE INDEX idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX idx_user_client_access_user ON user_client_access(user_id);

-- Trigger for updated_at
CREATE TRIGGER update_users_updated_at
    BEFORE UPDATE ON users
    FOR EACH ROW
    EXECUTE FUNCTION update_connect_updated_at();


File created successfully at: C:\Users\MikeSwanson\Claude\guru-connect\server\migrations\002_user_management.sql

-- GuruConnect User Management Schema
-- User authentication, roles, and per-client access control

-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username VARCHAR(64) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(255),
    role VARCHAR(32) NOT NULL DEFAULT 'viewer',
    enabled BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login TIMESTAMPTZ
);

-- Granular permissions (what actions a user can perform)
CREATE TABLE user_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    permission VARCHAR(64) NOT NULL,
    UNIQUE(user_id, permission)
);

-- Per-client access (which machines a user can access)
-- No entries = access to all clients (for admins)
CREATE TABLE user_client_access (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES connect_machines(id) ON DELETE CASCADE,
    UNIQUE(user_id, client_id)
);

-- Indexes
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_enabled ON users(enabled);
CREATE INDEX idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX idx_user_client_access_user ON user_client_access(user_id);

-- Trigger for updated_at
CREATE TRIGGER update_users_updated_at
    BEFORE UPDATE ON users
    FOR EACH ROW
    EXECUTE FUNCTION update_connect_updated_at();


Migration is done. Now let me check if there's a models directory or if I need to create one, then create the user model.


TodoWrite

Create database migration 002_user_management.sql

Create models/user.rs with User struct and DB operations

Implement auth/password.rs - Argon2 hashing

Implement auth/jwt.rs - Token create/validate

Rewrite auth/mod.rs - Real middleware

Create api/auth.rs - Login/logout endpoints

Create api/users.rs - User management endpoints

Update api/mod.rs and main.rs for new routes

Create login.html page

Update dashboard.html with auth checks

Create users.html management page

Run migration and test


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create database migration 002_user_management.sql

Create models/user.rs with User struct and DB operations

Implement auth/password.rs - Argon2 hashing

Implement auth/jwt.rs - Token create/validate

Rewrite auth/mod.rs - Real middleware

Create api/auth.rs - Login/logout endpoints

Create api/users.rs - User management endpoints

Update api/mod.rs and main.rs for new routes

Create login.html page

Update dashboard.html with auth checks

Create users.html management page

Run migration and test

Rewrite auth/mod.rs - Real middleware

Create login.html page

Update dashboard.html with auth checks

Create users.html management page

Run migration and test


I see there's already a `db` module with a structure. Let me read the db/mod.rs to understand how to add the user model.


     1→//! Database module for GuruConnect
     2→//!
     3→//! Handles persistence for machines, sessions, and audit logging.
     4→//! Optional - server works without database if DATABASE_URL not set.
     5→
     6→pub mod machines;
     7→pub mod sessions;
     8→pub mod events;
     9→pub mod support_codes;
    10→
    11→use anyhow::Result;
    12→use sqlx::postgres::PgPoolOptions;
    13→use sqlx::PgPool;
    14→use tracing::info;
    15→
    16→pub use machines::*;
    17→pub use sessions::*;
    18→pub use events::*;
    19→pub use support_codes::*;
    20→
    21→/// Database connection pool wrapper
    22→#[derive(Clone)]
    23→pub struct Database {
    24→    pool: PgPool,
    25→}
    26→
    27→impl Database {
    28→    /// Initialize database connection pool
    29→    pub async fn connect(database_url: &str, max_connections: u32) -> Result<Self> {
    30→        info!("Connecting to database...");
    31→        let pool = PgPoolOptions::new()
    32→            .max_connections(max_connections)
    33→            .connect(database_url)
    34→            .await?;
    35→
    36→        info!("Database connection established");
    37→        Ok(Self { pool })
    38→    }
    39→
    40→    /// Run database migrations
    41→    pub async fn migrate(&self) -> Result<()> {
    42→        info!("Running database migrations...");
    43→        sqlx::migrate!("./migrations").run(&self.pool).await?;
    44→        info!("Migrations complete");
    45→        Ok(())
    46→    }
    47→
    48→    /// Get reference to the connection pool
    49→    pub fn pool(&self) -> &PgPool {
    50→        &self.pool
    51→    }
    52→}
    53→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Database module for GuruConnect
//!
//! Handles persistence for machines, sessions, and audit logging.
//! Optional - server works without database if DATABASE_URL not set.

pub mod machines;
pub mod sessions;
pub mod events;
pub mod support_codes;

use anyhow::Result;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use tracing::info;

pub use machines::*;
pub use sessions::*;
pub use events::*;
pub use support_codes::*;

/// Database connection pool wrapper
#[derive(Clone)]
pub struct Database {
    pool: PgPool,
}

impl Database {
    /// Initialize database connection pool
    pub async fn connect(database_url: &str, max_connections: u32) -> Result<Self> {
        info!("Connecting to database...");
        let pool = PgPoolOptions::new()
            .max_connections(max_connections)
            .connect(database_url)
            .await?;

        info!("Database connection established");
        Ok(Self { pool })
    }

    /// Run database migrations
    pub async fn migrate(&self) -> Result<()> {
        info!("Running database migrations...");
        sqlx::migrate!("./migrations").run(&self.pool).await?;
        info!("Migrations complete");
        Ok(())
    }

    /// Get reference to the connection pool
    pub fn pool(&self) -> &PgPool {
        &self.pool
    }
}


I'll add a `users.rs` in the db module, then update `db/mod.rs` to include it.


Now I'll create the users module in the db folder and the auth modules: