Merge remote-tracking branch 'origin/main' into juancastro/put-1568-rename-srcguisrchelpers-files-to-camelcase

This commit is contained in:
Juan Castro
2026-08-25 15:10:29 -04:00
53 changed files with 2846 additions and 296 deletions
+41 -9
View File
@@ -17,6 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import parse_shared_path, { SHARED_PATH_PARAM } from './parseSharedPath.js';
/**
* Where to send the user after a successful login/signup started from the
* current page. Keeps the user on the page they authenticated from — most
@@ -49,18 +51,15 @@ export const get_auth_redirect_url = () => {
};
/**
* The `return_to` path to send along when starting an OIDC flow, or null if
* the current page isn't one the backend will return to. The backend strictly
* whitelists these (never a client-supplied URL): `/desktop`, `/dashboard`,
* and direct app landings (`/app/<name>`, plus the desktop-booted
* `/desktop/app/<name>`), so OIDC login started from an app landing comes back
* to the app — and to the same interface it was opened in.
* The pathname part of an OIDC `return_to`, or null when the current page isn't
* one the backend will return to. The root is in here only for the share links
* below — on its own it is where the flow already lands.
*
* @returns {string|null} whitelistable pathname, or null
* @returns {string|null}
*/
export const get_oidc_return_to = () => {
const oidc_return_path = () => {
const pathname = window.location.pathname;
if ( pathname === '/desktop' || pathname === '/dashboard' ) {
if ( pathname === '/' || pathname === '/desktop' || pathname === '/dashboard' ) {
return pathname;
}
// app landing: normalize away a trailing slash to match the backend whitelist
@@ -69,3 +68,36 @@ export const get_oidc_return_to = () => {
}
return null;
};
/**
* The `return_to` to send along when starting an OIDC flow, or null if the
* current page isn't one the backend will return to. The backend strictly
* whitelists these (never a client-supplied URL): `/desktop`, `/dashboard`,
* and direct app landings (`/app/<name>`, plus the desktop-booted
* `/desktop/app/<name>`), so OIDC login started from an app landing comes back
* to the app — and to the same interface it was opened in.
*
* A share link (`?shared=`, from an email) is carried along with the path: the
* recipient usually has to sign in before they can see what was shared, and an
* OIDC round trip leaves the origin, so the parameter has to travel through the
* flow or they come back to a bare Home. Only well-formed values go — the
* backend refuses the rest, and a hand-edited link is no one's destination.
*
* @returns {string|null} whitelistable path, with its share items, or null
*/
export const get_oidc_return_to = () => {
const path = oidc_return_path();
if ( path === null ) return null;
const shared = new URLSearchParams(window.location.search ?? '')
.getAll(SHARED_PATH_PARAM)
.filter(value => parse_shared_path(value) !== null);
if ( shared.length === 0 ) {
// The root is only a destination when it names something.
return path === '/' ? null : path;
}
const params = new URLSearchParams();
for ( const value of shared ) params.append(SHARED_PATH_PARAM, value);
return `${path}?${params.toString()}`;
};
+35 -2
View File
@@ -20,11 +20,21 @@
import { describe, it, expect, afterEach } from 'vitest';
import { get_oidc_return_to } from './authRedirect.js';
const at = (pathname) => {
globalThis.window = { location: { pathname } };
const at = (pathname, search = '') => {
globalThis.window = { location: { pathname, search } };
return get_oidc_return_to();
};
const SHARE_UUID = '11111111-2222-3333-4444-555555555555';
const shared_path = (name) => `/alice/${SHARE_UUID}/${name}`;
/** `window.location.search` for a page opened by a share link. */
const share_search = (...paths) => {
const params = new URLSearchParams();
for ( const path of paths ) params.append('shared', path);
return `?${params.toString()}`;
};
afterEach(() => {
delete globalThis.window;
});
@@ -45,6 +55,29 @@ describe('get_oidc_return_to', () => {
expect(at('/desktop/app/editor/')).toBe('/desktop/app/editor');
});
it('carries a share link so the item survives the round trip', () => {
expect(at('/', share_search(shared_path('Report.pdf')))).toBe(
`/${share_search(shared_path('Report.pdf'))}`,
);
expect(at('/desktop', share_search(shared_path('Report.pdf')))).toBe(
`/desktop${share_search(shared_path('Report.pdf'))}`,
);
expect(
at('/', share_search(shared_path('a.txt'), shared_path('b.txt'))),
).toBe(`/${share_search(shared_path('a.txt'), shared_path('b.txt'))}`);
});
it('leaves behind everything that is not a share link', () => {
// a hand-edited value the backend would refuse anyway
expect(at('/', share_search('/alice/Documents/Report.pdf'))).toBe(null);
expect(at('/', '?shared=')).toBe(null);
// other parameters are not ours to carry
expect(at('/desktop', '?app=editor')).toBe('/desktop');
expect(
at('/desktop', `${share_search(shared_path('a.txt'))}&app=editor`),
).toBe(`/desktop${share_search(shared_path('a.txt'))}`);
});
it('returns null for anything the backend would reject', () => {
expect(at('/')).toBe(null);
expect(at('/settings')).toBe(null);