Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions rust-server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ rand = "0.10.0"
argon2 = { version = "0.5.3", features = ["std"] }
uuid = { version = "1.23.0", features = ["serde", "v4"] }
resend-rs = "0.21.1"
urlencoding = "2.1.3"

[dev-dependencies]
axum-test = "19.1.1"
Expand Down
178 changes: 166 additions & 12 deletions rust-server/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ src/
│ ├── mod.rs
│ ├── auth/ # Authentication feature
│ │ ├── mod.rs
│ │ ├── types.rs # Auth data structures (RegisterData with validation)
│ │ ├── types.rs # Auth data structures (RegisterData, Token with validation)
│ │ ├── queries.rs # Auth-specific database operations
│ │ ├── service.rs # Auth business logic (user creation, token generation)
│ │ ├── routes.rs # Auth routes (/api/auth/register)
Expand All @@ -39,7 +39,8 @@ src/
└── utils/ # Utility modules
├── mod.rs
├── api_response.rs # Structured API response types
├── email.rs # Email sending utilities (verification emails)
├── email.rs # EmailService struct using Resend API
├── email_templates.rs # HTML and plain text email templates
├── password.rs # Password hashing and verification utilities
├── tracing.rs # init_tracing() function
└── validators.rs # Input validation functions (email, username, password)
Expand Down Expand Up @@ -71,7 +72,7 @@ tests/
| File | Purpose |
| ----------------- | ----------------------------------------------------------- |
| `api_response.rs` | Structured API response types for consistent JSON responses |
| `email.rs` | Email sending utilities (verification emails) |
| `email.rs` | EmailService struct using Resend API |
| `password.rs` | Password hashing and verification utilities |
| `tracing.rs` | Rust logging setup (`tracing_subscriber`) |
| `validators.rs` | Input validation functions for user data |
Comment thread
reijjo marked this conversation as resolved.
Expand Down Expand Up @@ -233,6 +234,7 @@ Input validation functions for user registration and authentication data.

**Key validation functions:**

- `validate_email()` - Validates email format using regex pattern
- `validate_username()` - Validates username format (alphanumeric, dots, underscores, hyphens)
- `validate_password()` - Validates password strength (uppercase, lowercase, number, special character)

