|
@@ -1,16 +1,26 @@
|
|
|
import { Router, Request, Response } from 'express';
|
|
import { Router, Request, Response } from 'express';
|
|
|
import bcrypt from 'bcryptjs';
|
|
import bcrypt from 'bcryptjs';
|
|
|
import jwt from 'jsonwebtoken';
|
|
import jwt from 'jsonwebtoken';
|
|
|
|
|
+import rateLimit from 'express-rate-limit';
|
|
|
import { prisma } from '../lib/prisma';
|
|
import { prisma } from '../lib/prisma';
|
|
|
import { authMiddleware } from '../lib/auth';
|
|
import { authMiddleware } from '../lib/auth';
|
|
|
|
|
|
|
|
const router = Router();
|
|
const router = Router();
|
|
|
|
|
|
|
|
-const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret';
|
|
|
|
|
|
|
+const JWT_SECRET = process.env.JWT_SECRET;
|
|
|
|
|
+if (!JWT_SECRET) throw new Error('JWT_SECRET environment variable is required but was not set');
|
|
|
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
|
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
|
|
|
|
|
|
|
|
|
+const authRateLimiter = rateLimit({
|
|
|
|
|
+ windowMs: 15 * 60 * 1000, // 15 minutes
|
|
|
|
|
+ max: 5, // 5 attempts per window per IP
|
|
|
|
|
+ standardHeaders: true,
|
|
|
|
|
+ legacyHeaders: false,
|
|
|
|
|
+ message: { error: 'Too many requests. Please try again in 15 minutes.' },
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
// POST /api/auth/register
|
|
// POST /api/auth/register
|
|
|
-router.post('/register', async (req: Request, res: Response) => {
|
|
|
|
|
|
|
+router.post('/register', authRateLimiter, async (req: Request, res: Response) => {
|
|
|
try {
|
|
try {
|
|
|
const { email, name, password, inviteToken } = req.body;
|
|
const { email, name, password, inviteToken } = req.body;
|
|
|
|
|
|
|
@@ -122,7 +132,7 @@ router.post('/register', async (req: Request, res: Response) => {
|
|
|
|
|
|
|
|
res.cookie('token', token, {
|
|
res.cookie('token', token, {
|
|
|
httpOnly: true,
|
|
httpOnly: true,
|
|
|
- secure: false,
|
|
|
|
|
|
|
+ secure: process.env.NODE_ENV === 'production',
|
|
|
sameSite: 'lax',
|
|
sameSite: 'lax',
|
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
|
});
|
|
});
|
|
@@ -135,7 +145,7 @@ router.post('/register', async (req: Request, res: Response) => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// POST /api/auth/login
|
|
// POST /api/auth/login
|
|
|
-router.post('/login', async (req: Request, res: Response) => {
|
|
|
|
|
|
|
+router.post('/login', authRateLimiter, async (req: Request, res: Response) => {
|
|
|
try {
|
|
try {
|
|
|
const { email, password } = req.body;
|
|
const { email, password } = req.body;
|
|
|
|
|
|
|
@@ -205,18 +215,11 @@ router.post('/login', async (req: Request, res: Response) => {
|
|
|
|
|
|
|
|
res.cookie('token', token, {
|
|
res.cookie('token', token, {
|
|
|
httpOnly: true,
|
|
httpOnly: true,
|
|
|
- secure: false,
|
|
|
|
|
|
|
+ secure: process.env.NODE_ENV === 'production',
|
|
|
sameSite: 'lax',
|
|
sameSite: 'lax',
|
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // Compute storageUsed from owned projects
|
|
|
|
|
- const ownedAssets = await prisma.asset.findMany({
|
|
|
|
|
- where: { project: { ownerId: user.id } },
|
|
|
|
|
- select: { fileSize: true },
|
|
|
|
|
- });
|
|
|
|
|
- const storageUsed = ownedAssets.reduce((s, a) => s + a.fileSize, 0);
|
|
|
|
|
-
|
|
|
|
|
res.json({
|
|
res.json({
|
|
|
user: {
|
|
user: {
|
|
|
id: user.id,
|
|
id: user.id,
|
|
@@ -225,7 +228,7 @@ router.post('/login', async (req: Request, res: Response) => {
|
|
|
globalRole: user.globalRole,
|
|
globalRole: user.globalRole,
|
|
|
avatarUrl: user.avatarUrl,
|
|
avatarUrl: user.avatarUrl,
|
|
|
storageQuota: user.storageQuota,
|
|
storageQuota: user.storageQuota,
|
|
|
- storageUsed,
|
|
|
|
|
|
|
+ storageUsed: user.storageUsed ?? 0,
|
|
|
},
|
|
},
|
|
|
token,
|
|
token,
|
|
|
acceptedProjects,
|
|
acceptedProjects,
|
|
@@ -249,7 +252,7 @@ router.get('/me', authMiddleware, async (req: Request, res: Response) => {
|
|
|
where: { id: req.user!.userId },
|
|
where: { id: req.user!.userId },
|
|
|
select: {
|
|
select: {
|
|
|
id: true, email: true, name: true, globalRole: true, avatarUrl: true,
|
|
id: true, email: true, name: true, globalRole: true, avatarUrl: true,
|
|
|
- storageQuota: true,
|
|
|
|
|
|
|
+ storageQuota: true, storageUsed: true,
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -258,14 +261,7 @@ router.get('/me', authMiddleware, async (req: Request, res: Response) => {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Compute storageUsed from owned projects
|
|
|
|
|
- const ownedAssets = await prisma.asset.findMany({
|
|
|
|
|
- where: { project: { ownerId: req.user!.userId } },
|
|
|
|
|
- select: { fileSize: true },
|
|
|
|
|
- });
|
|
|
|
|
- const storageUsed = ownedAssets.reduce((s, a) => s + a.fileSize, 0);
|
|
|
|
|
-
|
|
|
|
|
- res.json({ user: { ...user, storageUsed } });
|
|
|
|
|
|
|
+ res.json({ user: { ...user, storageUsed: user.storageUsed ?? 0 } });
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.error('Me error:', err);
|
|
console.error('Me error:', err);
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
res.status(500).json({ error: 'Internal server error' });
|