feat(onboarding): DB-driven resume — query notifiers/storages, check agent lastContact

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Théo LAGACHE
2026-06-18 15:47:04 +02:00
co-authored by Claude Sonnet 4.6
parent eaf1391601
commit 5ac7d86b6d
+70 -31
View File
@@ -3,19 +3,24 @@ import "server-only";
import { currentUser } from "@/lib/auth/current-user"; import { currentUser } from "@/lib/auth/current-user";
import { getOrganization } from "@/lib/auth/auth"; import { getOrganization } from "@/lib/auth/auth";
import { getOrganizationAgents } from "@/db/services/agent"; import { getOrganizationAgents } from "@/db/services/agent";
import { getOrganizationChannels } from "@/db/services/notification-channel";
import { getOrganizationStorageChannels } from "@/db/services/storage-channel";
import type { AgentWith } from "@/db/schema/08_agent"; import type { AgentWith } from "@/db/schema/08_agent";
import { db } from "@/db"; import { db } from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import type { OnboardingDatabase, OnboardingFlowData, OnboardingMeta } from "@/features/onboarding/onboarding.types"; import type {
OnboardingDatabase,
OnboardingFlowData,
OnboardingMeta,
} from "@/features/onboarding/onboarding.types";
export type ResolvedOnboardingState = export type ResolvedOnboardingState =
| { redirect: "/dashboard/home" } | { redirect: "/dashboard/home" }
| { stepId: string; flowData: Partial<OnboardingFlowData> }; | { stepId: string; flowData: Partial<OnboardingFlowData> };
export async function resolveOnboardingState(): Promise<ResolvedOnboardingState> { export async function resolveOnboardingState(): Promise<ResolvedOnboardingState> {
// Check settings first (fastest exit)
const settings = await db.query.setting.findFirst(); const settings = await db.query.setting.findFirst();
if (settings?.onboarding) { if (settings?.onboarding) {
return { redirect: "/dashboard/home" }; return { redirect: "/dashboard/home" };
@@ -23,17 +28,16 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
const meta: OnboardingMeta = { const meta: OnboardingMeta = {
passkeyEnabled: env.AUTH_PASSKEY_ENABLED === "true", passkeyEnabled: env.AUTH_PASSKEY_ENABLED === "true",
emailPasswordEnabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
hasExistingUsers: false, hasExistingUsers: false,
ssoProviders: [], ssoProviders: [],
defaultUserMode: !!(env.AUTH_DEFAULT_USER && env.AUTH_DEFAULT_PASSWORD), defaultUserMode: !!(env.AUTH_DEFAULT_USER && env.AUTH_DEFAULT_PASSWORD),
resumeStepId: "login", resumeStepId: "login",
}; };
// Check if any users exist
const anyUser = await db.query.user.findFirst(); const anyUser = await db.query.user.findFirst();
meta.hasExistingUsers = !!anyUser; meta.hasExistingUsers = !!anyUser;
// SSO providers from env
if (env.AUTH_OIDC_CLIENT) { if (env.AUTH_OIDC_CLIENT) {
meta.ssoProviders.push({ meta.ssoProviders.push({
id: env.AUTH_OIDC_ID ?? "oidc", id: env.AUTH_OIDC_ID ?? "oidc",
@@ -46,14 +50,45 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
return { stepId: "login", flowData: { meta } }; return { stepId: "login", flowData: { meta } };
} }
// User exists — check org
const org = await getOrganization({}); const org = await getOrganization({});
if (!org) { if (!org) {
meta.resumeStepId = "org-create"; meta.resumeStepId = "org-create";
return { stepId: "org-create", flowData: { meta } }; return { stepId: "org-create", flowData: { meta } };
} }
const orgData = { id: org.id, name: org.name, logoDataUrl: org.logo ?? undefined }; const orgData = { id: org.id, name: org.name };
// Query persisted channels
const notifierChannels = await getOrganizationChannels(org.id);
const storageChannels = await getOrganizationStorageChannels(org.id);
const notifiers = notifierChannels.map((n) => ({
id: n.id,
provider: n.provider,
label: n.provider,
name: n.name,
config: (n.config as Record<string, unknown>) ?? {},
}));
const storages = storageChannels.map((s) => ({
id: s.id,
provider: s.provider,
label: s.provider,
name: s.name,
config: (s.config as Record<string, unknown>) ?? {},
}));
// Resume at notifier if none configured
if (notifiers.length === 0) {
meta.resumeStepId = "notifier";
return { stepId: "notifier", flowData: { meta, org: orgData } };
}
// Resume at storage if none configured
if (storages.length === 0) {
meta.resumeStepId = "storage";
return { stepId: "storage", flowData: { meta, org: orgData, notifiers } };
}
// Check agents // Check agents
const agents = await getOrganizationAgents(org.id); const agents = await getOrganizationAgents(org.id);
@@ -61,59 +96,63 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
meta.resumeStepId = "agent-create"; meta.resumeStepId = "agent-create";
return { return {
stepId: "agent-create", stepId: "agent-create",
flowData: { flowData: { meta, org: orgData, notifiers, storages },
meta,
org: orgData,
},
}; };
} }
const agentData = agents.map((a) => ({ const agentData = agents.map((a) => ({ id: a.id, name: a.name }));
id: a.id,
name: a.name,
}));
const databases = (agents as AgentWith[]).flatMap((a) => const databases = (agents as AgentWith[]).flatMap((a) =>
(a.databases ?? []).map((d) => ({ (a.databases ?? []).map((d) => ({
id: d.id, id: d.id,
name: d.name, name: d.name,
engine: (d.dbms === "postgresql" ? "postgres" : d.dbms) as OnboardingDatabase["engine"], engine: (d.dbms === "postgresql"
})) ? "postgres"
: d.dbms) as OnboardingDatabase["engine"],
})),
); );
// Check agent connection status via direct DB query (no request context needed)
const firstAgent = agents[0];
let agentConnected = false;
if (firstAgent) {
const agentRow = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, firstAgent.id),
columns: { lastContact: true },
});
if (agentRow?.lastContact) {
const lastContact = new Date(agentRow.lastContact);
agentConnected = Date.now() - lastContact.getTime() < 60_000;
}
}
// Check project // Check project
const project = await db.query.project.findFirst({ const project = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.organizationId, org.id), where: eq(drizzleDb.schemas.project.organizationId, org.id),
}); });
if (!project) { if (!project) {
meta.resumeStepId = "agent-key"; // Agent connected but no project → project-create
// Agent not connected → agent-key
const stepId = agentConnected ? "project-create" : "agent-key";
meta.resumeStepId = stepId;
return { return {
stepId: "agent-key", stepId,
flowData: { flowData: { meta, org: orgData, notifiers, storages, agents: agentData, databases },
meta,
org: orgData,
agents: agentData,
databases,
},
}; };
} }
// All done
meta.resumeStepId = "finish"; meta.resumeStepId = "finish";
return { return {
stepId: "finish", stepId: "finish",
flowData: { flowData: {
meta, meta,
org: orgData, org: orgData,
notifiers,
storages,
agents: agentData, agents: agentData,
databases, databases,
project: { project: { id: project.id, name: project.name, description: "", databaseIds: [] },
id: project.id,
name: project.name,
description: "",
databaseIds: [],
},
}, },
}; };
} }