Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
{
appBaseUrl: normalizedAppBaseUrl,
serverUrl,
token,
}
);

Expand Down
8 changes: 8 additions & 0 deletions src/modules/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ async function getSessionContext(
userAuthModule: AuthModule
): Promise<SessionContext> {
if (!analyticsSharedState.sessionContext) {
// With no token there is no identity to resolve: `me()` can only answer 401,
// which the browser logs to the console before any handler here sees it. On
// a public page that request is the sole reason an error appears, so skip
// it. This is not memoized — a visitor who logs in later must still resolve.
if (!userAuthModule.hasToken()) {
return { user_id: null, session_id: getAnalyticsSessionId() };
}

if (!sessionContextPromise) {
const sessionId = getAnalyticsSessionId();
sessionContextPromise = userAuthModule
Expand Down
11 changes: 11 additions & 0 deletions src/modules/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,16 @@ export function createAuthModule(
pendingMe = null;
};

// Tracked here rather than read off `axios.defaults` so the answer stays tied
// to the identity transitions below (`setToken`, `logout`) instead of to the
// header a caller may have set on the instance directly.
let hasAccessToken = Boolean(options.token);

return {
hasToken() {
return hasAccessToken;
},

// Get current user information
async me() {
const request: Promise<User> =
Expand Down Expand Up @@ -189,6 +198,7 @@ export function createAuthModule(
// flight would otherwise resolve into callers that run after the logout.
clearPendingMe();
resetAnalyticsSessionContext();
hasAccessToken = false;

// Only do the rest if in a browser environment
if (typeof window !== "undefined") {
Expand Down Expand Up @@ -220,6 +230,7 @@ export function createAuthModule(
// resolved for the previous one must not be handed to later callers.
clearPendingMe();
resetAnalyticsSessionContext();
hasAccessToken = true;

// handle token change for axios clients
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
Expand Down
17 changes: 17 additions & 0 deletions src/modules/auth.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export interface AuthModuleOptions {
serverUrl: string;
/** Base URL for the app (used for login redirects). */
appBaseUrl: string;
/**
* Access token the client was constructed with, if any. Seeds the module's
* view of whether a session exists before {@link AuthModule.setToken} runs,
* which is how the server-side SDK reports a token it never sets explicitly.
*/
token?: string;
}

/**
Expand All @@ -120,6 +126,17 @@ export interface AuthModuleOptions {
* The auth module is only available in user authentication mode (`base44.auth`).
*/
export interface AuthModule {
/**
* Whether an access token is currently set on the client.
*
* Reports only the presence of a token, never its validity — an expired or
* revoked token still reads as `true`. Callers use this to skip requests that
* could not succeed without a session, not to decide that one is valid.
*
* @internal
*/
hasToken(): boolean;
Comment thread
guyofeck marked this conversation as resolved.
Outdated

/**
* Gets the current authenticated user's information.
*
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ describe("Analytics Module", () => {
createAxiosClient: vi.fn().mockImplementation(
() =>
({
// `setToken` and `logout` write through to these, so the mock needs
// them present per instance.
defaults: { headers: { common: {} as Record<string, string> } },
request: vi.fn().mockResolvedValue({
status: 200,
data: {
Expand Down Expand Up @@ -54,9 +57,12 @@ describe("Analytics Module", () => {
heartBeatInterval: undefined,
};

// Token-bearing by default: most tests here exercise the flush path that
// resolves an identity, and that lookup is skipped without a session.
base44 = createClient({
serverUrl,
appId,
token: "test-access-token",
});
});

Expand Down Expand Up @@ -126,6 +132,55 @@ describe("Analytics Module", () => {
expect(heartBeatState.isHeartBeatProcessing).toBeFalsy();
});

test("should not resolve an identity when no token is set", async () => {
resetAnalyticsSessionContext();

const anonymous = createClient({ serverUrl, appId });
const me = vi.spyOn(anonymous.auth, "me");

anonymous.analytics.track({ eventName: "public-page-event" });
await vi.waitFor(() => expect(sharedState?.requestsQueue.length).toBe(0));

// The whole point: on a public page `me()` can only answer 401, and the
// browser logs that to the console before any handler here sees it. The
// event still flushes -- anonymous events already reported user_id: null.
expect(me).not.toHaveBeenCalled();

anonymous.cleanup();
});

test("should resolve an identity once a token is set", async () => {
resetAnalyticsSessionContext();

const anonymous = createClient({ serverUrl, appId });
const me = vi
.spyOn(anonymous.auth, "me")
.mockResolvedValue({ id: "user-1" } as User);

// A visitor who logs in mid-session must start reporting their identity, so
// the skip above must not be memoized.
anonymous.auth.setToken("token-acquired-after-login", false);
anonymous.analytics.track({ eventName: "post-login-event" });

await vi.waitFor(() => expect(me).toHaveBeenCalled());

anonymous.cleanup();
});

test("should report token presence across identity changes", () => {
const client = createClient({ serverUrl, appId });

expect(client.auth.hasToken()).toBe(false);

client.auth.setToken("some-token", false);
expect(client.auth.hasToken()).toBe(true);

client.auth.logout();
expect(client.auth.hasToken()).toBe(false);

client.cleanup();
});

test("should track multiple events", async () => {
vi.useFakeTimers();

Expand Down
Loading