v0.165.0: Indítópult megosztása — guest launcher via capability URL (+ optional password, QR)
Mint a 160-bit capability URL (/s/<token>) serving a standalone read-only guest launcher: same tiles, opens apps in new tabs, no account, no admin session. Information only, zero control — every privilege stays behind each app's own auth. - /s/ pre-auth pass-through (after the claim gate) + session-CSRF exemption; guest password POST carries its own pre-auth HMAC CSRF. - Constant-time token match; empty stored token = disabled = byte-identical mux 404. - Optional per-share password: separate bcrypt hash + own attempt map; signed cookie = HMAC(token|passwordHash) keyed with web.session_secret, so rotate/change invalidates. - Guest labels ride the v0.164.0 ruling; never expose internal state vocabulary. - Token redacted in logs (/s/<redacted>); never in CHANGELOG/REPORT/CONTEXT. - Admin modal: copy-link, QR (go-qrcode), set/clear password, rotate, disable. - Tests: Groups A-G (14) + 3 red-proofs verified red.
This commit is contained in:
@@ -1,31 +1,41 @@
|
||||
{{define "launch_tile"}}
|
||||
<span class="launch-tile{{if .Off}} launch-tile--off{{end}}" style="background: {{tileColor .Slug .BrandColor}}">
|
||||
<span class="launch-mono">{{initial .DisplayName}}</span>
|
||||
<img class="launch-logo" src="{{logoURL .Slug}}" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
{{end}}
|
||||
|
||||
{{define "launcher"}}
|
||||
{{template "layout_start" .}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Indítópult</h2>
|
||||
<span class="domain-badge">{{.Domain}}</span>
|
||||
<button type="button" class="btn btn-outline share-open-btn" onclick="openShareModal()"><svg class="ico"><use href="#i-share"/></svg>Indítópult megosztása</button>
|
||||
</div>
|
||||
|
||||
{{if .ShareFlash}}
|
||||
<div class="alerts-container">
|
||||
<div class="alert-banner alert-banner-info">
|
||||
<span class="alert-icon"><svg class="ico"><use href="#i-info"/></svg></span>
|
||||
<span class="alert-message">{{.ShareFlash}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Apps}}
|
||||
<div class="launch-grid">
|
||||
{{range .Apps}}
|
||||
{{if isOperational .State}}
|
||||
<a class="launch-cell" href="https://{{.Subdomain}}.{{$.Domain}}{{.OpenPath}}" target="_blank" rel="noopener">
|
||||
<span class="launch-tile" style="background: {{tileColor .Slug .BrandColor}}">
|
||||
<span class="launch-mono">{{initial .DisplayName}}</span>
|
||||
<img class="launch-logo" src="{{logoURL .Slug}}" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
{{template "launch_tile" (dict "Slug" .Slug "BrandColor" .BrandColor "DisplayName" .DisplayName "Off" false)}}
|
||||
<span class="launch-name">{{.DisplayName}}</span>
|
||||
{{if ne (stateStr .State) "running"}}<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>{{end}}
|
||||
</a>
|
||||
{{else}}
|
||||
<div class="launch-cell launch-cell--off">
|
||||
<span class="launch-tile launch-tile--off" style="background: {{tileColor .Slug .BrandColor}}">
|
||||
<span class="launch-mono">{{initial .DisplayName}}</span>
|
||||
<img class="launch-logo" src="{{logoURL .Slug}}" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
{{template "launch_tile" (dict "Slug" .Slug "BrandColor" .BrandColor "DisplayName" .DisplayName "Off" true)}}
|
||||
<span class="launch-name">{{.DisplayName}}</span>
|
||||
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
|
||||
</div>
|
||||
@@ -39,5 +49,87 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="modal-overlay" id="share-modal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<h3>Indítópult megosztása</h3>
|
||||
<p class="share-lede">Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.</p>
|
||||
{{if .ShareEnabled}}
|
||||
<div class="share-field">
|
||||
<label class="share-label">Megosztási link</label>
|
||||
<div class="share-link-row">
|
||||
<input id="share-link-input" type="text" readonly value="{{.ShareURL}}">
|
||||
<button type="button" id="share-copy-btn" class="btn btn-outline" onclick="copyShareLink()">Link másolása</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="share-qr">
|
||||
<img src="/launcher/share/qr.png" alt="QR-kód a megosztási linkhez" width="200" height="200">
|
||||
<span class="share-qr-hint">Olvassa be telefonnal a gyors megnyitáshoz.</span>
|
||||
</div>
|
||||
<div class="share-field">
|
||||
{{if .SharePasswordSet}}
|
||||
<label class="share-label">Jelszó</label>
|
||||
<p class="share-note">A megosztás jelszóval védett.</p>
|
||||
<form method="POST" action="/launcher/share/password">
|
||||
{{.CSRFField}}
|
||||
<input type="hidden" name="action" value="clear">
|
||||
<button type="submit" class="btn btn-outline">Jelszó törlése</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<form method="POST" action="/launcher/share/password">
|
||||
{{.CSRFField}}
|
||||
<label class="share-label">Jelszó (nem kötelező)</label>
|
||||
<div class="share-link-row">
|
||||
<input type="password" name="password" autocomplete="new-password" placeholder="Legalább 8 karakter">
|
||||
<button type="submit" class="btn btn-outline">Jelszó beállítása</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="share-actions">
|
||||
<form method="POST" action="/launcher/share/rotate">
|
||||
{{.CSRFField}}
|
||||
<button type="submit" class="btn btn-outline" data-confirm="Új link készítése? A régi link és minden korábbi belépés érvénytelenné válik.">Új link készítése</button>
|
||||
</form>
|
||||
<form method="POST" action="/launcher/share/disable">
|
||||
{{.CSRFField}}
|
||||
<button type="submit" class="btn btn-danger" data-confirm="Biztosan kikapcsolja a megosztást? A link azonnal érvénytelenné válik.">Megosztás kikapcsolása</button>
|
||||
</form>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="share-note">A megosztás jelenleg ki van kapcsolva.</p>
|
||||
<form method="POST" action="/launcher/share/enable">
|
||||
{{.CSRFField}}
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeShareModal()">Bezárás</button>
|
||||
<button type="submit" class="btn btn-primary">Megosztás bekapcsolása</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if .ShareEnabled}}
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeShareModal()">Bezárás</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='flex';}
|
||||
function closeShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='none';}
|
||||
function copyShareLink(){
|
||||
var inp=document.getElementById('share-link-input');
|
||||
var btn=document.getElementById('share-copy-btn');
|
||||
if(!inp)return;
|
||||
function done(){if(btn){var o=btn.textContent;btn.textContent='Másolva';setTimeout(function(){btn.textContent=o;},1500);}}
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(inp.value).then(done,function(){inp.select();done();});}
|
||||
else{inp.select();try{document.execCommand('copy');}catch(e){}done();}
|
||||
}
|
||||
(function(){
|
||||
var m=document.getElementById('share-modal');
|
||||
if(m){m.addEventListener('click',function(e){if(e.target===m)closeShareModal();});}
|
||||
{{if .ShareFlash}}openShareModal();{{end}}
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{template "layout_end"}}
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{{define "launcher_share_password"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>Indítópult</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="guest-body guest-gate-body">
|
||||
<main class="guest-gate">
|
||||
<div class="guest-gate-card">
|
||||
<h2>Indítópult</h2>
|
||||
<p>Ez az oldal jelszóval védett.</p>
|
||||
{{if .Error}}<div class="gate-error">{{.Error}}</div>{{end}}
|
||||
<form method="POST" action="{{.Action}}">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<input type="password" name="password" placeholder="Jelszó" autocomplete="current-password" autofocus>
|
||||
<button type="submit" class="btn btn-primary">Belépés</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,43 @@
|
||||
{{define "launcher_shared"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>Indítópult</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="guest-body">
|
||||
<main class="guest-content">
|
||||
<div class="page-header">
|
||||
<h2>Indítópult</h2>
|
||||
<span class="domain-badge">{{.Domain}}</span>
|
||||
</div>
|
||||
{{if .Apps}}
|
||||
<div class="launch-grid">
|
||||
{{range .Apps}}
|
||||
{{if .Clickable}}
|
||||
<a class="launch-cell" href="{{.Href}}" target="_blank" rel="noopener noreferrer">
|
||||
{{template "launch_tile" (dict "Slug" .Slug "BrandColor" .BrandColor "DisplayName" .DisplayName "Off" false)}}
|
||||
<span class="launch-name">{{.DisplayName}}</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<div class="launch-cell launch-cell--off">
|
||||
{{template "launch_tile" (dict "Slug" .Slug "BrandColor" .BrandColor "DisplayName" .DisplayName "Off" true)}}
|
||||
<span class="launch-name">{{.DisplayName}}</span>
|
||||
<span class="launch-off-label">{{.Label}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<p>Jelenleg nincs elérhető alkalmazás.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -1181,6 +1181,28 @@ a.stat-card:hover {
|
||||
.launch-cell--off { cursor: default; }
|
||||
.launch-tile--off { opacity: .4; }
|
||||
.launch-cell--off .launch-name { color: var(--text-3); }
|
||||
.launch-off-label { font-size: .8rem; color: var(--text-3); text-align: center; }
|
||||
|
||||
/* Launcher share modal (v0.165.0) */
|
||||
.share-open-btn { margin-left: auto; }
|
||||
.share-lede { color: var(--text-2); font-size: .9rem; margin-bottom: 1rem; }
|
||||
.share-field { margin-bottom: 1rem; }
|
||||
.share-label { display: block; font-size: .85rem; color: var(--text-2); margin-bottom: .35rem; }
|
||||
.share-note { color: var(--text-2); font-size: .9rem; }
|
||||
.share-link-row { display: flex; gap: .5rem; align-items: center; }
|
||||
.share-link-row input { flex: 1; min-width: 0; }
|
||||
.share-qr { display: flex; flex-direction: column; align-items: center; gap: .35rem; margin-bottom: 1rem; }
|
||||
.share-qr img { border: 1px solid var(--line); border-radius: var(--radius); background: #fff; padding: .35rem; }
|
||||
.share-qr-hint { color: var(--text-3); font-size: .8rem; }
|
||||
.share-actions { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .5rem; }
|
||||
|
||||
/* Standalone guest launcher + password gate (v0.165.0) — no sidebar/nav */
|
||||
.guest-content { max-width: 960px; margin: 0 auto; padding: 2rem 1.25rem; }
|
||||
.guest-gate-body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
||||
.guest-gate { width: 100%; display: flex; justify-content: center; padding: 1.25rem; }
|
||||
.guest-gate-card { width: 100%; max-width: 360px; background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.75rem; text-align: center; }
|
||||
.guest-gate-card form { display: flex; flex-direction: column; gap: .75rem; margin-top: 1rem; }
|
||||
.gate-error { color: var(--crit); font-size: .9rem; margin-top: .5rem; }
|
||||
|
||||
/* Login page */
|
||||
.login-body {
|
||||
|
||||
Reference in New Issue
Block a user