Expand Down Expand Up @@ -299,37 +301,124 @@ pub fn validate_password(password: &str) -> Result<(), ValidationError> {
<details>
<summary><strong>email.rs</strong></summary>

Email sending utilities for verification emails and other notifications.
EmailService struct for sending verification emails using the Resend API.

**Key types:**

- `EmailService` - Struct containing Resend client and configuration

**Key functions:**

- `EmailService::new()` - Creates a new EmailService instance
- `send_verification_email()` - Sends a verification email to a user with a token

**Usage patterns:**

- Takes recipient email and verification token as parameters
- Returns `Result<(), AppError>` for error handling
- Currently a placeholder implementation (logs to stderr)
- Uses Resend API for actual email delivery
- Generates verification URL using frontend_url and token

**Example usage:**

```rs
// Create email service
let email_service = EmailService::new(
Resend::new(&config.resend_api_key),
&config.frontend_url,
&config.tarpit_domain,
);

// Send verification email after user registration
send_verification_email("user@example.com", "verification-token-123").await?;
email_service.send_verification_email("user@example.com", "verification-token-123").await?;
```

```rs
use resend_rs::Resend;
use resend_rs::types::CreateEmailBaseOptions;

use crate::errors::AppError;
use crate::utils::email_templates::{build_verification_html, build_verification_text};

pub async fn send_verification_email(to_email: &str, token: &str) -> Result<(), AppError> {
eprintln!("Sending verification email to: {to_email} with token: {token}");
#[derive(Clone)]
pub struct EmailService {
resend: Resend,
frontend_url: String,
from_email: String,
}

Ok(())
impl EmailService {
pub fn new(resend: Resend, frontend_url: &str, tarpit_domain: &str) -> Self {
Self {
resend,
frontend_url: frontend_url.to_string(),
from_email: format!("noreply@{tarpit_domain}"),
}
}

pub async fn send_verification_email(
&self,
to_email: &str,
token: &str,
) -> Result<(), AppError> {
let encoded_token = urlencoding::encode(token);
let verify_url = format!(
"{}/verify?token={}",
self.frontend_url.trim_end_matches('/'),
encoded_token
);

let html = build_verification_html(&verify_url);
let text = build_verification_text(&verify_url);

let email = CreateEmailBaseOptions::new(
&self.from_email,
[to_email],
"Welcome to Tarpit - verify your email",
)
.with_html(&html)
.with_text(&text);

self.resend.emails.send(email).await.map_err(|e| {
tracing::error!(?e, "Failed to send verification email");
AppError::internal("Failed to send verification email")
})?;

tracing::info!(to = to_email, "Verification email sent");
Ok(())
}
}
```

</details>

<details>
<summary><strong>email_templates.rs</strong></summary>

HTML and plain text email templates for verification emails.

**Key functions:**

- `build_verification_html()` - Generates HTML email template with verification link
- `build_verification_text()` - Generates plain text fallback for email clients

**Features:**

- Dark header with gradient background (with solid color fallback for email clients that don't support gradients)
- Text-based emoji logo (🎣) for maximum email client compatibility
- Responsive design with max-width container
- Yellow CTA button for verification link
- Fallback plain text link for accessibility

**Email client compatibility:**

- Uses `background-color` fallback before `linear-gradient` for Outlook and older clients
- Text-based logo instead of external images (images often blocked by email clients)
- Inline styles for maximum compatibility
- Table-based layout for consistent rendering

</details>

---

## 🛡️ src/middleware/
Expand Down Expand Up @@ -518,7 +607,7 @@ Example:
- Then create the schema in it:

```sql
CREATE TYPE user_role AS ENUM ('GUEST', 'GUEST2');
CREATE TYPE user_role AS ENUM ('GUEST', 'PAID', 'BOSS');


CREATE TABLE IF NOT EXISTS users (
Expand Down Expand Up @@ -669,6 +758,7 @@ async fn run_migrations(pool: &PgPool) -> Result<(), DbError> {
| Type | Purpose |
| -------------- | ----------------------------------------------------- |
| `RegisterData` | User registration data structure with full validation |
| `Token` | Verification token structure with expiration |

**RegisterData fields:**

Expand All @@ -690,12 +780,13 @@ Authentication-specific database operations for user registration and management

- `create_user()` - Creates a new user in the database and returns the user's UUID
- `create_verification_token()` - Creates a verification token for email verification
- `delete_user()` - Deletes a user from the database (used for compensating transactions)

**Usage patterns:**

- Takes database executor (pool or transaction) and user data as parameters
- Returns `Result<Uuid, AppError>` for user creation (returns the new user's ID)
- Returns `Result<(), AppError>` for token creation
- Returns `Result<(), AppError>` for token creation and user deletion
- Integrates with `AppError::Sql` for proper database error handling
- Used by authentication service layer for user registration workflow

Expand All @@ -708,6 +799,9 @@ let user_id = create_user(&db_pool, "user@example.com", "username", "hashed_pass
// Create verification token
let expires_at = chrono::Utc::now() + chrono::Duration::hours(24);
create_verification_token(&db_pool, user_id, "token-123", expires_at).await?;

// Delete user (compensating transaction if email fails)
delete_user(&db_pool, user_id).await?;
```

**Error handling:**
Expand Down Expand Up @@ -771,6 +865,20 @@ where

Ok(())
}

// Delete user - DELETE (compensating transaction for failed email)
pub async fn delete_user<'e, E>(db: E, user_id: Uuid) -> Result<(), AppError>
where
E: Executor<'e, Database = Postgres>,
{
sqlx::query("DELETE FROM users WHERE id = $1")
.bind(user_id)
.execute(db)
.await
.map_err(AppError::Sql)?;

Ok(())
}
```

</details>
Expand Down Expand Up @@ -987,7 +1095,7 @@ curl http://127.0.0.1:3001/THIS_DOES_NOT_EXIST

- Integration tests live in crate-root `tests/` (not inside `src/`).
- Single integration target entrypoint: `tests/api.rs`.
- Shared test server helper: `tests/api/common.rs`.
- Shared test server helper: `tests/api/common.rs` (includes `build_test_server()` and `build_test_server_without_db()`).
- Current API test modules: `tests/api/health.rs`.
- Tests use `axum-test::TestServer` to call routes in-process.
- Test helper forces `AppEnv::Test` and maps `db_url` to `db_test_url` for safety.
Expand Down Expand Up @@ -1204,6 +1312,52 @@ cargo add regex

</details>

<details>
<summary><strong>uuid</strong></summary>

**uuid** — UUID generation and parsing library.

- Version: `1.23.0`
- Features: `serde`, `v4`
- Purpose: UUID generation for user IDs and verification tokens
- Documentation: https://docs.rs/uuid/latest/uuid/

```bash
cargo add uuid --features serde,v4
```

</details>

<details>
<summary><strong>resend-rs</strong></summary>

**resend-rs** — Resend email API client.

- Version: `0.21.1`
- Purpose: Send transactional emails via Resend API
- Documentation: https://docs.rs/resend-rs/latest/resend_rs/

```bash
cargo add resend-rs
```

</details>

<details>
<summary><strong>urlencoding</strong></summary>

**urlencoding** — URL encoding/decoding library.

- Version: `2.1.3`
- Purpose: Encode verification tokens in email URLs
- Documentation: https://docs.rs/urlencoding/latest/urlencoding/

```bash
cargo add urlencoding
```

</details>

<details>
<summary><strong>sysinfo</strong></summary>

Expand Down
21 changes: 13 additions & 8 deletions rust-server/src/features/auth/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::db::queries::{find_user_by_email, find_user_by_username};
use crate::features::auth::queries::delete_user;
use crate::features::auth::service::new_user;
use crate::state::AppState;
use crate::utils::api_response::ApiResponse;
use crate::utils::email::send_verification_email;
use crate::utils::password::hash_password;
use crate::{errors::AppError, features::auth::types::RegisterData};
use axum::extract::rejection::JsonRejection;
Expand Down Expand Up @@ -51,13 +51,18 @@ pub async fn register_user(
)
.await?;

eprintln!("USER ID: {:#?}", user_id);
eprintln!("VERIFICATION TOKEN: {:#?}", token);

// TODO: SEND VERIFICATION EMAIL WITH RESEND
if !state.config.app_env.is_test() {
send_verification_email(&cleaned_data.email, &token).await?;
};
// Send verification email - if it fails, delete user (compensating transaction)
if !state.config.app_env.is_test()
&& let Err(email_err) = state
.email
.send_verification_email(&cleaned_data.email, &token)
.await
{
// Compensating action: delete the user we just created
tracing::error!("Failed to send verification email: {:#?}", email_err);
delete_user(db, user_id).await?;
return Err(email_err);
}
Comment thread
reijjo marked this conversation as resolved.

Ok(ApiResponse::created(
"Check your email to validate your account",
Expand Down
14 changes: 14 additions & 0 deletions rust-server/src/features/auth/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,17 @@ where

Ok(())
}

// Delete user - DELETE (compensating transaction for failed email)
pub async fn delete_user<'e, E>(db: E, user_id: Uuid) -> Result<(), AppError>
where
E: Executor<'e, Database = Postgres>,
{
sqlx::query("DELETE FROM users WHERE id = $1")
.bind(user_id)
.execute(db)
.await
.map_err(AppError::Sql)?;

Ok(())
}
Loading
Loading