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
2 changes: 1 addition & 1 deletion core.ref
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a8f017b4781c4ffe28870a4e79db69434379c176
7dfbc3d3b766fb5688903d1943d4050132ad064d
12 changes: 12 additions & 0 deletions src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@
"quietNoPortal": "Das Portal ist erreichbar, sobald das VPN verbunden ist.",
"killSwitchNote": "Hinweis zum Kill-Switch",
"saveFailed": "Einstellung konnte nicht gespeichert werden: {{ error }}"
},
"signin": {
"title": "Mit Firmenkonto anmelden",
"desc": "Statt mit einem Einrichtungscode: mit deinem Konto bei",
"microsoft": "Mit Microsoft anmelden",
"user": "E-Mail oder Benutzername",
"password": "Passwort",
"totp": "Bestätigungscode",
"totpHint": "6 Ziffern aus deiner Authenticator-App",
"submit": "Anmelden",
"running": "Anmeldung läuft …",
"browser": "Bitte melde dich im Browser an. Danach geht es hier von selbst weiter."
}
},
"vault": {
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@
"quietNoPortal": "The portal is available once the VPN is connected.",
"killSwitchNote": "Kill switch note",
"saveFailed": "Setting could not be saved: {{ error }}"
},
"signin": {
"title": "Sign in with company account",
"desc": "Instead of a setup code: with your account on",
"microsoft": "Sign in with Microsoft",
"user": "Email or username",
"password": "Password",
"totp": "Verification code",
"totpHint": "6 digits from your authenticator app",
"submit": "Sign in",
"running": "Signing in …",
"browser": "Please sign in in your browser. The app continues on its own afterwards."
}
},
"vault": {
Expand Down
26 changes: 26 additions & 0 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,32 @@ <h3 class="set-card-title" data-i18n="settings.server">Server</h3>
<div class="field-status" id="server-status" role="status" hidden></div>
</section>

<!-- Company sign-in: only when the server in "Server-URL" offers it (GET /api/v1/client/signin) -->
<section class="card set-card" id="signin-card" hidden>
<h3 class="set-card-title" data-i18n="ui.signin.title">Mit Firmenkonto anmelden</h3>
<p class="set-card-desc"><span data-i18n="ui.signin.desc">Statt mit einem Einrichtungscode: mit deinem Konto bei</span> <span class="mono" id="signin-host"></span></p>
<div class="field-actions" id="signin-ms" hidden></div>
<div class="field" id="signin-user-field">
<label for="signin-user" data-i18n="ui.signin.user">E-Mail oder Benutzername</label>
<input class="inp" id="signin-user" type="text" spellcheck="false" autocomplete="username" maxlength="256">
</div>
<div class="field" id="signin-pass-field">
<label for="signin-pass" data-i18n="ui.signin.password">Passwort</label>
<input class="inp" id="signin-pass" type="password" autocomplete="current-password" maxlength="1024">
<span class="field-hint" id="signin-hint" hidden></span>
</div>
<div class="field" id="signin-totp-field" hidden>
<label for="signin-totp" data-i18n="ui.signin.totp">Bestätigungscode</label>
<input class="inp mono" id="signin-totp" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="7" placeholder="123456" spellcheck="false">
<span class="field-hint" data-i18n="ui.signin.totpHint">6 Ziffern aus deiner Authenticator-App</span>
</div>
<div class="field-actions">
<button class="btn btn-pri" id="btn-signin" type="button" disabled><span data-i18n="ui.signin.submit">Anmelden</span></button>
<button class="btn btn-sec" id="btn-signin-cancel" type="button" hidden><span data-i18n="action.cancel">Abbrechen</span></button>
</div>
<div class="field-status" id="signin-status" role="status" hidden></div>
</section>

<section class="card set-card">
<h3 class="set-card-title" data-i18n="settings.importConfig">Konfiguration importieren</h3>
<p class="set-card-desc" data-i18n="ui.settings.importDesc"></p>
Expand Down
197 changes: 196 additions & 1 deletion src/renderer/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* UI-Logik und State Management
*/

const { tunnel, server, config, killSwitch, rdpAllow, autostart, logs, update, services, traffic, dns, shell, peer, permissions, onPortalUrl, portal, getVersion, getDeviceId, window: win, locale, policy: clientPolicy, notify: notifyApi, chat: chatApi, school: schoolApi, gatedrop: gatedropApi, vault: vaultApi } = window.gatecontrol;
const { tunnel, server, config, killSwitch, rdpAllow, autostart, logs, update, services, traffic, dns, shell, peer, permissions, onPortalUrl, portal, getVersion, getDeviceId, window: win, locale, policy: clientPolicy, notify: notifyApi, chat: chatApi, school: schoolApi, gatedrop: gatedropApi, vault: vaultApi, signin: signinApi } = window.gatecontrol;
const { t } = window.gatecontrol.i18n;

// Aktive Berechtigungen (werden beim Connect geladen)
Expand Down Expand Up @@ -217,6 +217,9 @@ function navigateTo(page) {
// Vault: footer in the status bar, secrets leave the screen with the page
if (page === 'vault' && prev !== 'vault') vaultView?.show();
else if (prev === 'vault' && page !== 'vault') vaultView?.hide();
// Company sign-in: offered for the server in "Server-URL"; a waiting browser sign-in ends with the page
if (page === 'settings' && prev !== 'settings') lookupSignin({ keep: true });
else if (prev === 'settings' && page !== 'settings') cancelSignin();
}

// Navigation aus dem Main Process
Expand Down Expand Up @@ -609,6 +612,196 @@ function showServerStatus(message, type) {
}
}

