Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 4 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export function createClient(config: CreateClientConfig): Base44Client {
headers: optionalHeaders,
} = config;

// Normalize appBaseUrl to always be a string (empty if not provided or invalid)
const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";

const socketConfig: RoomsSocketConfig = {
serverUrl,
mountPath: "/ws-user-apps/socket.io/",
Expand Down Expand Up @@ -135,7 +138,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
functionsAxiosClient,
appId,
{
appBaseUrl,
appBaseUrl: normalizedAppBaseUrl,
serverUrl,
}
);
Expand Down
42 changes: 20 additions & 22 deletions src/modules/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ export function createAuthModule(
: window.location.href;

// Build the login URL
const loginUrl = `${
options.appBaseUrl ?? ""
}/login?from_url=${encodeURIComponent(redirectUrl)}`;
const loginUrl = `${options.appBaseUrl}/login?from_url=${encodeURIComponent(redirectUrl)}`;

// Redirect to the login page
window.location.href = loginUrl;
Expand All @@ -65,7 +63,7 @@ export function createAuthModule(
// Build the provider login URL (google is the default, so no provider path needed)
const providerPath = provider === "google" ? "" : `/${provider}`;
const loginUrl = `${
options.serverUrl
options.appBaseUrl
}/api/apps/auth${providerPath}/login?app_id=${appId}&from_url=${encodeURIComponent(
redirectUrl
)}`;
Expand All @@ -75,29 +73,29 @@ export function createAuthModule(
},

// Logout the current user
// Removes the token from localStorage and optionally redirects to a URL or reloads the page
logout(redirectUrl?: string) {
// Remove token from axios headers
// Remove token from axios headers (always do this)
delete axios.defaults.headers.common["Authorization"];

// Remove token from localStorage
if (typeof window !== "undefined" && window.localStorage) {
try {
window.localStorage.removeItem("base44_access_token");
// Remove "token" that is set by the built-in SDK of platform version 2
window.localStorage.removeItem("token");
} catch (e) {
console.error("Failed to remove token from localStorage:", e);
}
}

// Redirect if a URL is provided
// Only do the rest if in a browser environment
if (typeof window !== "undefined") {
if (redirectUrl) {
window.location.href = redirectUrl;
} else {
window.location.reload();
// Remove token from localStorage
if (window.localStorage) {
try {
window.localStorage.removeItem("base44_access_token");
// Remove "token" that is set by the built-in SDK of platform version 2
window.localStorage.removeItem("token");
} catch (e) {
console.error("Failed to remove token from localStorage:", e);
}
}

// Determine the from_url parameter
const fromUrl = redirectUrl || window.location.href;

// Redirect to server-side logout endpoint to clear HTTP-only cookies
const logoutUrl = `${options.appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent(fromUrl)}`;
window.location.href = logoutUrl;
}
},

Expand Down
4 changes: 2 additions & 2 deletions src/modules/auth.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ export interface ResetPasswordParams {
export interface AuthModuleOptions {
/** Server URL for API requests. */
serverUrl: string;
/** Optional base URL for the app (used for login redirects). */
appBaseUrl?: string;
/** Base URL for the app (used for login redirects). */
appBaseUrl: string;
}

/**
Expand Down
72 changes: 47 additions & 25 deletions tests/unit/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,33 @@ describe('Auth Module', () => {
let scope;
const appId = 'test-app-id';
const serverUrl = 'https://api.base44.com';

const appBaseUrl = 'https://api.base44.com';

beforeEach(() => {
// Mock window.addEventListener and document for analytics module
if (typeof window !== 'undefined') {
if (!window.addEventListener) {
window.addEventListener = vi.fn();
window.removeEventListener = vi.fn();
}
}
if (typeof document === 'undefined') {
global.document = {
referrer: '',
visibilityState: 'visible'
};
}

// Create a new client for each test
base44 = createClient({
serverUrl,
appId,
appBaseUrl,
});

// Create a nock scope for mocking API calls
scope = nock(serverUrl);

// Enable request debugging for Nock
nock.disableNetConnect();
nock.emitter.on('no match', (req) => {
Expand Down Expand Up @@ -143,15 +159,15 @@ describe('Auth Module', () => {
global.window = {
location: mockLocation
};

const nextUrl = 'https://example.com/dashboard';
base44.auth.redirectToLogin(nextUrl);

// Verify the redirect URL was set correctly
expect(mockLocation.href).toBe(
`/login?from_url=${encodeURIComponent(nextUrl)}`
`${appBaseUrl}/login?from_url=${encodeURIComponent(nextUrl)}`
);

// Restore window
global.window = originalWindow;
});
Expand All @@ -169,7 +185,7 @@ describe('Auth Module', () => {

// Verify the redirect URL uses current URL
expect(mockLocation.href).toBe(
`/login?from_url=${encodeURIComponent(currentUrl)}`
`${appBaseUrl}/login?from_url=${encodeURIComponent(currentUrl)}`
);

// Restore window
Expand Down Expand Up @@ -204,6 +220,12 @@ describe('Auth Module', () => {
});

test('should use relative URL for login redirect when appBaseUrl is not provided', () => {
// Create a client without appBaseUrl
const clientWithoutAppBaseUrl = createClient({
serverUrl,
appId,
});

// Mock window.location
const originalWindow = global.window;
const mockLocation = { href: '', origin: 'https://current-app.com' };
Expand All @@ -212,7 +234,7 @@ describe('Auth Module', () => {
};

const nextUrl = 'https://example.com/dashboard';
base44.auth.redirectToLogin(nextUrl);
clientWithoutAppBaseUrl.auth.redirectToLogin(nextUrl);

// Verify the redirect URL uses a relative path (no appBaseUrl prefix)
expect(mockLocation.href).toBe(
Expand Down Expand Up @@ -316,33 +338,33 @@ describe('Auth Module', () => {
global.window = {
location: mockLocation
};

const redirectUrl = 'https://example.com/logout-success';
base44.auth.logout(redirectUrl);

// Verify redirect
expect(mockLocation.href).toBe(redirectUrl);


// Verify redirect to server-side logout endpoint with from_url parameter
const expectedUrl = `${appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent(redirectUrl)}`;
expect(mockLocation.href).toBe(expectedUrl);

// Restore window
global.window = originalWindow;
});

test('should reload page when no redirect URL is provided', async () => {
// Mock window object with reload function
const mockReload = vi.fn();
test('should redirect to logout endpoint when no redirect URL is provided', async () => {
// Mock window object
const mockLocation = { href: 'https://example.com/current-page' };
const originalWindow = global.window;
global.window = {
location: {
reload: mockReload
}
location: mockLocation
};

// Call logout without redirect URL
base44.auth.logout();

// Verify page reload was called
expect(mockReload).toHaveBeenCalledTimes(1);


// Verify redirect to server-side logout endpoint with current page as from_url
const expectedUrl = `${appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent('https://example.com/current-page')}`;
expect(mockLocation.href).toBe(expectedUrl);

// Restore window
global.window = originalWindow;
});
Expand Down
55 changes: 54 additions & 1 deletion tests/unit/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('Client Creation', () => {
serviceToken: 'service-token-123',
requiresAuth: true,
});

expect(client).toBeDefined();
expect(client.entities).toBeDefined();
expect(client.integrations).toBeDefined();
Expand All @@ -78,6 +78,59 @@ describe('Client Creation', () => {

});

describe('appBaseUrl Normalization', () => {
test('should use appBaseUrl when provided as a string', () => {
const customAppBaseUrl = 'https://custom-app.example.com';
const client = createClient({
appId: 'test-app-id',
appBaseUrl: customAppBaseUrl,
});

// Mock window.location
const originalWindow = global.window;
const mockLocation = { href: '', origin: 'https://current-app.com' };
global.window = {
location: mockLocation
};

const nextUrl = 'https://example.com/dashboard';
client.auth.redirectToLogin(nextUrl);

// Verify the redirect URL uses the custom appBaseUrl
expect(mockLocation.href).toBe(
`${customAppBaseUrl}/login?from_url=${encodeURIComponent(nextUrl)}`
);

// Restore window
global.window = originalWindow;
});

test('should normalize appBaseUrl to empty string when not provided', () => {
const client = createClient({
appId: 'test-app-id',
// appBaseUrl not provided
});

// Mock window.location
const originalWindow = global.window;
const mockLocation = { href: '', origin: 'https://current-app.com' };
global.window = {
location: mockLocation
};

const nextUrl = 'https://example.com/dashboard';
client.auth.redirectToLogin(nextUrl);

// Verify the redirect URL uses empty string (relative path)
expect(mockLocation.href).toBe(
`/login?from_url=${encodeURIComponent(nextUrl)}`
);

// Restore window
global.window = originalWindow;
});
});

describe('createClientFromRequest', () => {
test('should create client from request with all headers', () => {
const mockRequest = {
Expand Down