Skip to content

Commit 752b83a

Browse files
feat: implement environment RBAC, move selector to sidebar, and improve dialog interactions
1 parent 90210ef commit 752b83a

32 files changed

Lines changed: 1811 additions & 262 deletions

README.md

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,62 @@
11
<div align="center">
2-
<img src="resources/icon.jpg" alt="API Documenter Logo" width="150" />
2+
<img src="resources/icon.jpg" alt="API Documenter Logo" width="128" />
33

4-
<h1>API Documenter</h1>
4+
# API Documenter
55

6-
<p>
7-
<strong>A powerful, self-hosted, offline-first alternative to Postman and Insomnia.</strong>
8-
</p>
6+
**The ultimate self-hosted, offline-first API testing and documentation platform.**
97

10-
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11-
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
8+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9+
[![Version](https://img.shields.io/badge/version-1.0.15-emerald.svg)](package.json)
10+
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
11+
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS-lightgrey.svg)](#)
1212
</div>
1313

1414
---
1515

16-
**API Documenter** is designed for developers who need a robust API testing environment that works locally by default but scales into a team-oriented platform with secure database synchronization and granular access control.
16+
## 🌟 Overview
1717

18-
## ✨ Key Features
18+
**API Documenter** is a robust alternative to Postman and Insomnia, designed specifically for developers who value privacy, speed, and ownership. It starts as a high-performance local desktop app but scales instantly into a team-oriented platform with secure database synchronization and granular role-based access control.
1919

20-
- **Offline-First Desktop Environment**: Local performance with no cloud dependency for personal projects.
21-
- **Secure Team Workspaces**: Connect to **PostgreSQL** or **MySQL** for team synchronization via a secure Vercel-hosted proxy.
22-
- **Granular RBAC**: Manage Admins, Editors, and Viewers with folder-level permissions.
23-
- **Advanced Request Engine**: Full support for all HTTP methods, headers, parameters, and bodies.
24-
- **Premium Responsive UI**: A dark-themed, premium design with a custom font-scaling engine for perfect readability on any screen.
25-
- **Automated Updates**: Integrated GitHub update system for seamless version management.
20+
## 🚀 Key Features
2621

27-
## 🛠️ Built With
22+
### 1. Offline-First Excellence
23+
- **Local Storage**: Your sensitive API data stays on your machine by default using high-speed IndexedDB (Dexie).
24+
- **Zero Latency**: No "cloud sync" lag while you're prototyping or testing locally.
25+
- **Privacy by Design**: No mandatory accounts or telemetry.
2826

29-
- **Core**: Electron, React, TypeScript, Vite
30-
- **Database**: mysql2, pg, Dexie (IndexedDB)
31-
- **Styling**: Vanilla CSS (Custom Variable-based Scaling)
32-
- **Deployment**: Vercel & GitHub Actions
27+
### 2. Secure Team Synchronization
28+
- **Multi-DB Support**: Connect your workspace to a remote **PostgreSQL** or **MySQL** server.
29+
- **Vercel Proxy Deployment**: Deploy a production-ready server to Vercel in one click. This ensures your DB credentials are never exposed to the client side and all team traffic is securely authorized.
30+
- **Real-time Sync**: Collaborative editing with bi-directional synchronization between local and remote states.
3331

34-
## 🚀 Getting Started
32+
### 3. Folder-Level RBAC
33+
- **Admin**: Full control over project infrastructure, sync settings, and team management.
34+
- **Editor**: Full read/write access to folders, requests, and documentation.
35+
- **Viewer**: Read-only access for team members who need to consume documentation without modification.
3536

36-
### Prerequisites
37+
### 4. Advanced Request Engine
38+
- **Full HTTP Support**: GET, POST, PUT, DELETE, PATCH, OPTIONS, and more.
39+
- **Rich Payloads**: Seamlessly handle Headers, Query Params, and JSON/Text bodies.
40+
- **Smart Response Viewer**: Real-time status codes, response headers, body size, and execution benchmarks.
41+
42+
### 5. Enterprise-Grade Scaling
43+
- **Premium UI**: Sleek, glassmorphic dark-themed design.
44+
- **Custom Font Scaling**: Perfectly tailored readability for any screen size, from laptop screens to 4K monitors.
45+
- **GitHub Auto-Updates**: Silent background updates that keep you on the latest version without disruption.
46+
47+
## 🛠️ Tech Stack
3748

38-
Ensure you have the following installed on your local machine:
39-
- [Node.js](https://nodejs.org/) (v18 or higher)
49+
- **Frontend**: React 18, TypeScript, Vite
50+
- **Desktop Layer**: Electron (Standard IPC Integration)
51+
- **State Management**: Zustand & React Query
52+
- **Local Database**: Dexie (IndexedDB)
53+
- **Remote Bridge**: mysql2, pg (via Secure Proxy)
54+
- **CI/CD**: GitHub Actions & Electron-Builder
55+
56+
## � Getting Started
57+
58+
### Prerequisites
59+
- [Node.js](https://nodejs.org/) (v18.x or v20.x recommended)
4060
- [npm](https://www.npmjs.com/)
4161

4262
### Installation & Setup
@@ -45,15 +65,43 @@ Ensure you have the following installed on your local machine:
4565
```bash
4666
git clone https://github.com/PraneethKulukuri26/API-Documenter.git
4767
cd API-Documenter
68+
```
69+
4870
2. **Install dependencies:**
4971
```bash
5072
npm install
51-
3. **Run the application:**
73+
```
74+
75+
3. **Start Development Server:**
5276
```bash
5377
npm run dev
54-
4. **Build for production:**
55-
```bash
56-
npm run build
57-
5. **Deploy to Vercel:**
78+
```
79+
80+
4. **Build Production Application:**
5881
```bash
59-
npm run deploy
82+
# Build for Windows
83+
npm run build:win
84+
85+
# Build for macOS
86+
npm run build:mac
87+
```
88+
89+
## 🛡️ Security
90+
91+
API Documenter uses a unique **Security Proxy Architecture**. When you connect a database, the app deploys a serverless function to Vercel. This function acts as the sole gatekeeper for your database, ensuring that raw SQL credentials are never stored in the desktop application but remain securely in your Vercel environment variables.
92+
93+
For more details, see [SECURITY.md](SECURITY.md).
94+
95+
## 🤝 Contributing
96+
97+
We love contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
98+
99+
## 📄 License
100+
101+
Distributed under the MIT License. See [LICENSE](LICENSE) for more information.
102+
103+
---
104+
105+
<!-- <div align="center">
106+
Built with ❤️ by <a href="https://github.com/PraneethKulukuri26">Praneeth Kulukuri</a>
107+
</div> -->

electron.vite.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export default defineConfig({
1717
}
1818
},
1919
plugins: [react(), tailwindcss()],
20-
// server: {
21-
// port: 4174
22-
// }
20+
server: {
21+
port: 4174
22+
}
2323
}
2424
})

server/api/apis/[id].ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export default async function handler(req: any, res: any) {
2424
const id = url.pathname.split('/').pop();
2525

2626
if (!id) {
27-
await db.close();
2827
return res.status(400).json({ error: 'API ID is required' });
2928
}
3029

@@ -73,7 +72,5 @@ export default async function handler(req: any, res: any) {
7372
return res.status(405).json({ error: 'Method not allowed' });
7473
} catch (err: any) {
7574
return res.status(500).json({ error: err.message });
76-
} finally {
77-
await db.close();
7875
}
7976
}

server/api/apis/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export default async function handler(req: any, res: any) {
6767
url_params, headers, body_type, request_body, response_examples
6868
} = req.body;
6969

70-
if (!id || !folder_id || !name || !method || !path) {
70+
if (!id || !folder_id || !name || !method) {
7171
return res.status(400).json({ error: 'Missing required fields' });
7272
}
7373

@@ -93,7 +93,5 @@ export default async function handler(req: any, res: any) {
9393
return res.status(405).json({ error: 'Method not allowed' });
9494
} catch (err: any) {
9595
return res.status(500).json({ error: err.message });
96-
} finally {
97-
await db.close();
9896
}
9997
}

server/api/environments/[id].ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { authenticate } from '../../src/middleware/auth.js';
2+
import { rateLimit } from '../../src/middleware/rateLimit.js';
3+
import { checkEnvironmentAccess } from '../../src/middleware/rbac.js';
4+
5+
export default async function handler(req: any, res: any) {
6+
res.setHeader('Access-Control-Allow-Origin', '*');
7+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
8+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
9+
10+
if (req.method === 'OPTIONS') {
11+
return res.status(200).end();
12+
}
13+
14+
const { id } = req.query;
15+
if (!id) return res.status(400).json({ error: 'Environment ID is required' });
16+
17+
let context;
18+
try {
19+
context = await authenticate(req);
20+
const token = req.headers.authorization?.replace('Bearer ', '') || new URL(req.url, `http://${req.headers.host}`).searchParams.get('token');
21+
if (token) rateLimit(token);
22+
} catch (err: any) {
23+
return res.status(401).json({ error: err.message });
24+
}
25+
26+
const { db, user } = context;
27+
28+
try {
29+
if (req.method === 'PUT') {
30+
const { name, baseUrl, isGlobal, folderId, variables } = req.body;
31+
32+
// Check if environment belongs to this project
33+
const existing = await db.query('SELECT * FROM environments WHERE id = ? AND project_id = ?', [id, user.projectId]) as any[];
34+
if (!existing.length) return res.status(404).json({ error: 'Environment not found' });
35+
36+
// Enforce RBAC
37+
await checkEnvironmentAccess(context, id, 'write', Number(existing[0].is_global) === 1);
38+
39+
await db.execute(
40+
'UPDATE environments SET name = ?, base_url = ?, is_global = ?, folder_id = ?, variables = ? WHERE id = ? AND project_id = ?',
41+
[name, baseUrl || '', isGlobal ? 1 : 0, folderId || null, variables, id, user.projectId]
42+
);
43+
44+
return res.status(200).json({ success: true });
45+
}
46+
47+
if (req.method === 'DELETE') {
48+
const existing = await db.query('SELECT * FROM environments WHERE id = ? AND project_id = ?', [id, user.projectId]) as any[];
49+
if (!existing.length) return res.status(404).json({ error: 'Environment not found' });
50+
51+
// Enforce RBAC
52+
await checkEnvironmentAccess(context, id, 'delete', Number(existing[0].is_global) === 1);
53+
54+
await db.execute('DELETE FROM environments WHERE id = ? AND project_id = ?', [id, user.projectId]);
55+
56+
return res.status(200).json({ success: true });
57+
}
58+
59+
return res.status(405).json({ error: 'Method not allowed' });
60+
} catch (err: any) {
61+
return res.status(500).json({ error: err.message });
62+
}
63+
}

server/api/environments/index.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { authenticate } from '../../src/middleware/auth.js';
2+
import { checkFolderAccess } from '../../src/middleware/rbac.js';
3+
import { rateLimit } from '../../src/middleware/rateLimit.js';
4+
5+
export default async function handler(req: any, res: any) {
6+
res.setHeader('Access-Control-Allow-Origin', '*');
7+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
8+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
9+
10+
if (req.method === 'OPTIONS') {
11+
return res.status(200).end();
12+
}
13+
let context;
14+
try {
15+
context = await authenticate(req);
16+
const token = req.headers.authorization?.replace('Bearer ', '') || new URL(req.url, `http://${req.headers.host}`).searchParams.get('token');
17+
if (token) rateLimit(token);
18+
} catch (err: any) {
19+
return res.status(401).json({ error: err.message });
20+
}
21+
22+
const { db, user } = context;
23+
24+
try {
25+
if (req.method === 'GET') {
26+
const results = await db.query('SELECT * FROM environments WHERE project_id = ?', [user.projectId]);
27+
28+
// Ensure a Global environment exists at the database level if not present
29+
let foundGlobal = results.find((e: any) => Number(e.is_global) === 1);
30+
if (!foundGlobal) {
31+
const globalId = `global-${user.projectId}`;
32+
try {
33+
await db.execute(
34+
'INSERT INTO environments (id, project_id, name, is_global, variables) VALUES (?, ?, ?, ?, ?)',
35+
[globalId, user.projectId, 'Global', 1, '{}']
36+
);
37+
// Add it to the results list manually so the current response is complete
38+
results.push({
39+
id: globalId,
40+
project_id: user.projectId,
41+
name: 'Global',
42+
is_global: 1,
43+
variables: '{}',
44+
created_at: Date.now()
45+
});
46+
} catch (e) {
47+
// Might already exist but wasn't in original SELECT results (race condition or ID format)
48+
// We'll re-fetch just to be safe if insert fails
49+
const refetch = await db.query('SELECT * FROM environments WHERE id = ?', [globalId]);
50+
if (refetch.length > 0) results.push(refetch[0]);
51+
}
52+
}
53+
54+
// Map to frontend camelCase
55+
const mapped = results.map((env: any) => ({
56+
id: env.id,
57+
projectId: env.project_id,
58+
folderId: env.folder_id,
59+
name: env.name,
60+
baseUrl: env.base_url,
61+
isGlobal: Number(env.is_global) === 1,
62+
variables: env.variables,
63+
createdAt: env.created_at
64+
}));
65+
66+
// Deduplicate by ID just in case
67+
const unique = Array.from(new Map(mapped.map(item => [item.id, item])).values());
68+
69+
// RBAC Filter and Role Injection
70+
const filtered = unique.filter((env: any) => {
71+
if (user.role === 'admin') return true;
72+
if (env.isGlobal) return true;
73+
const allowedEnvs = user.allowedEnvironments || [];
74+
return allowedEnvs.some((p: any) => {
75+
const idOrName = typeof p === 'string' ? p : p.envId;
76+
return idOrName === '*' || idOrName === env.id || idOrName === env.name;
77+
});
78+
}).map((env: any) => {
79+
let effectiveRole = user.role;
80+
if (user.role !== 'admin') {
81+
const allowedEnvs = user.allowedEnvironments || [];
82+
const perm = allowedEnvs.find((p: any) => {
83+
const idOrName = typeof p === 'string' ? p : p.envId;
84+
return idOrName === env.id || idOrName === env.name || (env.isGlobal && (idOrName === 'global' || idOrName === 'Global'));
85+
});
86+
if (perm && typeof perm === 'object' && perm.role) {
87+
effectiveRole = perm.role;
88+
}
89+
}
90+
return { ...env, role: effectiveRole };
91+
});
92+
93+
return res.status(200).json(filtered);
94+
}
95+
96+
if (req.method === 'POST') {
97+
if (user.role === 'viewer') return res.status(403).json({ error: 'Viewer role cannot manage environments' });
98+
99+
const { id, name, baseUrl, isGlobal, folderId, variables } = req.body;
100+
if (!id || !name) return res.status(400).json({ error: 'ID and Name are required' });
101+
102+
await db.execute(
103+
'INSERT INTO environments (id, project_id, folder_id, name, base_url, is_global, variables) VALUES (?, ?, ?, ?, ?, ?, ?)',
104+
[id, user.projectId, folderId || null, name, baseUrl || '', isGlobal ? 1 : 0, variables || '{}']
105+
);
106+
107+
return res.status(201).json({ success: true });
108+
}
109+
110+
return res.status(405).json({ error: 'Method not allowed' });
111+
} catch (err: any) {
112+
return res.status(500).json({ error: err.message });
113+
}
114+
}

server/api/folders/[id].ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export default async function handler(req: any, res: any) {
2424
const id = url.pathname.split('/').pop();
2525

2626
if (!id) {
27-
await db.close();
2827
return res.status(400).json({ error: 'Folder ID is required' });
2928
}
3029

@@ -76,7 +75,5 @@ export default async function handler(req: any, res: any) {
7675
return res.status(405).json({ error: 'Method not allowed' });
7776
} catch (err: any) {
7877
return res.status(500).json({ error: err.message });
79-
} finally {
80-
await db.close();
8178
}
8279
}

0 commit comments

Comments
 (0)