Skip to content

Commit 2862491

Browse files
authored
Repo cleanup (#65)
* Create add_subscriber binary * Remove unused code * Delete unused method
1 parent 80e6c08 commit 2862491

File tree

7 files changed

+53
-233
lines changed

7 files changed

+53
-233
lines changed

Cargo.lock

Lines changed: 1 addition & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@ name = "hndigest-api"
1515
path = "src/bin/api.rs"
1616

1717
[[bin]]
18-
name = "migrate-tokens"
19-
path = "src/bin/migrate_tokens.rs"
18+
name = "add-subscriber"
19+
path = "src/bin/add_subscriber.rs"
2020

2121
[dependencies]
2222
anyhow = "1.0"
2323
askama = "0.15"
2424
aws-config = { version = "1.1.7", features = ["rustls"] }
2525
aws-sdk-dynamodb = "1.16.0"
2626
aws-sdk-sesv2 = "1.14.0"
27-
aws_lambda_events = { version = "0.15", default-features = false, features = ["apigw"] }
2827
chrono = { version = "0.4", features = ["serde"] }
2928
futures = "0.3"
3029
lambda_http = "1.0.2"

infrastructure/outputs.tf

Lines changed: 0 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,4 @@
1-
# Production outputs
2-
output "lambda_function_name" {
3-
description = "Name of the production Lambda function"
4-
value = aws_lambda_function.hndigest["prod"].function_name
5-
}
6-
7-
output "lambda_function_arn" {
8-
description = "ARN of the production Lambda function"
9-
value = aws_lambda_function.hndigest["prod"].arn
10-
}
11-
12-
output "dynamodb_table_name" {
13-
description = "Name of the production DynamoDB table"
14-
value = aws_dynamodb_table.hndigest["prod"].name
15-
}
16-
17-
output "dynamodb_table_arn" {
18-
description = "ARN of the production DynamoDB table"
19-
value = aws_dynamodb_table.hndigest["prod"].arn
20-
}
21-
22-
output "eventbridge_rule_arn" {
23-
description = "ARN of the EventBridge rule"
24-
value = aws_cloudwatch_event_rule.daily_digest["prod"].arn
25-
}
26-
271
output "github_actions_role_arn" {
282
description = "ARN of the IAM role for GitHub Actions"
293
value = aws_iam_role.github_actions.arn
304
}
31-
32-
# Staging outputs
33-
output "staging_lambda_function_name" {
34-
description = "Name of the staging Lambda function"
35-
value = local.create_staging ? aws_lambda_function.hndigest["staging"].function_name : null
36-
}
37-
38-
output "staging_lambda_function_arn" {
39-
description = "ARN of the staging Lambda function"
40-
value = local.create_staging ? aws_lambda_function.hndigest["staging"].arn : null
41-
}
42-
43-
output "staging_dynamodb_table_name" {
44-
description = "Name of the staging DynamoDB table"
45-
value = local.create_staging ? aws_dynamodb_table.hndigest["staging"].name : null
46-
}
47-
48-
# Landing page outputs
49-
output "landing_page_cloudfront_domain" {
50-
description = "CloudFront distribution domain name for the landing page"
51-
value = aws_cloudfront_distribution.landing_page["prod"].domain_name
52-
}
53-
54-
output "landing_page_cloudfront_hosted_zone_id" {
55-
description = "CloudFront distribution hosted zone ID (for Route53 alias records)"
56-
value = aws_cloudfront_distribution.landing_page["prod"].hosted_zone_id
57-
}
58-
59-
output "staging_landing_page_cloudfront_domain" {
60-
description = "CloudFront distribution domain name for staging"
61-
value = local.create_staging ? aws_cloudfront_distribution.landing_page["staging"].domain_name : null
62-
}
63-
64-
output "staging_landing_page_cloudfront_hosted_zone_id" {
65-
description = "CloudFront distribution hosted zone ID for staging (for Route53 alias records)"
66-
value = local.create_staging ? aws_cloudfront_distribution.landing_page["staging"].hosted_zone_id : null
67-
}
68-
69-
output "landing_page_s3_bucket" {
70-
description = "S3 bucket name for the landing page"
71-
value = aws_s3_bucket.landing_page.id
72-
}
73-
74-
output "acm_certificate_validation_records" {
75-
description = "DNS records needed for ACM certificate validation"
76-
value = {
77-
for dvo in aws_acm_certificate.landing_page.domain_validation_options : dvo.domain_name => {
78-
name = dvo.resource_record_name
79-
type = dvo.resource_record_type
80-
value = dvo.resource_record_value
81-
}
82-
}
83-
}
84-
85-
# API Gateway outputs
86-
output "api_gateway_url" {
87-
description = "API Gateway endpoint URL (production) - used internally by CloudFront"
88-
value = aws_apigatewayv2_api.hndigest["prod"].api_endpoint
89-
}
90-
91-
output "api_lambda_function_name" {
92-
description = "Name of the API Lambda function (production)"
93-
value = aws_lambda_function.hndigest_api["prod"].function_name
94-
}
95-
96-
output "staging_api_lambda_function_name" {
97-
description = "Name of the staging API Lambda function"
98-
value = local.create_staging ? aws_lambda_function.hndigest_api["staging"].function_name : null
99-
}

src/bin/add_subscriber.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use anyhow::{Context, Result};
2+
use aws_config::BehaviorVersion;
3+
use chrono::Utc;
4+
use hndigest::storage_adapter::StorageAdapter;
5+
use hndigest::strategies::DigestStrategy;
6+
use hndigest::types::Subscriber;
7+
use std::env;
8+
use std::str::FromStr;
9+
use std::sync::Arc;
10+
11+
#[tokio::main]
12+
async fn main() -> Result<()> {
13+
let args: Vec<String> = env::args().collect();
14+
if args.len() < 2 {
15+
eprintln!("Usage: cargo run --bin add-subscriber <email> [strategy]");
16+
eprintln!(
17+
"Example: DYNAMODB_TABLE=HNDigest-staging cargo run --bin add-subscriber test@example.com TOP_N#10"
18+
);
19+
std::process::exit(1);
20+
}
21+
22+
let email = &args[1];
23+
let strategy_str = if args.len() > 2 { &args[2] } else { "TOP_N#10" };
24+
25+
let strategy = DigestStrategy::from_str(strategy_str).context("Invalid strategy")?;
26+
27+
let dynamodb_table =
28+
env::var("DYNAMODB_TABLE").context("DYNAMODB_TABLE environment variable must be set")?;
29+
30+
println!("Initializing DynamoDB client...");
31+
let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
32+
let dynamodb_client = aws_sdk_dynamodb::Client::new(&config);
33+
let storage_adapter = Arc::new(StorageAdapter::new(dynamodb_client, dynamodb_table));
34+
35+
println!("Creating verified subscriber for {}", email);
36+
let mut subscriber = Subscriber::new(email.to_string(), strategy);
37+
subscriber.verified_at = Some(Utc::now());
38+
39+
storage_adapter.set_subscriber(&subscriber).await?;
40+
41+
println!("Successfully added verified subscriber:");
42+
println!(" Email: {}", subscriber.email);
43+
println!(" Strategy: {}", subscriber.strategy);
44+
println!(" Unsubscribe Token: {}", subscriber.unsubscribe_token);
45+
46+
Ok(())
47+
}

src/bin/migrate_tokens.rs

Lines changed: 0 additions & 85 deletions
This file was deleted.

src/storage_adapter.rs

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -116,25 +116,6 @@ impl StorageAdapter {
116116
.transpose()
117117
}
118118

119-
/// Get a single subscriber by email address.
120-
/// Returns None if the subscriber doesn't exist.
121-
pub async fn get_subscriber(&self, email: &str) -> Result<Option<Subscriber>> {
122-
let output = self
123-
.client
124-
.get_item()
125-
.table_name(&self.table_name)
126-
.key(
127-
"PK",
128-
AttributeValue::S(SUBSCRIBER_PARTITION_KEY.to_string()),
129-
)
130-
.key("SK", AttributeValue::S(email.to_lowercase()))
131-
.send()
132-
.await
133-
.context("Failed to get subscriber")?;
134-
135-
output.item.map(subscriber_from_item).transpose()
136-
}
137-
138119
/// Get a subscriber by their unsubscribe token.
139120
/// Returns None if no subscriber exists with this token.
140121
/// Fails if multiple subscribers have the same token (should never happen).
@@ -232,7 +213,7 @@ impl StorageAdapter {
232213
),
233214
(
234215
"unsubscribe_token".to_string(),
235-
AttributeValue::S(subscriber.unsubscribe_token.clone()),
216+
AttributeValue::S(subscriber.unsubscribe_token.to_string()),
236217
),
237218
]);
238219

