auth: route tenant-less sign-in to onboarding instead of erroring out

Previously, signing in with a kind toggle when the account had no
tenant_settings row in the lab database returned 'Bu hesap için kayıt
bulunamadı'. For a freshly-imported isletmem user that has never opened
DLS, the right behaviour is onboarding — not a dead end.

resolveTenantOnLogin now distinguishes three states:
  - no_tenants  → redirect /onboarding?kind=<pill>  (session stays)
  - mismatch    → real error naming the existing kind, session rolled back
  - matched     → existing tenant set as active, /dashboard

Onboarding page accepts ?kind= and the workspace form pre-selects it, so
the user keeps their login pill choice without re-picking. Also fixed the
'teams.total > 0' redirect — it now requires a tenant_settings row before
sending users off to /dashboard, otherwise users with cross-app teams but
no DLS workspace would bounce.
This commit is contained in:
kovakmedya
2026-05-21 19:39:29 +03:00
parent 9ea35e88cf
commit 7fb8288f79
3 changed files with 92 additions and 28 deletions
+58 -23
View File
@@ -53,23 +53,33 @@ async function setActiveTenantCookie(tenantId: string) {
});
}
async function pickTenantIdByKind(userId: string, kind: TenantKind): Promise<string | null> {
type LoginResolution =
| { state: "no_tenants" }
| { state: "mismatch"; existingKinds: TenantKind[] }
| { state: "matched"; tenantId: string };
async function resolveTenantOnLogin(
userId: string,
kind: TenantKind,
): Promise<LoginResolution> {
const { users, tablesDB } = createAdminClient();
const memberships = await users.listMemberships(userId);
const memberships = await users.listMemberships({ userId });
const teamIds = memberships.memberships.map((m) => m.teamId);
if (teamIds.length === 0) return null;
if (teamIds.length === 0) return { state: "no_tenants" };
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings,
queries: [
Query.equal("tenantId", teamIds),
Query.equal("kind", kind),
Query.limit(1),
],
queries: [Query.equal("tenantId", teamIds), Query.limit(50)],
});
const row = result.rows[0] as unknown as TenantSettings | undefined;
return row?.tenantId ?? null;
const rows = result.rows as unknown as TenantSettings[];
if (rows.length === 0) return { state: "no_tenants" };
const match = rows.find((r) => r.kind === kind);
if (match) return { state: "matched", tenantId: match.tenantId };
const existingKinds = Array.from(new Set(rows.map((r) => r.kind)));
return { state: "mismatch", existingKinds };
}
export async function signInAction(_prev: AuthState, formData: FormData): Promise<AuthState> {
@@ -98,37 +108,62 @@ export async function signInAction(_prev: AuthState, formData: FormData): Promis
}
// Invite flow short-circuits the kind check — invite code drives team membership
if (!inviteCode && kind && sessionUserId) {
const matchedTenantId = await pickTenantIdByKind(sessionUserId, kind);
if (!matchedTenantId) {
// Roll back session: user has no tenant of the requested kind
if (inviteCode) {
redirect(`/d/${inviteCode}`);
}
if (kind && sessionUserId) {
let resolution: LoginResolution;
try {
resolution = await resolveTenantOnLogin(sessionUserId, kind);
} catch (e) {
console.error("[signInAction] resolveTenantOnLogin", e);
// Tenant resolution failed: keep the session, send user to onboarding
// so they can recover instead of getting locked out.
redirect("/onboarding");
}
if (resolution.state === "no_tenants") {
// Authenticated user with no workspace yet — carry the kind into onboarding
redirect(`/onboarding?kind=${kind}`);
}
if (resolution.state === "mismatch") {
try {
const { users } = createAdminClient();
if (sessionId) await users.deleteSession(sessionUserId, sessionId);
if (sessionId) await users.deleteSession({ userId: sessionUserId, sessionId });
} catch {
/* best-effort */
}
(await cookies()).delete(APPWRITE_SESSION_COOKIE);
const label = kind === "lab" ? "laboratuvar" : "klinik";
const requested = kind === "lab" ? "laboratuvar" : "klinik";
const existingLabel = resolution.existingKinds
.map((k) => (k === "lab" ? "laboratuvar" : "klinik"))
.join(" / ");
return {
ok: false,
error: `Bu hesap için ${label} kaydı bulunamadı. Diğer hesap türünü seçin veya yeni çalışma alanı oluşturun.`,
error: `Bu hesap ${existingLabel} olarak kayıtlı. ${requested} sekmesinden değil, ${existingLabel} sekmesinden giriş yapın.`,
};
}
await setActiveTenantCookie(matchedTenantId);
// matched
await setActiveTenantCookie(resolution.tenantId);
try {
const { users } = createAdminClient();
const user = await users.get(sessionUserId);
await users.updatePrefs(sessionUserId, {
...(user.prefs ?? {}),
activeTenant: matchedTenantId,
const user = await users.get({ userId: sessionUserId });
await users.updatePrefs({
userId: sessionUserId,
prefs: {
...(user.prefs ?? {}),
activeTenant: resolution.tenantId,
},
});
} catch {
/* best-effort */
}
}
redirect(inviteCode ? `/d/${inviteCode}` : "/dashboard");
redirect("/dashboard");
}
export async function signUpAction(_prev: AuthState, formData: FormData): Promise<AuthState> {