Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ async function tryDemo() {
// Activate the demo org before navigating
const orgsResult = await authClient.organization.list()
const orgs = orgsResult.data
if (orgs && orgs.length > 0) {
await authClient.organization.setActive({ organizationId: orgs[0].id })
const firstOrg = orgs?.[0]
if (firstOrg) {
await authClient.organization.setActive({ organizationId: firstOrg.id })
}

// Hard navigation to avoid hydration mismatches between dark landing and light dashboard
Expand Down
5 changes: 4 additions & 1 deletion app/pages/onboarding/create-org.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ const autoSwitched = ref(false)
watch([orgs, isOrgsLoading], async ([orgList, loading]) => {
if (loading || autoSwitched.value || showCreateForm.value) return
if (orgList.length === 1) {
const firstOrg = orgList[0]
if (!firstOrg) return

autoSwitched.value = true
isLoading.value = true
try {
await switchOrg(orgList[0].id)
await switchOrg(firstOrg.id)
}
catch {
isLoading.value = false
Expand Down
1 change: 1 addition & 0 deletions server/api/__sitemap__/urls.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { eq } from 'drizzle-orm'
import { queryCollection } from '@nuxt/content/server'
import { job } from '../../database/schema'

/**
Expand Down
8 changes: 6 additions & 2 deletions server/middleware/demo-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,16 @@ export default defineEventHandler(async (event) => {

// Check if the current session belongs to the demo org
const session = await auth.api.getSession({ headers: event.headers })
if (!session?.session.activeOrganizationId) return
const activeOrganizationId = session
? (session.session as { activeOrganizationId?: string }).activeOrganizationId
: undefined

if (!activeOrganizationId) return

const guardedOrgId = await getDemoOrgId()
if (!guardedOrgId) return

if (session.session.activeOrganizationId === guardedOrgId) {
if (activeOrganizationId === guardedOrgId) {
throw createError({
statusCode: 403,
statusMessage: 'Demo mode — this action is disabled. Deploy your own instance to get full access.',
Expand Down
3 changes: 2 additions & 1 deletion server/plugins/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ export default defineNitroPlugin(async () => {

try {
// pg_try_advisory_lock returns true if lock acquired, false if another process holds it
const [{ locked }] = await db.execute<{ locked: boolean }>(
const lockResult = await db.execute<{ locked: boolean }>(
`SELECT pg_try_advisory_lock(${MIGRATION_LOCK_ID}) as locked`
)
const locked = lockResult[0]?.locked ?? false

if (!locked) {
console.log('[Applirank] Another instance is running migrations, skipping')
Expand Down
70 changes: 53 additions & 17 deletions server/scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ if (!DATABASE_URL) {
}

const DEMO_EMAIL = 'demo@applirank.com'
const DEMO_PASSWORD = 'demo1234'
const DEMO_PASSWORD = process.env.DEMO_PASSWORD ?? 'demo1234'
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEMO_PASSWORD uses nullish coalescing, so an explicitly-set but empty env var (DEMO_PASSWORD="") will be treated as a valid password. That can seed an account with an empty password (or fail later if Better Auth enforces a minimum). Consider trimming and falling back when the value is empty/whitespace (e.g., use a ?.trim() check) so the seed behavior is predictable.

Suggested change
const DEMO_PASSWORD = process.env.DEMO_PASSWORD ?? 'demo1234'
const rawDemoPassword = process.env.DEMO_PASSWORD
const DEMO_PASSWORD = rawDemoPassword && rawDemoPassword.trim() !== '' ? rawDemoPassword : 'demo1234'

Copilot uses AI. Check for mistakes.
const DEMO_ORG_NAME = 'Applirank Demo'
const DEMO_ORG_SLUG = 'applirank-demo'

Expand All @@ -58,8 +58,12 @@ function daysAgo(n: number): Date {
return d
}

function randomItem<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)]
function getArrayItemOrThrow<T>(arr: readonly T[], index: number, context: string): T {
const value = arr[index]
if (value === undefined) {
throw new Error(`Missing ${context} at index ${index}`)
}
return value
}

function generateSlug(title: string, uuid: string): string {
Expand Down Expand Up @@ -246,6 +250,11 @@ const JOB_APPLICATIONS = [JOB_0_APPS, JOB_1_APPS, JOB_2_APPS, JOB_3_APPS, []]

// Sample responses for questions
function generateResponses(jobIndex: number, candidateIndex: number): Record<string, string | string[] | boolean> {
const candidate = CANDIDATES_DATA[candidateIndex]
if (!candidate) {
return {}
}
Comment on lines +253 to +256
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: silently returns {} instead of throwing.

Every other guard in this PR throws on invalid seed data (lines 441, 481, 487), and getArrayItemOrThrow also throws. A missing candidate here indicates a seed configuration bug (out-of-bounds candidateIndex), which should fail loudly rather than silently producing applications without responses.

Suggested fix
   const candidate = CANDIDATES_DATA[candidateIndex]
   if (!candidate) {
-    return {}
+    throw new Error(`Missing candidate data at index ${candidateIndex}`)
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const candidate = CANDIDATES_DATA[candidateIndex]
if (!candidate) {
return {}
}
const candidate = CANDIDATES_DATA[candidateIndex]
if (!candidate) {
throw new Error(`Missing candidate data at index ${candidateIndex}`)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/scripts/seed.ts` around lines 253 - 256, The guard silently returning
{} when a candidate is missing is inconsistent with the rest of the seed script;
replace the early-return with an explicit thrown error so out-of-bounds seed
data fails loudly. Locate the block that reads const candidate =
CANDIDATES_DATA[candidateIndex] and change the handling of !candidate to throw a
descriptive Error (including candidateIndex and CANDIDATES_DATA length) rather
than returning {} so subsequent code that builds applications/responses fails
fast and clearly.


if (jobIndex === 0) {
const years = ['3', '4', '5', '6', '7', '8+']
const frameworks = ['Vue', 'React', 'Svelte', 'Vue', 'React', 'Vue']
Expand All @@ -259,12 +268,16 @@ function generateResponses(jobIndex: number, candidateIndex: number): Record<str
'Created a type-safe API client generator from OpenAPI specs that eliminated an entire class of runtime errors.',
]
const i = candidateIndex % years.length
const year = getArrayItemOrThrow(years, i, 'TypeScript years response')
const framework = getArrayItemOrThrow(frameworks, i, 'framework response')
const problem = getArrayItemOrThrow(problems, i, 'problem response')
const start = getArrayItemOrThrow(starts, i, 'start date response')
return {
'Years of TypeScript experience': years[i],
'Preferred frontend framework': frameworks[i],
'Describe a challenging technical problem you solved recently': problems[i],
'Link to your GitHub profile or portfolio': `https://github.com/${CANDIDATES_DATA[candidateIndex].firstName.toLowerCase()}${CANDIDATES_DATA[candidateIndex].lastName.toLowerCase().replace(/['\s]/g, '')}`,
'When can you start?': starts[i],
'Years of TypeScript experience': year,
'Preferred frontend framework': framework,
'Describe a challenging technical problem you solved recently': problem,
'Link to your GitHub profile or portfolio': `https://github.com/${candidate.firstName.toLowerCase()}${candidate.lastName.toLowerCase().replace(/['\s]/g, '')}`,
'When can you start?': start,
}
}
if (jobIndex === 1) {
Expand All @@ -276,10 +289,12 @@ function generateResponses(jobIndex: number, candidateIndex: number): Record<str
'I believe in rapid prototyping. Quick sketches → Figma prototypes → guerrilla testing → iteration. Speed of learning beats perfection.',
]
const i = candidateIndex % tools.length
const tool = getArrayItemOrThrow(tools, i, 'design tool response')
const process = getArrayItemOrThrow(processes, i, 'design process response')
return {
'Link to your portfolio': `https://dribbble.com/${CANDIDATES_DATA[candidateIndex].firstName.toLowerCase()}${CANDIDATES_DATA[candidateIndex].lastName.toLowerCase().charAt(0)}`,
'Primary design tool': tools[i],
'Walk us through your design process for a recent project': processes[i],
'Link to your portfolio': `https://dribbble.com/${candidate.firstName.toLowerCase()}${candidate.lastName.toLowerCase().charAt(0)}`,
'Primary design tool': tool,
'Walk us through your design process for a recent project': process,
'I have experience with design systems': candidateIndex % 2 === 0,
}
}
Expand All @@ -288,10 +303,13 @@ function generateResponses(jobIndex: number, candidateIndex: number): Record<str
const ciPlatforms = ['GitHub Actions', 'GitLab CI', 'GitHub Actions', 'Jenkins', 'GitHub Actions', 'CircleCI']
const dockerYears = ['3', '4', '5', '6', '2', '7']
const i = candidateIndex % platforms.length
const platformList = getArrayItemOrThrow(platforms, i, 'cloud platform response')
const ciPlatform = getArrayItemOrThrow(ciPlatforms, i, 'CI/CD platform response')
const dockerYear = getArrayItemOrThrow(dockerYears, i, 'docker years response')
return {
'Which cloud platforms have you worked with?': platforms[i],
'Years of Docker experience': dockerYears[i],
'Preferred CI/CD platform': ciPlatforms[i],
'Which cloud platforms have you worked with?': platformList,
'Years of Docker experience': dockerYear,
'Preferred CI/CD platform': ciPlatform,
}
}
return {}
Expand Down Expand Up @@ -418,17 +436,26 @@ async function seed() {

for (let jobIndex = 0; jobIndex < questionSets.length; jobIndex++) {
const questions = questionSets[jobIndex]
const jobId = jobIds[jobIndex]
if (!questions || !jobId) {
throw new Error(`Invalid seed configuration for questions at job index ${jobIndex}`)
}

const questionIds: { questionId: string; label: string }[] = []

for (let qi = 0; qi < questions.length; qi++) {
const q = questions[qi]
if (!q) {
continue
}

const questionId = id()
questionIds.push({ questionId, label: q.label })

await db.insert(schema.jobQuestion).values({
id: questionId,
organizationId: orgId,
jobId: jobIds[jobIndex],
jobId,
type: q.type,
label: q.label,
required: q.required,
Expand All @@ -449,16 +476,25 @@ async function seed() {

for (let jobIndex = 0; jobIndex < JOB_APPLICATIONS.length; jobIndex++) {
const apps = JOB_APPLICATIONS[jobIndex]
const jobId = jobIds[jobIndex]
if (!apps || !jobId) {
throw new Error(`Invalid seed configuration for applications at job index ${jobIndex}`)
}

for (const app of apps) {
const candidateId = candidateIds[app.candidateIndex]
if (!candidateId) {
throw new Error(`Missing candidate ID for candidate index ${app.candidateIndex}`)
}

const applicationId = id()
const createdDaysAgo = 1 + Math.floor(Math.random() * 15)

await db.insert(schema.application).values({
id: applicationId,
organizationId: orgId,
candidateId: candidateIds[app.candidateIndex],
jobId: jobIds[jobIndex],
candidateId,
jobId,
status: app.status,
score: app.score ?? null,
notes: app.notes ?? null,
Expand Down
7 changes: 6 additions & 1 deletion server/utils/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ function getDB(): DB {
export const db: DB = new Proxy({} as DB, {
get(_, prop) {
const instance = getDB()
const value = (instance as Record<string, unknown>)[prop]

if (typeof prop !== 'string') {
return Reflect.get(instance as object, prop)
}

const value = (instance as unknown as Record<string, unknown>)[prop]
// Bind methods so they keep the correct `this` context
return typeof value === 'function' ? value.bind(instance) : value
},
Expand Down
19 changes: 17 additions & 2 deletions server/utils/requireAuth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { H3Event } from 'h3'

type AuthSession = NonNullable<Awaited<ReturnType<typeof auth.api.getSession>>>
type AuthSessionWithActiveOrg = Omit<AuthSession, 'session'> & {
session: AuthSession['session'] & {
activeOrganizationId: string
}
}

/**
* Require an authenticated session with an active organization.
* Throws 401 if not authenticated, 403 if no active organization selected.
Expand All @@ -16,9 +23,17 @@ export async function requireAuth(event: H3Event) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}

if (!session.session.activeOrganizationId) {
const activeOrganizationId = (session.session as { activeOrganizationId?: string }).activeOrganizationId

if (!activeOrganizationId) {
throw createError({ statusCode: 403, statusMessage: 'No active organization' })
}

return session
return {
...session,
session: {
...session.session,
activeOrganizationId,
},
} as AuthSessionWithActiveOrg
}
Loading