@@ -365,15 +346,11 @@ fn subscriber_from_item(item: HashMap<String, AttributeValue>) -> Result<Subscri
365346
.transpose()
366347
.context("Invalid verified_at timestamp")?;
367348

368-
// TODO: After running the migration script (migrate-tokens), remove this fallback.
369-
// The unsubscribe_token field should be required, and deserialization should fail
370-
// if it's missing. This auto-generation is only here for backwards compatibility
371-
// during the migration period.
372349
let unsubscribe_token = item
373350
.get("unsubscribe_token")
374351
.and_then(|v| v.as_s().ok())
375-
.cloned()
376-
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
352+
.ok_or_else(|| anyhow::anyhow!("Missing unsubscribe_token field"))?
353+
.clone();
377354

378355
Ok(Subscriber {
379356
email,

src/types.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ pub struct Subscriber {
2020
pub strategy: DigestStrategy,
2121
pub subscribed_at: DateTime<Utc>,
2222
pub verified_at: Option<DateTime<Utc>>,
23-
/// Unique token for unsubscribe links (UUID v4)
2423
pub unsubscribe_token: String,
2524
}
2625

@@ -35,9 +34,4 @@ impl Subscriber {
3534
unsubscribe_token: uuid::Uuid::new_v4().to_string(),
3635
}
3736
}
38-
39-
/// Generate a new unsubscribe token for this subscriber.
40-
pub fn regenerate_token(&mut self) {
41-
self.unsubscribe_token = uuid::Uuid::new_v4().to_string();
42-
}
4337
}

0 commit comments

Comments
 (0)