mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-25 06:36:00 +00:00
Merge remote-tracking branch 'origin/main' into juancastro/put-1568-rename-srcguisrchelpers-files-to-camelcase
This commit is contained in:
@@ -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()}`;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user