Skip to content

Commit 13747e3

Browse files
committed
Update files
1 parent 21325ff commit 13747e3

5 files changed

Lines changed: 164 additions & 24 deletions

File tree

apps/account/src/hooks/useSession.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ export interface SessionState {
5757
setActiveOrganization: (organizationId: string) => Promise<void>;
5858
organizations: Organization[];
5959
organizationsLoading: boolean;
60+
/**
61+
* `true` once `organizations` has been fetched at least once for the
62+
* current user. Useful for callers that need to distinguish "user has no
63+
* orgs" from "we haven't asked the server yet" (e.g. the post-login
64+
* redirect flow).
65+
*/
66+
organizationsFetched: boolean;
6067
reloadOrganizations: () => Promise<void>;
6168
}
6269

@@ -93,6 +100,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
93100
const [error, setError] = useState<Error | null>(null);
94101
const [organizations, setOrganizations] = useState<Organization[]>([]);
95102
const [organizationsLoading, setOrganizationsLoading] = useState(false);
103+
const [organizationsFetched, setOrganizationsFetched] = useState(false);
96104

97105
const reloadOrganizations = useCallback(async () => {
98106
if (!client?.organizations) return;
@@ -104,6 +112,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
104112
setOrganizations([]);
105113
} finally {
106114
setOrganizationsLoading(false);
115+
setOrganizationsFetched(true);
107116
}
108117
}, [client]);
109118

@@ -134,6 +143,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
134143
reloadOrganizations();
135144
} else {
136145
setOrganizations([]);
146+
setOrganizationsFetched(false);
137147
}
138148
}, [user, reloadOrganizations]);
139149

@@ -145,6 +155,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
145155
setUser(null);
146156
setSession(null);
147157
setOrganizations([]);
158+
setOrganizationsFetched(false);
148159
}
149160
}, [client]);
150161

@@ -168,6 +179,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
168179
setActiveOrganization,
169180
organizations,
170181
organizationsLoading,
182+
organizationsFetched,
171183
reloadOrganizations,
172184
}),
173185
[
@@ -180,6 +192,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
180192
setActiveOrganization,
181193
organizations,
182194
organizationsLoading,
195+
organizationsFetched,
183196
reloadOrganizations,
184197
],
185198
);

apps/account/src/routes/login.tsx

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,69 @@ function LoginPage() {
4444
const navigate = useNavigate();
4545
const { redirect } = Route.useSearch();
4646
const client = useClient() as any;
47-
const { session, user, refresh } = useSession();
47+
const {
48+
session,
49+
user,
50+
refresh,
51+
organizations,
52+
organizationsLoading,
53+
organizationsFetched,
54+
setActiveOrganization,
55+
} = useSession();
4856
const [email, setEmail] = useState('');
4957
const [password, setPassword] = useState('');
5058
const [submitting, setSubmitting] = useState(false);
59+
const [autoSelectingOrg, setAutoSelectingOrg] = useState(false);
5160

5261
useEffect(() => {
5362
if (!user) return;
54-
if (isSafeRedirect(redirect)) {
55-
window.location.assign(resolveRedirect(redirect));
56-
return;
57-
}
63+
64+
// If the user has organizations but no active one, auto-select the first
65+
// org before navigating away. Otherwise consumers like the Console's
66+
// `RequireOrganization` guard would bounce the user from the redirect
67+
// target (e.g. `/_console/home`) to `/_console/organizations`, making
68+
// the post-login redirect look like a back-and-forth jump.
5869
if (!session?.activeOrganizationId) {
70+
// Wait for the org list to be fetched at least once before deciding.
71+
if (!organizationsFetched || organizationsLoading || autoSelectingOrg) return;
72+
if (organizations.length === 1) {
73+
setAutoSelectingOrg(true);
74+
setActiveOrganization(organizations[0].id)
75+
.catch(() => undefined)
76+
.finally(() => setAutoSelectingOrg(false));
77+
return;
78+
}
79+
if (organizations.length === 0) {
80+
// Brand-new account with no org yet — send to the org creation flow
81+
// instead of the org picker (which would be empty).
82+
navigate({ to: '/organizations/new' });
83+
return;
84+
}
85+
// Multiple orgs and no active selection — let the user choose.
5986
navigate({ to: '/organizations' });
6087
return;
6188
}
89+
90+
if (autoSelectingOrg) return;
91+
92+
if (isSafeRedirect(redirect)) {
93+
window.location.assign(resolveRedirect(redirect));
94+
return;
95+
}
6296
// Default landing after sign-in: the platform home, not the Account
6397
// profile page. Users can reach `/account` from the top-bar menu.
6498
window.location.assign('/');
65-
}, [user, session, navigate, redirect]);
99+
}, [
100+
user,
101+
session,
102+
navigate,
103+
redirect,
104+
organizations,
105+
organizationsLoading,
106+
organizationsFetched,
107+
autoSelectingOrg,
108+
setActiveOrganization,
109+
]);
66110

