mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-03-18 21:30:44 +01:00
More refining
This commit is contained in:
@@ -282,7 +282,9 @@ func execServe(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check values
|
// Check values
|
||||||
if databaseURL != "" && (authFile != "" || cacheFile != "" || webPushFile != "") {
|
if databaseURL != "" && !strings.HasPrefix(databaseURL, "postgres://") {
|
||||||
|
return errors.New("if database-url is set, it must start with postgres://")
|
||||||
|
} else if databaseURL != "" && (authFile != "" || cacheFile != "" || webPushFile != "") {
|
||||||
return errors.New("if database-url is set, auth-file, cache-file, and web-push-file must not be set")
|
return errors.New("if database-url is set, auth-file, cache-file, and web-push-file must not be set")
|
||||||
} else if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
} else if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||||
return errors.New("if set, FCM key file must exist")
|
return errors.New("if set, FCM key file must exist")
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||||||
<div class="cg-modal-dialog">
|
<div class="cg-modal-dialog">
|
||||||
<div class="cg-modal-header">
|
<div class="cg-modal-header">
|
||||||
<div class="cg-modal-header-left">
|
<div class="cg-modal-header-left">
|
||||||
<span class="cg-modal-title">Config generator</span>
|
<span class="cg-modal-title">Config generator</span><span class="cg-badge-beta">BETA</span>
|
||||||
<span class="cg-modal-desc">This generator helps you configure your self-hosted ntfy instance. It's not fully featured, but it is a good starting point.</span>
|
<span class="cg-modal-desc">This generator helps you configure your self-hosted ntfy instance. It's not fully featured, but it is a good starting point.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-modal-header-actions">
|
<div class="cg-modal-header-actions">
|
||||||
@@ -178,10 +178,11 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field cg-inline-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Is this an open or private server? <a href="/config/#access-control" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
<label>Will this ntfy server be open or private? <a href="/config/#access-control" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
||||||
<div class="cg-btn-group">
|
<div class="cg-btn-group">
|
||||||
<label><input type="radio" name="cg-server-type" value="open" checked><span>Open</span></label>
|
<label><input type="radio" name="cg-server-type" value="open" checked><span>Open</span></label>
|
||||||
<label><input type="radio" name="cg-server-type" value="private"><span>Private</span></label>
|
<label><input type="radio" name="cg-server-type" value="private"><span>Private</span></label>
|
||||||
|
<label><input type="radio" name="cg-server-type" value="custom"><span>Custom</span></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field cg-inline-field">
|
<div class="cg-field cg-inline-field">
|
||||||
@@ -210,7 +211,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field cg-inline-field" id="cg-wizard-db" style="display:none">
|
<div class="cg-field cg-inline-field" id="cg-wizard-db" style="display:none">
|
||||||
<label>Which database backend? <a href="/config/#database-options" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
<label>Which database backend would you like to use? <a href="/config/#database-options" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
||||||
<div class="cg-btn-group">
|
<div class="cg-btn-group">
|
||||||
<label><input type="radio" name="cg-db-type" value="sqlite" checked><span>SQLite</span></label>
|
<label><input type="radio" name="cg-db-type" value="sqlite" checked><span>SQLite</span></label>
|
||||||
<label><input type="radio" name="cg-db-type" value="postgres"><span>PostgreSQL</span></label>
|
<label><input type="radio" name="cg-db-type" value="postgres"><span>PostgreSQL</span></label>
|
||||||
@@ -224,7 +225,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||||||
<input type="text" data-key="auth-file" placeholder="/var/lib/ntfy/auth.db">
|
<input type="text" data-key="auth-file" placeholder="/var/lib/ntfy/auth.db">
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field cg-inline-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>What is the default access policy? <a href="/config/#access-control" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
<label>What should the default access policy be? <a href="/config/#access-control" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
||||||
<select id="cg-default-access-select">
|
<select id="cg-default-access-select">
|
||||||
<option value="read-write" selected>Read & Write</option>
|
<option value="read-write" selected>Read & Write</option>
|
||||||
<option value="read-only">Read Only</option>
|
<option value="read-only">Read Only</option>
|
||||||
@@ -233,14 +234,15 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field cg-inline-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Enable login page?</label>
|
<label>Should login to the web app be enabled?</label>
|
||||||
<div class="cg-btn-group">
|
<div class="cg-btn-group">
|
||||||
<label><input type="radio" name="cg-enable-login" value="no" checked><span>No</span></label>
|
<label><input type="radio" name="cg-login-mode" value="disabled" checked><span>Disabled</span></label>
|
||||||
<label><input type="radio" name="cg-enable-login" value="yes"><span>Yes</span></label>
|
<label><input type="radio" name="cg-login-mode" value="enabled"><span>Enabled</span></label>
|
||||||
|
<label><input type="radio" name="cg-login-mode" value="required"><span>Required</span></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field cg-inline-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Enable signup?</label>
|
<label>Should it be possible to sign up via the web app?</label>
|
||||||
<div class="cg-btn-group">
|
<div class="cg-btn-group">
|
||||||
<label><input type="radio" name="cg-enable-signup" value="no" checked><span>No</span></label>
|
<label><input type="radio" name="cg-enable-signup" value="no" checked><span>No</span></label>
|
||||||
<label><input type="radio" name="cg-enable-signup" value="yes"><span>Yes</span></label>
|
<label><input type="radio" name="cg-enable-signup" value="yes"><span>Yes</span></label>
|
||||||
@@ -248,6 +250,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||||||
</div>
|
</div>
|
||||||
<input type="hidden" data-key="auth-default-access">
|
<input type="hidden" data-key="auth-default-access">
|
||||||
<input type="checkbox" data-key="enable-login" id="cg-enable-login-hidden" style="display:none">
|
<input type="checkbox" data-key="enable-login" id="cg-enable-login-hidden" style="display:none">
|
||||||
|
<input type="checkbox" data-key="require-login" id="cg-require-login-hidden" style="display:none">
|
||||||
<input type="checkbox" data-key="enable-signup" id="cg-enable-signup-hidden" style="display:none">
|
<input type="checkbox" data-key="enable-signup" id="cg-enable-signup-hidden" style="display:none">
|
||||||
<div class="cg-field">
|
<div class="cg-field">
|
||||||
<label>Provisioned users <a href="/config/#users-and-roles" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
<label>Provisioned users <a href="/config/#users-and-roles" target="_blank" class="cg-help"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247m2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg></a></label>
|
||||||
@@ -297,56 +300,59 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||||||
</div>
|
</div>
|
||||||
<div class="cg-panel" id="cg-panel-webpush">
|
<div class="cg-panel" id="cg-panel-webpush">
|
||||||
<div class="cg-panel-desc">Enable browser push notifications via the Web Push API. VAPID keys are generated automatically. See <a href="/config/#web-push" target="_blank">web push</a> for details.</div>
|
<div class="cg-panel-desc">Enable browser push notifications via the Web Push API. VAPID keys are generated automatically. See <a href="/config/#web-push" target="_blank">web push</a> for details.</div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Public key</label>
|
<label>Where should web push data be stored?</label>
|
||||||
<input type="text" data-key="web-push-public-key" placeholder="Public key" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="cg-field">
|
|
||||||
<label>Private key</label>
|
|
||||||
<input type="text" data-key="web-push-private-key" placeholder="Private key" readonly>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="cg-regen-keys" class="cg-btn-add" style="margin-bottom:12px">Regenerate keys</button>
|
|
||||||
<div class="cg-field">
|
|
||||||
<label>Web push file</label>
|
|
||||||
<input type="text" data-key="web-push-file" placeholder="/var/lib/ntfy/webpush.db">
|
<input type="text" data-key="web-push-file" placeholder="/var/lib/ntfy/webpush.db">
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Email address</label>
|
<label>Contact email address</label>
|
||||||
<input type="text" data-key="web-push-email-address" placeholder="admin@example.com">
|
<input type="text" data-key="web-push-email-address" placeholder="admin@example.com">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cg-field cg-inline-field">
|
||||||
|
<label>Private key</label>
|
||||||
|
<input type="text" data-key="web-push-private-key" placeholder="Auto-generated" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="cg-field cg-inline-field">
|
||||||
|
<label>Public key</label>
|
||||||
|
<input type="text" data-key="web-push-public-key" placeholder="Auto-generated" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="cg-field cg-inline-field">
|
||||||
|
<label></label>
|
||||||
|
<button type="button" id="cg-regen-keys" class="cg-btn-add">Regenerate keys</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-panel" id="cg-panel-email">
|
<div class="cg-panel" id="cg-panel-email">
|
||||||
<div class="cg-panel-desc">Configure outgoing email notifications and/or incoming email publishing. See <a href="/config/#e-mail-notifications" target="_blank">email notifications</a> and <a href="/config/#e-mail-publishing" target="_blank">email publishing</a> for details.</div>
|
<div class="cg-panel-desc">Configure outgoing email notifications and/or incoming email publishing. See <a href="/config/#e-mail-notifications" target="_blank">email notifications</a> and <a href="/config/#e-mail-publishing" target="_blank">email publishing</a> for details.</div>
|
||||||
<div id="cg-email-out-section" style="display:none">
|
<div id="cg-email-out-section" style="display:none">
|
||||||
<div class="cg-field"><label><strong>Outgoing (notifications)</strong></label></div>
|
<div class="cg-field"><label><strong>Outgoing (notifications)</strong></label></div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>SMTP server address</label>
|
<label>SMTP server address</label>
|
||||||
<input type="text" data-key="smtp-sender-addr" placeholder="smtp.example.com:587">
|
<input type="text" data-key="smtp-sender-addr" placeholder="smtp.example.com:587">
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Sender email</label>
|
<label>Sender email</label>
|
||||||
<input type="text" data-key="smtp-sender-from" placeholder="ntfy@example.com">
|
<input type="text" data-key="smtp-sender-from" placeholder="ntfy@example.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>SMTP username</label>
|
<label>SMTP username</label>
|
||||||
<input type="text" data-key="smtp-sender-user" placeholder="Username">
|
<input type="text" data-key="smtp-sender-user" placeholder="Username">
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>SMTP password</label>
|
<label>SMTP password</label>
|
||||||
<input type="password" data-key="smtp-sender-pass" placeholder="Password">
|
<input type="password" data-key="smtp-sender-pass" placeholder="Password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cg-email-in-section" style="display:none">
|
<div id="cg-email-in-section" style="display:none">
|
||||||
<div class="cg-field"><label><strong>Incoming (publishing)</strong></label></div>
|
<div class="cg-field"><label><strong>Incoming (publishing)</strong></label></div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Listen address</label>
|
<label>Listen address</label>
|
||||||
<input type="text" data-key="smtp-server-listen" placeholder=":25">
|
<input type="text" data-key="smtp-server-listen" placeholder=":25">
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Domain</label>
|
<label>Domain</label>
|
||||||
<input type="text" data-key="smtp-server-domain" placeholder="ntfy.example.com">
|
<input type="text" data-key="smtp-server-domain" placeholder="ntfy.example.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="cg-field">
|
<div class="cg-field cg-inline-field">
|
||||||
<label>Address prefix</label>
|
<label>Address prefix</label>
|
||||||
<input type="text" data-key="smtp-server-addr-prefix" placeholder="ntfy-">
|
<input type="text" data-key="smtp-server-addr-prefix" placeholder="ntfy-">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
23
docs/static/css/config-generator.css
vendored
23
docs/static/css/config-generator.css
vendored
@@ -66,6 +66,19 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cg-badge-beta {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 8px;
|
||||||
|
margin-left: 8px;
|
||||||
|
background: var(--md-primary-fg-color);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.cg-modal-desc {
|
.cg-modal-desc {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
@@ -381,6 +394,10 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cg-panel:not(#cg-panel-general) .cg-inline-field > label {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.cg-inline-field > input[type="text"],
|
.cg-inline-field > input[type="text"],
|
||||||
.cg-inline-field > select {
|
.cg-inline-field > select {
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
@@ -400,6 +417,12 @@
|
|||||||
box-shadow: 0 0 0 2px rgba(51, 133, 116, 0.15);
|
box-shadow: 0 0 0 2px rgba(51, 133, 116, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cg-pg-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* Button group toggle */
|
/* Button group toggle */
|
||||||
.cg-btn-group {
|
.cg-btn-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
208
docs/static/js/config-generator.js
vendored
208
docs/static/js/config-generator.js
vendored
@@ -9,6 +9,7 @@
|
|||||||
{ key: "auth-file", env: "NTFY_AUTH_FILE", section: "auth" },
|
{ key: "auth-file", env: "NTFY_AUTH_FILE", section: "auth" },
|
||||||
{ key: "auth-default-access", env: "NTFY_AUTH_DEFAULT_ACCESS", section: "auth", def: "read-write" },
|
{ key: "auth-default-access", env: "NTFY_AUTH_DEFAULT_ACCESS", section: "auth", def: "read-write" },
|
||||||
{ key: "enable-login", env: "NTFY_ENABLE_LOGIN", section: "auth", type: "bool" },
|
{ key: "enable-login", env: "NTFY_ENABLE_LOGIN", section: "auth", type: "bool" },
|
||||||
|
{ key: "require-login", env: "NTFY_REQUIRE_LOGIN", section: "auth", type: "bool" },
|
||||||
{ key: "enable-signup", env: "NTFY_ENABLE_SIGNUP", section: "auth", type: "bool" },
|
{ key: "enable-signup", env: "NTFY_ENABLE_SIGNUP", section: "auth", type: "bool" },
|
||||||
{ key: "attachment-cache-dir", env: "NTFY_ATTACHMENT_CACHE_DIR", section: "attach" },
|
{ key: "attachment-cache-dir", env: "NTFY_ATTACHMENT_CACHE_DIR", section: "attach" },
|
||||||
{ key: "attachment-file-size-limit", env: "NTFY_ATTACHMENT_FILE_SIZE_LIMIT", section: "attach", def: "15M" },
|
{ key: "attachment-file-size-limit", env: "NTFY_ATTACHMENT_FILE_SIZE_LIMIT", section: "attach", def: "15M" },
|
||||||
@@ -381,6 +382,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var durationRegex = /^(\d+)\s*(d|days?|h|hours?|m|mins?|minutes?|s|secs?|seconds?)$/i;
|
||||||
|
var sizeRegex = /^(\d+)([tgmkb])?$/i;
|
||||||
|
|
||||||
|
function isValidDuration(s) {
|
||||||
|
return durationRegex.test(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSize(s) {
|
||||||
|
return sizeRegex.test(s);
|
||||||
|
}
|
||||||
|
|
||||||
function validate(modal, values) {
|
function validate(modal, values) {
|
||||||
var warnings = [];
|
var warnings = [];
|
||||||
var baseUrl = values["base-url"] || "";
|
var baseUrl = values["base-url"] || "";
|
||||||
@@ -401,6 +413,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// database-url must start with postgres://
|
||||||
|
if (values["database-url"] && values["database-url"].indexOf("postgres://") !== 0) {
|
||||||
|
warnings.push("database-url must start with postgres://");
|
||||||
|
}
|
||||||
|
|
||||||
// Web push requires all fields + base-url
|
// Web push requires all fields + base-url
|
||||||
var wpPublic = values["web-push-public-key"];
|
var wpPublic = values["web-push-public-key"];
|
||||||
var wpPrivate = values["web-push-private-key"];
|
var wpPrivate = values["web-push-private-key"];
|
||||||
@@ -453,12 +470,43 @@
|
|||||||
warnings.push("Enable signup requires enable-login to also be set");
|
warnings.push("Enable signup requires enable-login to also be set");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Duration field validation
|
||||||
|
var durationFields = [
|
||||||
|
{ key: "cache-duration", label: "Cache duration" },
|
||||||
|
{ key: "attachment-expiry-duration", label: "Attachment expiry duration" },
|
||||||
|
];
|
||||||
|
durationFields.forEach(function (f) {
|
||||||
|
if (values[f.key] && !isValidDuration(values[f.key])) {
|
||||||
|
warnings.push(f.label + " must be a valid duration (e.g. 12h, 3d, 30m, 60s)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Size field validation
|
||||||
|
var sizeFields = [
|
||||||
|
{ key: "attachment-file-size-limit", label: "Attachment file size limit" },
|
||||||
|
{ key: "attachment-total-size-limit", label: "Attachment total size limit" },
|
||||||
|
];
|
||||||
|
sizeFields.forEach(function (f) {
|
||||||
|
if (values[f.key] && !isValidSize(values[f.key])) {
|
||||||
|
warnings.push(f.label + " must be a valid size (e.g. 15M, 5G, 100K)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateToken() {
|
||||||
|
var chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
var token = "tk_";
|
||||||
|
for (var i = 0; i < 29; i++) {
|
||||||
|
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
function prefill(modal, key, value) {
|
function prefill(modal, key, value) {
|
||||||
var el = modal.querySelector('[data-key="' + key + '"]');
|
var el = modal.querySelector('[data-key="' + key + '"]');
|
||||||
if (el && !el.value.trim()) el.value = value;
|
if (el && !el.value.trim() && !el.dataset.cleared) el.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -469,20 +517,22 @@
|
|||||||
var isPostgres = modal.querySelector('input[name="cg-db-type"][value="postgres"]');
|
var isPostgres = modal.querySelector('input[name="cg-db-type"][value="postgres"]');
|
||||||
isPostgres = isPostgres && isPostgres.checked;
|
isPostgres = isPostgres && isPostgres.checked;
|
||||||
|
|
||||||
var isPrivate = modal.querySelector('input[name="cg-server-type"][value="private"]');
|
// Auto-enable auth when PostgreSQL is selected
|
||||||
isPrivate = isPrivate && isPrivate.checked;
|
if (isPostgres) {
|
||||||
|
var authCb = modal.querySelector("#cg-feat-auth");
|
||||||
|
if (authCb && !authCb.checked) {
|
||||||
|
authCb.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverTypeRadio = modal.querySelector('input[name="cg-server-type"]:checked');
|
||||||
|
var serverType = serverTypeRadio ? serverTypeRadio.value : "open";
|
||||||
|
var isPrivate = serverType === "private";
|
||||||
|
|
||||||
var isUnifiedPush = modal.querySelector('input[name="cg-unifiedpush"][value="yes"]');
|
var isUnifiedPush = modal.querySelector('input[name="cg-unifiedpush"][value="yes"]');
|
||||||
isUnifiedPush = isUnifiedPush && isUnifiedPush.checked;
|
isUnifiedPush = isUnifiedPush && isUnifiedPush.checked;
|
||||||
|
|
||||||
// Auto-check auth when private or UnifiedPush is selected
|
|
||||||
var authCheck = modal.querySelector("#cg-feat-auth");
|
var authCheck = modal.querySelector("#cg-feat-auth");
|
||||||
if (authCheck) {
|
|
||||||
var authForced = isPrivate || isUnifiedPush || isPostgres;
|
|
||||||
if (authForced) authCheck.checked = true;
|
|
||||||
authCheck.disabled = authForced;
|
|
||||||
}
|
|
||||||
|
|
||||||
var authEnabled = authCheck && authCheck.checked;
|
var authEnabled = authCheck && authCheck.checked;
|
||||||
|
|
||||||
var cacheEnabled = modal.querySelector("#cg-feat-cache");
|
var cacheEnabled = modal.querySelector("#cg-feat-cache");
|
||||||
@@ -536,21 +586,26 @@
|
|||||||
switchPanel(modal, "cg-panel-general");
|
switchPanel(modal, "cg-panel-general");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide auth-file and web-push-file if PostgreSQL
|
// Show "Using PostgreSQL" instead of file inputs when PostgreSQL is selected
|
||||||
var authFile = modal.querySelector('[data-key="auth-file"]');
|
["auth-file", "web-push-file", "cache-file"].forEach(function (key) {
|
||||||
if (authFile) {
|
var input = modal.querySelector('[data-key="' + key + '"]');
|
||||||
var authField = authFile.closest(".cg-field");
|
if (!input) return;
|
||||||
if (authField) authField.style.display = isPostgres ? "none" : "";
|
var field = input.closest(".cg-field");
|
||||||
}
|
if (!field) return;
|
||||||
var wpFile = modal.querySelector('[data-key="web-push-file"]');
|
input.style.display = isPostgres ? "none" : "";
|
||||||
if (wpFile) {
|
var pgLabel = field.querySelector(".cg-pg-label");
|
||||||
var wpField = wpFile.closest(".cg-field");
|
if (isPostgres) {
|
||||||
if (wpField) wpField.style.display = isPostgres ? "none" : "";
|
if (!pgLabel) {
|
||||||
}
|
pgLabel = document.createElement("span");
|
||||||
|
pgLabel.className = "cg-pg-label";
|
||||||
// Hide cache-file when PostgreSQL
|
pgLabel.textContent = "Using PostgreSQL";
|
||||||
var cacheFileField = modal.querySelector("#cg-cache-file-field");
|
input.parentNode.insertBefore(pgLabel, input.nextSibling);
|
||||||
if (cacheFileField) cacheFileField.style.display = isPostgres ? "none" : "";
|
}
|
||||||
|
pgLabel.style.display = "";
|
||||||
|
} else if (pgLabel) {
|
||||||
|
pgLabel.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Database tab — show only when PostgreSQL is selected and a DB-dependent feature is on
|
// Database tab — show only when PostgreSQL is selected and a DB-dependent feature is on
|
||||||
var navDb = modal.querySelector("#cg-nav-database");
|
var navDb = modal.querySelector("#cg-nav-database");
|
||||||
@@ -577,10 +632,13 @@
|
|||||||
accessHidden.value = accessSelect.value;
|
accessHidden.value = accessSelect.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login/signup radios → hidden checkboxes
|
// Login mode three-way toggle → hidden checkboxes
|
||||||
var loginYes = modal.querySelector('input[name="cg-enable-login"][value="yes"]');
|
var loginMode = modal.querySelector('input[name="cg-login-mode"]:checked');
|
||||||
|
var loginModeVal = loginMode ? loginMode.value : "disabled";
|
||||||
var loginHidden = modal.querySelector("#cg-enable-login-hidden");
|
var loginHidden = modal.querySelector("#cg-enable-login-hidden");
|
||||||
if (loginYes && loginHidden) loginHidden.checked = loginYes.checked;
|
var requireLoginHidden = modal.querySelector("#cg-require-login-hidden");
|
||||||
|
if (loginHidden) loginHidden.checked = (loginModeVal === "enabled" || loginModeVal === "required");
|
||||||
|
if (requireLoginHidden) requireLoginHidden.checked = (loginModeVal === "required");
|
||||||
|
|
||||||
var signupYes = modal.querySelector('input[name="cg-enable-signup"][value="yes"]');
|
var signupYes = modal.querySelector('input[name="cg-enable-signup"][value="yes"]');
|
||||||
var signupHidden = modal.querySelector("#cg-enable-signup-hidden");
|
var signupHidden = modal.querySelector("#cg-enable-signup-hidden");
|
||||||
@@ -594,30 +652,25 @@
|
|||||||
if (authEnabled) {
|
if (authEnabled) {
|
||||||
if (!isPostgres) prefill(modal, "auth-file", "/var/lib/ntfy/auth.db");
|
if (!isPostgres) prefill(modal, "auth-file", "/var/lib/ntfy/auth.db");
|
||||||
}
|
}
|
||||||
if (isPrivate) {
|
|
||||||
// Set default access select to deny-all
|
// Auto-detect server type based on current auth settings
|
||||||
if (accessSelect) accessSelect.value = "deny-all";
|
if (serverType !== "custom") {
|
||||||
if (accessHidden) accessHidden.value = "deny-all";
|
var currentAccess = accessSelect ? accessSelect.value : "read-write";
|
||||||
// Enable login
|
var currentLoginEnabled = loginModeVal !== "disabled";
|
||||||
var loginYesRadio = modal.querySelector('input[name="cg-enable-login"][value="yes"]');
|
var matchesOpen = currentAccess === "read-write" && !currentLoginEnabled;
|
||||||
if (loginYesRadio) loginYesRadio.checked = true;
|
var matchesPrivate = currentAccess === "deny-all" && currentLoginEnabled;
|
||||||
if (loginHidden) loginHidden.checked = true;
|
if (!matchesOpen && !matchesPrivate) {
|
||||||
} else {
|
var customRadio = modal.querySelector('input[name="cg-server-type"][value="custom"]');
|
||||||
// Open server: reset default access to read-write
|
if (customRadio) customRadio.checked = true;
|
||||||
if (accessSelect) accessSelect.value = "read-write";
|
}
|
||||||
if (accessHidden) accessHidden.value = "read-write";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheEnabled) {
|
if (cacheEnabled) {
|
||||||
if (!isPostgres) prefill(modal, "cache-file", "/var/cache/ntfy/cache.db");
|
if (!isPostgres) prefill(modal, "cache-file", "/var/cache/ntfy/cache.db");
|
||||||
prefill(modal, "cache-duration", "12h");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachEnabled) {
|
if (attachEnabled) {
|
||||||
prefill(modal, "attachment-cache-dir", "/var/cache/ntfy/attachments");
|
prefill(modal, "attachment-cache-dir", "/var/cache/ntfy/attachments");
|
||||||
prefill(modal, "attachment-file-size-limit", "15M");
|
|
||||||
prefill(modal, "attachment-total-size-limit", "5G");
|
|
||||||
prefill(modal, "attachment-expiry-duration", "3h");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webpushEnabled) {
|
if (webpushEnabled) {
|
||||||
@@ -654,7 +707,7 @@
|
|||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
'<input type="text" data-field="username" placeholder="Username">' +
|
'<input type="text" data-field="username" placeholder="Username">' +
|
||||||
'<input type="text" data-field="password" placeholder="Password hash (bcrypt)">' +
|
'<input type="text" data-field="password" placeholder="Password hash (bcrypt)">' +
|
||||||
'<select data-field="role"><option value="user">user</option><option value="admin">admin</option></select>' +
|
'<select data-field="role"><option value="user">User</option><option value="admin">Admin</option></select>' +
|
||||||
'<button type="button" class="cg-btn-remove" title="Remove">×</button>';
|
'<button type="button" class="cg-btn-remove" title="Remove">×</button>';
|
||||||
} else if (type === "acl") {
|
} else if (type === "acl") {
|
||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
@@ -665,7 +718,7 @@
|
|||||||
} else if (type === "token") {
|
} else if (type === "token") {
|
||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
'<input type="text" data-field="username" placeholder="Username">' +
|
'<input type="text" data-field="username" placeholder="Username">' +
|
||||||
'<input type="text" data-field="token" placeholder="Token">' +
|
'<input type="text" data-field="token" placeholder="Token" value="' + generateToken() + '">' +
|
||||||
'<input type="text" data-field="label" placeholder="Label (optional)">' +
|
'<input type="text" data-field="label" placeholder="Label (optional)">' +
|
||||||
'<button type="button" class="cg-btn-remove" title="Remove">×</button>';
|
'<button type="button" class="cg-btn-remove" title="Remove">×</button>';
|
||||||
}
|
}
|
||||||
@@ -704,9 +757,10 @@
|
|||||||
var resetBtn = document.getElementById("cg-reset-btn");
|
var resetBtn = document.getElementById("cg-reset-btn");
|
||||||
|
|
||||||
function resetAll() {
|
function resetAll() {
|
||||||
// Reset all text/password inputs
|
// Reset all text/password inputs and clear flags
|
||||||
modal.querySelectorAll('input[type="text"], input[type="password"]').forEach(function (el) {
|
modal.querySelectorAll('input[type="text"], input[type="password"]').forEach(function (el) {
|
||||||
el.value = "";
|
el.value = "";
|
||||||
|
delete el.dataset.cleared;
|
||||||
});
|
});
|
||||||
// Uncheck all checkboxes
|
// Uncheck all checkboxes
|
||||||
modal.querySelectorAll('input[type="checkbox"]').forEach(function (el) {
|
modal.querySelectorAll('input[type="checkbox"]').forEach(function (el) {
|
||||||
@@ -782,10 +836,70 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auth checkbox: clean up when unchecked
|
||||||
|
var authCheckbox = modal.querySelector("#cg-feat-auth");
|
||||||
|
if (authCheckbox) {
|
||||||
|
authCheckbox.addEventListener("change", function () {
|
||||||
|
if (!authCheckbox.checked) {
|
||||||
|
// Clear auth-file
|
||||||
|
var authFile = modal.querySelector('[data-key="auth-file"]');
|
||||||
|
if (authFile) { authFile.value = ""; delete authFile.dataset.cleared; }
|
||||||
|
// Reset default access
|
||||||
|
var accessSelect = modal.querySelector("#cg-default-access-select");
|
||||||
|
if (accessSelect) accessSelect.value = "read-write";
|
||||||
|
// Reset login mode to Disabled
|
||||||
|
var loginDisabled = modal.querySelector('input[name="cg-login-mode"][value="disabled"]');
|
||||||
|
if (loginDisabled) loginDisabled.checked = true;
|
||||||
|
var signupNo = modal.querySelector('input[name="cg-enable-signup"][value="no"]');
|
||||||
|
if (signupNo) signupNo.checked = true;
|
||||||
|
// Reset UnifiedPush to No
|
||||||
|
var upNo = modal.querySelector('input[name="cg-unifiedpush"][value="no"]');
|
||||||
|
if (upNo) upNo.checked = true;
|
||||||
|
// Remove provisioned users/ACLs/tokens
|
||||||
|
modal.querySelectorAll(".cg-auth-user-row, .cg-auth-acl-row, .cg-auth-token-row").forEach(function (row) {
|
||||||
|
row.remove();
|
||||||
|
});
|
||||||
|
// Switch server type to Open
|
||||||
|
var openRadio = modal.querySelector('input[name="cg-server-type"][value="open"]');
|
||||||
|
if (openRadio) openRadio.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server type radio: apply mode settings when clicked
|
||||||
|
modal.querySelectorAll('input[name="cg-server-type"]').forEach(function (radio) {
|
||||||
|
radio.addEventListener("change", function () {
|
||||||
|
var accessSelect = modal.querySelector("#cg-default-access-select");
|
||||||
|
var loginDisabledRadio = modal.querySelector('input[name="cg-login-mode"][value="disabled"]');
|
||||||
|
var loginRequiredRadio = modal.querySelector('input[name="cg-login-mode"][value="required"]');
|
||||||
|
if (radio.value === "open") {
|
||||||
|
if (accessSelect) accessSelect.value = "read-write";
|
||||||
|
if (loginDisabledRadio) loginDisabledRadio.checked = true;
|
||||||
|
var authCheck = modal.querySelector("#cg-feat-auth");
|
||||||
|
if (authCheck) authCheck.checked = false;
|
||||||
|
// Trigger the auth cleanup
|
||||||
|
authCheck.dispatchEvent(new Event("change"));
|
||||||
|
} else if (radio.value === "private") {
|
||||||
|
// Enable auth
|
||||||
|
var authCheck = modal.querySelector("#cg-feat-auth");
|
||||||
|
if (authCheck) authCheck.checked = true;
|
||||||
|
if (accessSelect) accessSelect.value = "deny-all";
|
||||||
|
if (loginRequiredRadio) loginRequiredRadio.checked = true;
|
||||||
|
}
|
||||||
|
// "custom" doesn't change anything
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// All form inputs trigger update
|
// All form inputs trigger update
|
||||||
modal.querySelectorAll("input, select").forEach(function (el) {
|
modal.querySelectorAll("input, select").forEach(function (el) {
|
||||||
var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input";
|
var evt = (el.type === "checkbox" || el.type === "radio") ? "change" : "input";
|
||||||
el.addEventListener(evt, function () {
|
el.addEventListener(evt, function () {
|
||||||
|
// Mark text fields as cleared when user empties them
|
||||||
|
if (el.type === "text" && el.dataset.key && !el.value.trim()) {
|
||||||
|
el.dataset.cleared = "1";
|
||||||
|
} else if (el.type === "text" && el.dataset.key && el.value.trim()) {
|
||||||
|
delete el.dataset.cleared;
|
||||||
|
}
|
||||||
updateVisibility();
|
updateVisibility();
|
||||||
updateOutput();
|
updateOutput();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user