Skip to content

Commit ecac80d

Browse files
committed
Add Privacy Policy page at /privacy
- Create PrivacyPolicy component with sections covering data collection, storage, authentication, third-party services, retention, disclaimers - Register /privacy route in RouterContext - Add Privacy path to NavigationPaths - Add Privacy Policy link in NavBar drawer (with Policy icon) - Add Privacy Policy link on LoginView page - Skip auth redirect for /privacy so unauthenticated users can read it - Hide NavBar on /privacy page (same as /login)
1 parent e2c9b90 commit ecac80d

6 files changed

Lines changed: 225 additions & 2 deletions

File tree

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class AppImpl extends React.Component<AppProps> {
8686
const silentOk = await loginSilently()
8787
if (silentOk) {
8888
await this.initializeAuthenticated()
89-
} else if (isAuthEnabled() && this.props.pathname !== NavigationPaths.Login) {
89+
} else if (isAuthEnabled() && this.props.pathname !== NavigationPaths.Login && this.props.pathname !== NavigationPaths.Privacy) {
9090
this.props.navigate(NavigationPaths.Login)
9191
}
9292

frontend/src/contexts/RouterContext.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LoginView } from '@/views/Authorization/LoginView'
77
import { TaskHistory } from '@/views/History/TaskHistory'
88
import { LabelView } from '@/views/Labels/LabelView'
99
import { NotFound } from '@/views/NotFound'
10+
import { PrivacyPolicy } from '@/views/PrivacyPolicy'
1011
import { Settings } from '@/views/Settings/Settings'
1112
import { MyTasks } from '@/views/Tasks/MyTasks'
1213
import { TaskEdit } from '@/views/Tasks/TaskEdit'
@@ -172,6 +173,10 @@ class RouterContextImpl extends React.Component<RouterContextProps, RouterContex
172173
path='/labels/'
173174
element={<LabelView />}
174175
/>
176+
<Route
177+
path='/privacy'
178+
element={<PrivacyPolicy />}
179+
/>
175180
</Route>
176181
</Routes>
177182

frontend/src/utils/navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const NavigationPaths = {
1717
TaskCreate: '/tasks/create',
1818
TaskEdit: (taskId: number) => `/tasks/${taskId}/edit`,
1919
TaskHistory: (taskId: number) => `/tasks/${taskId}/history`,
20+
Privacy: '/privacy',
2021
}
2122

2223
export interface WithNavigate {

frontend/src/views/Authorization/LoginView.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Button,
99
} from '@mui/joy'
1010
import React from 'react'
11+
import { Link } from 'react-router-dom'
1112
import { initializeMsal, loginSilently, loginWithRedirect } from '@/utils/msal'
1213
import { setTitle } from '@/utils/dom'
1314
import { NavigationPaths, WithNavigate } from '@/utils/navigation'
@@ -97,6 +98,17 @@ class LoginViewImpl extends React.Component<LoginViewProps, LoginViewState> {
9798
Sign in
9899
</Button>
99100
</Sheet>
101+
<Typography
102+
level='body-xs'
103+
sx={{ mt: 2, textAlign: 'center' }}
104+
>
105+
<Link
106+
to={NavigationPaths.Privacy}
107+
style={{ color: 'inherit' }}
108+
>
109+
Privacy Policy
110+
</Link>
111+
</Typography>
100112
</Box>
101113
</Container>
102114
)

frontend/src/views/Navigation/NavBar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
LocalLibrary,
66
Label,
77
Settings,
8+
Policy,
89
} from '@mui/icons-material'
910
import {
1011
IconButton,
@@ -57,7 +58,7 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
5758
}
5859

