Skip to content

Conversation

@anonfedora
Copy link
Contributor

Newsletter Subscriber Verification Feature

🎯 Overview

Closes #105

This PR implements a newsletter subscriber verification system that allows subscribers to verify their subscription by providing a token sent to them during signup. This prevents spam and ensures only verified subscribers receive newsletters.

📋 Features

  • Token-based verification - Subscribers receive a unique token during signup
  • Status tracking - Prevents double verification and tracks subscriber states
  • Input validation - Comprehensive validation using Garde
  • Error handling - Proper HTTP status codes and error messages
  • Database integration - Uses existing newsletter schema with proper constraints

🚀 API Endpoint

Verify Subscriber

POST /newsletter/verify

Request Body:

{
  "token": "verification-token-123"
}

Success Response (200):

{
  "message": "Subscriber successfully verified",
  "subscriber_id": "0198525e-16e4-7851-bd17-a2eab39641b9",
  "email": "user@example.com",
  "verified_at": "2025-07-28T18:49:14.730Z"
}

Error Responses:

  • 400 Bad Request - Empty or invalid token format
  • 404 Not Found - Token not found or expired
  • 422 Unprocessable Entity - Subscriber already verified

📁 Files Added/Modified

New Files

  • src/http/newsletter/mod.rs - Newsletter module router
  • src/http/newsletter/domain.rs - Domain types and enums
  • src/http/newsletter/verify_subscriber.rs - Verification endpoint
  • tests/api/newsletter.rs - Comprehensive test suite

Modified Files

  • src/http/mod.rs - Added newsletter module to main router
  • tests/api/main.rs - Added newsletter test module

🗄️ Database Schema

The feature uses the existing newsletter schema:

-- Subscriber status enum
CREATE TYPE subscriber_status AS ENUM (
    'pending',
    'active', 
    'unsubscribed',
    'bounced',
    'spam_complaint'
);

-- Newsletter subscribers table
CREATE TABLE newsletter_subscribers (
    id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(),
    email text NOT NULL UNIQUE CHECK (
      email ~* '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$'
    ),
    name varchar(255) NOT NULL CHECK (name <> ''),
    status subscriber_status NOT NULL DEFAULT 'pending',
    subscribed_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz
);

-- Subscription tokens table
CREATE TABLE subscription_token (
    subscription_token text PRIMARY KEY NOT NULL,
    subscriber_id uuid NOT NULL REFERENCES newsletter_subscribers (id)
);

🔍 Sample Queries

1. Create a New Subscriber

-- Insert subscriber with pending status
INSERT INTO newsletter_subscribers (id, email, name, status)
VALUES (
    '0198525e-16e4-7851-bd17-a2eab39641b9',
    'user@example.com',
    'John Doe',
    'pending'
);

-- Create verification token
INSERT INTO subscription_token (subscription_token, subscriber_id)
VALUES (
    'verification-token-123',
    '0198525e-16e4-7851-bd17-a2eab39641b9'
);

2. Verify Subscriber

-- Find subscriber by token
SELECT 
    ns.id,
    ns.email,
    ns.name,
    ns.status,
    ns.subscribed_at,
    ns.created_at,
    ns.updated_at
FROM newsletter_subscribers ns
INNER JOIN subscription_token st ON ns.id = st.subscriber_id
WHERE st.subscription_token = 'verification-token-123';

-- Update subscriber to active
UPDATE newsletter_subscribers
SET 
    status = 'active',
    subscribed_at = NOW(),
    updated_at = NOW()
WHERE id = '0198525e-16e4-7851-bd17-a2eab39641b9';

3. Check Subscriber Status

-- Get all active subscribers
SELECT 
    id,
    email,
    name,
    status,
    subscribed_at,
    created_at
FROM newsletter_subscribers
WHERE status = 'active'
ORDER BY subscribed_at DESC;

-- Get subscriber by email
SELECT 
    id,
    email,
    name,
    status,
    subscribed_at
FROM newsletter_subscribers
WHERE email = 'user@example.com';

