Makes AUTH_MODE=team usable end to end. - resolveTeamContext (middleware/ensure-user/team.ts): a session resolves to a membership in the single shared workspace. No per-user fallback org — a signed-in user with no membership is treated as signed out, so the owner can actually remove people. - teamProvisioning.ts: one path that writes user + credential account + member together (hashPassword from better-auth/crypto). Shared by both entry points. - /api/team-setup (raw route, outside auth middleware): GET reports whether an owner is needed; POST creates the first owner + the shared org, then self-disables once any user exists. - /setup route + sign-in redirect: first run sends you to create the owner. - teamUsers server functions (owner/admin-gated): list / create (with temp password) / reset password / remove. Removal drops membership + sessions, keeps the user row for historical attribution. - Settings gains a "Users" tab in team mode (TeamUsers.tsx). - docs/SELF_HOSTING_TEAM_MODE.md: activation runbook (.env, build, first owner). No DB migration — all rows are existing better-auth tables. tsc / oxlint / knip clean. New teamProvisioning.test.ts (4 cases) passes; suite otherwise unchanged (pre-existing samSkills.test.ts CRLF failure only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
import { getAuth, hasSessionAuthConfig } from "@/lib/auth";
|
|
import { AuthRepository } from "@/server/auth/repositories/AuthRepository";
|
|
import { AppError } from "@/server/lib/errors";
|
|
import type { EnsuredUserContext } from "./types";
|
|
|
|
// `team` mode: a Better Auth session resolves to a membership in the single
|
|
// shared workspace. Unlike hosted, there is no per-user fallback organization —
|
|
// a signed-in user with no membership was removed by the owner, so they are
|
|
// treated as signed out rather than handed a fresh personal workspace.
|
|
export async function resolveTeamContext(
|
|
headers: Headers,
|
|
): Promise<EnsuredUserContext> {
|
|
if (!hasSessionAuthConfig()) {
|
|
throw new AppError(
|
|
"AUTH_CONFIG_MISSING",
|
|
"team mode needs BETTER_AUTH_URL and BETTER_AUTH_SECRET (32+ characters) on the deployment.",
|
|
);
|
|
}
|
|
|
|
const session = await getAuth().api.getSession({ headers });
|
|
|
|
if (!session?.user?.id || !session.user.email) {
|
|
throw new AppError("UNAUTHENTICATED");
|
|
}
|
|
|
|
const organizationId = await AuthRepository.findFirstOrganizationIdForUser(
|
|
session.user.id,
|
|
);
|
|
|
|
if (!organizationId) {
|
|
throw new AppError("UNAUTHENTICATED");
|
|
}
|
|
|
|
const membership = await AuthRepository.getMembership(
|
|
session.user.id,
|
|
organizationId,
|
|
);
|
|
|
|
if (!membership) {
|
|
throw new AppError("UNAUTHENTICATED");
|
|
}
|
|
|
|
return {
|
|
userId: session.user.id,
|
|
userEmail: session.user.email,
|
|
// `team` mode has no email-verification step.
|
|
emailVerified: true,
|
|
organizationId,
|
|
role: membership.role,
|
|
};
|
|
}
|