diff --git a/apps/api/src/auth/auth.service.spec.ts b/apps/api/src/auth/auth.service.spec.ts
index 2507abf..451aa7e 100644
--- a/apps/api/src/auth/auth.service.spec.ts
+++ b/apps/api/src/auth/auth.service.spec.ts
@@ -6,15 +6,22 @@ import { SessionService } from './session.service';
import { getQueueToken } from '@nestjs/bullmq';
import { ForbiddenException } from '@nestjs/common';
import { randomBytes, scryptSync } from 'crypto';
+import { signupRequestSchema } from '@repo/contracts';
describe('AuthService', () => {
let service: AuthService;
- let prisma: { user: { findUnique: jest.Mock; create: jest.Mock } };
+ let prisma: {
+ user: { findUnique: jest.Mock; create: jest.Mock; update: jest.Mock };
+ };
let sessions: { createSession: jest.Mock; deleteSession: jest.Mock };
beforeEach(async () => {
prisma = {
- user: { findUnique: jest.fn(), create: jest.fn() },
+ user: {
+ findUnique: jest.fn(),
+ create: jest.fn(),
+ update: jest.fn(),
+ },
};
sessions = { createSession: jest.fn(), deleteSession: jest.fn() };
const module: TestingModule = await Test.createTestingModule({
@@ -45,6 +52,93 @@ describe('AuthService', () => {
expect(service).toBeDefined();
});
+ it('creates a hiring account without requiring organization details', async () => {
+ const signupData = signupRequestSchema.parse({
+ email: 'recruiter@example.com',
+ password: 'Password123!',
+ accountType: 'HIRING',
+ });
+ prisma.user.findUnique.mockResolvedValue(null);
+ prisma.user.create.mockResolvedValue({
+ id: '00000000-0000-4000-8000-000000000001',
+ email: 'recruiter@example.com',
+ accountType: 'HIRING',
+ developerProfile: null,
+ hiringProfile: null,
+ });
+
+ await expect(service.signup(signupData)).resolves.toMatchObject({
+ user: {
+ email: 'recruiter@example.com',
+ role: 'ORG_ADMIN',
+ },
+ });
+
+ expect(prisma.user.create).toHaveBeenCalledWith({
+ data: {
+ email: 'recruiter@example.com',
+ passwordHash: expect.any(String),
+ accountType: 'HIRING',
+ isConfirmed: false,
+ developerProfile: undefined,
+ },
+ include: {
+ developerProfile: true,
+ hiringProfile: true,
+ },
+ });
+ });
+
+ it('creates the hiring profile when onboarding is submitted', async () => {
+ prisma.user.findUnique.mockResolvedValue({
+ id: '00000000-0000-4000-8000-000000000001',
+ email: 'recruiter@example.com',
+ accountType: 'HIRING',
+ isConfirmed: true,
+ hasSeenDashboardTour: false,
+ developerProfile: null,
+ hiringProfile: null,
+ });
+ prisma.user.update.mockResolvedValue({
+ id: '00000000-0000-4000-8000-000000000001',
+ email: 'recruiter@example.com',
+ accountType: 'HIRING',
+ isConfirmed: true,
+ hasSeenDashboardTour: false,
+ developerProfile: null,
+ hiringProfile: {
+ id: '00000000-0000-4000-8000-000000000002',
+ organizationName: 'Acme Inc.',
+ organizationType: 'COMPANY',
+ jobTitle: 'Talent Lead',
+ linkedinUrl: null,
+ organizationWebsiteUrl: null,
+ },
+ });
+
+ await service.updateProfile('00000000-0000-4000-8000-000000000001', {
+ organizationName: 'Acme Inc.',
+ organizationType: 'COMPANY',
+ jobTitle: 'Talent Lead',
+ });
+
+ expect(prisma.user.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: {
+ hiringProfile: {
+ create: {
+ organizationName: 'Acme Inc.',
+ organizationType: 'COMPANY',
+ jobTitle: 'Talent Lead',
+ linkedinUrl: undefined,
+ organizationWebsiteUrl: undefined,
+ },
+ },
+ },
+ }),
+ );
+ });
+
it('does not create a session for a suspended account with valid credentials', async () => {
const password = 'Password123!';
const salt = randomBytes(16).toString('hex');
diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts
index c9f6512..a32f292 100644
--- a/apps/api/src/auth/auth.service.ts
+++ b/apps/api/src/auth/auth.service.ts
@@ -1,6 +1,7 @@
import {
Injectable,
Logger,
+ BadRequestException,
NotFoundException,
ConflictException,
ForbiddenException,
@@ -128,15 +129,6 @@ export class AuthService {
},
}
: undefined,
- hiringProfile:
- data.accountType === 'HIRING'
- ? {
- create: {
- organizationName: data.organizationName!,
- organizationType: data.organizationType!,
- },
- }
- : undefined,
},
include: {
developerProfile: true,
@@ -529,19 +521,33 @@ export class AuthService {
personalWebsiteUrl?: string | null;
};
};
- hiringProfile?: {
- update: {
- organizationName?: string;
- organizationType?:
- | 'COMPANY'
- | 'AGENCY'
- | 'INDIVIDUAL'
- | 'FREELANCE_CLIENT';
- jobTitle?: string | null;
- linkedinUrl?: string | null;
- organizationWebsiteUrl?: string | null;
- };
- };
+ hiringProfile?:
+ | {
+ update: {
+ organizationName?: string;
+ organizationType?:
+ | 'COMPANY'
+ | 'AGENCY'
+ | 'INDIVIDUAL'
+ | 'FREELANCE_CLIENT';
+ jobTitle?: string | null;
+ linkedinUrl?: string | null;
+ organizationWebsiteUrl?: string | null;
+ };
+ }
+ | {
+ create: {
+ organizationName: string;
+ organizationType:
+ | 'COMPANY'
+ | 'AGENCY'
+ | 'INDIVIDUAL'
+ | 'FREELANCE_CLIENT';
+ jobTitle?: string | null;
+ linkedinUrl?: string | null;
+ organizationWebsiteUrl?: string | null;
+ };
+ };
} = {};
if (data.hasSeenDashboardTour !== undefined) {
@@ -566,15 +572,33 @@ export class AuthService {
},
};
} else if (user.accountType === 'HIRING') {
- updatePayload.hiringProfile = {
- update: {
- organizationName: data.organizationName,
- organizationType: data.organizationType,
- jobTitle: data.jobTitle,
- linkedinUrl: data.linkedinUrl,
- organizationWebsiteUrl: data.organizationWebsiteUrl,
- },
+ const hiringProfileData = {
+ organizationName: data.organizationName,
+ organizationType: data.organizationType,
+ jobTitle: data.jobTitle,
+ linkedinUrl: data.linkedinUrl,
+ organizationWebsiteUrl: data.organizationWebsiteUrl,
};
+
+ if (user.hiringProfile) {
+ updatePayload.hiringProfile = {
+ update: hiringProfileData,
+ };
+ } else if (data.organizationName && data.organizationType) {
+ updatePayload.hiringProfile = {
+ create: {
+ organizationName: data.organizationName,
+ organizationType: data.organizationType,
+ jobTitle: data.jobTitle,
+ linkedinUrl: data.linkedinUrl,
+ organizationWebsiteUrl: data.organizationWebsiteUrl,
+ },
+ };
+ } else {
+ throw new BadRequestException(
+ 'Organization name and type are required to create a hiring profile',
+ );
+ }
}
const updatedUser = await this.prisma.user.update({
diff --git a/apps/web/app/(authenticated)/layout.tsx b/apps/web/app/(authenticated)/layout.tsx
index 13dfa06..ea66ed8 100644
--- a/apps/web/app/(authenticated)/layout.tsx
+++ b/apps/web/app/(authenticated)/layout.tsx
@@ -1,13 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
-import { useRouter } from 'next/navigation';
+import { usePathname, useRouter } from 'next/navigation';
import { useUser } from '@/hooks/use-auth';
import { isProfileComplete } from '@/lib/profile';
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
import { TopNavbar } from '@/components/top-navbar';
-import { DashboardTour } from '@/components/dashboard-tour'; // <-- Import it here
+import { DashboardTour } from '@/components/dashboard-tour';
import { Loader2 } from 'lucide-react';
export default function DashboardLayout({
@@ -17,16 +17,34 @@ export default function DashboardLayout({
}) {
const { user, isLoading } = useUser({ redirectOnUnauthenticated: true });
const router = useRouter();
+ const pathname = usePathname();
const [isRedirecting, setIsRedirecting] = useState(false);
useEffect(() => {
- if (!isLoading && user) {
- if (!isProfileComplete(user)) {
- setIsRedirecting(true);
- router.replace('/onboarding');
- }
+ if (isLoading || !user) return;
+
+ if (!isProfileComplete(user)) {
+ setIsRedirecting(true);
+ router.replace('/onboarding');
+ return;
}
- }, [user, isLoading, router]);
+
+ const hasSeenTourLocally =
+ localStorage.getItem(`hasSeenDashboardTour-${user.id}`) === 'true';
+ const shouldStartTourOnDashboard =
+ user.accountType !== 'SUPER_ADMIN' &&
+ !user.hasSeenDashboardTour &&
+ !hasSeenTourLocally &&
+ pathname !== '/dashboard';
+
+ if (shouldStartTourOnDashboard) {
+ setIsRedirecting(true);
+ router.replace('/dashboard');
+ return;
+ }
+
+ setIsRedirecting(false);
+ }, [user, isLoading, pathname, router]);
if (isLoading || isRedirecting || (user && !isProfileComplete(user))) {
return (
@@ -46,8 +64,7 @@ export default function DashboardLayout({
- {/* Render the Tour Overlay */}
-
@@ -25,7 +17,7 @@ function SettingsContent() { ); } - if (isLoading || !user || user.accountType === 'SUPER_ADMIN') { + if (isLoading || !user) { return (
- {errors.organizationName.message} -
- )} -- {errors.organizationType.message} -
- )} -