Skip to content

Commit 8154d6d

Browse files
committed
feat: find_or_create ledger
1 parent 7230550 commit 8154d6d

File tree

4 files changed

+241
-10
lines changed

4 files changed

+241
-10
lines changed

dashboard/src/components/App.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import { CloudTenantLedgersShow } from "../pages/cloud/tenants/ledgers/show.jsx"
2323
import { CloudTenantMembers } from "../pages/cloud/tenants/members.jsx";
2424
import { CloudNew, newCloudAction } from "../pages/cloud/tenants/new.jsx";
2525
// import { CloudTenantOverview } from "../pages/cloud/tenants/overview.jsx";
26+
import { ApiTokenAuto } from "../pages/cloud/api/token-auto.jsx";
27+
import { ApiToken, redirectBackUrl } from "../pages/cloud/api/token.jsx";
28+
import { LedgerAdmin } from "../pages/cloud/tenants/ledgers/admin.jsx";
29+
import { LedgerDelete } from "../pages/cloud/tenants/ledgers/delete.jsx";
30+
import { LedgerOverview } from "../pages/cloud/tenants/ledgers/overview.jsx";
2631
import { CloudTenantShow } from "../pages/cloud/tenants/show.jsx";
2732
import { Databases, databaseLoader } from "../pages/databases.jsx";
2833
import { DatabasesConnect, connectDatabasesLoader } from "../pages/databases/connect.jsx";
@@ -35,10 +40,6 @@ import { DocsShow } from "../pages/docs/show.jsx";
3540
import { Index, indexLoader } from "../pages/index.jsx";
3641
import { Login, loginLoader } from "../pages/login.jsx";
3742
import { SignUpPage, signupLoader } from "../pages/signup.jsx";
38-
import { ApiToken, redirectBackUrl } from "../pages/cloud/api/token.jsx";
39-
import { LedgerOverview } from "../pages/cloud/tenants/ledgers/overview.jsx";
40-
import { LedgerAdmin } from "../pages/cloud/tenants/ledgers/admin.jsx";
41-
import { LedgerDelete } from "../pages/cloud/tenants/ledgers/delete.jsx";
4243

