From cf146973f20d0f3d2d7e5b7d783561a0790ac424 Mon Sep 17 00:00:00 2001 From: essae Date: Sat, 24 May 2025 17:21:52 -0400 Subject: [PATCH] feat(unsubscribe): secure token-based unsubscribe flow with flexible services - Created TokenService for generic HMAC token generation and validation - Created UnsubscribeTokenService to wrap TokenService with email+timestamp logic and TTL - Updated UnsubscribeController to require valid signed tokens for GET /unsubscribe - Token is generated using email + ts + shared secret, validated against TTL (default 24h) - Confirm view now inaccessible unless accessed via system-generated link - Deprecated static helper in favor of service architecture --- app/Controllers/VerificationController.php | 4 +- app/Services/TokenService.php | 48 +++++++++++++++++ app/Services/UnsubscribeTokenService.php | 51 +++++++++++++++++++ app/Utilities/UnsubscribeTokenHelper.php | 45 ++++++++++++++++ resources/views/layouts/arsha.php | 4 +- resources/views/layouts/header.php | 4 +- resources/views/pages/404.php | 35 +++++++++++++ resources/views/pages/unsubscribe_confirm.php | 51 ++++++++++++------- resources/views/pages/unsubscribe_failed.php | 25 +++++++-- resources/views/pages/unsubscribe_success.php | 35 +++++++++---- resources/views/pages/verify_failed.php | 23 ++++----- resources/views/pages/verify_success.php | 12 ++--- 12 files changed, 280 insertions(+), 57 deletions(-) create mode 100644 app/Services/TokenService.php create mode 100644 app/Services/UnsubscribeTokenService.php create mode 100644 app/Utilities/UnsubscribeTokenHelper.php create mode 100644 resources/views/pages/404.php diff --git a/app/Controllers/VerificationController.php b/app/Controllers/VerificationController.php index 9bf13d0..87bc281 100644 --- a/app/Controllers/VerificationController.php +++ b/app/Controllers/VerificationController.php @@ -1,7 +1,7 @@ prepare("UPDATE $table SET is_verified = 1, verification_code = NULL WHERE id = ?"); + $update = $db->prepare("UPDATE $table SET is_verified = 1 WHERE id = ?"); $update->execute([$subscriber['id']]); Logger::info("Subscriber verified: ID {$subscriber['id']} via $type"); diff --git a/app/Services/TokenService.php b/app/Services/TokenService.php new file mode 100644 index 0000000..80fda19 --- /dev/null +++ b/app/Services/TokenService.php @@ -0,0 +1,48 @@ +generate($data, $secret); + if (!hash_equals($expected, $token)) { + return false; + } + + if ($timestamp !== null && abs(time() - $timestamp) > $ttlSeconds) { + return false; + } + + return true; + } +} diff --git a/app/Services/UnsubscribeTokenService.php b/app/Services/UnsubscribeTokenService.php new file mode 100644 index 0000000..1242c43 --- /dev/null +++ b/app/Services/UnsubscribeTokenService.php @@ -0,0 +1,51 @@ +tokenService = $tokenService; + $this->secret = $_ENV['UNSUBSCRIBE_SECRET'] ?? 'changeme'; + $this->ttl = 86400; // default: 24 hours + } + + /** + * Create an unsubscribe token. + * + * @param string $email + * @param int $timestamp + * @return string + */ + public function generate(string $email, int $timestamp): string + { + return $this->tokenService->generate($email . $timestamp, $this->secret); + } + + /** + * Validate an unsubscribe token. + * + * @param string $email + * @param int $timestamp + * @param string $token + * @return bool + */ + public function isValid(string $email, int $timestamp, string $token): bool + { + $data = $email . $timestamp; + return $this->tokenService->isValid($data, $token, $this->secret, $timestamp, $this->ttl); + } +} diff --git a/app/Utilities/UnsubscribeTokenHelper.php b/app/Utilities/UnsubscribeTokenHelper.php new file mode 100644 index 0000000..aea0f0d --- /dev/null +++ b/app/Utilities/UnsubscribeTokenHelper.php @@ -0,0 +1,45 @@ +
- + + + diff --git a/resources/views/layouts/header.php b/resources/views/layouts/header.php index 1422657..7532715 100644 --- a/resources/views/layouts/header.php +++ b/resources/views/layouts/header.php @@ -8,8 +8,10 @@ diff --git a/resources/views/pages/404.php b/resources/views/pages/404.php new file mode 100644 index 0000000..6d75e95 --- /dev/null +++ b/resources/views/pages/404.php @@ -0,0 +1,35 @@ + + +
+
+
+ +
+

Oops... This page got lost in the cloud

+

+ The page you’re looking for doesn’t exist or may have been moved.
+ But hey, we’re Wizdom Networks — we can find anything. Almost. +

+ +
+ 404 - Not Found +
+ + +
+
+ + $html, 'hideNavbar' => true]); +?> diff --git a/resources/views/pages/unsubscribe_confirm.php b/resources/views/pages/unsubscribe_confirm.php index 5781008..34450a7 100644 --- a/resources/views/pages/unsubscribe_confirm.php +++ b/resources/views/pages/unsubscribe_confirm.php @@ -1,29 +1,42 @@ -
+ + +
-
- +
+
-

Unsubscribe from “Words of Wizdom”

-

We’re sorry to see you go. If you’d like to stop receiving our emails, confirm below.

+

Are you sure you want to unsubscribe?

+

+ We'll miss you! If you'd still like to stop receiving emails from us, confirm below. +

- Unsubscribe + Unsubscribed
-
- + + -
- -
+
+ +
-
- -
-
- - +
+ +
+
+ + $html, 'hideNavbar' => true]); +?> diff --git a/resources/views/pages/unsubscribe_failed.php b/resources/views/pages/unsubscribe_failed.php index b462300..6afb044 100644 --- a/resources/views/pages/unsubscribe_failed.php +++ b/resources/views/pages/unsubscribe_failed.php @@ -1,17 +1,34 @@ -