Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
62a8a4b
onboarding fixes
KushagraJaiswar02 Jun 14, 2026
6862767
onboarding
KushagraJaiswar02 Jun 15, 2026
e9cbb37
onboarding finsihing and flow concerns
KushagraJaiswar02 Jun 15, 2026
5cf8f97
dashboard fixes
KushagraJaiswar02 Jun 15, 2026
a2432ed
fix: coderabbit review comments
yash-pouranik Jun 17, 2026
ae78c0a
Merge main into feat/onboarding
yash-pouranik Jun 17, 2026
c9ad197
Fix merge conflict issues: missing imports in routes
yash-pouranik Jun 17, 2026
a62a4f5
feat(ui): redesign onboarding and fix test api blocked issue
yash-pouranik Jun 17, 2026
34bebd1
feat(ui): refine onboarding colors and fix step 3 failure skip
yash-pouranik Jun 17, 2026
19a43d0
refactor(ui): apply flat minimalist design to onboarding and fix logi…
yash-pouranik Jun 17, 2026
3ae92ee
style(onboarding): restore hyperspeed as lightweight bg and force pur…
yash-pouranik Jun 17, 2026
ce5fb39
fix(onboarding): do not skip collection step on new project and prese…
yash-pouranik Jun 17, 2026
8d2fc2e
fix(onboarding): users collection interference, unique field, and but…
yash-pouranik Jun 17, 2026
264c39b
fix(onboarding): clarify GET request returns empty list in Step 3
yash-pouranik Jun 17, 2026
2885dd3
fix(onboarding): perform POST on api test and explicitly mark firstAp…
yash-pouranik Jun 17, 2026
19a459f
fix(onboarding): revert to GET, always enable launch button, mark fir…
yash-pouranik Jun 17, 2026
3471675
fix(onboarding): auto-satisfy firstApiCall prereqs, allow skip comple…
yash-pouranik Jun 17, 2026
d08c0ae
Potential fix for pull request finding 'CodeQL / Database query built…
yash-pouranik Jun 17, 2026
a1f204f
Potential fix for pull request finding 'CodeQL / Prototype-polluting …
yash-pouranik Jun 17, 2026
2fccd65
feat(dashboard): remove Activation Status, fix secret key revealed st…
yash-pouranik Jun 18, 2026
4b93987
fix(dashboard): remove pk copy, sk reveal once, unused state, and emo…
yash-pouranik Jun 18, 2026
c46730d
fix(tests): add missing loadProjectForAdmin mock, remove verifyEmail …
yash-pouranik Jun 18, 2026
4852554
fix(csrf): persist cookie 24h, auto-refresh and retry on EBADCSRFTOKEN
yash-pouranik Jun 18, 2026
ccf9f0a
fix(analytics): fix docs link to correct external URL
yash-pouranik Jun 18, 2026
01226b4
Merge remote-tracking branch 'origin/main' into feat/onboarding
yash-pouranik Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/consumer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ app.get('/', (_req, res) => {
res.status(200).send('consumer worker running');
});

const port = Number(process.env.PORT) || 3000;
let port = process.env.NODE_ENV === 'production'
? (Number(process.env.PORT) || 3000)
: (Number(process.env.CONSUMER_PORT) || 1237);

if (port < 1 || port > 65535 || isNaN(port)) {
console.warn(`[CONSUMER] Invalid port ${port} detected, defaulting to 1237`);
port = 1237;
}
let worker;
let server;