5960
render(): React.ReactNode {
60-
if (['/login'].includes(this.props.pathname)) {
61+
if (['/login', '/privacy'].includes(this.props.pathname)) {
6162
return null
6263
}
6364

@@ -153,6 +154,11 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
153154
size='md'
154155
onClick={this.openDrawer}
155156
>
157+
<NavBarLink
158+
to={NavigationPaths.Privacy}
159+
icon={<Policy />}
160+
label='Privacy Policy'
161+
/>
156162
<ListItemButton
157163
onClick={this.logout}
158164
sx={{
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {
2+
Container,
3+
Divider,
4+
List,
5+
ListItem,
6+
ListItemDecorator,
7+
Typography,
8+
} from '@mui/joy'
9+
import {
10+
CheckCircle,
11+
RemoveCircle,
12+
} from '@mui/icons-material'
13+
import React from 'react'
14+
15+
export class PrivacyPolicy extends React.Component {
16+
render(): React.ReactNode {
17+
return (
18+
<Container
19+
maxWidth='md'
20+
sx={{ py: 4, px: 2 }}
21+
>
22+
<Typography level='h1'>Privacy Policy</Typography>
23+
<Typography
24+
level='body-sm'
25+
sx={{ mt: 1, mb: 3, color: 'text.tertiary' }}
26+
>
27+
Last updated: March 2026
28+
</Typography>
29+
30+
<Section title='Overview'>
31+
<Typography>
32+
Task Wizard is a self-hostable, privacy-focused task management
33+
application. Because you can host it on your own infrastructure, you
34+
retain full control over your data. No user information is sent to the
35+
Task Wizard maintainers or any centralized service operated by them.
36+
</Typography>
37+
</Section>
38+
39+
<Section title='Data We Collect'>
40+
<Typography sx={{ mb: 1 }}>
41+
Task Wizard is designed to collect the minimum data necessary to
42+
function:
43+
</Typography>
44+
<BulletList
45+
icon={<CheckCircle color='success' />}
46+
items={[
47+
'Authentication identifiers from Microsoft Entra ID (directory ID and object ID) — used solely to identify your account',
48+
'Task data you create: titles, due dates, recurrence rules, completion status, and labels',
49+
'Notification preferences and scheduled notification metadata',
50+
'Request metadata for operational logging: IP address, user agent, HTTP method, route, and status code (request bodies and tokens are never logged)',
51+
]}
52+
/>
53+
</Section>
54+
55+
<Section title='Data We Do NOT Collect'>
56+
<BulletList
57+
icon={<RemoveCircle color='disabled' />}
58+
items={[
59+
'Personal names, email addresses, or passwords — these fields were explicitly omitted from the data model',
60+
'Tracking cookies, analytics identifiers, or advertising data',
61+
'Telemetry or usage analytics',
62+
]}
63+
/>
64+
</Section>
65+
66+
<Section title='How Your Data Is Stored'>
67+
<Typography>
68+
All data is stored in a database (SQLite by default, or MySQL) on
69+
a backend server.
70+
</Typography>
71+
<Typography sx={{ mt: 1 }}>
72+
The database is not encrypted at rest by default. Server
73+
administrators are encouraged to apply operating-system-level or
74+
disk-level encryption to protect stored data.
75+
</Typography>
76+
</Section>
77+
78+
<Section title='Authentication and Security'>
79+
<BulletList
80+
icon={<CheckCircle color='success' />}
81+
items={[
82+
'Authentication is delegated to Microsoft Entra ID using OAuth 2.0 and OpenID Connect — no passwords are stored or processed by the application',
83+
'JWT tokens are verified against Entra ID\'s public signing keys (JWKS) on every request',
84+
'WebSocket connections are authenticated using the same token verification',
85+
'Rate limiting is applied (300 requests per minute per IP address) to mitigate abuse',
86+
'All database queries use parameterized statements to prevent SQL injection',
87+
'CORS is configurable per deployment to restrict cross-origin access',
88+
'HTTPS is recommended and should be configured via a reverse proxy in front of the application',
89+
]}
90+
/>
91+
</Section>
92+
93+
<Section title='Third-Party Services'>
94+
<BulletList
95+
icon={<CheckCircle color='success' />}
96+
items={[
97+
'Microsoft Entra ID - contacted for authentication only; no task or personal data is shared',
98+
'Gotify or webhook endpoints (optional) - if configured by you, only minimal task completion text is sent to the endpoint you choose',
99+
]}
100+
/>
101+
<Typography sx={{ mt: 1 }}>
102+
No other external services are contacted by the application.
103+
</Typography>
104+
</Section>
105+
106+
<Section title='Data Retention and Deletion'>
107+
<BulletList
108+
icon={<CheckCircle color='success' />}
109+
items={[
110+
'Sent notifications are automatically deleted within 10 minutes',
111+
'Deleting your account removes all associated data including tasks, labels, and notifications',
112+
'As a self-hostable application, the server administrator has full control over data retention, backups, and purging',
113+
]}
114+
/>
115+
</Section>
116+
117+
<Section title='Open Source and Transparency'>
118+
<Typography>
119+
Task Wizard is open-source software. The entire codebase is publicly
120+
available and can be audited by anyone. Automated security scanning
121+
is performed via CodeQL and dependency updates are managed through
122+
Dependabot.
123+
</Typography>
124+
</Section>
125+
126+
<Divider sx={{ my: 3 }} />
127+
128+
<Section title='Disclaimers'>
129+
<Typography>
130+
This software is provided &ldquo;as is&rdquo;, without warranty of
131+
any kind, express or implied. While the project follows security best
132+
practices, no system is perfect and vulnerabilities may exist.
133+
</Typography>
134+
<Typography sx={{ mt: 1, fontWeight: 600 }}>
135+
Use at your own risk.
136+
</Typography>
137+
<Typography sx={{ mt: 1 }}>
138+
The maintainers are not liable for any data loss, security breaches,
139+
or damages arising from the use of this software. Because Task
140+
Wizard is self-hostable, the security of your deployment ultimately
141+
depends on you: keep your server, reverse proxy, operating system,
142+
and dependencies up to date.
143+
</Typography>
144+
</Section>
145+
146+
<Section title='Changes to This Policy'>
147+
<Typography>
148+
This policy may be updated as the application evolves. Changes will
149+
be reflected in the &ldquo;last updated&rdquo; date at the top of
150+
this page.
151+
</Typography>
152+
</Section>
153+
</Container>
154+
)
155+
}
156+
}
157+
158+
interface SectionProps {
159+
title: string
160+
children: React.ReactNode
161+
}
162+
163+
class Section extends React.Component<SectionProps> {
164+
render(): React.ReactNode {
165+
return (
166+
<section style={{ marginBottom: 24 }}>
167+
<Typography
168+
level='h3'
169+
sx={{ mb: 1 }}
170+
>
171+
{this.props.title}
172+
</Typography>
173+
{this.props.children}
174+
</section>
175+
)
176+
}
177+
}
178+
179+
interface BulletListProps {
180+
icon: React.ReactNode
181+
items: string[]
182+
}
183+
184+
class BulletList extends React.Component<BulletListProps> {
185+
render(): React.ReactNode {
186+
return (
187+
<List size='sm'>
188+
{this.props.items.map((item, index) => (
189+
<ListItem key={index}>
190+
<ListItemDecorator sx={{ alignSelf: 'flex-start', mt: 0.5 }}>
191+
{this.props.icon}
192+
</ListItemDecorator>
193+
{item}
194+
</ListItem>
195+
))}
196+
</List>
197+
)
198+
}
199+
}

0 commit comments

Comments
 (0)