4344
export function App() {
4445
const ctx = useContext(AppContext);
@@ -53,10 +54,12 @@ export function App() {
5354
{/* <Route path="/fp/cloud" element={<Cloud />} loader={cloudLoader}> */}
5455

5556
<Route path="/token" element={<ApiToken />} loader={redirectBackUrl} />
57+
<Route path="/token-auto" element={<ApiTokenAuto />} />
5658

5759
<Route path="/fp/cloud" element={<Cloud />}>
5860
<Route index element={<CloudIndex />} />
5961
<Route path="api/token" element={<ApiToken />} loader={redirectBackUrl} />
62+
<Route path="api/token-auto" element={<ApiTokenAuto />} />
6063

6164
<Route path="tenants">
6265
<Route path="new" element={<CloudNew />} action={newCloudAction(ctx)} />
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import React, { useContext, useEffect, useState } from "react";
2+
import { URI } from "@adviser/cement";
3+
import { useMutation, useQuery } from "@tanstack/react-query";
4+
import { base64url } from "jose";
5+
import { Navigate, useSearchParams } from "react-router-dom";
6+
import { AppContext } from "../../../app-context.jsx";
7+
import { TenantLedger } from "@fireproof/core-types-protocols-cloud";
8+
import { ListTenantsLedgersByUser } from "../../../cloud-context.jsx";
9+
10+
interface TenantLedgerWithName {
11+
readonly tenant: string;
12+
readonly ledger: string;
13+
readonly name: string;
14+
}
15+
16+
export function ApiTokenAuto() {
17+
const { cloud } = useContext(AppContext);
18+
const buri = URI.from(window.location.href);
19+
const [searchParams] = useSearchParams();
20+
21+
const [ledgerInfo, setLedgerInfo] = useState<TenantLedgerWithName | null>(null);
22+
const [redirectCountdown, setRedirectCountdown] = useState(3);
23+
24+
// Get parameters from URL
25+
const resultId = searchParams.get("result_id");
26+
const ledgerName = searchParams.get("local_ledger_name") || searchParams.get("ledger_name") || "default";
27+
const tenantId = searchParams.get("tenant");
28+
const ledgerId = searchParams.get("ledger");
29+
const backUrl = searchParams.get("back_url");
30+
const countdownSecs = parseInt(searchParams.get("countdownSecs") ?? "3");
31+
32+
// Check if user is signed in
33+
if (cloud._clerkSession?.isSignedIn === false) {
34+
const tos = buri.build().pathname("/login").cleanParams().setParam("redirect_url", base64url.encode(buri.toString()));
35+
36+
const fromApp = buri.getParam("fromApp");
37+
if (fromApp) {
38+
tos.setParam("fromApp", fromApp);
39+
}
40+
41+
return <Navigate to={tos.withoutHostAndSchema} />;
42+
}
43+
44+
if (!resultId || resultId.length < 5) {
45+
return (
46+
<div style={{ padding: "20px", textAlign: "center" }}>
47+
<h2>Invalid Request</h2>
48+
<p>The authentication request is missing required information.</p>
49+
<p style={{ fontSize: "14px", color: "#666" }}>Please try again from your application.</p>
50+
</div>
51+
);
52+
}
53+
54+
// Get user's tenants and ledgers
55+
const { data: tenantsData, error: tenantsError } = cloud.getListTenantsLedgersByUser();
56+
57+
// Create ledger mutation
58+
const createLedgerMutation = useMutation({
59+
mutationFn: async ({ tenantId, name }: { tenantId: string; name: string }) => {
60+
const res = await cloud.api.createLedger({
61+
ledger: {
62+
tenantId,
63+
name,
64+
},
65+
});
66+
if (res.isErr()) {
67+
throw res.Err();
68+
}
69+
return res.Ok();
70+
},
71+
});
72+
73+
// Process ledger data when tenants are loaded
74+
useEffect(() => {
75+
if (!tenantsData || tenantsData.length === 0 || ledgerInfo) {
76+
return;
77+
}
78+
79+
// If we have both tenant and ledger IDs, use them directly
80+
if (tenantId && ledgerId) {
81+
setLedgerInfo({
82+
tenant: tenantId,
83+
ledger: ledgerId,
84+
name: ledgerName,
85+
});
86+
return;
87+
}
88+
89+
// Look for existing ledger by name or ID
90+
let foundLedger: TenantLedgerWithName | null = null;
91+
92+
for (const tenant of tenantsData) {
93+
for (const ledger of tenant.ledgers) {
94+
if (ledger.name === ledgerName || (ledgerId && ledger.ledgerId === ledgerId)) {
95+
foundLedger = {
96+
tenant: tenant.tenant.tenantId,
97+
ledger: ledger.ledgerId,
98+
name: ledger.name,
99+
};
100+
break;
101+
}
102+
}
103+
if (foundLedger) break;
104+
}
105+
106+
if (foundLedger) {
107+
setLedgerInfo(foundLedger);
108+
} else {
109+
// Need to create a new ledger
110+
const targetTenant = tenantId
111+
? tenantsData.find((t: ListTenantsLedgersByUser) => t.tenant.tenantId === tenantId)?.tenant
112+
: tenantsData[0]?.tenant;
113+
114+
if (targetTenant && !createLedgerMutation.isPending && !createLedgerMutation.isSuccess) {
115+
createLedgerMutation.mutate({
116+
tenantId: targetTenant.tenantId,
117+
name: ledgerName,
118+
});
119+
}
120+
}
121+
}, [tenantsData, tenantId, ledgerId, ledgerName, ledgerInfo, createLedgerMutation]);
122+
123+
// Handle successful ledger creation
124+
useEffect(() => {
125+
if (createLedgerMutation.isSuccess && createLedgerMutation.data) {
126+
const created = createLedgerMutation.data;
127+
setLedgerInfo({
128+
tenant: created.ledger.tenantId,
129+
ledger: created.ledger.ledgerId,
130+
name: created.ledger.name,
131+
});
132+
}
133+
}, [createLedgerMutation.isSuccess, createLedgerMutation.data]);
134+
135+
// Get cloud session token
136+
const { data: cloudToken, error: errorToken } = useQuery({
137+
queryKey: ["cloudToken", ledgerInfo?.ledger, ledgerInfo?.tenant, resultId],
138+
queryFn: async () => {
139+
if (!ledgerInfo || !resultId) {
140+
throw new Error("No ledger selected or result_id missing");
141+
}
142+
143+
const rToken = await cloud.api.getCloudSessionToken({
144+
resultId: resultId,
145+
selected: {
146+
tenant: ledgerInfo.tenant,
147+
ledger: ledgerInfo.ledger,
148+
} as TenantLedger,
149+
});
150+
151+
if (rToken.isErr()) {
152+
throw rToken.Err();
153+
}
154+
155+
return rToken.Ok().token;
156+
},
157+
enabled: !!ledgerInfo && !!resultId,
158+
});
159+
160+
// Handle redirect countdown and auto-close
161+
useEffect(() => {
162+
if (cloudToken && backUrl) {
163+
const timer = setInterval(() => {
164+
setRedirectCountdown((prev) => {
165+
if (prev <= 1) {
166+
clearInterval(timer);
167+
// Close the current window
168+
window.open("", "_self")?.close();
169+
return 0;
170+
}
171+
return prev - 1;
172+
});
173+
}, 1000);
174+
175+
return () => clearInterval(timer);
176+
} else if (cloudToken && !backUrl) {
177+
// If no back URL, try to close the window
178+
setTimeout(() => {
179+
window.open("", "_self")?.close();
180+
}, countdownSecs * 1000);
181+
}
182+
}, [cloudToken, backUrl, countdownSecs]);
183+
184+
// Show errors
185+
const error = tenantsError || createLedgerMutation.error || errorToken;
186+
if (error) {
187+
return (
188+
<div style={{ padding: "40px", textAlign: "center" }}>
189+
<p>Something went wrong. Please try again.</p>
190+
<details style={{ marginTop: "10px" }}>
191+
<summary style={{ cursor: "pointer" }}>Details</summary>
192+
<pre style={{ fontSize: "12px", marginTop: "10px" }}>{(error as Error).message}</pre>
193+
</details>
194+
</div>
195+
);
196+
}
197+
198+
// Success state
199+
if (cloudToken) {
200+
return (
201+
<div style={{ padding: "40px", textAlign: "center" }}>
202+
<p>All set! This window will close in {redirectCountdown}...</p>
203+
</div>
204+
);
205+
}
206+
207+
// Loading state - single message for all loading states
208+
return (
209+
<div style={{ padding: "40px", textAlign: "center" }}>
210+
<div
211+
style={{
212+
display: "inline-block",
213+
width: "20px",
214+
height: "20px",
215+
border: "2px solid #f3f3f3",
216+
borderTop: "2px solid #333",
217+
borderRadius: "50%",
218+
animation: "spin 1s linear infinite",
219+
}}
220+
></div>
221+
<p style={{ marginTop: "20px" }}>Setting things up...</p>
222+
<style>{`
223+
@keyframes spin {
224+
0% { transform: rotate(0deg); }
225+
100% { transform: rotate(360deg); }
226+
}
227+
`}</style>
228+
</div>
229+
);
230+
}

dashboard/src/pages/cloud/api/token.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import React, { useContext, useEffect, useState } from "react";
21
import { URI } from "@adviser/cement";
2+
import { LedgerUser, UserTenant } from "@fireproof/core-protocols-dashboard";
3+
import { TenantLedger } from "@fireproof/core-types-protocols-cloud";
34
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
45
import { base64url } from "jose";
6+
import React, { useContext, useEffect, useState } from "react";
57
import { Navigate, useSearchParams } from "react-router-dom";
68
import { AppContext } from "../../../app-context.jsx";
79
import { SelectedTenantLedger } from "./selected-tenant-ledger.jsx";
8-
import { TenantLedger } from "@fireproof/core-types-protocols-cloud";
9-
import { LedgerUser, UserTenant } from "@fireproof/core-protocols-dashboard";
1010

1111
export function redirectBackUrl() {
1212
const uri = URI.from(window.location.href);

dashboard/src/pages/login.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { useContext } from "react";
21
import { URI } from "@adviser/cement";
32
import { SignIn } from "@clerk/clerk-react";
43
import { base64url } from "jose";
4+
import React, { useContext } from "react";
55
import { Navigate } from "react-router-dom";
66
import { AppContext } from "../app-context.jsx";
77
const slides = [
@@ -56,13 +56,11 @@ export function Login() {
5656
}
5757

5858
const to = URI.from(decodedUrl).withoutHostAndSchema;
59-
console.log("login-tos", to);
6059
return <Navigate to={to} />;
6160
}
6261

6362
// const fromUrl = URI.from(window.location.href).getParam("redirect_url", "/fp/cloud")
6463
const redirect_url = URI.from(window.location.href).toString();
65-
console.log("login-redirect_url", window.location.href);
6664

6765
const fromApp = URI.from(window.location.href).getParam("fromApp");
6866
if (fromApp) {

0 commit comments

Comments
 (0)