headscale: share the invite's https link, drop the deep link
This commit is contained in:
@@ -95,13 +95,25 @@ async function relay(res: Response | string, wrap: (body: Record<string, unknown
|
|||||||
return Response.json(wrap(body));
|
return Response.json(wrap(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `POST /_officer/enroll/invites` — mint an invite. The response carries the link, and only this once. */
|
/**
|
||||||
|
* `POST /_officer/enroll/invites` — mint an invite. The response carries the link, and only this once.
|
||||||
|
*
|
||||||
|
* `url` is an ordinary HTTPS link to a page on the server's own domain, which bounces into the app; the
|
||||||
|
* companion also returns `deepLink`, the `officer-offscale://` scheme that page redirects to. That one is
|
||||||
|
* dropped here rather than passed on: it carries the same claim token in its fragment, and a second copy of
|
||||||
|
* a single-use credential in the browser is a second chance to leak it. Nothing on our side opens it.
|
||||||
|
*/
|
||||||
async function create(creds: HeadscaleServerCredentials, ctx: OfficerContext): Promise<Response> {
|
async function create(creds: HeadscaleServerCredentials, ctx: OfficerContext): Promise<Response> {
|
||||||
const input = parseCreate(await readJson(ctx.req));
|
const input = parseCreate(await readJson(ctx.req));
|
||||||
if (input instanceof Response) return input;
|
if (input instanceof Response) return input;
|
||||||
|
|
||||||
const res = await callCompanion(creds, { path: INVITES_PATH, method: 'POST', body: input });
|
const res = await callCompanion(creds, { path: INVITES_PATH, method: 'POST', body: input });
|
||||||
return relay(res, (body) => ({ available: true, invite: body.invite ?? body }));
|
return relay(res, (body) => {
|
||||||
|
const raw = body.invite ?? body;
|
||||||
|
const invite = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
|
||||||
|
const { deepLink: _deepLink, ...rest } = invite;
|
||||||
|
return { available: true, invite: rest };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -80,13 +80,12 @@ type InviteLinkPanelProps = { invite: HeadscaleInviteCreated; onDismiss: () => v
|
|||||||
const InviteLinkPanel = ({ invite, onDismiss }: InviteLinkPanelProps) => {
|
const InviteLinkPanel = ({ invite, onDismiss }: InviteLinkPanelProps) => {
|
||||||
const [showQr, setShowQr] = useState(true);
|
const [showQr, setShowQr] = useState(true);
|
||||||
|
|
||||||
// The link goes in `text`, not `url`: it is a custom scheme (`officer-offscale://join#…`) and several
|
// Plain https now — the link lands on a page the companion serves, which bounces into the app. It goes in
|
||||||
// share-sheet implementations only accept http(s) in the url field, rejecting the whole call. As text it
|
// `url` rather than `text` so share targets treat it as a link and preserve the fragment. A cancelled sheet
|
||||||
// is passed through verbatim by every messenger. A cancelled sheet rejects too — nothing to report there,
|
// rejects — nothing to report there, the link is still on screen.
|
||||||
// the link is still on screen.
|
|
||||||
const share = () => {
|
const share = () => {
|
||||||
void navigator
|
void navigator
|
||||||
.share?.({ title: 'Join the tailnet', text: `Tap to join as ${invite.user}: ${invite.url}` })
|
.share?.({ title: 'Join the tailnet', text: `Tap to join as ${invite.user}`, url: invite.url })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user