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
@@ -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
|
## Authentication
|
||||||
|
|
||||||
| Variable | Required | Description |
|
| Variable | Required | Description |
|
||||||
@@ -110,6 +134,11 @@ CPANEL_USERNAME=myuser
|
|||||||
CPANEL_API_TOKEN=your_token
|
CPANEL_API_TOKEN=your_token
|
||||||
CPANEL_INSECURE=false
|
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
|
# Auth
|
||||||
JWT_SECRET=your-long-random-secret-here
|
JWT_SECRET=your-long-random-secret-here
|
||||||
JWT_EXPIRES_IN=24h
|
JWT_EXPIRES_IN=24h
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Sloth Manager
|
# 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
|
## Requirements
|
||||||
|
|
||||||
@@ -92,9 +92,9 @@ All files are created automatically on first use.
|
|||||||
|
|
||||||
| File | Contents |
|
| 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 |
|
| `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
|
│ │ │ ├── records.js
|
||||||
│ │ │ ├── settings.js
|
│ │ │ ├── settings.js
|
||||||
│ │ │ ├── auth.js
|
│ │ │ ├── auth.js
|
||||||
|
│ │ │ ├── oidc.js # Authentik SSO (OIDC)
|
||||||
│ │ │ └── users.js
|
│ │ │ └── users.js
|
||||||
│ │ └── adapters/
|
│ │ └── adapters/
|
||||||
│ │ ├── cloudflare.js
|
│ │ ├── cloudflare.js
|
||||||
│ │ ├── loopia.js
|
│ │ ├── loopia.js
|
||||||
│ │ ├── pihole.js
|
│ │ ├── pihole.js
|
||||||
│ │ ├── azure.js
|
│ │ ├── azure.js
|
||||||
│ │ └── cpanel.js
|
│ │ ├── cpanel.js
|
||||||
|
│ │ └── technitium.js
|
||||||
│ ├── dns-cache.json # created on first sync
|
│ ├── dns-cache.json # created on first sync
|
||||||
│ ├── settings.json # created on first save
|
│ ├── settings.json # created on first save
|
||||||
│ ├── users.json # created on first start
|
│ ├── users.json # created on first start
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ CPANEL_INSECURE=false # set to true if cPanel uses a self-signed certificate
|
|||||||
# Example: DISABLED_PROVIDERS=loopia,cpanel
|
# Example: DISABLED_PROVIDERS=loopia,cpanel
|
||||||
DISABLED_PROVIDERS=
|
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'))"
|
# 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_SECRET=change-this-to-a-long-random-string
|
||||||
JWT_EXPIRES_IN=24h
|
JWT_EXPIRES_IN=24h
|
||||||
|
|||||||
Generated
+29
-12
@@ -11,7 +11,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"recharts": "^3.8.1"
|
"recharts": "^3.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -14020,9 +14020,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/recharts": {
|
"node_modules/recharts": {
|
||||||
"version": "3.8.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz",
|
||||||
"integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
|
"integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"www"
|
"www"
|
||||||
@@ -14033,9 +14033,9 @@
|
|||||||
"decimal.js-light": "^2.5.1",
|
"decimal.js-light": "^2.5.1",
|
||||||
"es-toolkit": "^1.39.3",
|
"es-toolkit": "^1.39.3",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"immer": "^10.1.1",
|
"immer": "^11.1.8",
|
||||||
"react-redux": "8.x.x || 9.x.x",
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
"reselect": "5.1.1",
|
"reselect": "5.2.0",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
"use-sync-external-store": "^1.2.2",
|
"use-sync-external-store": "^1.2.2",
|
||||||
"victory-vendor": "^37.0.2"
|
"victory-vendor": "^37.0.2"
|
||||||
@@ -14056,9 +14056,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/recharts/node_modules/immer": {
|
"node_modules/recharts/node_modules/immer": {
|
||||||
"version": "10.2.0",
|
"version": "11.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz",
|
||||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
"integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -14246,9 +14246,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/reselect": {
|
"node_modules/reselect": {
|
||||||
"version": "5.1.1",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
|
||||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/resolve": {
|
"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": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"recharts": "^3.8.1"
|
"recharts": "^3.10.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|||||||
@@ -112,6 +112,109 @@ body {
|
|||||||
|
|
||||||
.sidebar-footer a:hover { text-decoration: underline; }
|
.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 ===== */
|
||||||
.secrets-alert-banner {
|
.secrets-alert-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+33
-2
@@ -12,6 +12,7 @@ import SecretsPage from './components/SecretsPage';
|
|||||||
import IpamPage from './components/IpamPage';
|
import IpamPage from './components/IpamPage';
|
||||||
import DiagnosticsPage from './components/DiagnosticsPage';
|
import DiagnosticsPage from './components/DiagnosticsPage';
|
||||||
import DomainsPage from './components/DomainsPage';
|
import DomainsPage from './components/DomainsPage';
|
||||||
|
import PrivacyPage from './components/PrivacyPage';
|
||||||
import { useProviderColors, providerBadgeStyle } from './context/ProviderColors';
|
import { useProviderColors, providerBadgeStyle } from './context/ProviderColors';
|
||||||
import { useTheme } from './context/Theme';
|
import { useTheme } from './context/Theme';
|
||||||
import ConfirmDialog from './components/ConfirmDialog';
|
import ConfirmDialog from './components/ConfirmDialog';
|
||||||
@@ -246,6 +247,13 @@ function AppShell({ currentUser, onLogout }) {
|
|||||||
|
|
||||||
<div className="sidebar-footer">
|
<div className="sidebar-footer">
|
||||||
By <a href="https://bobbantech.com" target="_blank" rel="noreferrer">bobbantech</a>
|
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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -273,6 +281,7 @@ function AppShell({ currentUser, onLogout }) {
|
|||||||
{!selectedProvider && view === 'audit' && <AuditPage />}
|
{!selectedProvider && view === 'audit' && <AuditPage />}
|
||||||
{!selectedProvider && view === 'diag' && <DiagnosticsPage />}
|
{!selectedProvider && view === 'diag' && <DiagnosticsPage />}
|
||||||
{!selectedProvider && view === 'dashboard' && <Dashboard />}
|
{!selectedProvider && view === 'dashboard' && <Dashboard />}
|
||||||
|
{!selectedProvider && view === 'privacy' && <PrivacyPage />}
|
||||||
|
|
||||||
{selectedProvider && !selectedZone && (
|
{selectedProvider && !selectedZone && (
|
||||||
<ProviderOverview
|
<ProviderOverview
|
||||||
@@ -349,6 +358,7 @@ function AppShell({ currentUser, onLogout }) {
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const [currentUser, setCurrentUser] = useState(null);
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
const [authChecked, setAuthChecked] = useState(false);
|
const [authChecked, setAuthChecked] = useState(false);
|
||||||
|
const [showPrivacyModal, setShowPrivacyModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getMe()
|
getMe()
|
||||||
@@ -362,6 +372,12 @@ export default function App() {
|
|||||||
return () => window.removeEventListener('auth:logout', handler);
|
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 handleLogin(user) { setCurrentUser(user); }
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
@@ -370,6 +386,21 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!authChecked) return null;
|
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'}
|
{loading ? 'Signing in…' : 'Sign in'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
||||||
</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