metatroncubeswdev acc35c055c
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
Phase 3: team-mode owner setup, user management, shared workspace
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>
2026-09-09 08:13:01 -04:00

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,
};
}