Expand Down
230 changes: 225 additions & 5 deletions apps/dashboard-api/src/__tests__/auth.controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ jest.mock('@urbackend/common', () => {
}
},
sendOtp: jest.fn().mockResolvedValue(undefined),
normalizeOnboarding: jest.fn((onboarding = {}) => {
const steps = onboarding.steps || {};
return {
completed: Boolean(onboarding.completed),
steps: {
projectCreated: Boolean(steps.projectCreated),
collectionCreated: Boolean(steps.collectionCreated),
firstApiCall: Boolean(steps.firstApiCall),
},
activationAt: onboarding.activationAt || null,
};
}),
// Use real zod shapes so validation logic is exercised.
loginSchema: z.object({
email: z.string().email(),
Expand All @@ -108,6 +120,26 @@ jest.mock('@urbackend/common', () => {
otp: z.string(),
newPassword: z.string().min(6),
}),
updateOnboardingSchema: z.object({
completed: z.boolean().optional(),
steps: z.object({
projectCreated: z.boolean().optional(),
collectionCreated: z.boolean().optional(),
firstApiCall: z.boolean().optional(),
}).strict().optional(),
}).strict().refine(
(data) => data.completed !== undefined || data.steps !== undefined,
{ message: 'At least one onboarding field must be provided.' }
),
updateDeveloperOnboarding: jest.fn().mockResolvedValue({
completed: false,
steps: {
projectCreated: true,
collectionCreated: false,
firstApiCall: false,
},
activationAt: null,
}),
};
});

Expand All @@ -117,7 +149,7 @@ jest.mock('@urbackend/common', () => {

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { Developer, Otp, Project, sendOtp, AppError } = require('@urbackend/common');
const { Developer, Otp, Project, sendOtp, AppError, updateDeveloperOnboarding } = require('@urbackend/common');
const authController = require('../controllers/auth.controller');

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -159,22 +191,43 @@ describe('auth.controller', () => {

// -----------------------------------------------------------------------
describe('register', () => {
test('returns 201 and success message on valid new-user registration', async () => {
test('returns 201 with session payload on valid new-user registration', async () => {
Developer.findOne.mockResolvedValue(null);
bcrypt.genSalt.mockResolvedValue('salt');
bcrypt.hash.mockResolvedValue('hashed_password');
jwt.sign.mockReturnValue('signed_token');

const createdUser = {
_id: 'new_dev_1',
email: 'new@example.com',
isVerified: false,
maxProjects: 1,
password: 'hashed_password',
save: jest.fn().mockResolvedValue(undefined),
};
const mockSave = jest.fn().mockResolvedValue(undefined);
Developer.mockImplementation(() => ({ save: mockSave }));
Developer.mockImplementation((data) => ({ ...createdUser, ...data, save: mockSave }));

const req = makeReq({ email: 'new@example.com', password: 'password123' });
const res = makeRes();

await authController.register(req, res, next);


expect(Developer.findOne).toHaveBeenCalledWith({ email: 'new@example.com' });
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith({ success: true, data: {}, message: 'Registered successfully' });
expect(res.cookie).toHaveBeenCalledWith('accessToken', expect.any(String), expect.any(Object));
expect(res.cookie).toHaveBeenCalledWith('refreshToken', expect.any(String), expect.any(Object));
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
success: true,
data: expect.objectContaining({
redirectTo: '/onboarding',
user: expect.objectContaining({
email: 'new@example.com',
isVerified: false,
}),
}),
}));
});

test('returns 400 when email already exists', async () => {
Expand Down Expand Up @@ -202,14 +255,15 @@ describe('auth.controller', () => {

// -----------------------------------------------------------------------
describe('login', () => {
const mockUser = () => ({
const mockUser = (overrides = {}) => ({
_id: 'dev_id_1',
email: 'test@example.com',
isVerified: true,
maxProjects: 3,
refreshToken: null,
password: 'hashed_password',
save: jest.fn().mockResolvedValue(undefined),
...overrides,
});

test('returns 200 with cookie tokens on valid credentials', async () => {
Expand All @@ -232,6 +286,79 @@ describe('auth.controller', () => {
);
});

test('returns onboarding redirect for authenticated users with incomplete onboarding', async () => {
const user = mockUser();
Developer.findOne.mockReturnValue(Developer.__mockQuery(user));
bcrypt.compare.mockResolvedValue(true);
jwt.sign.mockReturnValue('signed_token');

const req = makeReq({ email: 'test@example.com', password: 'correctpass' });
const res = makeRes();

await authController.login(req, res, next);

expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
redirectTo: '/onboarding',
user: expect.objectContaining({
onboarding: expect.objectContaining({ completed: false }),
}),
}),
}));
});

test('returns onboarding redirect for unverified users', async () => {
const user = mockUser({ isVerified: false });
Developer.findOne.mockReturnValue(Developer.__mockQuery(user));
bcrypt.compare.mockResolvedValue(true);
jwt.sign.mockReturnValue('signed_token');

const req = makeReq({ email: 'test@example.com', password: 'correctpass' });
const res = makeRes();

await authController.login(req, res, next);

expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
redirectTo: '/onboarding',
user: expect.objectContaining({
isVerified: false,
}),
}),
}));
});

test('returns dashboard redirect for authenticated users with completed onboarding', async () => {
const user = mockUser({
onboarding: {
completed: true,
steps: {
projectCreated: true,
collectionCreated: true,
firstApiCall: true,
},
activationAt: new Date('2026-06-08T00:00:00.000Z'),
},
});
Developer.findOne.mockReturnValue(Developer.__mockQuery(user));
bcrypt.compare.mockResolvedValue(true);
jwt.sign.mockReturnValue('signed_token');

const req = makeReq({ email: 'test@example.com', password: 'correctpass' });
const res = makeRes();

await authController.login(req, res, next);

expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
redirectTo: '/dashboard',
user: expect.objectContaining({
onboarding: expect.objectContaining({ completed: true }),
}),
}),
}));
});

