Skip to content

Commit c99184e

Browse files
author
Lasim
committed
docs: update API documentation and plugin security features for clarity and consistency
1 parent 9ea843e commit c99184e

File tree

8 files changed

+25
-10
lines changed

8 files changed

+25
-10
lines changed

services/backend/API_DOCUMENTATION.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ fastify.post<{ Body: RequestBody }>(
178178
### What NOT to Do (Anti-patterns)
179179

180180
**Don't do manual validation in handlers:**
181+
181182
```typescript
182183
// BAD: Manual validation (redundant)
183184
const parsedBody = myRequestBodySchema.safeParse(request.body);
@@ -197,6 +198,7 @@ if (request.body.type !== 'mysql' && request.body.type !== 'sqlite') {
197198
```
198199

199200
**Do trust Fastify's automatic validation:**
201+
200202
```typescript
201203
// GOOD: Trust the validation - if handler runs, data is valid
202204
const { name, count, type } = request.body; // Already validated by Fastify
@@ -206,7 +208,7 @@ const { name, count, type } = request.body; // Already validated by Fastify
206208

207209
The validation chain works as follows:
208210

209-
**Zod Schema → JSON Schema → Fastify Validation → Handler**
211+
#### Zod Schema → JSON Schema → Fastify Validation → Handler
210212

211213
1. **Zod Schema**: Define validation rules using Zod
212214
2. **JSON Schema**: Convert to OpenAPI format using `zodToJsonSchema()`
@@ -218,6 +220,7 @@ If validation fails, Fastify automatically returns a 400 error **before** your h
218220
### Real-World Examples
219221

220222
See these files for complete examples of proper Zod validation:
223+
221224
- `src/routes/db/setup.ts` - Database setup with enum validation
222225
- `src/routes/db/status.ts` - Simple GET endpoint with response schemas
223226
- `src/routes/auth/loginEmail.ts` - Login with required string fields
@@ -329,7 +332,7 @@ All plugin routes are automatically namespaced under `/api/plugin/<plugin-name>/
329332

330333
For a plugin with ID `example-plugin`:
331334

332-
```
335+
```bash
333336
GET /api/plugin/example-plugin/examples
334337
GET /api/plugin/example-plugin/examples/:id
335338
POST /api/plugin/example-plugin/examples

services/backend/PLUGINS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ DeployStack's plugin architecture allows for extensible, modular development wit
1616
## Security Features
1717

1818
### Route Isolation & Security
19+
1920
DeployStack implements strict route isolation to ensure plugins cannot interfere with core functionality or each other:
2021

2122
- **Automatic Namespacing**: All plugin routes are automatically prefixed with `/api/plugin/<plugin-id>/`
@@ -24,13 +25,15 @@ DeployStack implements strict route isolation to ensure plugins cannot interfere
2425
- **Core Route Protection**: Plugins cannot access or modify core routes (`/api/auth/*`, `/api/users/*`, etc.)
2526

2627
### Security Benefits
28+
2729
1. **Prevents Route Hijacking**: Malicious plugins cannot override authentication or user management routes
2830
2. **Eliminates Route Conflicts**: Multiple plugins cannot register conflicting routes
2931
3. **Predictable API Surface**: All plugin APIs follow consistent `/api/plugin/<name>/` structure
3032
4. **Easy Auditing**: Route ownership is immediately clear from the URL structure
3133
5. **Fail-Safe Design**: Plugins that don't follow the new system simply won't have routes registered
3234

3335
### Example Security Enforcement
36+
3437
```typescript
3538
// ❌ This will NOT work - no direct app access
3639
async initialize(app: FastifyInstance, db: AnyDatabase | null) {

services/backend/src/plugins/example-plugin/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import {
55
type GlobalSettingsExtension,
66
type PluginRouteManager
77
} from '../../plugin-system/types';
8-
import { type FastifyInstance } from 'fastify';
8+
99
import { type AnyDatabase, getSchema } from '../../db'; // Import getSchema
1010
import { type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; // For type guard
1111
import { type NodePgDatabase } from 'drizzle-orm/node-postgres'; // For casting db
1212
import { type SQLiteTable } from 'drizzle-orm/sqlite-core'; // For casting table from schema
1313
import { type PgTable } from 'drizzle-orm/pg-core'; // For casting table from schema
1414
// import { exampleEntities } from './schema'; // No longer directly used for queries
15-
import { eq, sql } from 'drizzle-orm';
15+
import { sql } from 'drizzle-orm';
1616

1717
// Helper type guard to check for BetterSQLite3Database specific methods
1818
function isSQLiteDB(db: AnyDatabase): db is BetterSQLite3Database<any> {
@@ -144,6 +144,7 @@ class ExamplePlugin implements Plugin {
144144
};
145145

146146
// Initialize the plugin (non-route initialization only)
147+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
147148
async initialize(db: AnyDatabase | null) {
148149
console.log(`[${this.meta.id}] Initializing...`);
149150
// Non-route initialization only - routes are now registered via registerRoutes method

services/backend/src/plugins/example-plugin/routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type PgTable } from 'drizzle-orm/pg-core';
77
import { eq } from 'drizzle-orm';
88

99
// Helper type guard to check for BetterSQLite3Database specific methods
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1011
function isSQLiteDB(db: AnyDatabase): db is BetterSQLite3Database<any> {
1112
return typeof (db as BetterSQLite3Database).get === 'function' &&
1213
typeof (db as BetterSQLite3Database).all === 'function' &&
@@ -58,6 +59,7 @@ export async function registerRoutes(routeManager: PluginRouteManager, db: AnyDa
5859

5960
if (isSQLiteDB(db)) {
6061
// Cast to SQLiteTable to access its 'id' column for the 'eq' condition
62+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6163
const typedTable = table as SQLiteTable & { id: any };
6264
example = await db
6365
.select()
@@ -66,6 +68,7 @@ export async function registerRoutes(routeManager: PluginRouteManager, db: AnyDa
6668
.get();
6769
} else {
6870
// Cast to PgTable to access its 'id' column for the 'eq' condition
71+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6972
const typedTable = table as PgTable & { id: any };
7073
const rows = await (db as NodePgDatabase)
7174
.select()

services/backend/src/routes/auth/updateProfile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export default async function updateProfileRoute(fastify: FastifyInstance) {
135135
}
136136

137137
// Prepare update data - only include fields that are provided
138+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
138139
const updateData: any = {
139140
updated_at: new Date()
140141
};

services/backend/src/services/emailVerificationService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getDb, getSchema } from '../db';
2-
import { eq, and, lt, gt } from 'drizzle-orm';
2+
import { eq, lt, gt } from 'drizzle-orm';
33
import { generateId } from 'lucia';
44
import { hash, verify } from '@node-rs/argon2';
55
import { EmailService } from '../email';

services/backend/tests/unit/plugin-system/plugin-manager.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ describe('PluginManager', () => {
143143
describe('initializePlugins', () => {
144144
it('should initialize all loaded plugins', async () => {
145145
await pluginManager.initializePlugins();
146-
expect(plugin1.initialize).toHaveBeenCalledWith(mockApp, mockDb);
147-
expect(plugin2.initialize).toHaveBeenCalledWith(mockApp, mockDb);
146+
expect(plugin1.initialize).toHaveBeenCalledWith(mockDb);
147+
expect(plugin2.initialize).toHaveBeenCalledWith(mockDb);
148148
expect(pluginManager['initialized']).toBe(true);
149149
});
150150

services/backend/tests/unit/routes/auth/registerEmail.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { hash } from '@node-rs/argon2';
66
import { generateId } from 'lucia';
77
import { TeamService } from '../../../../src/services/teamService';
88
import { GlobalSettingsInitService } from '../../../../src/global-settings';
9+
import { EmailVerificationService } from '../../../../src/services/emailVerificationService';
910

1011
// Mock dependencies
1112
vi.mock('../../../../src/db');
1213
vi.mock('@node-rs/argon2');
1314
vi.mock('lucia');
1415
vi.mock('../../../../src/services/teamService');
1516
vi.mock('../../../../src/global-settings');
17+
vi.mock('../../../../src/services/emailVerificationService');
1618
vi.mock('../../../../src/lib/lucia', () => ({
1719
getLucia: vi.fn(() => ({
1820
createSessionCookie: vi.fn(() => ({
@@ -30,6 +32,7 @@ const mockHash = hash as MockedFunction<typeof hash>;
3032
const mockGenerateId = generateId as MockedFunction<typeof generateId>;
3133
const mockTeamService = TeamService as any;
3234
const mockGlobalSettingsInitService = GlobalSettingsInitService as any;
35+
const mockEmailVerificationService = EmailVerificationService as any;
3336

3437
describe('Register Email Route', () => {
3538
let mockFastify: Partial<FastifyInstance>;
@@ -86,6 +89,7 @@ describe('Register Email Route', () => {
8689
mockGenerateId.mockReturnValueOnce('user-id-123').mockReturnValueOnce('session-id-123');
8790
mockGlobalSettingsInitService.isEmailRegistrationEnabled = vi.fn().mockResolvedValue(true);
8891
mockTeamService.createDefaultTeamForUser = vi.fn().mockResolvedValue({ id: 'team-123', name: 'Default Team' });
92+
mockEmailVerificationService.isVerificationRequired = vi.fn().mockResolvedValue(false);
8993

9094
// Setup route handlers storage
9195
routeHandlers = {};
@@ -179,7 +183,7 @@ describe('Register Email Route', () => {
179183
expect(mockReply.status).toHaveBeenCalledWith(201);
180184
expect(mockReply.send).toHaveBeenCalledWith({
181185
success: true,
182-
message: 'User registered successfully. Please log in to continue.',
186+
message: 'User registered successfully. You are now logged in as the global administrator.',
183187
user: {
184188
id: 'user-id-123',
185189
username: 'testuser',
@@ -235,7 +239,7 @@ describe('Register Email Route', () => {
235239
expect(mockReply.status).toHaveBeenCalledWith(201);
236240
expect(mockReply.send).toHaveBeenCalledWith({
237241
success: true,
238-
message: 'User registered successfully. Please log in to continue.',
242+
message: 'User registered successfully. You can now log in to your account.',
239243
user: expect.objectContaining({
240244
role_id: 'global_user',
241245
}),
@@ -285,7 +289,7 @@ describe('Register Email Route', () => {
285289
expect(mockReply.status).toHaveBeenCalledWith(201);
286290
expect(mockReply.send).toHaveBeenCalledWith({
287291
success: true,
288-
message: 'User registered successfully. Please log in to continue.',
292+
message: 'User registered successfully. You are now logged in as the global administrator.',
289293
user: expect.objectContaining({
290294
first_name: null,
291295
last_name: null,

0 commit comments

Comments
 (0)