@bradtaylorsf/alpha-loop 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/templates/agents/implementer.md +1 -1
- package/templates/skills/api-contracts/SKILL.md +0 -676
- package/templates/skills/api-patterns/SKILL.md +0 -346
- package/templates/skills/api-patterns/examples/complete-rest-api.ts +0 -293
- package/templates/skills/api-patterns/templates/express-router-template.ts +0 -294
- package/templates/skills/jest-mock-patterns/SKILL.md +0 -397
- package/templates/skills/playwright-testing/SKILL.md +0 -124
- package/templates/skills/sqlite-patterns/SKILL.md +0 -229
- package/templates/skills/test-caching/SKILL.md +0 -99
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: api-patterns
|
|
3
|
-
description: REST API best practices including request validation, error handling, authentication, rate limiting, and OpenAPI documentation. Use when building backend APIs.
|
|
4
|
-
auto_load: backend-developer
|
|
5
|
-
priority: high
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# API Patterns Skill
|
|
9
|
-
|
|
10
|
-
## Quick Reference
|
|
11
|
-
|
|
12
|
-
**Use when**: Building REST APIs, implementing authentication, handling errors, validating inputs
|
|
13
|
-
|
|
14
|
-
**Key Patterns**:
|
|
15
|
-
- Request validation with Zod
|
|
16
|
-
- Standardized error responses
|
|
17
|
-
- JWT authentication
|
|
18
|
-
- Rate limiting
|
|
19
|
-
- API documentation
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 1. Request Validation (Zod)
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { z } from 'zod';
|
|
27
|
-
|
|
28
|
-
// ✅ Define schemas
|
|
29
|
-
const CreateUserSchema = z.object({
|
|
30
|
-
email: z.string().email('Invalid email format'),
|
|
31
|
-
password: z.string()
|
|
32
|
-
.min(12, 'Password must be at least 12 characters')
|
|
33
|
-
.regex(/[A-Z]/, 'Must contain uppercase')
|
|
34
|
-
.regex(/[0-9]/, 'Must contain number'),
|
|
35
|
-
name: z.string().min(2).max(100),
|
|
36
|
-
age: z.number().int().min(18).optional()
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
type CreateUserDto = z.infer<typeof CreateUserSchema>;
|
|
40
|
-
|
|
41
|
-
// ✅ Validate in route handler
|
|
42
|
-
router.post('/api/users', async (req, res, next) => {
|
|
43
|
-
try {
|
|
44
|
-
const userData = CreateUserSchema.parse(req.body);
|
|
45
|
-
const user = await createUser(userData);
|
|
46
|
-
res.status(201).json(user);
|
|
47
|
-
} catch (err) {
|
|
48
|
-
if (err instanceof z.ZodError) {
|
|
49
|
-
return res.status(400).json({
|
|
50
|
-
error: 'Validation failed',
|
|
51
|
-
details: err.errors
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
next(err);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Reusable Validation Middleware**:
|
|
60
|
-
```typescript
|
|
61
|
-
export function validateRequest(schema: z.ZodSchema) {
|
|
62
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
63
|
-
try {
|
|
64
|
-
req.body = schema.parse(req.body);
|
|
65
|
-
next();
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (err instanceof z.ZodError) {
|
|
68
|
-
return res.status(400).json({
|
|
69
|
-
error: 'Validation failed',
|
|
70
|
-
details: err.errors
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
next(err);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Usage
|
|
79
|
-
router.post('/api/users', validateRequest(CreateUserSchema), createUserHandler);
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## 2. Standardized Error Responses
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
// ✅ Custom error class
|
|
88
|
-
export class AppError extends Error {
|
|
89
|
-
constructor(
|
|
90
|
-
message: string,
|
|
91
|
-
public statusCode: number = 500,
|
|
92
|
-
public code?: string,
|
|
93
|
-
public details?: any
|
|
94
|
-
) {
|
|
95
|
-
super(message);
|
|
96
|
-
this.name = 'AppError';
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ✅ Error handling middleware
|
|
101
|
-
export function errorHandler(
|
|
102
|
-
err: Error,
|
|
103
|
-
req: Request,
|
|
104
|
-
res: Response,
|
|
105
|
-
next: NextFunction
|
|
106
|
-
) {
|
|
107
|
-
if (err instanceof AppError) {
|
|
108
|
-
return res.status(err.statusCode).json({
|
|
109
|
-
error: err.message,
|
|
110
|
-
code: err.code,
|
|
111
|
-
details: err.details
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Log unexpected errors
|
|
116
|
-
console.error('Unexpected error:', err);
|
|
117
|
-
|
|
118
|
-
res.status(500).json({
|
|
119
|
-
error: 'Internal server error',
|
|
120
|
-
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Usage
|
|
125
|
-
throw new AppError('User not found', 404, 'USER_NOT_FOUND');
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## 3. JWT Authentication
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
import jwt from 'jsonwebtoken';
|
|
134
|
-
|
|
135
|
-
const JWT_SECRET = process.env.JWT_SECRET!;
|
|
136
|
-
|
|
137
|
-
// ✅ Generate token
|
|
138
|
-
export function generateToken(userId: string): string {
|
|
139
|
-
return jwt.sign(
|
|
140
|
-
{ userId },
|
|
141
|
-
JWT_SECRET,
|
|
142
|
-
{ expiresIn: '7d' }
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ✅ Auth middleware
|
|
147
|
-
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
|
148
|
-
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
149
|
-
|
|
150
|
-
if (!token) {
|
|
151
|
-
throw new AppError('Authentication required', 401, 'NO_TOKEN');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
|
156
|
-
req.userId = decoded.userId;
|
|
157
|
-
next();
|
|
158
|
-
} catch (err) {
|
|
159
|
-
throw new AppError('Invalid token', 401, 'INVALID_TOKEN');
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ✅ Optional auth
|
|
164
|
-
export function optionalAuth(req: Request, res: Response, next: NextFunction) {
|
|
165
|
-
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
166
|
-
|
|
167
|
-
if (token) {
|
|
168
|
-
try {
|
|
169
|
-
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
|
170
|
-
req.userId = decoded.userId;
|
|
171
|
-
} catch {
|
|
172
|
-
// Ignore invalid tokens in optional auth
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
next();
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## 4. Rate Limiting
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
import rateLimit from 'express-rate-limit';
|
|
186
|
-
|
|
187
|
-
// ✅ Global rate limit
|
|
188
|
-
const globalLimiter = rateLimit({
|
|
189
|
-
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
190
|
-
max: 100, // 100 requests per window
|
|
191
|
-
message: 'Too many requests from this IP'
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
app.use('/api/', globalLimiter);
|
|
195
|
-
|
|
196
|
-
// ✅ Strict limit for auth endpoints
|
|
197
|
-
const authLimiter = rateLimit({
|
|
198
|
-
windowMs: 15 * 60 * 1000,
|
|
199
|
-
max: 5, // Only 5 login attempts
|
|
200
|
-
message: 'Too many login attempts. Try again later.',
|
|
201
|
-
skipSuccessfulRequests: true
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
router.post('/api/auth/login', authLimiter, loginHandler);
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
## 5. Pagination
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
// ✅ Standard pagination pattern
|
|
213
|
-
const PaginationSchema = z.object({
|
|
214
|
-
page: z.coerce.number().int().min(1).default(1),
|
|
215
|
-
limit: z.coerce.number().int().min(1).max(100).default(20)
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
router.get('/api/users', async (req, res) => {
|
|
219
|
-
const { page, limit } = PaginationSchema.parse(req.query);
|
|
220
|
-
const offset = (page - 1) * limit;
|
|
221
|
-
|
|
222
|
-
const [users, total] = await Promise.all([
|
|
223
|
-
db.query('SELECT * FROM users LIMIT $1 OFFSET $2', [limit, offset]),
|
|
224
|
-
db.query('SELECT COUNT(*) FROM users')
|
|
225
|
-
]);
|
|
226
|
-
|
|
227
|
-
res.json({
|
|
228
|
-
data: users.rows,
|
|
229
|
-
pagination: {
|
|
230
|
-
page,
|
|
231
|
-
limit,
|
|
232
|
-
total: parseInt(total.rows[0].count),
|
|
233
|
-
totalPages: Math.ceil(parseInt(total.rows[0].count) / limit)
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
## 6. API Response Format
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
// ✅ Consistent response structure
|
|
245
|
-
interface ApiResponse<T> {
|
|
246
|
-
data?: T;
|
|
247
|
-
error?: string;
|
|
248
|
-
meta?: {
|
|
249
|
-
pagination?: PaginationMeta;
|
|
250
|
-
timestamp: string;
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Success response
|
|
255
|
-
res.json({
|
|
256
|
-
data: users,
|
|
257
|
-
meta: {
|
|
258
|
-
pagination: { page, limit, total },
|
|
259
|
-
timestamp: new Date().toISOString()
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Error response
|
|
264
|
-
res.status(400).json({
|
|
265
|
-
error: 'Validation failed',
|
|
266
|
-
meta: {
|
|
267
|
-
timestamp: new Date().toISOString()
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## 7. OpenAPI Documentation
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
/**
|
|
278
|
-
* @openapi
|
|
279
|
-
* /api/users:
|
|
280
|
-
* get:
|
|
281
|
-
* summary: List all users
|
|
282
|
-
* tags: [Users]
|
|
283
|
-
* parameters:
|
|
284
|
-
* - in: query
|
|
285
|
-
* name: page
|
|
286
|
-
* schema:
|
|
287
|
-
* type: integer
|
|
288
|
-
* minimum: 1
|
|
289
|
-
* description: Page number
|
|
290
|
-
* - in: query
|
|
291
|
-
* name: limit
|
|
292
|
-
* schema:
|
|
293
|
-
* type: integer
|
|
294
|
-
* minimum: 1
|
|
295
|
-
* maximum: 100
|
|
296
|
-
* description: Items per page
|
|
297
|
-
* responses:
|
|
298
|
-
* 200:
|
|
299
|
-
* description: List of users
|
|
300
|
-
* content:
|
|
301
|
-
* application/json:
|
|
302
|
-
* schema:
|
|
303
|
-
* type: object
|
|
304
|
-
* properties:
|
|
305
|
-
* data:
|
|
306
|
-
* type: array
|
|
307
|
-
* items:
|
|
308
|
-
* $ref: '#/components/schemas/User'
|
|
309
|
-
*/
|
|
310
|
-
router.get('/api/users', listUsers);
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## Checklist for New Endpoints
|
|
316
|
-
|
|
317
|
-
- ✅ Input validation with Zod
|
|
318
|
-
- ✅ Authentication/authorization check
|
|
319
|
-
- ✅ Rate limiting configured
|
|
320
|
-
- ✅ Error handling with AppError
|
|
321
|
-
- ✅ Consistent response format
|
|
322
|
-
- ✅ OpenAPI documentation
|
|
323
|
-
- ✅ Tests with mocked APIs (MSW)
|
|
324
|
-
- ✅ Logging for debugging
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## Common Patterns
|
|
329
|
-
|
|
330
|
-
**CRUD Operations**:
|
|
331
|
-
- `GET /api/resources` - List (with pagination)
|
|
332
|
-
- `GET /api/resources/:id` - Get one
|
|
333
|
-
- `POST /api/resources` - Create
|
|
334
|
-
- `PUT /api/resources/:id` - Update
|
|
335
|
-
- `DELETE /api/resources/:id` - Delete
|
|
336
|
-
|
|
337
|
-
**Status Codes**:
|
|
338
|
-
- `200` - Success (GET, PUT)
|
|
339
|
-
- `201` - Created (POST)
|
|
340
|
-
- `204` - No Content (DELETE)
|
|
341
|
-
- `400` - Bad Request (validation)
|
|
342
|
-
- `401` - Unauthorized (auth required)
|
|
343
|
-
- `403` - Forbidden (insufficient permissions)
|
|
344
|
-
- `404` - Not Found
|
|
345
|
-
- `429` - Too Many Requests (rate limit)
|
|
346
|
-
- `500` - Internal Server Error
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Complete REST API Example with Best Practices
|
|
3
|
-
* Demonstrates: validation, auth, rate limiting, error handling
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import express, { Request, Response, NextFunction } from 'express';
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
import rateLimit from 'express-rate-limit';
|
|
9
|
-
import helmet from 'helmet';
|
|
10
|
-
import jwt from 'jsonwebtoken';
|
|
11
|
-
import bcrypt from 'bcrypt';
|
|
12
|
-
|
|
13
|
-
const app = express();
|
|
14
|
-
const PORT = process.env.PORT || 3000;
|
|
15
|
-
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
16
|
-
|
|
17
|
-
// Middleware
|
|
18
|
-
app.use(express.json());
|
|
19
|
-
app.use(helmet());
|
|
20
|
-
|
|
21
|
-
// Custom error class
|
|
22
|
-
export class AppError extends Error {
|
|
23
|
-
constructor(
|
|
24
|
-
message: string,
|
|
25
|
-
public statusCode: number = 500,
|
|
26
|
-
public code?: string,
|
|
27
|
-
public details?: any
|
|
28
|
-
) {
|
|
29
|
-
super(message);
|
|
30
|
-
this.name = 'AppError';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Validation schemas
|
|
35
|
-
const CreateUserSchema = z.object({
|
|
36
|
-
email: z.string().email('Invalid email format'),
|
|
37
|
-
password: z.string()
|
|
38
|
-
.min(12, 'Password must be at least 12 characters')
|
|
39
|
-
.regex(/[A-Z]/, 'Must contain uppercase')
|
|
40
|
-
.regex(/[0-9]/, 'Must contain number'),
|
|
41
|
-
name: z.string().min(2).max(100)
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const LoginSchema = z.object({
|
|
45
|
-
email: z.string().email(),
|
|
46
|
-
password: z.string()
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const PaginationSchema = z.object({
|
|
50
|
-
page: z.coerce.number().int().min(1).default(1),
|
|
51
|
-
limit: z.coerce.number().int().min(1).max(100).default(20)
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Type inference
|
|
55
|
-
type CreateUserDto = z.infer<typeof CreateUserSchema>;
|
|
56
|
-
type LoginDto = z.infer<typeof LoginSchema>;
|
|
57
|
-
type PaginationDto = z.infer<typeof PaginationSchema>;
|
|
58
|
-
|
|
59
|
-
// Validation middleware
|
|
60
|
-
function validateRequest(schema: z.ZodSchema) {
|
|
61
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
62
|
-
try {
|
|
63
|
-
const target = req.method === 'GET' ? req.query : req.body;
|
|
64
|
-
req.body = schema.parse(target);
|
|
65
|
-
next();
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (err instanceof z.ZodError) {
|
|
68
|
-
return res.status(400).json({
|
|
69
|
-
error: 'Validation failed',
|
|
70
|
-
details: err.errors
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
next(err);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Auth middleware
|
|
79
|
-
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
|
80
|
-
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
81
|
-
|
|
82
|
-
if (!token) {
|
|
83
|
-
throw new AppError('Authentication required', 401, 'NO_TOKEN');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
|
88
|
-
(req as any).userId = decoded.userId;
|
|
89
|
-
next();
|
|
90
|
-
} catch (err) {
|
|
91
|
-
throw new AppError('Invalid token', 401, 'INVALID_TOKEN');
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Rate limiting
|
|
96
|
-
const globalLimiter = rateLimit({
|
|
97
|
-
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
98
|
-
max: 100, // 100 requests per window
|
|
99
|
-
message: 'Too many requests from this IP'
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const authLimiter = rateLimit({
|
|
103
|
-
windowMs: 15 * 60 * 1000,
|
|
104
|
-
max: 5, // Only 5 login attempts
|
|
105
|
-
message: 'Too many login attempts. Try again later.',
|
|
106
|
-
skipSuccessfulRequests: true
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
app.use('/api/', globalLimiter);
|
|
110
|
-
|
|
111
|
-
// Mock database
|
|
112
|
-
const users: any[] = [];
|
|
113
|
-
|
|
114
|
-
// Routes
|
|
115
|
-
|
|
116
|
-
// POST /api/auth/register
|
|
117
|
-
app.post(
|
|
118
|
-
'/api/auth/register',
|
|
119
|
-
validateRequest(CreateUserSchema),
|
|
120
|
-
async (req: Request, res: Response, next: NextFunction) => {
|
|
121
|
-
try {
|
|
122
|
-
const { email, password, name } = req.body as CreateUserDto;
|
|
123
|
-
|
|
124
|
-
// Check if user exists
|
|
125
|
-
if (users.find(u => u.email === email)) {
|
|
126
|
-
throw new AppError('Email already registered', 409, 'EMAIL_EXISTS');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Hash password
|
|
130
|
-
const passwordHash = await bcrypt.hash(password, 12);
|
|
131
|
-
|
|
132
|
-
// Create user
|
|
133
|
-
const user = {
|
|
134
|
-
id: String(users.length + 1),
|
|
135
|
-
email,
|
|
136
|
-
name,
|
|
137
|
-
passwordHash,
|
|
138
|
-
createdAt: new Date().toISOString()
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
users.push(user);
|
|
142
|
-
|
|
143
|
-
// Generate token
|
|
144
|
-
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
|
|
145
|
-
|
|
146
|
-
res.status(201).json({
|
|
147
|
-
data: {
|
|
148
|
-
user: { id: user.id, email: user.email, name: user.name },
|
|
149
|
-
token
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
} catch (err) {
|
|
153
|
-
next(err);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// POST /api/auth/login
|
|
159
|
-
app.post(
|
|
160
|
-
'/api/auth/login',
|
|
161
|
-
authLimiter,
|
|
162
|
-
validateRequest(LoginSchema),
|
|
163
|
-
async (req: Request, res: Response, next: NextFunction) => {
|
|
164
|
-
try {
|
|
165
|
-
const { email, password } = req.body as LoginDto;
|
|
166
|
-
|
|
167
|
-
// Find user
|
|
168
|
-
const user = users.find(u => u.email === email);
|
|
169
|
-
if (!user) {
|
|
170
|
-
throw new AppError('Invalid credentials', 401, 'INVALID_CREDENTIALS');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Verify password
|
|
174
|
-
const isValid = await bcrypt.compare(password, user.passwordHash);
|
|
175
|
-
if (!isValid) {
|
|
176
|
-
throw new AppError('Invalid credentials', 401, 'INVALID_CREDENTIALS');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Generate token
|
|
180
|
-
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
|
|
181
|
-
|
|
182
|
-
res.json({
|
|
183
|
-
data: {
|
|
184
|
-
user: { id: user.id, email: user.email, name: user.name },
|
|
185
|
-
token
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
} catch (err) {
|
|
189
|
-
next(err);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// GET /api/users (paginated)
|
|
195
|
-
app.get(
|
|
196
|
-
'/api/users',
|
|
197
|
-
requireAuth,
|
|
198
|
-
validateRequest(PaginationSchema),
|
|
199
|
-
async (req: Request, res: Response) => {
|
|
200
|
-
const { page, limit } = req.body as PaginationDto;
|
|
201
|
-
const offset = (page - 1) * limit;
|
|
202
|
-
|
|
203
|
-
const paginatedUsers = users
|
|
204
|
-
.slice(offset, offset + limit)
|
|
205
|
-
.map(({ passwordHash, ...user }) => user);
|
|
206
|
-
|
|
207
|
-
res.json({
|
|
208
|
-
data: paginatedUsers,
|
|
209
|
-
pagination: {
|
|
210
|
-
page,
|
|
211
|
-
limit,
|
|
212
|
-
total: users.length,
|
|
213
|
-
totalPages: Math.ceil(users.length / limit)
|
|
214
|
-
},
|
|
215
|
-
meta: {
|
|
216
|
-
timestamp: new Date().toISOString()
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// GET /api/users/:id
|
|
223
|
-
app.get(
|
|
224
|
-
'/api/users/:id',
|
|
225
|
-
requireAuth,
|
|
226
|
-
async (req: Request, res: Response, next: NextFunction) => {
|
|
227
|
-
try {
|
|
228
|
-
const user = users.find(u => u.id === req.params.id);
|
|
229
|
-
|
|
230
|
-
if (!user) {
|
|
231
|
-
throw new AppError('User not found', 404, 'USER_NOT_FOUND');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const { passwordHash, ...userData } = user;
|
|
235
|
-
res.json({ data: userData });
|
|
236
|
-
} catch (err) {
|
|
237
|
-
next(err);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
// DELETE /api/users/:id
|
|
243
|
-
app.delete(
|
|
244
|
-
'/api/users/:id',
|
|
245
|
-
requireAuth,
|
|
246
|
-
async (req: Request, res: Response, next: NextFunction) => {
|
|
247
|
-
try {
|
|
248
|
-
const userId = (req as any).userId;
|
|
249
|
-
const targetId = req.params.id;
|
|
250
|
-
|
|
251
|
-
// Check authorization (can only delete own account unless admin)
|
|
252
|
-
if (userId !== targetId) {
|
|
253
|
-
throw new AppError('Forbidden', 403, 'FORBIDDEN');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const index = users.findIndex(u => u.id === targetId);
|
|
257
|
-
if (index === -1) {
|
|
258
|
-
throw new AppError('User not found', 404, 'USER_NOT_FOUND');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
users.splice(index, 1);
|
|
262
|
-
res.status(204).send();
|
|
263
|
-
} catch (err) {
|
|
264
|
-
next(err);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// Error handling middleware
|
|
270
|
-
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
271
|
-
if (err instanceof AppError) {
|
|
272
|
-
return res.status(err.statusCode).json({
|
|
273
|
-
error: err.message,
|
|
274
|
-
code: err.code,
|
|
275
|
-
details: err.details
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Log unexpected errors
|
|
280
|
-
console.error('Unexpected error:', err);
|
|
281
|
-
|
|
282
|
-
res.status(500).json({
|
|
283
|
-
error: 'Internal server error',
|
|
284
|
-
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Start server
|
|
289
|
-
app.listen(PORT, () => {
|
|
290
|
-
console.log(`Server running on port ${PORT}`);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
export default app;
|