67111
const handleSubmit = async (e: React.FormEvent) => {
68112
e.preventDefault();

apps/account/src/routes/register.tsx

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,67 @@ function RegisterPage() {
3636
const navigate = useNavigate();
3737
const { redirect } = Route.useSearch();
3838
const client = useClient() as any;
39-
const { session, user, refresh } = useSession();
39+
const {
40+
session,
41+
user,
42+
refresh,
43+
organizations,
44+
organizationsLoading,
45+
organizationsFetched,
46+
setActiveOrganization,
47+
} = useSession();
4048
const [name, setName] = useState('');
4149
const [email, setEmail] = useState('');
4250
const [password, setPassword] = useState('');
4351
const [submitting, setSubmitting] = useState(false);
52+
const [autoSelectingOrg, setAutoSelectingOrg] = useState(false);
4453

4554
useEffect(() => {
4655
if (!user) return;
47-
if (isSafeRedirect(redirect)) {
48-
window.location.assign(resolveRedirect(redirect));
56+
57+
// If the freshly-signed-up user already has organizations (the auth
58+
// plugin auto-provisions a personal workspace, or they accepted an
59+
// invitation), make sure one is active before navigating away. Without
60+
// this the redirect target's `RequireOrganization` guard would bounce
61+
// the user to `/_console/organizations`.
62+
if (!session?.activeOrganizationId) {
63+
// Wait until the org list has been fetched at least once before
64+
// deciding — otherwise we'd race the post-signup org provisioning.
65+
if (!organizationsFetched || organizationsLoading || autoSelectingOrg) return;
66+
if (organizations.length === 1) {
67+
setAutoSelectingOrg(true);
68+
setActiveOrganization(organizations[0].id)
69+
.catch(() => undefined)
70+
.finally(() => setAutoSelectingOrg(false));
71+
return;
72+
}
73+
if (organizations.length > 1) {
74+
navigate({ to: '/organizations' });
75+
return;
76+
}
77+
// No orgs at all — the user needs to create one.
78+
navigate({ to: '/organizations/new' });
4979
return;
5080
}
51-
// If the user already has an org (e.g. signed up via an invitation),
52-
// hand off to the platform home. Otherwise, send them to the org
53-
// creation page — they need an org before they can use the product.
54-
if (session?.activeOrganizationId) {
55-
window.location.assign('/');
81+
82+
if (autoSelectingOrg) return;
83+
84+
if (isSafeRedirect(redirect)) {
85+
window.location.assign(resolveRedirect(redirect));
5686
return;
5787
}
58-
navigate({ to: '/organizations/new' });
59-
}, [user, session, navigate, redirect]);
88+
window.location.assign('/');
89+
}, [
90+
user,
91+
session,
92+
navigate,
93+
redirect,
94+
organizations,
95+
organizationsLoading,
96+
organizationsFetched,
97+
autoSelectingOrg,
98+
setActiveOrganization,
99+
]);
60100

61101
const handleSubmit = async (e: React.FormEvent) => {
62102
e.preventDefault();

apps/cloud/wrangler.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ compatibility_flags = ["nodejs_compat"]
4545
# rebuild + re-push to ship a new version, then run `wrangler deploy`.
4646
[[containers]]
4747
class_name = "CloudContainer"
48-
image = "registry.cloudflare.com/2846eb40a60f4738e292b90dcd8cce10/objectstack-cloud:turso-token-v3"
48+
image = "registry.cloudflare.com/2846eb40a60f4738e292b90dcd8cce10/objectstack-cloud:publish-diag"
4949
max_instances = 3
5050
instance_type = "standard-1"
5151

packages/services/service-cloud/src/multi-project-plugin.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function createTemplateSeeder(
9090
kernelManager: KernelManager,
9191
templates: Record<string, ProjectTemplate>,
9292
envRegistry: EnvironmentDriverRegistry,
93-
publishCtx: { controlDriver: IDataDriver; getStorage: () => any | null; storageAdapter: string; keyPrefix: string },
93+
publishCtx: { controlDriver: IDataDriver; getStorage: () => any | Promise<any> | null; storageAdapter: string; keyPrefix: string },
9494
): TemplateSeeder {
9595
const seedBundleForProject = async (projectId: string, bundle: any): Promise<void> => {
9696
const items = bundle ? extractMetadataItems(bundle) : [];
@@ -275,14 +275,32 @@ function createTemplateSeeder(
275275
// step, the seeded data only lives in this project's metadata
276276
// DB on the cloud container's filesystem and is invisible to
277277
// any other process.
278+
let publishStatus: { stage: string; ok: boolean; error?: string; detail?: any } = {
279+
stage: 'start', ok: false,
280+
};
278281
try {
279-
const storage = publishCtx.getStorage();
280-
if (storage) {
282+
const storage = await Promise.resolve(publishCtx.getStorage());
283+
publishStatus.stage = 'getStorage';
284+
publishStatus.detail = {
285+
hasStorage: !!storage,
286+
storageType: storage ? (storage.constructor?.name ?? typeof storage) : 'null',
287+
bundleHasObjects: Array.isArray(bundle?.objects) ? bundle.objects.length : 0,
288+
bundleHasApps: Array.isArray(bundle?.apps) ? bundle.apps.length : 0,
289+
bundleHasViews: Array.isArray(bundle?.views) ? bundle.views.length : 0,
290+
};
291+
if (!storage) {
292+
publishStatus.error = 'no-file-storage-service';
293+
console.warn(
294+
`[MultiProjectPlugin] No file-storage service registered; skipping artifact publish for project ${projectId}.`,
295+
);
296+
} else {
297+
publishStatus.stage = 'findProject';
281298
const projectRow = await (publishCtx.controlDriver as any).findOne(
282299
'sys_project',
283300
{ where: { id: projectId } },
284301
);
285302
if (projectRow) {
303+
publishStatus.stage = 'publishProjectRevision';
286304
const { publishProjectRevision } = await import('./cloud-artifact-helpers.js');
287305
await publishProjectRevision({
288306
driver: publishCtx.controlDriver,
@@ -293,15 +311,34 @@ function createTemplateSeeder(
293311
bundle,
294312
note: 'template-seed',
295313
});
314+
publishStatus.ok = true;
315+
publishStatus.stage = 'done';
316+
console.log(`[MultiProjectPlugin] Published artifact bundle for project ${projectId}`);
317+
} else {
318+
publishStatus.error = 'project-not-found';
296319
}
297320
}
298321
} catch (err: any) {
299-
// eslint-disable-next-line no-console
322+
publishStatus.error = err?.message ?? String(err);
323+
publishStatus.detail = { ...(publishStatus.detail ?? {}), stack: err?.stack };
300324
console.error(
301325
`[MultiProjectPlugin] Failed to publish seeded artifact for project ${projectId}:`,
302326
err?.stack ?? err?.message ?? err,
303327
);
304328
}
329+
// Persist publish diagnostic into sys_project.metadata.publishStatus so
330+
// operators can see why publish skipped without container logs.
331+
try {
332+
const cur = await (publishCtx.controlDriver as any).findOne('sys_project', { where: { id: projectId } });
333+
const meta = cur && typeof cur.metadata === 'string'
334+
? JSON.parse(cur.metadata)
335+
: (cur?.metadata ?? {});
336+
await (publishCtx.controlDriver as any).update(
337+
'sys_project',
338+
{ where: { id: projectId } },
339+
{ metadata: JSON.stringify({ ...meta, publishStatus, publishStatusAt: new Date().toISOString() }) },
340+
);
341+
} catch { /* best-effort diagnostic */ }
305342
};
306343

307344
return {
@@ -486,14 +523,20 @@ export class MultiProjectPlugin implements Plugin {
486523
controlDriver: this.config.controlDriver,
487524
storageAdapter: (process.env.OS_STORAGE_ADAPTER ?? 'local').toLowerCase(),
488525
keyPrefix: process.env.OS_STORAGE_KEY_PREFIX ?? 'artifacts',
489-
getStorage: () => {
526+
getStorage: async () => {
490527
// Resolve once-per-call so the storage plugin (registered
491528
// in parallel) is available even when MultiProjectPlugin
492-
// initialised first.
529+
// initialised first. Try sync getService first; fall back
530+
// to async getServiceAsync (StorageServicePlugin may register
531+
// via async lifecycle on cold start).
493532
try {
494533
const sync = (hostKernel as any).getService?.('file-storage');
495534
if (sync && typeof sync.upload === 'function') return sync;
496-
} catch { /* async-only — fall through */ }
535+
} catch { /* fall through */ }
536+
try {
537+
const async = await (hostKernel as any).getServiceAsync?.('file-storage');
538+
if (async && typeof async.upload === 'function') return async;
539+
} catch { /* not registered yet */ }
497540
return null;
498541
},
499542
},

0 commit comments

Comments
 (0)