Skip to content

Commit 371445c

Browse files
hotlongCopilot
andcommitted
feat(account): migrate user-settings to better-auth SDK + add missing features
- Extend @objectstack/client SDK with typed wrappers for all better-auth endpoints: user management, sessions, 2FA, linked accounts, organization members/invitations/teams. - Enable teams plugin on better-auth organization config. - Rewrite all account portal pages onto the SDK (profile, security, sessions, two-factor, setup, verify-email, accept-invitation). - Rewrite useOrganizationMembers hooks to drop direct sys_invitation data-API calls; add useOrganizationInvitations, useMyInvitations. - Add new pages: /account/invitations, /account/linked-accounts, /organizations/$orgId/teams. - Upgrade members page: inline role edit, bulk invite, leave org, copy invite link, resend invitation. - Upgrade setup page: invite teammates step with batch invitations. - Upgrade two-factor page: backup codes panel. - Update sidebar navigation and routeTree.gen.ts for new routes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 033923f commit 371445c

18 files changed

Lines changed: 2346 additions & 414 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **Account portal: full better-auth SDK migration** — All user-settings pages (`/account/*`) and the first-run `/setup` wizard now use typed SDK methods from `@objectstack/client` instead of raw `fetch()` calls against `/api/v1/auth/…`. This fixes several silent bugs (wrong parameter names, invented endpoints) and adds missing features:
12+
- **SDK extensions** (`packages/client`): `auth.{updateUser, changePassword, changeEmail, sendVerificationEmail, verifyEmail, deleteUser, bootstrapStatus}`, `auth.sessions.{list, revoke, revokeOthers, revokeAll}`, `auth.twoFactor.{enable, verifyTotp, disable, generateBackupCodes, verifyBackupCode}`, `auth.accounts.{list, unlink, linkSocial}`, `organizations.{removeMember, updateMemberRole, getActiveMember, leave}`, `organizations.invitations.{list, listMine, cancel, accept, reject, resend}`, `organizations.teams.{list, create, update, delete, listMembers, addMember, removeMember}`.
13+
- **Teams plugin enabled** (`plugin-auth`): `teams: { enabled: true }` on better-auth organization plugin.
14+
- **New pages**: `/account/invitations` (pending invitation inbox), `/account/linked-accounts` (OAuth provider link/unlink), `/organizations/$orgId/teams` (team CRUD + membership).
15+
- **Refactored pages**: `/setup` (multi-step with invite teammates), `/account/profile` (avatar upload), `/account/security` (change email + delete account), `/account/sessions` (current-session badge), `/account/two-factor` (backup codes panel), `/organizations/$orgId/members` (inline role edit, bulk invite, leave org, copy invite link, resend).
16+
- **Hooks rewritten**: `useOrganizationMembers` — drops direct `sys_invitation` data-API calls; adds `useOrganizationInvitations`, `useMyInvitations`.
1117
- **Phase-1 RBAC end-to-end enforcement (multi-tenant isolation)** — Every authenticated REST request now arrives at the SecurityPlugin middleware with a populated `ExecutionContext`, so RLS/FLS/CRUD checks actually fire. Three previously-silent context-drop sites were closed: (1) `@objectstack/objectql` `protocol.{find,get,create,update,delete}Data` now forward `request.context` into the engine call options; (2) `@objectstack/rest` `RestServer` gained `resolveExecCtx()` plus an `authServiceProvider` constructor hook (wired in `RestApiPlugin` from `ctx.getService('auth')`) that resolves the better-auth session for both single-kernel and multi-kernel deployments and threads `context` into all five CRUD handlers; (3) `@objectstack/plugin-hono-server` raw `/data/:object` fallback handlers now resolve the same context inline and map `PermissionDeniedError` → HTTP 403. `@objectstack/runtime` `resolveExecutionContext()` wraps plain header objects as Web `Headers` so better-auth's cookie lookup works. New seed link tables `sys_user_permission_set` / `sys_role_permission_set` (in `@objectstack/platform-objects`) plus default permission sets `admin_full_access` / `member_default` / `viewer_readonly`; `member_default` carries a wildcard `object: '*'` RLS policy (`tenant_id = current_user.tenant_id`) that SecurityPlugin rewrites onto the configured `tenantField` (default `organization_id`) and skips for tables that lack the field. Three further fixes landed alongside the wiring work: (a) `@objectstack/objectql` lost its legacy `registerTenantMiddleware` (a hardcoded `where.tenant_id = ctx.tenantId` injection that pre-dated SecurityPlugin, masked RLS bugs in older snapshots, and silently broke any table without a `tenant_id` column); SecurityPlugin is now the sole authority for tenant isolation. (b) The previous abstract-`tenant_id` indirection in `@objectstack/plugin-security` (`SecurityPluginOptions.tenantField` + a regex that rewrote `tenant_id = current_user.tenant_id` onto the configured physical column at compile time) has been **removed**. The placeholder, the physical column, and `RLSUserContext.organization_id` now use the same canonical name end-to-end (`organization_id`), eliminating an entire class of silent-drop bugs caused by greedy rewrites and removing the most confusing piece of the security DX. Schemas with a different physical tenant column should fork the default permission sets — the runtime `ExecutionContext.tenantId` is unchanged. (c) `member_default` and `viewer_readonly` gained explicit per-object overrides for the two global tables that lack `organization_id`: `sys_organization_self` (`id = current_user.organization_id`) and `sys_user_self` (`id = current_user.id`). Verified end-to-end on `pnpm dev:crm`: two users in different organizations each create records and only see their own org's rows on subsequent LISTs across `sys_organization`, `sys_member`, `sys_user`, and the new `sys_user_permission_set` / `sys_role_permission_set` link tables. **Known follow-up:** anonymous REST traffic still bypasses enforcement (SecurityPlugin short-circuits when `userId` is absent) — default-deny tightening, Sharing Rule evaluator, Studio RLS visual editor, per-user×org permission cache, and audit UI / denied-access logging remain queued.
1218
- **`@objectstack/driver-mongodb` — first-class MongoDB driver (`packages/plugins/driver-mongodb`)** — A new built-in driver that implements the full `IDataDriver` contract on top of the official `mongodb@^6` Node.js driver. Highlights: per-collection `id` strategy (16-char nanoid stored as a top-level string field with a unique index; the internal `_id` is **never** exposed to consumers — every `find`/`findOne`/`findStream` uses `projection: { _id: 0 }`); pluggable filter translator (`mongodb-filter.ts`) that maps every ObjectStack operator (`$eq`/`$ne`/`$gt`/`$gte`/`$lt`/`$lte`/`$in`/`$nin`/`$exists`/`$contains`/`$startsWith`/`$endsWith`/`$null`/`$between`/`$not`/`$and`/`$or`/`$nor`) plus the legacy `[field, op, value]` tuple form into a native MongoDB query; aggregation pipeline builder (`mongodb-aggregation.ts`) supporting `count`/`sum`/`avg`/`min`/`max`/`count_distinct`/`string_agg` with `$group + $project` flattening; declarative schema sync that creates collections + unique/lookup/text/compound indexes (`mongodb-schema.ts`); cursor-based async streaming via `findStream`; `bulkCreate`/`bulkUpdate`/`bulkDelete` powered by `bulkWrite`; multi-document transactions (requires a replica set) via `MongoClient.startSession()` + `session.withTransaction()`. Ships with **75 unit tests** (filter, aggregation, full driver) running against `mongodb-memory-server` (`onlyBuiltDependencies` whitelist updated in the root `package.json` so the postinstall is allowed). Plugin entry exports a default `onEnable` hook so it can be registered as `kernel.use(mongodbPlugin, { url })` or instantiated directly via `new MongoDBDriver({ url })` and wrapped in `DriverPlugin`.
1319
- **`OS_DATABASE_DRIVER` / `OS_DATABASE_URL` recognised by the CLI, with URL-scheme inference** — `packages/cli/src/commands/serve.ts` now selects the storage driver from `OS_DATABASE_URL` automatically: `mongodb://` / `mongodb+srv://` → `MongoDBDriver`, `postgres://` / `postgresql://` → `SqlDriver(client:'pg')`, `mysql://` / `mysql2://` → `SqlDriver(client:'mysql2')`, `libsql://` or `https://*.turso.…` → `TursoDriver`, `file:` / `sqlite:` / `:memory:` / `*.db` / `*.sqlite` → `SqlDriver(client:'better-sqlite3')`. `OS_DATABASE_DRIVER` remains an explicit override (`mongodb`/`mongo`/`postgres`/`pg`/`mysql`/`sqlite`/`turso`/`libsql`). The same selection logic is mirrored in `packages/services/service-cloud/src/{artifact-environment-registry,environment-registry,project-kernel-factory}.ts` so cloud-runtime nodes can also pin a project to MongoDB. `apps/objectos`, `packages/cli`, and `packages/services/service-cloud` now declare `@objectstack/driver-mongodb` (and the CLI now also declares `@objectstack/driver-turso`) as workspace dependencies so the dynamic `await import()` resolves.

