Skip to content

Commit 34857b5

Browse files
committed
🌟 feat: Implement user management (CRUD, avatar upload), authentication routes, and API rate limiting with accompanying documentation.
1 parent 0c955aa commit 34857b5

File tree

3 files changed

+76
-4
lines changed

3 files changed

+76
-4
lines changed

‎src/server/controllers/users.controller.ts‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Request, Response } from 'express';
2+
import multer from 'multer';
23
import bcrypt from 'bcrypt';
3-
import { sendBadRequest, sendSuccess } from '../utils/response.js';
4+
import { sendBadRequest, sendSuccess, sendUnauthorized } from '../utils/response.js';
45
import { query, queryOne, getSqliteClient } from '../utils/db.js';
56
import { User } from '../types/user.js';
67
import { getAuthConfig, logAuditEvent } from '../utils/auth.js';
8+
import { UploadController } from './upload.controller.js';
79

810
export class UsersController {
911
/**
@@ -262,4 +264,65 @@ export class UsersController {
262264
sendBadRequest(res, error.message || 'Failed to delete user.');
263265
}
264266
}
267+
268+
/**
269+
* Upload user avatar
270+
*/
271+
static async uploadAvatar(req: Request, res: Response): Promise<void> {
272+
try {
273+
const user = (req as any).user;
274+
if (!user) {
275+
sendUnauthorized(res, 'Authentication required.');
276+
return;
277+
}
278+
279+
const storage = 'avatars';
280+
const upload = multer({
281+
storage: UploadController.storage(storage),
282+
limits: {
283+
fileSize: 5 * 1024 * 1024 // 5MB limit
284+
},
285+
fileFilter: (req, file, cb) => {
286+
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
287+
if (allowedTypes.includes(file.mimetype)) {
288+
cb(null, true);
289+
} else {
290+
cb(new Error('Invalid file type. Only JPG, PNG and WebP are allowed.'));
291+
}
292+
}
293+
}).single('avatar');
294+
295+
upload(req, res, async (err: any) => {
296+
if (err instanceof multer.MulterError) {
297+
sendBadRequest(res, `Upload error: ${err.message}`);
298+
return;
299+
} else if (err) {
300+
sendBadRequest(res, err.message);
301+
return;
302+
}
303+
304+
if (!req.file) {
305+
sendBadRequest(res, 'No file uploaded.');
306+
return;
307+
}
308+
309+
const avatarUrl = `/api/image/${req.file.filename}`;
310+
311+
// Update user avatar in database
312+
const sqlite = getSqliteClient();
313+
const stmt = sqlite.prepare('UPDATE users SET avatar = ?, updated_at = datetime(\'now\') WHERE id = ?');
314+
stmt.run(avatarUrl, user.id);
315+
316+
await logAuditEvent(user.id, 'USER_AVATAR_UPLOADED', req, {
317+
avatarUrl
318+
});
319+
320+
sendSuccess(res, { avatarUrl }, 'Avatar uploaded successfully');
321+
});
322+
323+
} catch (error: any) {
324+
console.error('Upload avatar error:', error);
325+
sendBadRequest(res, error.message || 'Failed to upload avatar.');
326+
}
327+
}
265328
}

‎src/server/middlewares/rate-limit.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const fileUploadRateLimiter = rateLimit({
7171
*/
7272
export const loginRateLimiter = rateLimit({
7373
windowMs: limitedMins, // 15 minutes
74-
max: 5, // 5 login attempts per window
74+
max: 10, // 10 login attempts per window
7575
message: {
7676
status: 429,
7777
message: 'Too many login attempts. Please try again in 15 minutes.'

‎src/server/routes/auth.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ const router = express.Router();
1515
* @returns { accessToken, refreshToken, expiresAt, refreshExpiresAt, user }
1616
*
1717
* @security
18-
* - Rate limited to 5 attempts per 15 minutes
19-
* - Account lockout after 5 failed attempts
18+
* - Rate limited to 10 attempts per 15 minutes
19+
* - Account lockout after 10 failed attempts
2020
* - Bcrypt password hashing (12 rounds)
2121
* - JWT tokens with expiration
2222
*/
@@ -122,4 +122,13 @@ router.put('/users/:userId', jwtAuthMiddleware, adminOnlyMiddleware, UsersContro
122122
*/
123123
router.delete('/users/:userId', jwtAuthMiddleware, adminOnlyMiddleware, UsersController.deleteUser);
124124

125+
/**
126+
* Upload current user avatar
127+
*
128+
* @method POST /api/auth/me/avatar
129+
* @header Authorization: Bearer <accessToken>
130+
* @body { avatar: File }
131+
*/
132+
router.post('/me/avatar', jwtAuthMiddleware, UsersController.uploadAvatar);
133+
125134
export default router;

0 commit comments

Comments
 (0)