4. Clean Up Expired Tokens

-- Remove tokens for already verified subscribers
DELETE FROM subscription_token st
WHERE EXISTS (
    SELECT 1 FROM newsletter_subscribers ns
    WHERE ns.id = st.subscriber_id
    AND ns.status = 'active'
);

5. Newsletter Distribution Query

-- Get all verified subscribers for newsletter distribution
SELECT 
    email,
    name,
    subscribed_at
FROM newsletter_subscribers
WHERE status = 'active'
AND subscribed_at IS NOT NULL
ORDER BY subscribed_at ASC;

🧪 Test Coverage

Screenshot 2025-07-28 at 20 10 01

Test Cases

  1. Empty Token Validation - Returns 400 for empty token
  2. Invalid Token Handling - Returns 404 for non-existent token
  3. Already Verified Subscriber - Returns 422 for double verification
  4. Successful Verification - Returns 200 with subscriber details

Test Results

test newsletter::test_verify_subscriber_empty_token ... ok
test newsletter::test_verify_subscriber_invalid_token ... ok
test newsletter::test_verify_subscriber_already_verified ... ok
test newsletter::test_verify_subscriber_success ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured

🔧 Implementation Details

Domain Types

#[derive(Debug, Clone, PartialEq, sqlx::Type, serde::Serialize)]
#[sqlx(type_name = "subscriber_status", rename_all = "lowercase")]
pub enum SubscriberStatus {
    Pending,
    Active,
    Unsubscribed,
    Bounced,
    SpamComplaint,
}

#[derive(Debug, Deserialize, Validate)]
pub struct VerifySubscriberRequest {
    #[garde(ascii, length(min = 1))]
    pub token: String,
}

#[derive(Debug, Serialize)]
pub struct VerifySubscriberResponse {
    pub message: String,
    pub subscriber_id: Uuid,
    pub email: String,
    pub verified_at: chrono::DateTime<chrono::Utc>,
}

Key Functions

  • verify_subscriber() - Main endpoint handler
  • find_subscriber_by_token() - Database query with proper error handling
  • update_subscriber_status() - Atomic status update

🛡️ Security Features

  • Input validation using Garde with length and format checks
  • Token-based verification prevents unauthorized subscriptions
  • Status tracking prevents double verification attacks
  • Database constraints ensure data integrity
  • Proper error handling with appropriate HTTP status codes

📈 Performance Considerations

  • Indexed queries on subscription_token table
  • Atomic updates prevent race conditions
  • Efficient joins between subscribers and tokens
  • Proper error handling reduces unnecessary database calls

✅ Checklist

  • API endpoint implemented
  • Input validation added
  • Error handling implemented
  • Database integration complete
  • Tests written and passing
  • Documentation updated
  • Code follows project conventions
  • No breaking changes to existing functionality

Ready for review and merge! 🎉

Copy link
Contributor

@Abeeujah Abeeujah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Job with your implementation 👍

.merge(project::router())
.merge(support_ticket::router())
.merge(escrow::router())
.merge(newsletter::router())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Unsubscribed,
Bounced,
SpamComplaint,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good 👍

pub subscribed_at: Option<chrono::DateTime<chrono::Utc>>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job with the domain modelling. 👍

"/newsletter/verify",
post(verify_subscriber::verify_subscriber),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

.await?;

Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Job with this implementation, the thought process, the modularization, everything, you did a great job 👍

let res = app.request(req).await;

assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Job with the tests, it's exhaustive and covers happy paths and known edge cases well 👍

@ONEONUORA
Copy link
Contributor

@anonfedora Pls fix the conflict

@ONEONUORA
Copy link
Contributor

@anonfedora Pls your test is not passing. Make it to pass

Copy link
Contributor

@ONEONUORA ONEONUORA left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice implementation @anonfedora Keep up the good work

@ONEONUORA ONEONUORA merged commit fc9c009 into skill-mind:master Jul 30, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verify Subscriber

3 participants