apps/account/src/components/account-sidebar.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@
2727
import { Link, useLocation } from '@tanstack/react-router';
2828
import {
2929
Building2,
30+
Inbox,
3031
KeyRound,
32+
Link2,
3133
Monitor,
3234
PanelLeft,
3335
Settings,
3436
Shield,
3537
ShieldCheck,
3638
User,
3739
Users,
40+
Users2,
3841
} from 'lucide-react';
3942
import { useObjectTranslation } from '@object-ui/i18n';
4043
import {
@@ -58,6 +61,8 @@ interface NavItem {
5861
| '/account/security'
5962
| '/account/sessions'
6063
| '/account/two-factor'
64+
| '/account/invitations'
65+
| '/account/linked-accounts'
6166
| '/account/oauth-applications'
6267
| '/organizations';
6368
label: string;
@@ -69,6 +74,8 @@ const ACCOUNT_ITEMS: NavItem[] = [
6974
{ to: '/account/security', label: 'security', icon: Shield },
7075
{ to: '/account/sessions', label: 'sessions', icon: Monitor },
7176
{ to: '/account/two-factor', label: 'twoFactor', icon: ShieldCheck },
77+
{ to: '/account/invitations', label: 'invitations', icon: Inbox },
78+
{ to: '/account/linked-accounts', label: 'linkedAccounts', icon: Link2 },
7279
];
7380

7481
export function AccountSidebar() {
@@ -154,6 +161,18 @@ export function AccountSidebar() {
154161
</Link>
155162
</SidebarMenuButton>
156163
</SidebarMenuItem>
164+
<SidebarMenuItem>
165+
<SidebarMenuButton
166+
asChild
167+
isActive={pathname === `/organizations/${activeOrgId}/teams`}
168+
tooltip={t('sidebar.items.teams')}
169+
>
170+
<Link to="/organizations/$orgId/teams" params={{ orgId: activeOrgId }}>
171+
<Users2 className="size-4" />
172+
<span>{t('sidebar.items.teams')}</span>
173+
</Link>
174+
</SidebarMenuButton>
175+
</SidebarMenuItem>
157176
</>
158177
)}
159178
</SidebarMenu>

0 commit comments

Comments
 (0)