updated versions and added privacy page
This commit is contained in:
1 parent
fd9ae07602
commit
069553f0eb
9 files changed
+368
-20
No files matched your search
Generated
+29
-12
@@ -11,7 +11,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"recharts": "^3.8.1"
|
||||
"recharts": "^3.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -14020,9 +14020,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
|
||||
"integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz",
|
||||
"integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"www"
|
||||
@@ -14033,9 +14033,9 @@
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"immer": "^11.1.8",
|
||||
"react-redux": "8.x.x || 9.x.x",
|
||||
"reselect": "5.1.1",
|
||||
"reselect": "5.2.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-sync-external-store": "^1.2.2",
|
||||
"victory-vendor": "^37.0.2"
|
||||
@@ -14056,9 +14056,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/recharts/node_modules/immer": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"version": "11.1.18",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz",
|
||||
"integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -14246,9 +14246,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
|
||||
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
@@ -15789,6 +15789,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"recharts": "^3.8.1"
|
||||
"recharts": "^3.10.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@@ -112,6 +112,109 @@ body {
|
||||
|
||||
.sidebar-footer a:hover { text-decoration: underline; }
|
||||
|
||||
.sidebar-footer-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sidebar-footer-link:hover { text-decoration: underline; }
|
||||
|
||||
/* ===== Privacy page ===== */
|
||||
.privacy-page {
|
||||
max-width: 760px;
|
||||
padding: 32px;
|
||||
}
|
||||
.privacy-page h2 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.privacy-updated {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.privacy-page section {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.privacy-page h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.privacy-page h4 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 14px 0 6px;
|
||||
}
|
||||
.privacy-page p, .privacy-page li {
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
}
|
||||
.privacy-page ul {
|
||||
padding-left: 20px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.privacy-page li { margin-bottom: 4px; }
|
||||
|
||||
.privacy-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.privacy-table th, .privacy-table td {
|
||||
text-align: left;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
}
|
||||
.privacy-table th {
|
||||
background: var(--hover);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== Privacy modal (pre-login) ===== */
|
||||
.privacy-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.privacy-modal {
|
||||
position: relative;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
width: min(760px, 95vw);
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.privacy-modal-close {
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
float: right;
|
||||
margin: 12px 16px 0 0;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
.privacy-modal-close:hover { color: var(--text); }
|
||||
|
||||
/* ===== Secrets ===== */
|
||||
.secrets-alert-banner {
|
||||
display: flex;
|
||||
|
||||
+33
-2
@@ -12,6 +12,7 @@ import SecretsPage from './components/SecretsPage';
|
||||
import IpamPage from './components/IpamPage';
|
||||
import DiagnosticsPage from './components/DiagnosticsPage';
|
||||
import DomainsPage from './components/DomainsPage';
|
||||
import PrivacyPage from './components/PrivacyPage';
|
||||
import { useProviderColors, providerBadgeStyle } from './context/ProviderColors';
|
||||
import { useTheme } from './context/Theme';
|
||||
import ConfirmDialog from './components/ConfirmDialog';
|
||||
@@ -246,6 +247,13 @@ function AppShell({ currentUser, onLogout }) {
|
||||
|
||||
<div className="sidebar-footer">
|
||||
By <a href="https://bobbantech.com" target="_blank" rel="noreferrer">bobbantech</a>
|
||||
{' · '}
|
||||
<button
|
||||
className="sidebar-footer-link"
|
||||
onClick={() => { setSelectedProvider(null); setSelectedZone(null); setView('privacy'); }}
|
||||
>
|
||||
Privacy
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -273,6 +281,7 @@ function AppShell({ currentUser, onLogout }) {
|
||||
{!selectedProvider && view === 'audit' && <AuditPage />}
|
||||
{!selectedProvider && view === 'diag' && <DiagnosticsPage />}
|
||||
{!selectedProvider && view === 'dashboard' && <Dashboard />}
|
||||
{!selectedProvider && view === 'privacy' && <PrivacyPage />}
|
||||
|
||||
{selectedProvider && !selectedZone && (
|
||||
<ProviderOverview
|
||||
@@ -349,6 +358,7 @@ function AppShell({ currentUser, onLogout }) {
|
||||
export default function App() {
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [authChecked, setAuthChecked] = useState(false);
|
||||
const [showPrivacyModal, setShowPrivacyModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getMe()
|
||||
@@ -362,6 +372,12 @@ export default function App() {
|
||||
return () => window.removeEventListener('auth:logout', handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setShowPrivacyModal(true);
|
||||
window.addEventListener('show-privacy', handler);
|
||||
return () => window.removeEventListener('show-privacy', handler);
|
||||
}, []);
|
||||
|
||||
function handleLogin(user) { setCurrentUser(user); }
|
||||
|
||||
function handleLogout() {
|
||||
@@ -370,6 +386,21 @@ export default function App() {
|
||||
}
|
||||
|
||||
if (!authChecked) return null;
|
||||
if (!currentUser) return <LoginPage onLogin={handleLogin} />;
|
||||
return <AppShell currentUser={currentUser} onLogout={handleLogout} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!currentUser
|
||||
? <LoginPage onLogin={handleLogin} />
|
||||
: <AppShell currentUser={currentUser} onLogout={handleLogout} />
|
||||
}
|
||||
{showPrivacyModal && (
|
||||
<div className="privacy-modal-overlay" onClick={() => setShowPrivacyModal(false)}>
|
||||
<div className="privacy-modal" onClick={e => e.stopPropagation()}>
|
||||
<button className="privacy-modal-close" onClick={() => setShowPrivacyModal(false)}>✕</button>
|
||||
<PrivacyPage />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -152,7 +152,11 @@ export default function LoginPage({ onLogin }) {
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
<p className="login-footer">By <a href="https://bobbantech.com" target="_blank" rel="noreferrer">bobbantech</a></p>
|
||||
<p className="login-footer">
|
||||
By <a href="https://bobbantech.com" target="_blank" rel="noreferrer">bobbantech</a>
|
||||
{' · '}
|
||||
<a href="/privacy" onClick={e => { e.preventDefault(); window.dispatchEvent(new CustomEvent('show-privacy')); }}>Privacy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="privacy-page">
|
||||
<h2>Privacy Notice</h2>
|
||||
<p className="privacy-updated">Last updated: September 2026</p>
|
||||
|
||||
<section>
|
||||
<h3>1. About this notice</h3>
|
||||
<p>
|
||||
Sloth Manager is a self-hosted application operated by the organisation that deployed it
|
||||
(the <strong>operator</strong>). This notice describes what personal data Sloth Manager
|
||||
stores, why it stores it, and how long it is kept. It applies to all users of this
|
||||
installation.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>2. Data we store</h3>
|
||||
|
||||
<h4>User accounts</h4>
|
||||
<table className="privacy-table">
|
||||
<thead>
|
||||
<tr><th>Data</th><th>Purpose</th><th>Retention</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Username</td>
|
||||
<td>Identifies you within the application and appears in the audit log</td>
|
||||
<td>Until the account is deleted by an administrator</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password (bcrypt hash)</td>
|
||||
<td>Authenticates you on login — the original password is never stored</td>
|
||||
<td>Until the account is deleted or the password is changed</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Audit log</h4>
|
||||
<table className="privacy-table">
|
||||
<thead>
|
||||
<tr><th>Data</th><th>Purpose</th><th>Retention</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Username, timestamp, action, DNS provider, zone, record details</td>
|
||||
<td>
|
||||
Maintains an accountable history of all DNS record changes made through
|
||||
the application
|
||||
</td>
|
||||
<td>Rolling window of the latest 500 entries; oldest entries are removed automatically</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Notification settings</h4>
|
||||
<table className="privacy-table">
|
||||
<thead>
|
||||
<tr><th>Data</th><th>Purpose</th><th>Retention</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Email address (SMTP "To" field)</td>
|
||||
<td>Sends DNS-change notifications to the configured recipient</td>
|
||||
<td>Until removed from Settings by an administrator</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>API tokens / URLs (Gotify, ntfy, webhook)</td>
|
||||
<td>Delivers notifications to the configured channels</td>
|
||||
<td>Until removed from Settings by an administrator</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>SSO (Authentik)</h4>
|
||||
<p>
|
||||
If single sign-on is enabled, Sloth Manager receives your <strong>username</strong> and
|
||||
<strong> email address</strong> from Authentik during login. Only the username is stored
|
||||
locally (as a user account). No SSO tokens or session data are persisted beyond the
|
||||
duration of your login session.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>3. What we do not store</h3>
|
||||
<ul>
|
||||
<li>Browser cookies or tracking identifiers</li>
|
||||
<li>IP addresses or device information</li>
|
||||
<li>Analytics or usage statistics</li>
|
||||
<li>Any data from third-party advertisers</li>
|
||||
</ul>
|
||||
<p>
|
||||
Login sessions use a short-lived JWT token stored in your browser's local storage.
|
||||
It expires automatically and contains only your username and user ID.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>4. Legal basis for processing</h3>
|
||||
<p>
|
||||
Personal data is processed on the basis of <strong>legitimate interests</strong> —
|
||||
specifically the secure operation of the DNS management tool and maintaining an
|
||||
accountable record of infrastructure changes. User accounts are required to access
|
||||
the application.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>5. Data sharing</h3>
|
||||
<p>
|
||||
Sloth Manager does not share personal data with third parties. Data is stored locally
|
||||
on the server where the application is hosted. Notification channels (Gotify, ntfy,
|
||||
SMTP, webhooks) receive message content only — they do not receive account data.
|
||||
</p>
|
||||
<p>
|
||||
DNS operations are performed against the configured DNS providers (Cloudflare, Loopia,
|
||||
Pi-hole, Azure DNS, cPanel, Technitium). These providers receive only the DNS record
|
||||
data necessary to fulfil each operation — not user or account information.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>6. Your rights</h3>
|
||||
<p>
|
||||
Under the GDPR you have the right to access, correct, or erase your personal data.
|
||||
Contact the operator of this installation to exercise these rights. Administrators can:
|
||||
</p>
|
||||
<ul>
|
||||
<li>View and update your username in <strong>Settings → Users</strong></li>
|
||||
<li>Delete your account in <strong>Settings → Users</strong></li>
|
||||
<li>Clear the audit log via the Diagnostics page</li>
|
||||
</ul>
|
||||
<p>
|
||||
You can change your own password at any time in <strong>👤 My Profile</strong>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>7. Data location & security</h3>
|
||||
<p>
|
||||
All data is stored on the server where this instance of Sloth Manager is hosted.
|
||||
The operator is responsible for securing that server, applying backups, and ensuring
|
||||
appropriate access controls. Passwords are stored as bcrypt hashes (cost factor 10)
|
||||
and are not recoverable.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>8. Contact</h3>
|
||||
<p>
|
||||
For questions about how your data is handled, contact the administrator of this
|
||||
Sloth Manager installation.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user