dsh-remote 0.4.0: edge auth – slaves log in to an auth proxy in front of the master
- lib/edge-auth.mjs: basic / bearer / oauth2 client_credentials (Authentik M2M) / custom headers (Cloudflare Access), proxy session cookie jar, retry with fresh token, clear edge-refusal vs DSH-refusal errors - link + enroll go through the edge; master.edgeAuth stored at pair time, secrets as DSH credential refs only; config edgeAuth override - /dsh-remote/edge-auth: show/change/probe; Settings UI section + pair form - pair.mjs / install-mac.sh: --edge … flags, secrets prompted via /dev/tty - tests: fake Authentik-like proxy for all modes + WebSocket through the edge
This commit is contained in:
1 parent
5dff41a3d0
commit
78fce8e1ce
12 files changed
+833
-40
No files matched your search
@@ -251,11 +251,89 @@ window.__ModuleLoader__.load({
|
||||
h('button', { style: S.btn, onClick: save }, 'Save sync policy'), ' ', msg && h('span', { style: { fontSize: 12, opacity: .75 } }, msg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth proxy (edge) credentials for reaching the master through Authentik /
|
||||
* oauth2-proxy / Cloudflare Access / Basic auth. Secrets are sent once and stored by
|
||||
* the host as DSH credential refs; the form never shows them again.
|
||||
*/
|
||||
function EdgeAuthFields({ value, onChange }) {
|
||||
const v = value || { mode: 'none' };
|
||||
const set = (patch) => onChange({ ...v, ...patch });
|
||||
const row = (label, el, hint) => h(Field, { label, hint }, el);
|
||||
const input = (key, props = {}) => h('input', { style: S.input, value: v[key] || '', onChange: (e) => set({ [key]: e.target.value }), ...props });
|
||||
return h('div', { style: { margin: '6px 0 10px', padding: '8px 10px', border: '1px dashed rgba(127,127,127,.35)', borderRadius: 8 } },
|
||||
row('Auth proxy in front of the master',
|
||||
h('select', { style: S.input, value: v.mode, onChange: (e) => onChange({ mode: e.target.value }) },
|
||||
h('option', { value: 'none' }, 'None (LAN / proxy exempts /dsh-remote/*)'),
|
||||
h('option', { value: 'basic' }, 'Basic: user + (app) password (Authentik, nginx, Traefik)'),
|
||||
h('option', { value: 'oauth2' }, 'OAuth2 client credentials (Authentik machine-to-machine)'),
|
||||
h('option', { value: 'bearer' }, 'Bearer token'),
|
||||
h('option', { value: 'headers' }, 'Custom headers (Cloudflare Access service token, …)')),
|
||||
'The slave logs in to the proxy itself, keeps the session cookie and replays it on the link.'),
|
||||
v.mode === 'basic' && h(React.Fragment, null,
|
||||
row('Username', input('username', { placeholder: 'e.g. dsh-remote-mac (Authentik service account)' })),
|
||||
row('Password / app password', input('password', { type: 'password', placeholder: '(stored as credential DSH_REMOTE_EDGE_PASSWORD)' }),
|
||||
'Authentik: Directory → Tokens and App passwords → create an App password for the user, and enable “Intercept header authentication” on the proxy provider.')),
|
||||
v.mode === 'oauth2' && h(React.Fragment, null,
|
||||
row('Token URL', input('tokenUrl', { placeholder: 'https://auth.rizzy.dev/application/o/token/' })),
|
||||
row('Client ID', input('clientId', { placeholder: 'proxy provider client_id' })),
|
||||
row('Username (Authentik service account)', input('username')),
|
||||
row('App password', input('password', { type: 'password' })),
|
||||
row('Client secret (optional)', input('clientSecret', { type: 'password' }))),
|
||||
v.mode === 'bearer' && row('Token', input('token', { type: 'password', placeholder: '(stored as credential DSH_REMOTE_EDGE_TOKEN)' })),
|
||||
v.mode === 'headers' && row('Headers (one per line, "Name: value")',
|
||||
h('textarea', { style: { ...S.input, minHeight: 60, fontFamily: 'ui-monospace,monospace' }, value: v.headersText || '', placeholder: 'CF-Access-Client-Id: xxx.access\nCF-Access-Client-Secret: yyy', onChange: (e) => set({ headersText: e.target.value }) })));
|
||||
}
|
||||
function edgeSpec(v) {
|
||||
if (!v || v.mode === 'none') return { mode: 'none' };
|
||||
if (v.mode === 'headers') {
|
||||
const headers = {};
|
||||
for (const line of String(v.headersText || '').split('\n')) {
|
||||
const at = line.indexOf(':');
|
||||
if (at > 0) headers[line.slice(0, at).trim()] = line.slice(at + 1).trim();
|
||||
}
|
||||
return { mode: 'headers', headers };
|
||||
}
|
||||
const { headersText, ...rest } = v;
|
||||
return rest;
|
||||
}
|
||||
|
||||
function EdgeAuthCard({ s }) {
|
||||
const [edit, setEdit] = useState(null);
|
||||
const [info, setInfo] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const run = async (body) => {
|
||||
setBusy(true);
|
||||
try { setInfo(body ? await post('edge-auth', body) : await api('edge-auth')); if (body) setEdit(null); }
|
||||
catch (e) { setInfo({ error: e.message }); }
|
||||
setBusy(false); store.refresh();
|
||||
};
|
||||
if (s.role !== 'slave' || !s.master?.url) return null;
|
||||
const cur = info?.edgeAuth || s.master.edgeAuth || { mode: 'none' };
|
||||
return h('div', { style: S.card },
|
||||
h('strong', null, 'Auth proxy (edge) login'),
|
||||
h('div', { style: { fontSize: 13, margin: '4px 0 8px' } },
|
||||
'Mode: ', h('code', null, cur.mode || 'none'),
|
||||
cur.username ? h('span', null, ' · user ', h('code', null, cur.username)) : null,
|
||||
cur.tokenValidUntil ? ` · token valid until ${new Date(cur.tokenValidUntil).toLocaleTimeString()}` : '',
|
||||
cur.cookies?.length ? ` · proxy session cookie: ${cur.cookies.join(', ')}` : ''),
|
||||
info?.probe && h('div', { style: { fontSize: 13, margin: '0 0 8px', color: info.probe.reachedDsh ? '#3fb950' : '#f85149' } },
|
||||
info.probe.reachedDsh ? '✓ The proxy lets this device through to the master DSH.' : `✗ ${info.probe.error}${info.probe.hint ? ` (${info.probe.hint})` : ''}`),
|
||||
info?.error && h('div', { style: S.err }, info.error),
|
||||
edit ? h('div', null,
|
||||
h(EdgeAuthFields, { value: edit, onChange: setEdit }),
|
||||
h('button', { style: S.btnPrimary, disabled: busy, onClick: () => run({ edgeAuth: edgeSpec(edit) }) }, busy ? 'Saving…' : 'Save & reconnect'), ' ',
|
||||
h('button', { style: S.btn, onClick: () => setEdit(null) }, 'Cancel'))
|
||||
: h('span', null,
|
||||
h('button', { style: S.btn, disabled: busy, onClick: () => run(null) }, busy ? 'Testing…' : 'Test'), ' ',
|
||||
h('button', { style: S.btn, onClick: () => setEdit({ mode: cur.mode || 'none', username: cur.username, tokenUrl: cur.tokenUrl, clientId: cur.clientId }) }, 'Change')));
|
||||
}
|
||||
function SettingsPanel() {
|
||||
const st = useStore();
|
||||
const [code, setCode] = useState(null);
|
||||
const [masterUrl, setMasterUrl] = useState('');
|
||||
const [pairCode, setPairCode] = useState('');
|
||||
const [edge, setEdge] = useState({ mode: 'none' });
|
||||
const [msg, setMsg] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [open, setOpen] = useState({});
|
||||
@@ -284,8 +362,9 @@ window.__ModuleLoader__.load({
|
||||
h(Field, { label: 'Master URL', hint: 'e.g. https://dsh.rizzy.dev or http://192.168.0.111:3080' },
|
||||
h('input', { style: S.input, value: masterUrl, placeholder: 'https://…', onChange: (e) => setMasterUrl(e.target.value) })),
|
||||
h(Field, { label: 'Pairing code' }, h('input', { style: { ...S.input, fontFamily: 'ui-monospace,monospace' }, value: pairCode, placeholder: 'ABCD-EFGH-…', onChange: (e) => setPairCode(e.target.value.toUpperCase()) })),
|
||||
h(EdgeAuthFields, { value: edge, onChange: setEdge }),
|
||||
h('button', { style: S.btnPrimary, disabled: busy || !masterUrl || !pairCode, onClick: () => act(async () => {
|
||||
const r = await post('pair', { masterUrl, code: pairCode });
|
||||
const r = await post('pair', { masterUrl, code: pairCode, edgeAuth: edgeSpec(edge) });
|
||||
setMsg(r.restartRequired ? 'Paired. Restart DSH on this machine to switch it to slave mode.' : `Paired with ${r.master?.name}.`);
|
||||
}) }, 'Pair'),
|
||||
s.master?.url && h('span', null, ' ', h('button', { style: S.btn, disabled: busy, onClick: () => act(() => post('unpair', {})) }, 'Unpair')));
|
||||
@@ -321,7 +400,7 @@ window.__ModuleLoader__.load({
|
||||
}) }, s.role === 'master' ? 'Push sync to all devices' : 'Sync now'),
|
||||
syncRes && s.role === 'master' && h('span', { style: { fontSize: 12, opacity: .7 } }, ` notified ${syncRes.pushed} device(s)`));
|
||||
|
||||
return h('div', { 'data-dsh-plugin': 'dsh-remote' }, header, pairing, devices, syncCard, msg && h('div', { style: { ...S.card, fontSize: 13 } }, msg));
|
||||
return h('div', { 'data-dsh-plugin': 'dsh-remote' }, header, pairing, h(EdgeAuthCard, { s }), devices, syncCard, msg && h('div', { style: { ...S.card, fontSize: 13 } }, msg));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ toasts
|
||||
|
||||
Reference in new issue
Block a user