// ── Settings: Company sign-in ("Mit Firmenkonto anmelden") ──
// GateControl ≥ 1.201.0: for the address in "Server-URL" the app asks the
// server which sign-in methods it offers. Older servers (404) or no answer →
// the card stays hidden. Password and Microsoft sign-in end with a setup code
// that the main process redeems through the normal setup-code path; the page
// never sees the code.
const SIGNIN_LOOKUP_DELAY_MS = 500;
let signinOffer = null; // { url, methods }
let signinLookupUrl = '';
let signinLookupId = 0;
let signinLookupTimer = null;
let signinBusy = null; // null | 'password' | 'browser'

function signinServerUrl() {
const url = el.serverUrl.value.trim();
if (!url) return '';
return /^https?:\/\//i.test(url) ? url : `https://${url}`;
}

function signinHost(url) {
try { return new URL(url).host; } catch { return url; }
}

/** keep: do not ask again when the address did not change (opening Settings). */
function lookupSignin({ keep = false } = {}) {
const url = signinServerUrl();
if (keep && url === signinLookupUrl) return;
if (signinBusy) return;
clearTimeout(signinLookupTimer);
const id = ++signinLookupId;
signinLookupUrl = url;
signinOffer = null;
$('#signin-card').hidden = true;
if (!url || !signinApi) return;
signinLookupTimer = setTimeout(async () => {
let methods = null;
try { methods = await signinApi.methods(url); } catch { methods = null; }
if (id !== signinLookupId) return; // the address changed meanwhile
if (methods && methods.available) {
signinOffer = { url, methods };
renderSignin();
}
}, keep ? 0 : SIGNIN_LOOKUP_DELAY_MS);
}

function cancelSignin() {
if (signinBusy === 'browser' && signinApi) signinApi.cancel().catch(() => {});
}

function renderSignin() {
const card = $('#signin-card');
if (!signinOffer || policyState.locks?.server) {
card.hidden = true;
return;
}
const { url, methods } = signinOffer;
$('#signin-host').textContent = signinHost(url);

const ms = $('#signin-ms');
ms.textContent = '';
methods.microsoft.forEach((m) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-sec';
btn.dataset.id = String(m.id);
const label = document.createElement('span');
label.dataset.i18n = 'ui.signin.microsoft';
label.textContent = t('ui.signin.microsoft');
btn.appendChild(label);
if (m.label) {
const sub = document.createElement('span');
sub.className = 'muted';
sub.textContent = `· ${m.label}`;
btn.appendChild(sub);
}
btn.addEventListener('click', () => signinMicrosoft(m.id));
ms.appendChild(btn);
});
ms.hidden = methods.microsoft.length === 0;