test('returns 400 when user is not found', async () => {
Developer.findOne.mockReturnValue(Developer.__mockQuery(null));

Expand Down Expand Up @@ -395,6 +522,37 @@ describe('auth.controller', () => {
);
});

test('returns default onboarding state for users without stored onboarding', async () => {
const mockSelect = jest.fn().mockResolvedValue({
_id: 'dev_id_1',
email: 'test@example.com',
});
Developer.findById.mockReturnValue({ select: mockSelect });

const req = makeReq({}, { _id: 'dev_id_1' });
const res = makeRes();

await authController.getMe(req, res, next);

expect(res.json).toHaveBeenCalledWith({
success: true,
data: {
user: expect.objectContaining({
onboarding: {
completed: false,
steps: {
projectCreated: false,
collectionCreated: false,
firstApiCall: false,
},
activationAt: null,
},
}),
},
message: 'Success',
});
});

test('returns 404 when user does not exist', async () => {
Developer.findById.mockReturnValue({
select: jest.fn().mockResolvedValue(null),
Expand All @@ -411,6 +569,68 @@ describe('auth.controller', () => {
});
});

// -----------------------------------------------------------------------
describe('updateOnboarding', () => {
test('updates only the authenticated developer onboarding state', async () => {
const req = makeReq(
{ completed: false, steps: { projectCreated: true } },
{ _id: 'dev_id_1' }
);
const res = makeRes();

await authController.updateOnboarding(req, res, next);

expect(updateDeveloperOnboarding).toHaveBeenCalledWith('dev_id_1', {
completed: false,
steps: { projectCreated: true },
});
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: {
onboarding: {
completed: false,
steps: {
projectCreated: true,
collectionCreated: false,
firstApiCall: false,
},
activationAt: null,
},
},
message: 'Onboarding updated successfully',
});
});

test('rejects activationAt updates', async () => {
const req = makeReq(
{ activationAt: '2026-06-08T00:00:00.000Z' },
{ _id: 'dev_id_1' }
);
const res = makeRes();

await authController.updateOnboarding(req, res, next);

expect(updateDeveloperOnboarding).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
});

test('rejects malformed nested step objects', async () => {
const req = makeReq(
{ steps: { firstApiCall: 'yes' } },
{ _id: 'dev_id_1' }
);
const res = makeRes();

await authController.updateOnboarding(req, res, next);

expect(updateDeveloperOnboarding).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
});
});

// -----------------------------------------------------------------------
describe('changePassword', () => {
test('returns 200 on successful password change', async () => {
Expand Down
Loading
Loading