updated versions and added privacy page

This commit is contained in:
bobban committed 2026-09-04 23:54:37 +02:00
1 parent fd9ae07602
commit 069553f0eb
9 files changed
+368 -20

No files matched your search

+29
View File
@@ -62,6 +62,30 @@ The cPanel account must own the domains you want to manage. Uses the cPanel UAPI
---
## Authentik SSO
Single sign-on via Authentik (optional). When configured, a **Sign in with Authentik** button appears on the login page alongside the regular username/password form. Users who sign in via SSO for the first time are automatically created as local accounts.
| Variable | Required | Description |
|----------|----------|-------------|
| `AUTHENTIK_URL` | Yes | Issuer URL of your Authentik application, e.g. `https://auth.example.com/application/o/sloth-manager` |
| `AUTHENTIK_CLIENT_ID` | Yes | Client ID from the Authentik OAuth2/OIDC Provider |
| `AUTHENTIK_CLIENT_SECRET` | Yes | Client secret from the Authentik OAuth2/OIDC Provider |
**Setup steps in Authentik:**
1. Go to **Applications → Providers → Create** and choose **OAuth2/OpenID Provider**.
2. Set **Client type** to `Confidential`.
3. Under **Redirect URIs**, add your Sloth Manager URL with a trailing slash, e.g. `https://slothmgmt.example.com/`.
The trailing slash is required — Authentik does exact-match on redirect URIs.
4. Copy the **Client ID** and **Client Secret** into your `.env`.
5. Go to **Applications → Applications → Create**, select the provider, and note the **Slug**.
6. Set `AUTHENTIK_URL` to `https://your-authentik-host/application/o/<slug>`.
SSO is disabled (and the button is hidden) when any of the three `AUTHENTIK_*` variables are missing.
---
## Authentication
| Variable | Required | Description |
@@ -110,6 +134,11 @@ CPANEL_USERNAME=myuser
CPANEL_API_TOKEN=your_token
CPANEL_INSECURE=false
# Authentik SSO (optional)
AUTHENTIK_URL=https://auth.example.com/application/o/sloth-manager
AUTHENTIK_CLIENT_ID=your_client_id
AUTHENTIK_CLIENT_SECRET=your_client_secret
# Auth
JWT_SECRET=your-long-random-secret-here
JWT_EXPIRES_IN=24h
+6 -4
View File
@@ -1,6 +1,6 @@
# Sloth Manager
A self-hosted web app to manage DNS records across Cloudflare, Loopia, Pi-hole, Azure DNS, and cPanel from a single interface.
A self-hosted web app to manage DNS records across Cloudflare, Loopia, Pi-hole, Azure DNS, cPanel, and Technitium from a single interface. Supports local accounts and Authentik SSO.
## Requirements
@@ -92,9 +92,9 @@ All files are created automatically on first use.
| File | Contents |
|------|---------|
| `ENVIRONMENT.md` | Every `.env` variable explained |
| `ENVIRONMENT.md` | Every `.env` variable explained, including Authentik SSO setup |
| `API-ACCESS.md` | API credentials and permissions for each provider |
| `NOTIFICATIONS.md` | Gotify notification setup and troubleshooting |
| `NOTIFICATIONS.md` | Notification setup (Gotify, ntfy, SMTP, webhook) |
---
@@ -133,13 +133,15 @@ dns-manager/
│ │ │ ├── records.js
│ │ │ ├── settings.js
│ │ │ ├── auth.js
│ │ │ ├── oidc.js # Authentik SSO (OIDC)
│ │ │ └── users.js
│ │ └── adapters/
│ │ ├── cloudflare.js
│ │ ├── loopia.js
│ │ ├── pihole.js
│ │ ├── azure.js
│ │ └── cpanel.js
│ │ ├── cpanel.js
│ │ └── technitium.js
│ ├── dns-cache.json # created on first sync
│ ├── settings.json # created on first save
│ ├── users.json # created on first start
+5
View File
@@ -25,6 +25,11 @@ CPANEL_INSECURE=false # set to true if cPanel uses a self-signed certificate
# Example: DISABLED_PROVIDERS=loopia,cpanel
DISABLED_PROVIDERS=
# Authentik SSO (optional — leave blank to disable the SSO button)
# AUTHENTIK_URL=https://auth.example.com/application/o/sloth-manager
# AUTHENTIK_CLIENT_ID=your_client_id
# AUTHENTIK_CLIENT_SECRET=your_client_secret
# Auth — generate a strong random secret, e.g: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
JWT_SECRET=change-this-to-a-long-random-string
JWT_EXPIRES_IN=24h
+29 -12
View File
@@ -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",
+1 -1
View File
@@ -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",
+103
View File
@@ -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
View File
@@ -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>
)}
</>
);
}
+5 -1
View File
@@ -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>
);
+157
View File
@@ -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 &amp; 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>
);
}