mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 00:06:42 +02:00
feat(spaces): move access tier from user to space
Migration from user-level tier to Space-level tier, following the
Spaces foundation plan. User-visible effect: the tier that gates
module access now belongs to the active Space, not the user account.
Personal Spaces inherit the user's old tier on signup so nothing
downgrades.
shared-types:
- New SpaceTier type ('guest' | 'public' | 'beta' | 'alpha' | 'founder').
- New spaceTierMeets(actual, required) helper.
- SpaceMetadata gains an optional `tier` field.
mana-auth:
- createPersonalSpaceFor reads user.accessTier and stamps it into the
personal Space's metadata.tier. A founder-tier user setting up their
first Space keeps founder access in that Space.
- databaseHooks.user.create.after now forwards accessTier into the
personal-space creator.
apps/web (scope layer):
- ActiveSpace gains a required `tier: SpaceTier`; rawToActiveSpace
reads it from organization.metadata, defaulting to 'public' if
missing or invalid.
- New getEffectiveTier(userFallback) helper resolves the tier to use
for gating: prefers the active Space's tier, falls back to the
caller-supplied user tier during the boot window.
apps/web ((app) layout):
- `effectiveTier` $derived replaces every authStore.user?.tier reference
in the layout's access-gating logic (appItems, routeBlocked,
routeTierLabels). AuthGate deeper in the UI keeps using user.tier as
its own fallback — the tier move is additive, not destructive.
What this does NOT do yet:
- The user.accessTier column still exists and is still the initial
source for personal-space tier. Removing it is a later cleanup once
every code path reads through the Space primitive.
- No admin API for setting tier on a Space (PUT /api/v1/admin/spaces/
:id/tier). Follow-up when admin tooling needs it — today admins still
set user.accessTier, which flows to the personal space on next
signup.
Resolves the MANA_APPS-tier-patch workaround memory: future sessions
can adjust tier per Space instead of per User.
0 errors across 7151 files. 10/10 scope tests pass.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
88e3adb9d3
commit
79a6da3e2e
8 changed files with 117 additions and 15 deletions
|
|
@ -226,6 +226,7 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
accessTier: (user as { accessTier?: string | null }).accessTier,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { isSpaceTier, type SpaceTier } from '@mana/shared-types';
|
||||
import { organizations, members } from '../db/schema/organizations';
|
||||
import type { Database } from '../db/connection';
|
||||
import { buildSpaceMetadata } from './metadata';
|
||||
|
|
@ -111,7 +112,7 @@ export function dbSlugTaken(db: Database): SlugTakenLookup {
|
|||
*/
|
||||
export async function createPersonalSpaceFor(
|
||||
db: Database,
|
||||
user: { id: string; email: string; name?: string | null }
|
||||
user: { id: string; email: string; name?: string | null; accessTier?: string | null }
|
||||
): Promise<{ organizationId: string; slug: string; created: boolean }> {
|
||||
// Idempotency guard — check for existing personal space via member join.
|
||||
const existing = await db
|
||||
|
|
@ -133,12 +134,19 @@ export async function createPersonalSpaceFor(
|
|||
const memberId = nanoid();
|
||||
const displayName = user.name?.trim() || user.email.split('@', 1)[0] || 'Personal';
|
||||
|
||||
// Carry the user's existing access tier onto the personal Space so
|
||||
// the user→space tier migration doesn't downgrade anyone. A founder
|
||||
// account setting up their first space stays at founder in that
|
||||
// space. Invalid or missing values default to 'public' — matches the
|
||||
// Better Auth user.accessTier default.
|
||||
const inheritedTier: SpaceTier = isSpaceTier(user.accessTier) ? user.accessTier : 'public';
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(organizations).values({
|
||||
id: orgId,
|
||||
name: displayName,
|
||||
slug,
|
||||
metadata: buildSpaceMetadata('personal'),
|
||||
metadata: buildSpaceMetadata('personal', { tier: inheritedTier }),
|
||||
logo: null,
|
||||
});
|
||||
await tx.insert(members).values({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue