Skip to content

Commit 7a0f7fc

Browse files
nikosdionclaude
andcommitted
Add PII self-management: legal policies, user consent, data export, and account self-deletion
Implements GDPR-compliant features for user data management (#727): publicly accessible Terms of Service and Privacy Policy pages with per-language support and admin editor, a captive consent flow that requires users to accept policies before accessing the application (only when user registration is enabled), XML data export of user profile and associated data, and account self-deletion with sole-administrator protection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 454968e commit 7a0f7fc

18 files changed

Lines changed: 1131 additions & 3 deletions

File tree

ViewTemplates/Login/default.blade.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ class="btn btn-link text-decoration-none p-0 text-start"
9999
</div>
100100
</div>
101101

102+
<div class="mt-3 text-center small">
103+
<a href="@route('index.php?view=policies&task=tos')" class="text-decoration-none" target="_blank">
104+
@lang('PANOPTICON_POLICIES_TITLE_TOS')
105+
</a>
106+
<span class="text-muted mx-1">|</span>
107+
<a href="@route('index.php?view=policies&task=privacy')" class="text-decoration-none" target="_blank">
108+
@lang('PANOPTICON_POLICIES_TITLE_PRIVACY')
109+
</a>
110+
</div>
111+
102112
<input type="hidden" name="token" value="@token()">
103113
<input type="hidden" name="return"
104114
value="<?= empty($this->returnUrl) ? '' : base64_encode($this->returnUrl) ?>">
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
/**
3+
* @package panopticon
4+
* @copyright Copyright (c)2023-2026 Nicholas K. Dionysopoulos / Akeeba Ltd
5+
* @license https://www.gnu.org/licenses/agpl-3.0.txt GNU Affero General Public License, version 3 or later
6+
*/
7+
8+
defined('AKEEBA') || die;
9+
10+
/** @var \Akeeba\Panopticon\View\Policies\Html $this */
11+
12+
?>
13+
14+
<div class="container my-4">
15+
<div class="card">
16+
<div class="card-header">
17+
<h3 class="card-title m-0">
18+
<span class="fa fa-file-contract me-2" aria-hidden="true"></span>
19+
@lang('PANOPTICON_POLICIES_TITLE_TOS')
20+
</h3>
21+
</div>
22+
<div class="card-body">
23+
{!! $this->policyContent !!}
24+
</div>
25+
</div>
26+
27+
<p class="mt-3 text-center">
28+
<a href="javascript:history.back()" class="btn btn-outline-secondary">
29+
<span class="fa fa-arrow-left me-1" aria-hidden="true"></span>
30+
@lang('PANOPTICON_BTN_PREV')
31+
</a>
32+
</p>
33+
</div>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
/**
3+
* @package panopticon
4+
* @copyright Copyright (c)2023-2026 Nicholas K. Dionysopoulos / Akeeba Ltd
5+
* @license https://www.gnu.org/licenses/agpl-3.0.txt GNU Affero General Public License, version 3 or later
6+
*/
7+
8+
defined('AKEEBA') || die;
9+
10+
/** @var \Akeeba\Panopticon\View\Policies\Html $this */
11+
12+
$token = $this->container->session->getCsrfToken()->getValue();
13+
14+
?>
15+
16+
<form action="@route('index.php?view=policies&task=save')" method="post"
17+
name="adminForm" id="adminForm" class="py-3"
18+
>
19+
20+
<div class="row mb-3">
21+
<label for="type" class="col-sm-3 col-form-label">
22+
@lang('PANOPTICON_POLICIES_LBL_TYPE')
23+
</label>
24+
<div class="col-sm-9">
25+
<select name="type" id="type" class="form-select"
26+
onchange="document.location='@route('index.php?view=policies&task=edit')' + '&type=' + this.value + '&language=' + document.getElementById('language').value">
27+
<option value="tos" {{ $this->policyType === 'tos' ? 'selected' : '' }}>
28+
@lang('PANOPTICON_POLICIES_TITLE_TOS')
29+
</option>
30+
<option value="privacy" {{ $this->policyType === 'privacy' ? 'selected' : '' }}>
31+
@lang('PANOPTICON_POLICIES_TITLE_PRIVACY')
32+
</option>
33+
</select>
34+
</div>
35+
</div>
36+
37+
<div class="row mb-3">
38+
<label for="language" class="col-sm-3 col-form-label">
39+
@lang('PANOPTICON_POLICIES_LBL_LANGUAGE')
40+
</label>
41+
<div class="col-sm-9">
42+
{{ $this->getContainer()->helper->setup->languageOptions(
43+
$this->policyLanguage,
44+
name: 'language',
45+
id: 'language',
46+
attribs: [
47+
'class' => 'form-select',
48+
'onchange' => "document.location='" . $this->container->router->route('index.php?view=policies&task=edit') . "&type=" . $this->policyType . "&language=' + this.value",
49+
],
50+
) }}
51+
</div>
52+
</div>
53+
54+
<div class="row mb-3">
55+
<label for="content" class="col-sm-3 col-form-label">
56+
@lang('PANOPTICON_POLICIES_LBL_CONTENT')
57+
</label>
58+
<div class="col-sm-9">
59+
{{ \Akeeba\Panopticon\Library\Editor\TinyMCE::editor(
60+
'content',
61+
$this->policyContent,
62+
[
63+
'id' => 'content',
64+
'relative_urls' => true,
65+
]
66+
) }}
67+
</div>
68+
</div>
69+
70+
<div class="row mb-3">
71+
<div class="col-sm-9 offset-sm-3">
72+
<div class="d-flex flex-row gap-2">
73+
<button type="submit" class="btn btn-primary">
74+
<span class="fa fa-save me-1" aria-hidden="true"></span>
75+
@lang('PANOPTICON_BTN_SAVE')
76+
</button>
77+
<a href="@route('index.php?view=sysconfig')" class="btn btn-outline-secondary">
78+
<span class="fa fa-xmark me-1" aria-hidden="true"></span>
79+
@lang('PANOPTICON_BTN_CANCEL')
80+
</a>
81+
</div>
82+
</div>
83+
</div>
84+
85+
<div class="row mb-3">
86+
<div class="col-sm-9 offset-sm-3">
87+
<div class="d-flex flex-row gap-2">
88+
<a href="@route('index.php?view=policies&task=tos')" class="btn btn-outline-info btn-sm" target="_blank">
89+
<span class="fa fa-eye me-1" aria-hidden="true"></span>
90+
@lang('PANOPTICON_POLICIES_LBL_PREVIEW_TOS')
91+
</a>
92+
<a href="@route('index.php?view=policies&task=privacy')" class="btn btn-outline-info btn-sm" target="_blank">
93+
<span class="fa fa-eye me-1" aria-hidden="true"></span>
94+
@lang('PANOPTICON_POLICIES_LBL_PREVIEW_PRIVACY')
95+
</a>
96+
</div>
97+
</div>
98+
</div>
99+
100+
<input type="hidden" name="@token(true)" value="1">
101+
102+
</form>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
/**
3+
* @package panopticon
4+
* @copyright Copyright (c)2023-2026 Nicholas K. Dionysopoulos / Akeeba Ltd
5+
* @license https://www.gnu.org/licenses/agpl-3.0.txt GNU Affero General Public License, version 3 or later
6+
*/
7+
8+
defined('AKEEBA') || die;
9+
10+
/** @var \Akeeba\Panopticon\View\Policies\Html $this */
11+
12+
?>
13+
14+
<div class="container my-4">
15+
<div class="card">
16+
<div class="card-header">
17+
<h3 class="card-title m-0">
18+
<span class="fa fa-shield-halved me-2" aria-hidden="true"></span>
19+
@lang('PANOPTICON_POLICIES_TITLE_PRIVACY')
20+
</h3>
21+
</div>
22+
<div class="card-body">
23+
{!! $this->policyContent !!}
24+
</div>
25+
</div>
26+
27+
<p class="mt-3 text-center">
28+
<a href="javascript:history.back()" class="btn btn-outline-secondary">
29+
<span class="fa fa-arrow-left me-1" aria-hidden="true"></span>
30+
@lang('PANOPTICON_BTN_PREV')
31+
</a>
32+
</p>
33+
</div>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
/**
3+
* @package panopticon
4+
* @copyright Copyright (c)2023-2026 Nicholas K. Dionysopoulos / Akeeba Ltd
5+
* @license https://www.gnu.org/licenses/agpl-3.0.txt GNU Affero General Public License, version 3 or later
6+
*/
7+
8+
defined('AKEEBA') || die;
9+
10+
/** @var \Akeeba\Panopticon\View\Userconsent\Html $this */
11+
12+
$token = $this->container->session->getCsrfToken()->getValue();
13+
14+
?>
15+
16+
<div class="container my-4">
17+
{{-- Header --}}
18+
<div class="alert alert-info">
19+
<h4 class="alert-heading">
20+
<span class="fa fa-handshake me-2" aria-hidden="true"></span>
21+
@lang('PANOPTICON_USERCONSENT_HEAD_REQUIRED')
22+
</h4>
23+
<p class="mb-0">
24+
@lang('PANOPTICON_USERCONSENT_LBL_EXPLANATION')
25+
</p>
26+
</div>
27+
28+
{{-- Terms of Service --}}
29+
<div class="accordion mb-3" id="policyAccordion">
30+
<div class="accordion-item">
31+
<h2 class="accordion-header" id="headingTos">
32+
<button class="accordion-button collapsed" type="button"
33+
data-bs-toggle="collapse" data-bs-target="#collapseTos"
34+
aria-expanded="false" aria-controls="collapseTos">
35+
<span class="fa fa-file-contract me-2" aria-hidden="true"></span>
36+
@lang('PANOPTICON_POLICIES_TITLE_TOS')
37+
</button>
38+
</h2>
39+
<div id="collapseTos" class="accordion-collapse collapse" aria-labelledby="headingTos"
40+
data-bs-parent="#policyAccordion">
41+
<div class="accordion-body" style="max-height: 400px; overflow-y: auto;">
42+
{!! $this->tosContent !!}
43+
</div>
44+
<div class="accordion-footer p-2 text-end border-top">
45+
<a href="@route('index.php?view=policies&task=tos')" target="_blank" class="btn btn-sm btn-outline-secondary">
46+
<span class="fa fa-external-link me-1" aria-hidden="true"></span>
47+
@lang('PANOPTICON_USERCONSENT_LBL_VIEW_FULL')
48+
</a>
49+
</div>
50+
</div>
51+
</div>
52+
53+
<div class="accordion-item">
54+
<h2 class="accordion-header" id="headingPrivacy">
55+
<button class="accordion-button collapsed" type="button"
56+
data-bs-toggle="collapse" data-bs-target="#collapsePrivacy"
57+
aria-expanded="false" aria-controls="collapsePrivacy">
58+
<span class="fa fa-shield-halved me-2" aria-hidden="true"></span>
59+
@lang('PANOPTICON_POLICIES_TITLE_PRIVACY')
60+
</button>
61+
</h2>
62+
<div id="collapsePrivacy" class="accordion-collapse collapse" aria-labelledby="headingPrivacy"
63+
data-bs-parent="#policyAccordion">
64+
<div class="accordion-body" style="max-height: 400px; overflow-y: auto;">
65+
{!! $this->privacyContent !!}
66+
</div>
67+
<div class="accordion-footer p-2 text-end border-top">
68+
<a href="@route('index.php?view=policies&task=privacy')" target="_blank" class="btn btn-sm btn-outline-secondary">
69+
<span class="fa fa-external-link me-1" aria-hidden="true"></span>
70+
@lang('PANOPTICON_USERCONSENT_LBL_VIEW_FULL')
71+
</a>
72+
</div>
73+
</div>
74+
</div>
75+
</div>
76+
77+
{{-- Consent Buttons --}}
78+
<div class="card mb-3">
79+
<div class="card-body text-center">
80+
<p class="mb-3">
81+
@lang('PANOPTICON_USERCONSENT_LBL_AGREE_PROMPT')
82+
</p>
83+
<div class="d-flex flex-row gap-3 justify-content-center">
84+
<form action="@route('index.php?view=userconsent&task=agree')" method="post" class="d-inline">
85+
<input type="hidden" name="{{ $token }}" value="1">
86+
<button type="submit" class="btn btn-primary btn-lg">
87+
<span class="fa fa-check me-1" aria-hidden="true"></span>
88+
@lang('PANOPTICON_USERCONSENT_BTN_AGREE')
89+
</button>
90+
</form>
91+
<a href="@route('index.php?view=userconsent&task=decline')" class="btn btn-outline-secondary btn-lg">
92+
<span class="fa fa-xmark me-1" aria-hidden="true"></span>
93+
@lang('PANOPTICON_USERCONSENT_BTN_DECLINE')
94+
</a>
95+
</div>
96+
</div>
97+
</div>
98+
99+
{{-- Data Export --}}
100+
<div class="card mb-3">
101+
<div class="card-header">
102+
<h5 class="card-title m-0">
103+
<span class="fa fa-download me-2" aria-hidden="true"></span>
104+
@lang('PANOPTICON_USERCONSENT_HEAD_EXPORT')
105+
</h5>
106+
</div>
107+
<div class="card-body">
108+
<p>
109+
@lang('PANOPTICON_USERCONSENT_LBL_EXPORT_DESC')
110+
</p>
111+
<a href="@route('index.php?view=userconsent&task=export')" class="btn btn-outline-primary">
112+
<span class="fa fa-file-export me-1" aria-hidden="true"></span>
113+
@lang('PANOPTICON_USERCONSENT_BTN_EXPORT')
114+
</a>
115+
</div>
116+
</div>
117+
118+
{{-- Account Deletion --}}
119+
<div class="card mb-3 border-danger">
120+
<div class="card-header bg-danger text-white">
121+
<h5 class="card-title m-0">
122+
<span class="fa fa-triangle-exclamation me-2" aria-hidden="true"></span>
123+
@lang('PANOPTICON_USERCONSENT_HEAD_DELETE')
124+
</h5>
125+
</div>
126+
<div class="card-body">
127+
<div class="alert alert-warning">
128+
<span class="fa fa-exclamation-circle me-1" aria-hidden="true"></span>
129+
@lang('PANOPTICON_USERCONSENT_LBL_DELETE_WARNING')
130+
</div>
131+
132+
@if (!$this->canSelfDelete)
133+
<div class="alert alert-danger">
134+
<span class="fa fa-ban me-1" aria-hidden="true"></span>
135+
@lang('PANOPTICON_USERCONSENT_LBL_DELETE_SOLE_ADMIN')
136+
</div>
137+
@else
138+
<form action="@route('index.php?view=userconsent&task=deleteaccount')" method="post"
139+
onsubmit="return confirm('{{{ $this->getLanguage()->text('PANOPTICON_USERCONSENT_LBL_DELETE_CONFIRM') }}}')">
140+
<input type="hidden" name="{{ $token }}" value="1">
141+
142+
<div class="mb-3">
143+
<label for="confirm_username" class="form-label">
144+
@lang('PANOPTICON_USERCONSENT_LBL_DELETE_TYPE_USERNAME')
145+
</label>
146+
<input type="text" name="confirm_username" id="confirm_username"
147+
class="form-control" required
148+
autocomplete="off"
149+
placeholder="{{{ $this->username }}}">
150+
</div>
151+
152+
<button type="submit" class="btn btn-danger">
153+
<span class="fa fa-trash me-1" aria-hidden="true"></span>
154+
@lang('PANOPTICON_USERCONSENT_BTN_DELETE')
155+
</button>
156+
</form>
157+
@endif
158+
</div>
159+
</div>
160+
</div>

languages/en-GB.ini

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,6 +2312,41 @@ PANOPTICON_PASSKEYS_ERR_AJAX_INVALIDACTION="Invalid ajax action"
23122312
PANOPTICON_PASSKEYS_ERR_XHR_INITCREATE="Cannot get the passkey registration information from your site."
23132313
PANOPTICON_PASSKEYS_BTN_LOGIN="Passkey login"
23142314

2315+
[Policies]
2316+
PANOPTICON_POLICIES_TITLE="Legal Policies"
2317+
PANOPTICON_POLICIES_TITLE_TOS="Terms of Service"
2318+
PANOPTICON_POLICIES_TITLE_PRIVACY="Privacy Policy"
2319+
PANOPTICON_POLICIES_TITLE_EDIT="Edit Legal Policies"
2320+
PANOPTICON_POLICIES_LBL_TYPE="Policy Type"
2321+
PANOPTICON_POLICIES_LBL_LANGUAGE="Language"
2322+
PANOPTICON_POLICIES_LBL_CONTENT="Content"
2323+
PANOPTICON_POLICIES_LBL_PREVIEW_TOS="Preview Terms of Service"
2324+
PANOPTICON_POLICIES_LBL_PREVIEW_PRIVACY="Preview Privacy Policy"
2325+
PANOPTICON_POLICIES_MSG_SAVED="The policy has been saved."
2326+
2327+
[User Consent]
2328+
PANOPTICON_USERCONSENT_TITLE="Terms and Conditions"
2329+
PANOPTICON_USERCONSENT_HEAD_REQUIRED="Your Consent is Required"
2330+
PANOPTICON_USERCONSENT_LBL_EXPLANATION="Before you can use this service, you must review and agree to our Terms of Service and Privacy Policy. You can also export your personal data or delete your account from this page."
2331+
PANOPTICON_USERCONSENT_LBL_VIEW_FULL="View full page"
2332+
PANOPTICON_USERCONSENT_LBL_AGREE_PROMPT="By clicking &ldquo;I Agree&rdquo;, you confirm that you have read and agree to the Terms of Service and Privacy Policy."
2333+
PANOPTICON_USERCONSENT_BTN_AGREE="I Agree"
2334+
PANOPTICON_USERCONSENT_BTN_DECLINE="I Decline (Log Out)"
2335+
PANOPTICON_USERCONSENT_MSG_AGREED="Thank you for accepting the Terms of Service. You can now use the application."
2336+
PANOPTICON_USERCONSENT_MSG_DECLINED="You have declined the Terms of Service and have been logged out."
2337+
PANOPTICON_USERCONSENT_HEAD_EXPORT="Export Your Data"
2338+
PANOPTICON_USERCONSENT_LBL_EXPORT_DESC="Download a copy of all personal data we hold about you, in XML format. This includes your profile information, owned sites, MFA methods, and passkey labels."
2339+
PANOPTICON_USERCONSENT_BTN_EXPORT="Download My Data"
2340+
PANOPTICON_USERCONSENT_HEAD_DELETE="Permanently Delete My Account"
2341+
PANOPTICON_USERCONSENT_LBL_DELETE_WARNING="Deleting your account is permanent and cannot be undone. All your data, including sites you own, will be permanently removed."
2342+
PANOPTICON_USERCONSENT_LBL_DELETE_SOLE_ADMIN="You are the only administrator account. You cannot delete your own account as this would leave the system without an administrator."
2343+
PANOPTICON_USERCONSENT_LBL_DELETE_TYPE_USERNAME="To confirm, type your username below:"
2344+
PANOPTICON_USERCONSENT_LBL_DELETE_CONFIRM="Are you absolutely sure you want to permanently delete your account? This action CANNOT be undone."
2345+
PANOPTICON_USERCONSENT_BTN_DELETE="Permanently Delete My Account"
2346+
PANOPTICON_USERCONSENT_ERR_USERNAME_MISMATCH="The username you entered does not match your account username. Account deletion was cancelled."
2347+
PANOPTICON_USERCONSENT_ERR_CANNOT_SELF_DELETE="You cannot delete your account. You may be the only administrator."
2348+
PANOPTICON_USERCONSENT_MSG_ACCOUNT_DELETED="Your account has been permanently deleted."
2349+
23152350
[System Errors]
23162351
PANOPTICON_SYSERROR_FORBIDDEN_TITLE="Access Denied"
23172352
PANOPTICON_SYSERROR_FORBIDDEN_HEAD="Access Denied"

0 commit comments

Comments
 (0)