Code Structure
Repository Layout
Frontend — Next.js 16 Frontend
The frontend uses Next.js 16 App Router with React 19 and TypeScript. All pages are React Server Components by default; client interactivity uses the "use client" directive.
| File | Route | Purpose |
|---|---|---|
app/page.tsx | / | Landing / marketing page |
app/login/page.tsx | /login | Login form |
app/signup/page.tsx | /signup | Registration form with password strength meter |
app/profile/page.tsx | /profile | User profile / change password (protected) |
app/dashboard/layout.tsx | /dashboard/* | Dashboard shell: sidebar navigation + top header |
app/dashboard/page.tsx | /dashboard | Dashboard home |
app/dashboard/blueprint/page.tsx | /dashboard/blueprint | My IoT Blueprint 5-step wizard |
app/dashboard/chat/page.tsx | /dashboard/chat | Jim AI chat with multi-session support |
app/dashboard/decrypt/page.tsx | /dashboard/decrypt | Review Existing Blueprint (decrypt & inspect) |
app/dashboard/admin/page.tsx | /dashboard/admin | Admin panel (admin role required) |
Page Components
My IoT Blueprint — 5-Step Wizard
Client component. Encrypts blueprint data in Step 5 using the Web Crypto API. Key encryption logic:
// 1. Derive key via PBKDF2
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const keyMaterial = await crypto.subtle.importKey(
"raw", encode(passphrase), "PBKDF2", false, ["deriveKey"]
);
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 390000, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false, ["encrypt"]
);
// 2. Encrypt and bundle
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, encode(JSON.stringify(blueprint))
);
download(bundle({ salt, iv, ciphertext }), `${name}.hassar`);
Jim AI Chat (dashboard/chat)
The most complex page. Manages multiple sessions in localStorage, handles blueprint upload and decryption, and forwards messages to the backend with up to 20 messages of history.
localStorage key: hassar_sessions_{email} — stores the full sessions array per user.
| Constant | Value |
|---|---|
| Max sessions per user | 20 |
| Max messages per session | 100 |
| History sent to AI | Last 20 messages |
System Admin Panel (dashboard/admin)
Client component. Tabbed interface: Overview · Users · Logs · AI Config. On mount, checks if the user's role is "admin"; redirects non-admins to /dashboard.
Review Existing Blueprint (dashboard/decrypt)
Client component. Accessed from the Blueprint Hub "Review Existing Blueprint" button. User selects a .hassar file, enters passphrase, and the decrypted blueprint is displayed on screen.
Utilities
Provides auth state across all client components. Key exports:
| Export | Purpose |
|---|---|
useAuth() | Hook: returns { user, login, logout, loading } |
silentRefresh() | Calls POST /api/auth/refresh and saves new access token. Called by chat and profile pages on 401. |
Token stored in sessionStorage as hassar_token. Idle timeout: 10 min with 1 min warning. Auto-refresh 120 s before token expiry.
Implements blueprint AES-256-GCM encryption and decryption using the browser's Web Crypto API. Used by both the Blueprint wizard (encrypt) and the decrypt page (decrypt).
toBase64() helper uses an explicit loop (not spread operator) to convert large Uint8Array to base64 — avoiding a stack overflow on large blueprint files.Backend — FastAPI Backend
Creates the FastAPI app, registers CORS middleware, rate limiter exception handler, and calls run_migrations() at startup. Mounts three routers under /api:
| Router | Prefix |
|---|---|
auth_router | /api/auth |
features_router | /api |
admin_router | /api/admin |
def get_db():
db = SessionLocal()
try:
yield db
except Exception:
db.rollback()
raise
finally:
db.close()
Data Models
| Column | Type | Notes |
|---|---|---|
id | Integer PK | Auto-increment |
email | String UNIQUE | Login identifier |
password_hash | Text | bcrypt hash (cost 12) |
role | String | "user" or "admin" |
created_at | DateTime TZ | Server default: NOW() |
failed_login_attempts | Integer | Account lockout counter |
locked_until | DateTime TZ nullable | Lockout expiry (15 min after 5 failures) |
| Column | Type | Notes |
|---|---|---|
key | String PK | e.g. "agim_url", "ollama_model" |
value | Text nullable | Config value |
updated_at | DateTime TZ | Auto-updated |
Stores AI config (AGIM URL/Key, Ollama URL/Model) and per-user blueprint hashes (blueprint_hash:{email}) as key-value rows.
API Routes
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register | None | Register. Returns 201 regardless (prevents email enumeration). |
| POST | /auth/login | None | Login with JSON body. Sets access + refresh cookies. |
| POST | /auth/token | None | OAuth2 form login. Returns access_token in body. |
| POST | /auth/refresh | Refresh cookie | Issue new access token. Checks lockout. |
| GET | /auth/profile | Bearer | Get current user (id, email, role). |
| POST | /auth/change-password | Bearer | Change password (verifies current password; 5/min rate limit). |
| POST | /auth/logout | – | Clear auth cookies. |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / | None | Root health check |
| GET | /ai/status | Bearer | Returns active AI mode (agim or ollama). Does not expose AGIM URL. |
| POST | /chat | Bearer | Send message to Jim. Routes to AGIM first, falls back to Ollama. |
| POST | /blueprint/register | Bearer | Store SHA-256 hash + size of encrypted blueprint for ownership verification. |
| Method | Path | Description |
|---|---|---|
| GET | /admin/users | List all users (id, email, role) |
| PATCH | /admin/users/{id}/role | Toggle user role (user ↔ admin). Cannot change own role. |
| DELETE | /admin/users/{id} | Delete user. Cannot delete own account. |
| GET | /admin/system-logs | System event logs (no email PII in messages). |
| GET | /admin/ai-config | Get AI config. AGIM key is masked (********XXXX). |
| POST | /admin/ai-config | Save AI config. Validates both URLs for SSRF. Preserves existing AGIM key if masked placeholder submitted. |
Data Flow Diagrams
Browser Backend Database
│ │ │
├── POST /api/auth/login ───────► │
│ { email, password } ├── SELECT user WHERE email ──►│
│ │◄── User row ─────────────────┤
│ ├── verify_password() │
│ ├── create_access_token() │
│ ├── create_refresh_token() │
│◄── 200 { access_token } ──────┤ │
│ Set-Cookie: refresh_token │ │
│ │ │
│ [30 min later] │ │
├── POST /api/auth/refresh ─────► │
│ Cookie: refresh_token ├── decode + verify type │
│ ├── check locked_until ────────►│
│◄── 200 { access_token } ──────┤ │
Browser only — server is never involved
User enters passphrase
│
▼
crypto.subtle.importKey("PBKDF2")
│
▼
crypto.subtle.deriveKey (390,000 iterations, SHA-256, 16-byte salt)
│
▼
AES-256-GCM key (256-bit)
│
▼
crypto.subtle.encrypt(blueprint JSON, key, 12-byte IV)
│
▼
{ version: 1, salt: b64, iv: b64, ciphertext: b64 }
│
▼
Download as <name>.hassar (wrapped in ENC_STREAM header)
Browser Backend AGIM / Ollama
│ │ │
├── POST /api/chat ────────────► │
│ { message, history, │ │
│ blueprint_context } ├── Build AGIM payload │
│ │ (chat_history, query, │
│ │ user_context) │
│ │ │
│ ├── POST to AGIM ───────────────►
│ │ [AGIM responds]
│ │◄── { response: "..." } ───────┤
│ │ │
│ │ [if AGIM fails or unconfigured]
│ ├── POST to Ollama ─────────────►
│ │◄── { message: { content } } ──┤
│ │ │
│◄── 200 { content } ──────────┤ │
│ │ │
[Stored in localStorage]
Key Design Decisions
| Decision | Reason | Trade-off |
|---|---|---|
| Client-side-only blueprint encryption | Core privacy guarantee — even the platform operator cannot access user blueprint data | Lost passphrase = unrecoverable data; documented clearly |
| Chat sessions in localStorage | Keeps potentially large blueprint context off-server; aligns with client-side privacy model | History not synced across devices or browsers |
| JWT dual-token pattern | sessionStorage prevents access token persisting across tabs; httpOnly cookie prevents XSS stealing refresh token |
Requires /auth/refresh call on tab reopen |
| AI two-tier fallback (AGIM → Ollama) | AGIM is production-grade but an external dependency; Ollama provides reliability during outages | Ollama requires local server setup; quality may differ |
| SSRF prevention on AI config URLs | Admin-supplied URLs are validated against private/loopback IP ranges before use | Admins cannot target internal services via AI config |
Environment & Configuration
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_API_URL | Yes | Backend API base URL (e.g. https://api.hassar.ai) |
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | PostgreSQL connection string |
JWT_SECRET_KEY | Yes | JWT signing key (min 32 chars) |
APP_ENV | No | "production" enables prod-specific behaviour (secure cookies, HSTS, CF-Connecting-IP trust) |
AGIM and Ollama settings are managed through System Admin → AI Config and stored in the system_config database table.
Dependencies
| Package | Version | Purpose |
|---|---|---|
next | 16.x | Framework (App Router) |
react | 19.x | UI library |
react-dom | 19.x | DOM rendering |
tailwindcss | 4.x | CSS framework |
typescript | 5.x | Type system |
| Package | Purpose |
|---|---|
fastapi | REST API framework |
uvicorn | ASGI server |
sqlalchemy | ORM |
pydantic | Data validation / schemas |
python-jose[cryptography] | JWT handling |
bcrypt | Password hashing |
slowapi | Rate limiting |
httpx | Async HTTP client (AGIM/Ollama requests) |
psycopg2-binary | PostgreSQL driver |