const pw = methods.password.enabled;
$('#signin-user-field').hidden = !pw;
$('#signin-pass-field').hidden = !pw;
$('#btn-signin').hidden = !pw;
const hint = $('#signin-hint');
hint.textContent = methods.password.hint || '';
hint.hidden = !methods.password.hint;
$('#signin-totp-field').hidden = true;
$('#signin-totp').value = '';
$('#signin-status').hidden = true;
card.hidden = false;
setSigninBusy(null);
}

function updateSigninSubmit() {
const totpOn = !$('#signin-totp-field').hidden;
const ok = $('#signin-user').value.trim() && $('#signin-pass').value
&& (!totpOn || /^\d{6}$/.test($('#signin-totp').value.replace(/\s/g, '')));
$('#btn-signin').disabled = !!signinBusy || !ok;
}

function setSigninBusy(mode) {
signinBusy = mode;
$$('#signin-ms .btn').forEach((b) => { b.disabled = !!mode; });
['#signin-user', '#signin-pass', '#signin-totp'].forEach((sel) => { $(sel).disabled = !!mode; });
$('#btn-signin-cancel').hidden = mode !== 'browser';
updateSigninSubmit();
}

function showSigninStatus(message, type) {
const status = $('#signin-status');
status.hidden = false;
status.textContent = message;
status.className = `field-status ${type}`;
}

function signinDone(result) {
if (result.success) {
const url = signinOffer.url;
el.serverUrl.value = url;
el.apiKey.value = '';
view.serverUrl = url;
renderOverviewHeader();
updateSideSub();
$('#signin-pass').value = '';
$('#signin-totp').value = '';
$('#signin-totp-field').hidden = true;
updateSigninSubmit();
showSigninStatus(t('server.enrolled', { peerId: result.peerId }), 'success');
return;
}
if (result.cancelled) {
$('#signin-status').hidden = true;
return;
}
if (result.totpRequired) {
const field = $('#signin-totp-field');
const first = field.hidden;
field.hidden = false;
$('#signin-totp').value = '';
updateSigninSubmit();
$('#signin-totp').focus();
showSigninStatus(result.error, first && result.code === 'totp_required' ? 'info' : 'error');
return;
}
showSigninStatus(result.error || t('server.signin.failedCode', { error: 'unknown' }), 'error');
}

async function signinPassword() {
if (signinBusy || !signinOffer || $('#btn-signin').disabled) return;
const totpOn = !$('#signin-totp-field').hidden;
showSigninStatus(t('ui.signin.running'), 'info');
setSigninBusy('password');
let result;
try {
result = await signinApi.password({
url: signinOffer.url,
username: $('#signin-user').value.trim(),
password: $('#signin-pass').value,
totp: totpOn ? $('#signin-totp').value.replace(/\s/g, '') : undefined,
});
} catch (err) {
result = { success: false, error: err.message };
}
setSigninBusy(null);
signinDone(result);
}

async function signinMicrosoft(id) {
if (signinBusy || !signinOffer) return;
showSigninStatus(t('ui.signin.browser'), 'info');
setSigninBusy('browser');
let result;
try {
result = await signinApi.microsoft({ url: signinOffer.url, id });
} catch (err) {
result = { success: false, error: err.message };
}
setSigninBusy(null);
signinDone(result);
}

el.serverUrl.addEventListener('input', () => lookupSignin());
$('#btn-signin').addEventListener('click', signinPassword);
$('#btn-signin-cancel').addEventListener('click', cancelSignin);
['#signin-user', '#signin-pass', '#signin-totp'].forEach((sel) => {
$(sel).addEventListener('input', updateSigninSubmit);
$(sel).addEventListener('keydown', (e) => { if (e.key === 'Enter') signinPassword(); });
});

// ── Settings: Config Import ──────────────────────────────
$('#btn-import-file').addEventListener('click', async () => {
const result = await config.importFile();
Expand Down Expand Up @@ -1584,6 +1777,8 @@ function applyPolicyUi() {
const importCard = $('#btn-import-file')?.closest('section');
if (serverCard) serverCard.hidden = !!l.server;
if (importCard) importCard.hidden = !!l.server;
if (l.server) cancelSignin();
$('#signin-card').hidden = !!l.server || !signinOffer;
const serverHint = $('#policy-server-hint');
if (serverHint) {
serverHint.hidden = !l.server;
Expand Down
Loading
Loading