Skip to content

Commit 0eb61e0

Browse files
committed
feat: dashboard password recovery, and tighten free-tier limits
1 parent 05e0af9 commit 0eb61e0

8 files changed

Lines changed: 384 additions & 11 deletions

File tree

‎apps/dashboard-api/src/__tests__/auth.controller.test.js‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,4 +472,55 @@ describe('auth.controller', () => {
472472
);
473473
});
474474
});
475+
476+
// -----------------------------------------------------------------------
477+
describe('resetPassword', () => {
478+
test('clears stored refresh token after successful password reset', async () => {
479+
const mockOtpDoc = {
480+
attempts: 0,
481+
otp: 'hashed_otp',
482+
deleteOne: jest.fn().mockResolvedValue(undefined),
483+
save: jest.fn().mockResolvedValue(undefined),
484+
};
485+
const mockUser = {
486+
_id: 'dev_id_1',
487+
email: 'test@example.com',
488+
password: 'old_hashed_password',
489+
refreshToken: 'active_refresh_token',
490+
save: jest.fn().mockResolvedValue(undefined),
491+
};
492+
493+
Developer.findOne.mockResolvedValue(mockUser);
494+
Otp.findOne.mockResolvedValue(mockOtpDoc);
495+
bcrypt.compare.mockResolvedValue(true);
496+
bcrypt.genSalt.mockResolvedValue('salt');
497+
bcrypt.hash.mockResolvedValue('new_hashed_password');
498+
499+
const req = makeReq({
500+
email: 'test@example.com',
501+
otp: '123456',
502+
newPassword: 'newpassword123',
503+
});
504+
const res = makeRes();
505+
506+
await authController.resetPassword(req, res);
507+
508+
expect(mockUser.refreshToken).toBeNull();
509+
expect(mockUser.save).toHaveBeenCalled();
510+
expect(res.cookie).toHaveBeenCalledWith(
511+
'accessToken',
512+
'none',
513+
expect.objectContaining({ httpOnly: true })
514+
);
515+
expect(res.cookie).toHaveBeenCalledWith(
516+
'refreshToken',
517+
'none',
518+
expect.objectContaining({ httpOnly: true })
519+
);
520+
expect(res.status).toHaveBeenCalledWith(200);
521+
expect(res.json).toHaveBeenCalledWith({
522+
message: 'Password reset successfully. Please log in with your new password.'
523+
});
524+
});
525+
});
475526
});

‎apps/dashboard-api/src/controllers/auth.controller.js‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,25 @@ module.exports.resetPassword = async (req, res) => {
263263
await otpDoc.deleteOne();
264264
const salt = await bcrypt.genSalt(10);
265265
dev.password = await bcrypt.hash(newPassword, salt);
266+
// Invalidate existing dashboard refresh sessions after a successful reset.
267+
dev.refreshToken = null;
266268
await dev.save();
267269

268-
res.status(200).json({ message: "Password reset successfully. Please log in with your new password." });
270+
res
271+
.status(200)
272+
.cookie('accessToken', 'none', {
273+
expires: new Date(Date.now() + 10 * 1000),
274+
httpOnly: true,
275+
secure: process.env.NODE_ENV === 'production',
276+
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax'
277+
})
278+
.cookie('refreshToken', 'none', {
279+
expires: new Date(Date.now() + 10 * 1000),
280+
httpOnly: true,
281+
secure: process.env.NODE_ENV === 'production',
282+
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax'
283+
})
284+
.json({ message: "Password reset successfully. Please log in with your new password." });
269285
} catch (err) {
270286
if (err.status) return res.status(err.status).json({ error: err.message });
271287
if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors });
@@ -323,7 +339,6 @@ module.exports.refreshToken = async (req, res) => {
323339

324340
await sendTokenResponse(user, 200, res);
325341
} catch (err) {
326-
console.error(err);
327342
res.status(403).json({ error: "Invalid or expired refresh token" });
328343
}
329344
};

‎apps/dashboard-api/src/controllers/project.controller.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ module.exports.createProject = async (req, res) => {
189189

190190
// GET MAX PROJECTS
191191
const dev = await Developer.findById(req.user._id);
192-
const MAX_PROJECTS = dev?.maxProjects || 3;
192+
const MAX_PROJECTS = dev?.maxProjects || 1;
193193

194194
const isUserAdmin = dev.email === ADMIN_EMAIL;
195195
const projectCount = await Project.countDocuments({ owner: req.user._id });

‎apps/web-dashboard/src/App.jsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Docs from './pages/Docs';
2020
import Auth from './pages/Auth';
2121
import AuthCallback from './pages/AuthCallback';
2222
import OtpVerification from './pages/OtpVerification';
23+
import ForgotPassword from './pages/ForgotPassword';
2324
import Settings from './pages/Settings';
2425
import ProjectSettings from './pages/ProjectSettings';
2526

@@ -44,6 +45,7 @@ function App() {
4445
<Route path="/login" element={<Login />} />
4546
<Route path="/signup" element={<Signup />} />
4647
<Route path="/verify-otp" element={<OtpVerification />} />
48+
<Route path="/forgot-password" element={<ForgotPassword />} />
4749
<Route path="/auth/callback" element={<AuthCallback />} />
4850

4951

0 commit comments

Comments
 (0)