Mail: account cards, choose which mail apps to show, optional framed webmail
- Mail page redesigned as one card per account (inbox + compose) - Settings -> Mail apps: tick the apps you use; saved on the server per person - WEBMAIL_URL shows your own webmail (e.g. Roundcube) inside the Mail tab; provider shortcuts one tap away - README: Roundcube setup and framing headers Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de1430928a
commit
318006dba3
7 files changed
+89
-21
No files matched your search
@@ -19,3 +19,6 @@ AUDIOBOOKS_LOGIN_LINK=
|
|||||||
# or per-person signed links (optional): shared secret the audiobooks site uses to verify signed links
|
# or per-person signed links (optional): shared secret the audiobooks site uses to verify signed links
|
||||||
AUDIOBOOKS_SSO_SECRET=
|
AUDIOBOOKS_SSO_SECRET=
|
||||||
#AUDIOBOOKS_SSO_URL=https://audiobooks.necloud.us/sso
|
#AUDIOBOOKS_SSO_URL=https://audiobooks.necloud.us/sso
|
||||||
|
|
||||||
|
# Your own webmail to show inside the Mail tab (Roundcube, SnappyMail...). Leave blank to keep the provider shortcuts. See README (Mail).
|
||||||
|
WEBMAIL_URL=
|
||||||
@@ -55,8 +55,24 @@ e.g. hub.necloud.us. Settings: `SEAFILE_URL`, `ONLYOFFICE_URL` env vars in docke
|
|||||||
`{u, id, exp, n}` and `sig` is the base64url HMAC-SHA256 of `body`. Without either, the plain site opens and the saved username can be copied.
|
`{u, id, exp, n}` and `sig` is the base64url HMAC-SHA256 of `body`. Without either, the plain site opens and the saved username can be copied.
|
||||||
- **Podcast** and **Podcast Admin**: launchers that open `PODCAST_URL` (a home-network address) and `VERSE_URL` in their own tabs.
|
- **Podcast** and **Podcast Admin**: launchers that open `PODCAST_URL` (a home-network address) and `VERSE_URL` in their own tabs.
|
||||||
- **Office**: embeds onlyoffice.necloud.us in a frame, with an "open in new tab" fallback if it blocks framing.
|
- **Office**: embeds onlyoffice.necloud.us in a frame, with an "open in new tab" fallback if it blocks framing.
|
||||||
- **Mail**: deep links to Gmail, Outlook.com, Microsoft 365 and iCloud webmail (inbox + compose).
|
- **Mail**: with `WEBMAIL_URL` set, your own webmail opens inside the app (a frame that keeps running between tabs, with reload, full screen and open-in-new-tab);
|
||||||
Providers forbid framing, and a real unified inbox would need Google/Azure app registrations.
|
a button there opens the Gmail / Outlook.com / Microsoft 365 / iCloud shortcuts. Without it, Mail is those shortcuts (inbox + compose, only for the apps you tick
|
||||||
|
in Settings -> Mail apps). Gmail, iCloud and Outlook refuse to be framed, so to read all your accounts in the app, run a webmail you control and connect it to them
|
||||||
|
over IMAP (Gmail and iCloud with an app-specific password). Roundcube example:
|
||||||
|
|
||||||
|
roundcube:
|
||||||
|
image: roundcube/roundcubemail:latest-apache
|
||||||
|
environment:
|
||||||
|
ROUNDCUBEMAIL_DEFAULT_HOST: ssl://imap.gmail.com # or imap.mail.me.com, etc.
|
||||||
|
ROUNDCUBEMAIL_DEFAULT_PORT: 993
|
||||||
|
ROUNDCUBEMAIL_SMTP_SERVER: ssl://smtp.gmail.com
|
||||||
|
ROUNDCUBEMAIL_SMTP_PORT: 465
|
||||||
|
ROUNDCUBEMAIL_PLUGINS: archive,zipdownload
|
||||||
|
volumes: [ ./roundcube:/var/roundcube/config ] # a file here can add the lines below
|
||||||
|
|
||||||
|
Put this in a config file there: `$config['x_frame_options'] = false;` `$config['imap_host'] = ['ssl://imap.gmail.com:993' => 'Gmail', 'ssl://imap.mail.me.com:993' => 'iCloud'];`
|
||||||
|
and have the reverse proxy for your webmail send `Content-Security-Policy: frame-ancestors https://<your hub address>` (and no `X-Frame-Options`), so only the hub can frame it.
|
||||||
|
Keep the webmail on the same site as the hub (for example mail.necloud.us and hub.necloud.us) so its sign-in cookie works inside the frame.
|
||||||
|
|
||||||
## Install on iPhone / iPad (PWA)
|
## Install on iPhone / iPad (PWA)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ services:
|
|||||||
AUDIOBOOKS_SSO_SECRET: ${AUDIOBOOKS_SSO_SECRET:-}
|
AUDIOBOOKS_SSO_SECRET: ${AUDIOBOOKS_SSO_SECRET:-}
|
||||||
AUDIOBOOKS_SSO_URL: ${AUDIOBOOKS_SSO_URL:-}
|
AUDIOBOOKS_SSO_URL: ${AUDIOBOOKS_SSO_URL:-}
|
||||||
VERSE_URL: ${VERSE_URL:-https://versebyversewithnate.us/admin}
|
VERSE_URL: ${VERSE_URL:-https://versebyversewithnate.us/admin}
|
||||||
|
WEBMAIL_URL: ${WEBMAIL_URL:-}
|
||||||
PODCAST_URL: ${PODCAST_URL:-https://podcast.necloud.us/}
|
PODCAST_URL: ${PODCAST_URL:-https://podcast.necloud.us/}
|
||||||
ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-} # same value as the OnlyOffice server's JWT secret
|
ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-} # same value as the OnlyOffice server's JWT secret
|
||||||
PUBLIC_URL: ${PUBLIC_URL:-} # e.g. https://hub.necloud.us (OnlyOffice posts saved edits here)
|
PUBLIC_URL: ${PUBLIC_URL:-} # e.g. https://hub.necloud.us (OnlyOffice posts saved edits here)
|
||||||
|
|||||||
+47
-17
@@ -10,7 +10,7 @@ const $ = s => document.querySelector(s);
|
|||||||
const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
// Preferences are kept in the browser for speed AND saved on the server, so they survive cleared browser data, a different
|
// Preferences are kept in the browser for speed AND saved on the server, so they survive cleared browser data, a different
|
||||||
// device, or the separate storage of the iOS home-screen app. Only the music queue stays local.
|
// device, or the separate storage of the iOS home-screen app. Only the music queue stays local.
|
||||||
const SYNCED = new Set(['mail_hints', 'ab_creds', 'sf_token', 'plex_lib', 'office_loc', 'cal_default', 'cal_view', 'cal_range', 'task_list', 'task_show_done', 'recents']);
|
const SYNCED = new Set(['mail_hints', 'mail_on', 'ab_creds', 'sf_token', 'plex_lib', 'office_loc', 'cal_default', 'cal_view', 'cal_range', 'task_list', 'task_show_done', 'recents']);
|
||||||
const outbox = new Map(); let outTimer = null, warned = false;
|
const outbox = new Map(); let outTimer = null, warned = false;
|
||||||
function flushSettings() {
|
function flushSettings() {
|
||||||
outTimer = null;
|
outTimer = null;
|
||||||
@@ -96,6 +96,8 @@ const MAIL = [
|
|||||||
|
|
||||||
// Saved account addresses (browser-only, no passwords) are passed as sign-in hints so the provider opens the right mailbox.
|
// Saved account addresses (browser-only, no passwords) are passed as sign-in hints so the provider opens the right mailbox.
|
||||||
const mailHints = () => store.get('mail_hints', {});
|
const mailHints = () => store.get('mail_hints', {});
|
||||||
|
// Which mail apps this person wants (all of them until they choose).
|
||||||
|
const mailOn = () => { const v = store.get('mail_on', null); return Array.isArray(v) ? v : MAIL.map(m => m.id); };
|
||||||
function mailUrl(m, kind) {
|
function mailUrl(m, kind) {
|
||||||
const base = m[kind], hint = (mailHints()[m.id] || '').trim();
|
const base = m[kind], hint = (mailHints()[m.id] || '').trim();
|
||||||
if (!hint) return base;
|
if (!hint) return base;
|
||||||
@@ -105,7 +107,7 @@ function mailUrl(m, kind) {
|
|||||||
return base; // iCloud has no sign-in hint
|
return base; // iCloud has no sign-in hint
|
||||||
}
|
}
|
||||||
|
|
||||||
let cfg = { podcast: 'https://podcast.necloud.us/', verse: 'https://versebyversewithnate.us/admin', audiobooks: 'https://audiobooks.necloud.us', seafile: 'https://seafile.necloud.us', onlyoffice: 'https://onlyoffice.necloud.us' };
|
let cfg = { webmail: '', podcast: 'https://podcast.necloud.us/', verse: 'https://versebyversewithnate.us/admin', audiobooks: 'https://audiobooks.necloud.us', seafile: 'https://seafile.necloud.us', onlyoffice: 'https://onlyoffice.necloud.us' };
|
||||||
|
|
||||||
// ---- Seafile client (via same-origin proxy)
|
// ---- Seafile client (via same-origin proxy)
|
||||||
const sf = {
|
const sf = {
|
||||||
@@ -702,17 +704,40 @@ async function renderTasksCard(el) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewMail() {
|
// Mail: with WEBMAIL_URL set, your own webmail (which you can allow to be framed) opens inside the app and keeps running between tabs.
|
||||||
$('#view').innerHTML = `<div class="wrap"><h1>Mail</h1>
|
// The provider shortcuts (Gmail, Outlook, ...) stay one tap away, because those sites refuse to be framed.
|
||||||
<p class="sub">Opens each provider's own webmail in a new tab. Nothing is stored here.</p>
|
function viewMail(args = []) {
|
||||||
<h2>Inboxes</h2><div class="list">${MAIL.map(m => `
|
if (cfg.webmail && args[0] !== 'accounts') return viewWebmail();
|
||||||
<a class="row" href="${esc(mailUrl(m, 'inbox'))}" target="_blank" rel="noopener">
|
return viewMailCards();
|
||||||
<span class="ic" style="background:${m.color}">${icon('mail')}</span>
|
}
|
||||||
<span class="grow"><b>${esc(m.name)}</b><small>Inbox</small></span>${icon('chev', 'chev')}</a>`).join('')}</div>
|
function viewWebmail() {
|
||||||
<h2>Compose</h2><div class="list">${MAIL.map(m => `
|
const old = $('#audiowrap iframe[data-k="mail"]');
|
||||||
<a class="row" href="${esc(mailUrl(m, 'compose'))}" target="_blank" rel="noopener">
|
$('#view').innerHTML = `<div class="abbar"><span class="grow"><b>Mail</b><small id="abst">${old ? '' : 'Opening…'}</small></span>
|
||||||
<span class="ic" style="background:${m.color}">${icon('edit')}</span>
|
<a class="ibtn" href="#/mail/accounts" aria-label="Gmail, Outlook and other shortcuts" title="Gmail, Outlook and other shortcuts">${icon('mail')}</a>
|
||||||
<span class="grow"><b>New message</b><small>from ${esc(m.name)}</small></span>${icon('chev', 'chev')}</a>`).join('')}</div></div>`;
|
<button class="ibtn" id="ar" aria-label="Reload" title="Reload">${icon('refresh')}</button>
|
||||||
|
<button class="ibtn" id="af" aria-label="Full screen" title="Full screen">${icon('full')}</button>
|
||||||
|
<a class="ibtn" href="${esc(cfg.webmail)}" target="_blank" rel="noopener" aria-label="Open in new tab" title="Open in new tab">${icon('ext')}</a></div>`;
|
||||||
|
const setSt = t => { const e = $('#abst'); if (e) e.textContent = t; };
|
||||||
|
const open = fresh => {
|
||||||
|
setSt('Opening…'); let f = $('#audiowrap iframe[data-k="mail"]'); if (f) f.remove();
|
||||||
|
showFrame('mail', cfg.webmail + (fresh ? (cfg.webmail.includes('?') ? '&' : '?') + '_=' + Date.now() : ''), 'Mail', 'fullscreen; clipboard-write; clipboard-read');
|
||||||
|
f = $('#audiowrap iframe[data-k="mail"]'); let done = false;
|
||||||
|
f.addEventListener('load', () => { done = true; setSt(''); });
|
||||||
|
setTimeout(() => { if (!done) setSt('Still loading… if it says it refused to connect, the webmail must allow framing (see the README)'); }, 10000);
|
||||||
|
};
|
||||||
|
if (!old) open(false); else { showFrame('mail', '', 'Mail', ''); setSt(''); }
|
||||||
|
$('#ar').onclick = () => open(true);
|
||||||
|
$('#af').onclick = () => { const f = $('#audiowrap iframe[data-k="mail"]'); if (f && f.requestFullscreen) f.requestFullscreen().catch(() => toast('Full screen is not available here')); else toast('Full screen is not available here'); };
|
||||||
|
}
|
||||||
|
function viewMailCards() {
|
||||||
|
const hints = mailHints(), on = mailOn(), shown = MAIL.filter(m => on.includes(m.id)), anyHint = shown.some(m => hints[m.id]);
|
||||||
|
$('#view').innerHTML = `<div class="wrap">${cfg.webmail ? `<div class="bar" style="margin-top:0"><a class="btn ghost" href="#/mail">${icon('back')}Webmail</a></div>` : ''}<h1>Mail</h1>
|
||||||
|
<p class="sub">Each account opens in its own webmail tab. Nothing is stored here.</p>
|
||||||
|
${shown.length ? '' : '<p class="empty">No mail apps chosen. Pick the ones you use in <a href="#/settings" style="color:var(--accent)">Settings</a>.</p>'}<div class="mailgrid">${shown.map(m => `<div class="mcard" style="--mc:${m.color}">
|
||||||
|
<div class="mhd"><span class="mbadge">${icon('mail')}</span><span class="grow"><b>${esc(m.name)}</b><small>${esc(hints[m.id] || (m.id === 'icloud' ? 'Opens iCloud Mail' : 'No address saved'))}</small></span></div>
|
||||||
|
<div class="mact"><a class="btn" href="${esc(mailUrl(m, 'inbox'))}" target="_blank" rel="noopener">${icon('mail')}Inbox</a>
|
||||||
|
<a class="btn ghost" href="${esc(mailUrl(m, 'compose'))}" target="_blank" rel="noopener">${icon('edit')}Compose</a></div></div>`).join('')}</div>
|
||||||
|
${anyHint || !shown.length ? '' : '<p class="sub" style="margin-top:14px">Tip: save your address for each account in <a href="#/settings" style="color:var(--accent)">Settings</a> so the right mailbox opens.</p>'}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const officeHash = (repo, path) => '#/office/doc/' + encodeURIComponent(repo) + path.split('/').filter(Boolean).map(x => '/' + encodeURIComponent(x)).join('');
|
const officeHash = (repo, path) => '#/office/doc/' + encodeURIComponent(repo) + path.split('/').filter(Boolean).map(x => '/' + encodeURIComponent(x)).join('');
|
||||||
@@ -1390,10 +1415,11 @@ function viewSettings() {
|
|||||||
<div class="row" id="row-oo"><span class="grow"><b>OnlyOffice</b><small>${esc(cfg.onlyoffice)}</small></span></div>
|
<div class="row" id="row-oo"><span class="grow"><b>OnlyOffice</b><small>${esc(cfg.onlyoffice)}</small></span></div>
|
||||||
</div>
|
</div>
|
||||||
${signed ? '<div class="bar"><button class="btn danger" id="out">Sign out of Seafile</button></div>' : ''}</div>
|
${signed ? '<div class="bar"><button class="btn danger" id="out">Sign out of Seafile</button></div>' : ''}</div>
|
||||||
<div id="sec-mail"><h2>Mail accounts</h2><p class="sub">Your address for each account, so its inbox and new-message buttons open the right mailbox. Saved on your server, so every device sees them. Passwords are never saved here.</p>
|
<div id="sec-mail"><h2>Mail apps</h2><p class="sub">Choose the mail apps you use and add your address for each, so its inbox and compose buttons open the right mailbox. Saved on your server, so every device sees them. Passwords are never saved here.</p>
|
||||||
<div class="card" style="max-width:none">${MAIL.filter(m => m.id !== 'icloud').map(m => `<label for="h-${m.id}">${esc(m.name)}</label>
|
<div class="card" style="max-width:none">${MAIL.map(m => `<div class="macct"><label class="chk" style="margin:${m.id === MAIL[0].id ? 0 : 14}px 0 0"><input type="checkbox" data-on="${m.id}" ${mailOn().includes(m.id) ? 'checked' : ''}><span>${esc(m.name)}</span></label>
|
||||||
<input type="email" id="h-${m.id}" data-id="${m.id}" placeholder="you@example.com" autocomplete="off" autocapitalize="off" value="${esc(mailHints()[m.id] || '')}">`).join('')}
|
${m.id === 'icloud' ? '<small class="sub" style="display:block;margin:2px 0 0 32px">iCloud Mail can\'t take a hint; use Keychain autofill or stay signed in.</small>'
|
||||||
<p class="sub" style="margin-top:12px">iCloud Mail can't take a hint; use Keychain autofill or stay signed in.</p></div>
|
: `<input type="email" id="h-${m.id}" data-id="${m.id}" placeholder="you@example.com" autocomplete="off" autocapitalize="off" value="${esc(mailHints()[m.id] || '')}" style="margin-top:6px" ${mailOn().includes(m.id) ? '' : 'hidden'}>`}</div>`).join('')}
|
||||||
|
</div>
|
||||||
</div><div id="sec-audio"><h2>Audiobooks sign-in</h2><p class="sub">Saved on your server (encrypted) and shared with all your devices. Leave blank to skip. Use the copy button on the Audiobooks tab to paste them into the site's sign-in form.</p>
|
</div><div id="sec-audio"><h2>Audiobooks sign-in</h2><p class="sub">Saved on your server (encrypted) and shared with all your devices. Leave blank to skip. Use the copy button on the Audiobooks tab to paste them into the site's sign-in form.</p>
|
||||||
<div class="card" style="max-width:none"><label for="ab-u">Username</label>
|
<div class="card" style="max-width:none"><label for="ab-u">Username</label>
|
||||||
<input type="text" id="ab-u" autocomplete="off" autocapitalize="off" value="${esc(abCreds().u)}">
|
<input type="text" id="ab-u" autocomplete="off" autocapitalize="off" value="${esc(abCreds().u)}">
|
||||||
@@ -1405,6 +1431,10 @@ function viewSettings() {
|
|||||||
hide(!allowed('mail'), '#sec-mail'); hide(!allowed('audio'), '#sec-audio'); hide(!(allowed('files') || allowed('office')), '#row-sf, #out');
|
hide(!allowed('mail'), '#sec-mail'); hide(!allowed('audio'), '#sec-audio'); hide(!(allowed('files') || allowed('office')), '#row-sf, #out');
|
||||||
hide(!allowed('music'), '#row-plex'); hide(!allowed('office'), '#row-oo');
|
hide(!allowed('music'), '#row-plex'); hide(!allowed('office'), '#row-oo');
|
||||||
if (!document.querySelector('#sec-conn .list .row')) hide(true, '#sec-conn'); // nothing to show here for this person
|
if (!document.querySelector('#sec-conn .list .row')) hide(true, '#sec-conn'); // nothing to show here for this person
|
||||||
|
document.querySelectorAll('input[data-on]').forEach(c => c.onchange = () => {
|
||||||
|
const on = [...document.querySelectorAll('input[data-on]:checked')].map(x => x.dataset.on); store.set('mail_on', on);
|
||||||
|
const h = document.getElementById('h-' + c.dataset.on); if (h) h.hidden = !c.checked;
|
||||||
|
});
|
||||||
document.querySelectorAll('input[data-id]').forEach(i => i.oninput = () => {
|
document.querySelectorAll('input[data-id]').forEach(i => i.oninput = () => {
|
||||||
const h = mailHints(); const v = i.value.trim(); if (v) h[i.dataset.id] = v; else delete h[i.dataset.id]; store.set('mail_hints', h);
|
const h = mailHints(); const v = i.value.trim(); if (v) h[i.dataset.id] = v; else delete h[i.dataset.id]; store.set('mail_hints', h);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -301,3 +301,20 @@ input, textarea, [contenteditable], .sheet p, .card p, #view .sub { -webkit-user
|
|||||||
#navbar #nvt { color: var(--muted); font-size: 13px; margin-left: 6px; }
|
#navbar #nvt { color: var(--muted); font-size: 13px; margin-left: 6px; }
|
||||||
body:not(.editing) #view { padding-top: 8px; }
|
body:not(.editing) #view { padding-top: 8px; }
|
||||||
@media (min-width: 768px) { #navbar { padding-left: 24px; } body:not(.editing) #view { padding-top: 12px; } }
|
@media (min-width: 768px) { #navbar { padding-left: 24px; } body:not(.editing) #view { padding-top: 12px; } }
|
||||||
|
|
||||||
|
/* Mail */
|
||||||
|
.mailgrid { display: grid; gap: 14px; grid-template-columns: 1fr; margin-top: 16px; }
|
||||||
|
@media (min-width: 768px) { .mailgrid { grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
.mcard { background: var(--panel); border: 1px solid var(--line); border-radius: 18px; padding: 16px; position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
.mcard::before { content: ''; position: absolute; left: 0; top: 0; right: 0; height: 4px; background: var(--mc); }
|
||||||
|
.mhd { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.mhd .grow { flex: 1; min-width: 0; }
|
||||||
|
.mhd b, .mhd small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.mhd b { font-size: 17px; }
|
||||||
|
.mhd small { color: var(--muted); }
|
||||||
|
.mbadge { width: 48px; height: 48px; border-radius: 14px; background: var(--mc); color: #fff; display: grid; place-items: center; flex: none; }
|
||||||
|
.mbadge svg { width: 26px; height: 26px; }
|
||||||
|
.mact { display: flex; gap: 8px; }
|
||||||
|
.mact .btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 46px; }
|
||||||
|
.mact .btn svg { width: 20px; height: 20px; }
|
||||||
|
.mact .btn:not(.ghost) { background: var(--mc); color: #fff; }
|
||||||
@@ -10,6 +10,7 @@ const PORT = process.env.PORT || 8080;
|
|||||||
const SEAFILE_URL = (process.env.SEAFILE_URL || 'https://seafile.necloud.us').replace(/\/$/, '');
|
const SEAFILE_URL = (process.env.SEAFILE_URL || 'https://seafile.necloud.us').replace(/\/$/, '');
|
||||||
const ONLYOFFICE_URL = (process.env.ONLYOFFICE_URL || 'https://onlyoffice.necloud.us').replace(/\/$/, '');
|
const ONLYOFFICE_URL = (process.env.ONLYOFFICE_URL || 'https://onlyoffice.necloud.us').replace(/\/$/, '');
|
||||||
const AUDIOBOOKS_URL = (process.env.AUDIOBOOKS_URL || 'https://audiobooks.necloud.us').replace(/\/$/, '');
|
const AUDIOBOOKS_URL = (process.env.AUDIOBOOKS_URL || 'https://audiobooks.necloud.us').replace(/\/$/, '');
|
||||||
|
const WEBMAIL_URL = (process.env.WEBMAIL_URL || '').replace(/\/$/, ''); // your own webmail (Roundcube, SnappyMail...) shown inside the Mail tab
|
||||||
const AUDIOBOOKS_SSO_URL = process.env.AUDIOBOOKS_SSO_URL || AUDIOBOOKS_URL + '/sso';
|
const AUDIOBOOKS_SSO_URL = process.env.AUDIOBOOKS_SSO_URL || AUDIOBOOKS_URL + '/sso';
|
||||||
const AUDIOBOOKS_SSO_SECRET = process.env.AUDIOBOOKS_SSO_SECRET || '';
|
const AUDIOBOOKS_SSO_SECRET = process.env.AUDIOBOOKS_SSO_SECRET || '';
|
||||||
// Back-door sign-in for the audiobooks site. AUDIOBOOKS_LOGIN_LINK is the site's own secret link (the key is part of it); anyone who has it
|
// Back-door sign-in for the audiobooks site. AUDIOBOOKS_LOGIN_LINK is the site's own secret link (the key is part of it); anyone who has it
|
||||||
@@ -430,7 +431,7 @@ async function handleRequest(req, res) {
|
|||||||
const c = t => users.can(user, t);
|
const c = t => users.can(user, t);
|
||||||
res.writeHead(200, { 'content-type': 'application/json' });
|
res.writeHead(200, { 'content-type': 'application/json' });
|
||||||
return res.end(JSON.stringify({ seafile: c('files') || c('office') ? SEAFILE_URL : '', onlyoffice: c('office') ? ONLYOFFICE_URL : '', audiobooks: c('audio') ? AUDIOBOOKS_URL : '', verse: c('verse') ? VERSE_URL : '',
|
return res.end(JSON.stringify({ seafile: c('files') || c('office') ? SEAFILE_URL : '', onlyoffice: c('office') ? ONLYOFFICE_URL : '', audiobooks: c('audio') ? AUDIOBOOKS_URL : '', verse: c('verse') ? VERSE_URL : '',
|
||||||
podcast: c('podcast') ? PODCAST_URL : '', editor: c('office') || c('files'), jwt: !!JWT_SECRET, build: BUILD }));
|
podcast: c('podcast') ? PODCAST_URL : '', webmail: c('mail') ? WEBMAIL_URL : '', editor: c('office') || c('files'), jwt: !!JWT_SECRET, build: BUILD }));
|
||||||
}
|
}
|
||||||
if (req.method === 'POST' && req.url === '/api/oo/config') return ooConfig(req, res);
|
if (req.method === 'POST' && req.url === '/api/oo/config') return ooConfig(req, res);
|
||||||
if (pathname === '/api/ddg') return ddgAnswer(req, res);
|
if (pathname === '/api/ddg') return ddgAnswer(req, res);
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ const users = require('./users');
|
|||||||
const file = () => path.join(users.dir(), 'settings.json'); // each person has their own
|
const file = () => path.join(users.dir(), 'settings.json'); // each person has their own
|
||||||
|
|
||||||
// Only these keys are accepted. (The music queue stays in the browser.)
|
// Only these keys are accepted. (The music queue stays in the browser.)
|
||||||
const KEYS = new Set(['mail_hints', 'ab_creds', 'sf_token', 'plex_lib', 'office_loc', 'cal_default', 'cal_view', 'cal_range', 'task_list', 'task_show_done', 'recents']);
|
const KEYS = new Set(['mail_hints', 'mail_on', 'ab_creds', 'sf_token', 'plex_lib', 'office_loc', 'cal_default', 'cal_view', 'cal_range', 'task_list', 'task_show_done', 'recents']);
|
||||||
const SECRET = new Set(['ab_creds', 'sf_token']);
|
const SECRET = new Set(['ab_creds', 'sf_token']);
|
||||||
const MAX_VALUE = 20000; // bytes of JSON per key
|
const MAX_VALUE = 20000; // bytes of JSON per key
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user