From 5ad5b3d313af9113fb8e024d590087ef151b71c2 Mon Sep 17 00:00:00 2001 From: Selem Delul Date: Fri, 10 Oct 2025 14:05:30 -0400 Subject: [PATCH 1/5] feat: add shareWithUser endpoint --- core/protocols/dashboard/msg-is.ts | 29 ++-- core/protocols/dashboard/msg-types.ts | 19 +++ dashboard/backend/api.ts | 184 +++++++++++++++++++++----- dashboard/backend/create-handler.ts | 12 +- 4 files changed, 193 insertions(+), 51 deletions(-) diff --git a/core/protocols/dashboard/msg-is.ts b/core/protocols/dashboard/msg-is.ts index 12da46b2a..e08487cbb 100644 --- a/core/protocols/dashboard/msg-is.ts +++ b/core/protocols/dashboard/msg-is.ts @@ -1,23 +1,24 @@ import { - ReqDeleteTenant, - ReqUpdateTenant, + ReqCloudSessionToken, + ReqCreateLedger, ReqCreateTenant, ReqDeleteInvite, - ReqListInvites, - ReqInviteUser, - ReqFindUser, - ReqRedeemInvite, + ReqDeleteLedger, + ReqDeleteTenant, ReqEnsureUser, + ReqExtendToken, + ReqFindUser, + ReqInviteUser, + ReqListInvites, + ReqListLedgersByUser, ReqListTenantsByUser, - ReqUpdateUserTenant, - ReqCloudSessionToken, + ReqRedeemInvite, + ReqShareWithUser, ReqTokenByResultId, - ReqListLedgersByUser, - ReqCreateLedger, ReqUpdateLedger, - ReqDeleteLedger, + ReqUpdateTenant, + ReqUpdateUserTenant, ResTokenByResultId, - ReqExtendToken, } from "./msg-types.js"; interface FPApiMsgInterface { @@ -40,6 +41,7 @@ interface FPApiMsgInterface { isUpdateLedger(jso: unknown): jso is ReqUpdateLedger; isDeleteLedger(jso: unknown): jso is ReqDeleteLedger; isReqExtendToken(jso: unknown): jso is ReqExtendToken; + isReqShareWithUser(jso: unknown): jso is ReqShareWithUser; } function hasType(jso: unknown, t: string): jso is { type: string } { @@ -103,4 +105,7 @@ export class FAPIMsgImpl implements FPApiMsgInterface { isReqExtendToken(jso: unknown): jso is ReqExtendToken { return hasType(jso, "reqExtendToken"); } + isReqShareWithUser(jso: unknown): jso is ReqShareWithUser { + return hasType(jso, "reqShareWithUser"); + } } diff --git a/core/protocols/dashboard/msg-types.ts b/core/protocols/dashboard/msg-types.ts index 75d039dd5..6947ce50e 100644 --- a/core/protocols/dashboard/msg-types.ts +++ b/core/protocols/dashboard/msg-types.ts @@ -404,6 +404,25 @@ export interface ResDeleteLedger { readonly type: "resDeleteLedger"; } +export interface ReqShareWithUser { + readonly type: "reqShareWithUser"; + readonly auth: AuthType; + readonly email: string; + readonly role?: Role; + readonly right?: ReadWrite; +} + +export interface ResShareWithUser { + readonly type: "resShareWithUser"; + readonly success: boolean; + readonly message: string; + readonly ledgerId: string; + readonly userId: string; + readonly email: string; + readonly role: Role; + readonly right: ReadWrite; +} + export interface ReqCloudSessionToken { readonly type: "reqCloudSessionToken"; readonly auth: AuthType; diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index b6a11cd4c..f7aad533b 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -1,12 +1,13 @@ import { Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core"; -import { gte, and, eq, gt, inArray, lt, ne, or } from "drizzle-orm/sql/expressions"; +import { and, eq, gt, gte, inArray, lt, ne, or } from "drizzle-orm/sql/expressions"; // import type { LibSQLDatabase } from "drizzle-orm/libsql"; -import { jwtVerify } from "jose"; import { AuthType, ClerkClaim, ClerkVerifyAuth, + FAPIMsgImpl, + FPApiParameters, InCreateTenantParams, InviteTicket, InvitedParams, @@ -26,6 +27,7 @@ import { ReqListLedgersByUser, ReqListTenantsByUser, ReqRedeemInvite, + ReqShareWithUser, ReqTokenByResultId, ReqUpdateLedger, ReqUpdateTenant, @@ -44,6 +46,7 @@ import { ResListLedgersByUser, ResListTenantsByUser, ResRedeemInvite, + ResShareWithUser, ResTokenByResultId, ResUpdateLedger, ResUpdateTenant, @@ -52,19 +55,18 @@ import { User, UserStatus, VerifiedAuth, - FAPIMsgImpl, - FPApiParameters, } from "@fireproof/core-protocols-dashboard"; +import { sts } from "@fireproof/core-runtime"; +import { FPCloudClaim, ReadWrite, Role, toReadWrite, toRole } from "@fireproof/core-types-protocols-cloud"; +import { jwtVerify } from "jose"; +import { FPTokenContext, createFPToken, getFPTokenContext } from "./create-fp-token.js"; +import { DashSqlite } from "./create-handler.js"; import { prepareInviteTicket, sqlInviteTickets, sqlToInviteTickets } from "./invites.js"; import { sqlLedgerUsers, sqlLedgers, sqlToLedgers } from "./ledgers.js"; import { queryCondition, queryEmail, queryNick, toBoolean, toUndef } from "./sql-helper.js"; import { sqlTenantUsers, sqlTenants } from "./tenants.js"; import { sqlTokenByResultId } from "./token-by-result-id.js"; import { UserNotFoundError, getUser, isUserNotFound, queryUser, sqlUsers, upsetUserByProvider } from "./users.js"; -import { createFPToken, FPTokenContext, getFPTokenContext } from "./create-fp-token.js"; -import { Role, ReadWrite, toRole, toReadWrite, FPCloudClaim } from "@fireproof/core-types-protocols-cloud"; -import { sts } from "@fireproof/core-runtime"; -import { DashSqlite } from "./create-handler.js"; import { getTableColumns } from "drizzle-orm/utils"; function sqlToOutTenantParams(sql: typeof sqlTenants.$inferSelect): OutTenantParams { @@ -112,6 +114,7 @@ export interface FPApiInterface { listLedgersByUser(req: ReqListLedgersByUser): Promise>; updateLedger(req: ReqUpdateLedger): Promise>; deleteLedger(req: ReqDeleteLedger): Promise>; + shareWithUser(req: ReqShareWithUser): Promise>; // listLedgersByTenant(req: ReqListLedgerByTenant): Promise @@ -1778,6 +1781,109 @@ export class FPApiSQL implements FPApiInterface { ledgerId: req.ledger.ledgerId, }); } + + async shareWithUser(req: ReqShareWithUser, ictx: Partial = {}): Promise> { + // 1. Get JWT context and verify token + const rCtx = await getFPTokenContext(this.sthis, ictx); + if (rCtx.isErr()) { + return Result.Err(rCtx.Err()); + } + const ctx = rCtx.Ok(); + + const rPayload = await this.verifyFPToken(req.auth.token, ctx); + if (rPayload.isErr()) { + return Result.Err(rPayload.Err()); + } + const payload = rPayload.Ok(); + + // 2. Extract user ID and ledger ID from token + if (!payload.userId) { + return Result.Err("No user ID in token"); + } + + if (!payload.selected?.ledger) { + return Result.Err("No ledger selected in token"); + } + + const userId = payload.userId; + const ledgerId = payload.selected.ledger; + + // 3. Check if user is admin of the ledger + if (!(await this.isAdminOfLedger(userId, ledgerId))) { + return Result.Err("Not authorized to share this ledger. Admin access required."); + } + + // 4. Get ledger details + const ledger = await this.db + .select() + .from(sqlLedgers) + .where(and(eq(sqlLedgers.ledgerId, ledgerId), eq(sqlLedgers.status, "active"))) + .get(); + + if (!ledger) { + return Result.Err("Ledger not found or inactive"); + } + + // 5. Find user by email + const rUser = await queryUser(this.db, { byString: req.email }); + if (rUser.isErr()) { + return Result.Err(rUser.Err()); + } + + const users = rUser.Ok(); + if (users.length === 0) { + return Result.Err(`User with email ${req.email} not found. User must sign up first.`); + } + + if (users.length > 1) { + return Result.Err(`Multiple users found for email ${req.email}`); + } + + const targetUser = users[0]; + + // Prevent self-sharing + if (targetUser.userId === userId) { + return Result.Err("Cannot share ledger with yourself"); + } + + // 6. Add user to tenant first + const rAddUserToTenant = await this.addUserToTenant(this.db, { + userName: req.email, + tenantId: ledger.tenantId, + userId: targetUser.userId, + role: "member", + }); + + if (rAddUserToTenant.isErr()) { + return Result.Err(rAddUserToTenant.Err()); + } + + // 7. Add user to ledger + const rAddUser = await this.addUserToLedger(this.db, { + userName: req.email, + ledgerId: ledgerId, + tenantId: ledger.tenantId, + userId: targetUser.userId, + role: req.role || "member", + right: req.right || "read", + }); + + if (rAddUser.isErr()) { + return Result.Err(rAddUser.Err()); + } + + return Result.Ok({ + type: "resShareWithUser", + success: true, + message: `Successfully shared ledger with ${req.email}`, + ledgerId: ledgerId, + userId: targetUser.userId, + email: req.email, + role: req.role || "member", + right: req.right || "write", + }); + } + async listLedgersByUser(req: ReqListLedgersByUser): Promise> { const rAuth = await this.activeUser(req); if (rAuth.isErr()) { @@ -1961,21 +2067,10 @@ export class FPApiSQL implements FPApiInterface { }); } - /** - * Extract token from request, validate it, and extend expiry by 1 day - */ - async extendToken(req: ReqExtendToken, ictx: Partial = {}): Promise> { - const rCtx = await getFPTokenContext(this.sthis, ictx); - if (rCtx.isErr()) { - return Result.Err(rCtx.Err()); - } - const ctx = rCtx.Ok(); + private async verifyFPToken(token: string, ctx: FPTokenContext): Promise> { try { - // Get the public key for verification const pubKey = await sts.env2jwk(ctx.publicToken, "ES256"); - - // Verify the token - const verifyResult = await jwtVerify(req.token, pubKey, { + const verifyResult = await jwtVerify(token, pubKey, { issuer: ctx.issuer, audience: ctx.audience, }); @@ -1986,22 +2081,41 @@ export class FPApiSQL implements FPApiInterface { if (!payload.exp || payload.exp * 1000 <= now) { return Result.Err("Token is expired"); } - // Create new token with extended expiry using the private key - // JWT expects expiration time in seconds, not milliseconds - const newToken = await createFPToken( - { - ...ctx, - validFor: ctx.extendValidFor, - }, - payload, - ); - return Result.Ok({ - type: "resExtendToken", - token: newToken, - }); + + return Result.Ok(payload); } catch (error) { - return Result.Err(`Token validation failed: ${error instanceof Error ? error.message : String(error)}`); + return Result.Err(`Token verification failed: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** + * Extract token from request, validate it, and extend expiry by 1 day + */ + async extendToken(req: ReqExtendToken, ictx: Partial = {}): Promise> { + const rCtx = await getFPTokenContext(this.sthis, ictx); + if (rCtx.isErr()) { + return Result.Err(rCtx.Err()); + } + const ctx = rCtx.Ok(); + + const rPayload = await this.verifyFPToken(req.token, ctx); + if (rPayload.isErr()) { + return Result.Err(rPayload.Err()); } + + // Create new token with extended expiry + const newToken = await createFPToken( + { + ...ctx, + validFor: ctx.extendValidFor, + }, + rPayload.Ok(), + ); + + return Result.Ok({ + type: "resExtendToken", + token: newToken, + }); } } diff --git a/dashboard/backend/create-handler.ts b/dashboard/backend/create-handler.ts index 872da916e..768f4633b 100644 --- a/dashboard/backend/create-handler.ts +++ b/dashboard/backend/create-handler.ts @@ -3,12 +3,12 @@ import { CoercedHeadersInit, HttpHeader, Lazy, LoggerImpl, Result, exception2Res import { verifyToken } from "@clerk/backend"; import { verifyJwt } from "@clerk/backend/jwt"; import { SuperThis, SuperThisOpts } from "@fireproof/core"; -import { FPAPIMsg, FPApiSQL, FPApiToken } from "./api.js"; -import type { Env } from "./cf-serve.js"; import { VerifiedAuth } from "@fireproof/core-protocols-dashboard"; -import { ensureSuperThis, ensureLogger, coerceInt } from "@fireproof/core-runtime"; -import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"; +import { coerceInt, ensureLogger, ensureSuperThis } from "@fireproof/core-runtime"; import { ResultSet } from "@libsql/client"; +import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"; +import { FPAPIMsg, FPApiSQL, FPApiToken } from "./api.js"; +import type { Env } from "./cf-serve.js"; import { getCloudPubkeyFromEnv } from "./get-cloud-pubkey-from-env.js"; // import { jwtVerify } from "jose/jwt/verify"; // import { JWK } from "jose"; @@ -301,6 +301,10 @@ export async function createHandler(db: T, env: Record Date: Fri, 10 Oct 2025 20:48:53 -0400 Subject: [PATCH 2/5] chore: remove CORS headers from responses as it is handled outside # Conflicts: # dashboard/backend/cf-serve.ts # dashboard/backend/create-handler.ts --- dashboard/backend/cf-serve.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index e8f651c49..35f611bca 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -2,6 +2,9 @@ import { drizzle } from "drizzle-orm/d1"; import { D1Database, Fetcher, Request as CFRequest, Response as CFResponse } from "@cloudflare/workers-types"; import { DefaultHttpHeaders, createHandler } from "./create-handler.js"; import { URI } from "@adviser/cement"; +import { Request as CFRequest, Response as CFResponse, D1Database, Fetcher } from "@cloudflare/workers-types"; +import { drizzle } from "drizzle-orm/d1"; +import { CORS, createHandler } from "./create-handler.js"; import { resWellKnownJwks } from "./well-known-jwks.js"; export interface Env { From e6f2752aaebf8bd566b42dc1c2bdbdfe90be3a3e Mon Sep 17 00:00:00 2001 From: Selem Delul Date: Fri, 31 Oct 2025 14:15:33 -0400 Subject: [PATCH 3/5] chore: rebase with main. share only at ledger level. --- dashboard/backend/api.ts | 24 +++----- dashboard/backend/cf-serve.ts | 3 - dashboard/src/pages/cloud/api/token-auto.tsx | 61 +++++++++++++------- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index f7aad533b..42f46dbd2 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -1846,19 +1846,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err("Cannot share ledger with yourself"); } - // 6. Add user to tenant first - const rAddUserToTenant = await this.addUserToTenant(this.db, { - userName: req.email, - tenantId: ledger.tenantId, - userId: targetUser.userId, - role: "member", - }); - - if (rAddUserToTenant.isErr()) { - return Result.Err(rAddUserToTenant.Err()); - } - - // 7. Add user to ledger + // 6. Add user to ledger (no need to add to tenant for sharing) const rAddUser = await this.addUserToLedger(this.db, { userName: req.email, ledgerId: ledgerId, @@ -1872,15 +1860,17 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAddUser.Err()); } + const addedUser = rAddUser.Ok(); + return Result.Ok({ type: "resShareWithUser", success: true, message: `Successfully shared ledger with ${req.email}`, - ledgerId: ledgerId, - userId: targetUser.userId, + ledgerId: addedUser.ledgerId, + userId: addedUser.userId, email: req.email, - role: req.role || "member", - right: req.right || "write", + role: addedUser.role, + right: addedUser.right, }); } diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index 35f611bca..e8f651c49 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -2,9 +2,6 @@ import { drizzle } from "drizzle-orm/d1"; import { D1Database, Fetcher, Request as CFRequest, Response as CFResponse } from "@cloudflare/workers-types"; import { DefaultHttpHeaders, createHandler } from "./create-handler.js"; import { URI } from "@adviser/cement"; -import { Request as CFRequest, Response as CFResponse, D1Database, Fetcher } from "@cloudflare/workers-types"; -import { drizzle } from "drizzle-orm/d1"; -import { CORS, createHandler } from "./create-handler.js"; import { resWellKnownJwks } from "./well-known-jwks.js"; export interface Env { diff --git a/dashboard/src/pages/cloud/api/token-auto.tsx b/dashboard/src/pages/cloud/api/token-auto.tsx index 8d1b51914..dffcc5b1a 100644 --- a/dashboard/src/pages/cloud/api/token-auto.tsx +++ b/dashboard/src/pages/cloud/api/token-auto.tsx @@ -1,10 +1,10 @@ -import React, { useContext, useEffect, useState } from "react"; import { URI } from "@adviser/cement"; +import { TenantLedger } from "@fireproof/core-types-protocols-cloud"; import { useMutation, useQuery } from "@tanstack/react-query"; import { base64url } from "jose"; +import React, { useContext, useEffect, useState } from "react"; import { Navigate, useSearchParams } from "react-router-dom"; import { AppContext } from "../../../app-context.jsx"; -import { TenantLedger } from "@fireproof/core-types-protocols-cloud"; import { ListTenantsLedgersByUser } from "../../../cloud-context.jsx"; interface TenantLedgerWithName { @@ -51,9 +51,12 @@ export function ApiTokenAuto() { ); } - // Get user's tenants and ledgers + // Get user's tenants and ledgers (includes both owned and shared ledgers) const { data: tenantsData, error: tenantsError } = cloud.getListTenantsLedgersByUser(); + // Also get all ledgers user is a member of (including shared ones not in their tenants) + const { data: allUserLedgers, error: allUserLedgersError } = cloud.getListLedgersByUser(); + // Create ledger mutation const createLedgerMutation = useMutation({ mutationFn: async ({ tenantId, name }: { tenantId: string; name: string }) => { @@ -70,9 +73,9 @@ export function ApiTokenAuto() { }, }); - // Process ledger data when tenants are loaded + // Process ledger data when tenants/ledgers are loaded useEffect(() => { - if (!tenantsData || tenantsData.length === 0 || ledgerInfo) { + if (ledgerInfo) { return; } @@ -86,27 +89,45 @@ export function ApiTokenAuto() { return; } - // Look for existing ledger by name or ID + // Look for existing ledger by name or ID in all user's ledgers (including shared) let foundLedger: TenantLedgerWithName | null = null; - for (const tenant of tenantsData) { - for (const ledger of tenant.ledgers) { - if (ledger.name === ledgerName || (ledgerId && ledger.ledgerId === ledgerId)) { - foundLedger = { - tenant: tenant.tenant.tenantId, - ledger: ledger.ledgerId, - name: ledger.name, - }; - break; + // First check in allUserLedgers (includes shared ledgers) + if (allUserLedgers?.ledgers) { + const matchingLedger = allUserLedgers.ledgers.find( + (ledger) => ledger.name === ledgerName || (ledgerId && ledger.ledgerId === ledgerId), + ); + + if (matchingLedger) { + foundLedger = { + tenant: matchingLedger.tenantId, + ledger: matchingLedger.ledgerId, + name: matchingLedger.name, + }; + } + } + + // Fallback: check in tenantsData if not found + if (!foundLedger && tenantsData && tenantsData.length > 0) { + for (const tenant of tenantsData) { + for (const ledger of tenant.ledgers) { + if (ledger.name === ledgerName || (ledgerId && ledger.ledgerId === ledgerId)) { + foundLedger = { + tenant: tenant.tenant.tenantId, + ledger: ledger.ledgerId, + name: ledger.name, + }; + break; + } } + if (foundLedger) break; } - if (foundLedger) break; } if (foundLedger) { setLedgerInfo(foundLedger); - } else { - // Need to create a new ledger + } else if (tenantsData && tenantsData.length > 0) { + // Only create a new ledger if user has tenants (don't create for shared-only users) const targetTenant = tenantId ? tenantsData.find((t: ListTenantsLedgersByUser) => t.tenant.tenantId === tenantId)?.tenant : tenantsData[0]?.tenant; @@ -118,7 +139,7 @@ export function ApiTokenAuto() { }); } } - }, [tenantsData, tenantId, ledgerId, ledgerName, ledgerInfo, createLedgerMutation]); + }, [tenantsData, allUserLedgers, tenantId, ledgerId, ledgerName, ledgerInfo, createLedgerMutation]); // Handle successful ledger creation useEffect(() => { @@ -182,7 +203,7 @@ export function ApiTokenAuto() { }, [cloudToken, backUrl, countdownSecs]); // Show errors - const error = tenantsError || createLedgerMutation.error || errorToken; + const error = tenantsError || allUserLedgersError || createLedgerMutation.error || errorToken; if (error) { return (
From ef6f3018b82a1eb42215d3479e5b22a776ed3d7b Mon Sep 17 00:00:00 2001 From: Selem Delul Date: Thu, 6 Nov 2025 18:56:35 -0500 Subject: [PATCH 4/5] Revert "chore: rebase with main. share only at ledger level." This reverts commit 1a588624a1ab1954212e0a2e42c151a59f8940f8. --- dashboard/backend/api.ts | 24 +++++--- dashboard/backend/cf-serve.ts | 3 + dashboard/src/pages/cloud/api/token-auto.tsx | 61 +++++++------------- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index 42f46dbd2..f7aad533b 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -1846,7 +1846,19 @@ export class FPApiSQL implements FPApiInterface { return Result.Err("Cannot share ledger with yourself"); } - // 6. Add user to ledger (no need to add to tenant for sharing) + // 6. Add user to tenant first + const rAddUserToTenant = await this.addUserToTenant(this.db, { + userName: req.email, + tenantId: ledger.tenantId, + userId: targetUser.userId, + role: "member", + }); + + if (rAddUserToTenant.isErr()) { + return Result.Err(rAddUserToTenant.Err()); + } + + // 7. Add user to ledger const rAddUser = await this.addUserToLedger(this.db, { userName: req.email, ledgerId: ledgerId, @@ -1860,17 +1872,15 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAddUser.Err()); } - const addedUser = rAddUser.Ok(); - return Result.Ok({ type: "resShareWithUser", success: true, message: `Successfully shared ledger with ${req.email}`, - ledgerId: addedUser.ledgerId, - userId: addedUser.userId, + ledgerId: ledgerId, + userId: targetUser.userId, email: req.email, - role: addedUser.role, - right: addedUser.right, + role: req.role || "member", + right: req.right || "write", }); } diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index e8f651c49..35f611bca 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -2,6 +2,9 @@ import { drizzle } from "drizzle-orm/d1"; import { D1Database, Fetcher, Request as CFRequest, Response as CFResponse } from "@cloudflare/workers-types"; import { DefaultHttpHeaders, createHandler } from "./create-handler.js"; import { URI } from "@adviser/cement"; +import { Request as CFRequest, Response as CFResponse, D1Database, Fetcher } from "@cloudflare/workers-types"; +import { drizzle } from "drizzle-orm/d1"; +import { CORS, createHandler } from "./create-handler.js"; import { resWellKnownJwks } from "./well-known-jwks.js"; export interface Env { diff --git a/dashboard/src/pages/cloud/api/token-auto.tsx b/dashboard/src/pages/cloud/api/token-auto.tsx index dffcc5b1a..8d1b51914 100644 --- a/dashboard/src/pages/cloud/api/token-auto.tsx +++ b/dashboard/src/pages/cloud/api/token-auto.tsx @@ -1,10 +1,10 @@ +import React, { useContext, useEffect, useState } from "react"; import { URI } from "@adviser/cement"; -import { TenantLedger } from "@fireproof/core-types-protocols-cloud"; import { useMutation, useQuery } from "@tanstack/react-query"; import { base64url } from "jose"; -import React, { useContext, useEffect, useState } from "react"; import { Navigate, useSearchParams } from "react-router-dom"; import { AppContext } from "../../../app-context.jsx"; +import { TenantLedger } from "@fireproof/core-types-protocols-cloud"; import { ListTenantsLedgersByUser } from "../../../cloud-context.jsx"; interface TenantLedgerWithName { @@ -51,12 +51,9 @@ export function ApiTokenAuto() { ); } - // Get user's tenants and ledgers (includes both owned and shared ledgers) + // Get user's tenants and ledgers const { data: tenantsData, error: tenantsError } = cloud.getListTenantsLedgersByUser(); - // Also get all ledgers user is a member of (including shared ones not in their tenants) - const { data: allUserLedgers, error: allUserLedgersError } = cloud.getListLedgersByUser(); - // Create ledger mutation const createLedgerMutation = useMutation({ mutationFn: async ({ tenantId, name }: { tenantId: string; name: string }) => { @@ -73,9 +70,9 @@ export function ApiTokenAuto() { }, }); - // Process ledger data when tenants/ledgers are loaded + // Process ledger data when tenants are loaded useEffect(() => { - if (ledgerInfo) { + if (!tenantsData || tenantsData.length === 0 || ledgerInfo) { return; } @@ -89,45 +86,27 @@ export function ApiTokenAuto() { return; } - // Look for existing ledger by name or ID in all user's ledgers (including shared) + // Look for existing ledger by name or ID let foundLedger: TenantLedgerWithName | null = null; - // First check in allUserLedgers (includes shared ledgers) - if (allUserLedgers?.ledgers) { - const matchingLedger = allUserLedgers.ledgers.find( - (ledger) => ledger.name === ledgerName || (ledgerId && ledger.ledgerId === ledgerId), - ); - - if (matchingLedger) { - foundLedger = { - tenant: matchingLedger.tenantId, - ledger: matchingLedger.ledgerId, - name: matchingLedger.name, - }; - } - } - - // Fallback: check in tenantsData if not found - if (!foundLedger && tenantsData && tenantsData.length > 0) { - for (const tenant of tenantsData) { - for (const ledger of tenant.ledgers) { - if (ledger.name === ledgerName || (ledgerId && ledger.ledgerId === ledgerId)) { - foundLedger = { - tenant: tenant.tenant.tenantId, - ledger: ledger.ledgerId, - name: ledger.name, - }; - break; - } + for (const tenant of tenantsData) { + for (const ledger of tenant.ledgers) { + if (ledger.name === ledgerName || (ledgerId && ledger.ledgerId === ledgerId)) { + foundLedger = { + tenant: tenant.tenant.tenantId, + ledger: ledger.ledgerId, + name: ledger.name, + }; + break; } - if (foundLedger) break; } + if (foundLedger) break; } if (foundLedger) { setLedgerInfo(foundLedger); - } else if (tenantsData && tenantsData.length > 0) { - // Only create a new ledger if user has tenants (don't create for shared-only users) + } else { + // Need to create a new ledger const targetTenant = tenantId ? tenantsData.find((t: ListTenantsLedgersByUser) => t.tenant.tenantId === tenantId)?.tenant : tenantsData[0]?.tenant; @@ -139,7 +118,7 @@ export function ApiTokenAuto() { }); } } - }, [tenantsData, allUserLedgers, tenantId, ledgerId, ledgerName, ledgerInfo, createLedgerMutation]); + }, [tenantsData, tenantId, ledgerId, ledgerName, ledgerInfo, createLedgerMutation]); // Handle successful ledger creation useEffect(() => { @@ -203,7 +182,7 @@ export function ApiTokenAuto() { }, [cloudToken, backUrl, countdownSecs]); // Show errors - const error = tenantsError || allUserLedgersError || createLedgerMutation.error || errorToken; + const error = tenantsError || createLedgerMutation.error || errorToken; if (error) { return (
From 7916aaa9f58d97c5f97c9baa1e5436b7f4d31383 Mon Sep 17 00:00:00 2001 From: Selem Delul Date: Thu, 6 Nov 2025 19:04:55 -0500 Subject: [PATCH 5/5] chore: clean up imports in cf-serve.ts --- dashboard/backend/cf-serve.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index 35f611bca..6aa026789 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -1,10 +1,7 @@ -import { drizzle } from "drizzle-orm/d1"; -import { D1Database, Fetcher, Request as CFRequest, Response as CFResponse } from "@cloudflare/workers-types"; -import { DefaultHttpHeaders, createHandler } from "./create-handler.js"; import { URI } from "@adviser/cement"; import { Request as CFRequest, Response as CFResponse, D1Database, Fetcher } from "@cloudflare/workers-types"; import { drizzle } from "drizzle-orm/d1"; -import { CORS, createHandler } from "./create-handler.js"; +import { DefaultHttpHeaders, createHandler } from "./create-handler.js"; import { resWellKnownJwks } from "./well-known-jwks.js"; export interface Env {