revelation_user/lib.rs
1// SPDX-FileCopyrightText: 2025 Revelation Team
2// SPDX-License-Identifier: MIT
3
4//! # revelation-user
5//!
6//! A professional user domain crate for the Revelation ecosystem.
7//!
8//! This crate provides a complete user management solution with:
9//! - Type-safe user entity with builder pattern
10//! - Multiple authentication methods (Telegram, email, phone)
11//! - Framework-agnostic extractors for axum and actix-web
12//! - Extensible architecture for application-specific user types
13//!
14//! ## Quick Start
15//!
16//! Add to your `Cargo.toml`:
17//!
18//! ```toml
19//! [dependencies]
20//! revelation-user = { version = "0.1", features = ["axum"] }
21//! ```
22//!
23//! Create users with the fluent builder API:
24//!
25//! ```rust
26//! use revelation_user::RUser;
27//!
28//! // From Telegram authentication
29//! let user = RUser::from_telegram(123456789);
30//!
31//! // From email authentication
32//! let user = RUser::from_email("john@example.com");
33//! ```
34//!
35//! ## Features
36//!
37//! | Feature | Description |
38//! |---------|-------------|
39//! | `db` | Database support via sqlx (PostgreSQL) |
40//! | `api` | OpenAPI schema generation via utoipa |
41//! | `axum` | Axum framework extractors |
42//! | `actix` | Actix-web framework extractors |
43//!
44//! **Note**: `axum` and `actix` features are mutually exclusive.
45//!
46//! ## Core Types
47//!
48//! ### Entity
49//!
50//! - [`RUser`] - The core user entity with all fields
51//! - [`Claims`] - JWT claims for authentication
52//!
53//! ### Projections
54//!
55//! Projections are read-only views optimized for specific contexts:
56//!
57//! - [`RUserPublic`] - Safe for API responses (excludes sensitive data)
58//! - [`RUserAuth`] - For JWT/session context (includes role)
59//!
60//! ```rust
61//! use revelation_user::{RUser, RUserAuth, RUserPublic, RUserRole};
62//!
63//! let user = RUser::from_telegram(123456789);
64//!
65//! // Convert to public view (safe for API)
66//! let public: RUserPublic = user.clone().into();
67//!
68//! // Convert to auth view (for JWT)
69//! let auth = RUserAuth::from_user(&user, RUserRole::User);
70//! ```
71//!
72//! ### DTOs
73//!
74//! Request/response data transfer objects:
75//!
76//! - [`CreateUserRequest`] - Create new user
77//! - [`UpdateProfileRequest`] - Update user profile
78//! - [`BindTelegram`], [`BindEmail`], [`BindPhone`] - Bind contact methods
79//!
80//! ## Extending Users
81//!
82//! Use the [`extend_user!`] macro to create application-specific user types:
83//!
84//! ```rust,ignore
85//! use revelation_user::{extend_user, RUser};
86//! use uuid::Uuid;
87//!
88//! extend_user! {
89//! /// Corporate user with company-specific fields.
90//! pub struct CorpUser {
91//! pub company_id: Uuid,
92//!
93//! #[builder(into)]
94//! pub department: String,
95//! }
96//! }
97//!
98//! // Use with fluent API
99//! let user = CorpUser::from_telegram(123456789)
100//! .name("John")
101//! .then()
102//! .company_id(Uuid::now_v7())
103//! .department("Engineering")
104//! .build();
105//!
106//! // Access fields transparently via Deref
107//! assert!(user.telegram_id.is_some()); // RUser field
108//! ```
109//!
110//! ## Framework Integration
111//!
112//! ### Axum
113//!
114//! ```rust,ignore
115//! use axum::{Router, Json, routing::get};
116//! use revelation_user::{Claims, RUserPublic};
117//!
118//! async fn me(claims: Claims) -> Json<RUserPublic> {
119//! // Claims extracted from JWT cookie or Authorization header
120//! todo!()
121//! }
122//!
123//! let app = Router::new().route("/me", get(me));
124//! ```
125//!
126//! ### Actix-web
127//!
128//! ```rust,ignore
129//! use actix_web::{web, HttpResponse};
130//! use revelation_user::{Claims, RUserPublic};
131//!
132//! async fn me(claims: Claims) -> HttpResponse {
133//! // Claims extracted automatically
134//! HttpResponse::Ok().json(claims.user_id())
135//! }
136//! ```
137//!
138//! ## Validation
139//!
140//! All DTOs support validation via the `validator` crate:
141//!
142//! ```rust
143//! use revelation_user::UpdateProfileRequest;
144//! use validator::Validate;
145//!
146//! let req = UpdateProfileRequest {
147//! name: Some("J".into()), // Too short!
148//! gender: None,
149//! birth_date: None,
150//! confession_id: None
151//! };
152//!
153//! assert!(req.validate().is_err());
154//! ```
155//!
156//! ## Module Structure
157//!
158//! - [`entity`] - Core user entity and JWT claims
159//! - [`projections`] - Read-only user views
160//! - [`dto`] - Request/response data transfer objects
161//! - [`extend`] - Extension macro for custom user types
162//! - [`extract`] - Framework extractors (feature-gated)
163//! - [`ports`] - Repository trait definitions
164
165use std::sync::LazyLock;
166
167use regex::Regex;
168
169pub mod dto;
170pub mod entity;
171pub mod extend;
172mod gender;
173mod notification;
174mod permissions;
175pub mod projections;
176mod role;
177
178#[cfg(any(feature = "axum", feature = "actix"))]
179pub mod extract;
180
181pub mod ports;
182
183// Re-exports for convenience
184pub use dto::*;
185pub use entity::*;
186#[cfg(any(feature = "axum", feature = "actix"))]
187pub use extract::*;
188pub use gender::*;
189pub use notification::*;
190pub use permissions::*;
191pub use projections::*;
192pub use role::*;
193
194/// E.164 international phone number format regex.
195///
196/// Matches phone numbers in the format `+[country code][number]`:
197/// - Must start with `+`
198/// - Country code: 1-3 digits (first digit non-zero)
199/// - Number: 9-14 additional digits
200///
201/// # Examples
202///
203/// Valid formats:
204/// - `+14155551234` (US)
205/// - `+442071234567` (UK)
206/// - `+79991234567` (Russia)
207///
208/// # Usage
209///
210/// ```rust
211/// use revelation_user::PHONE_REGEX;
212///
213/// assert!(PHONE_REGEX.is_match("+14155551234"));
214/// assert!(!PHONE_REGEX.is_match("14155551234")); // Missing +
215/// assert!(!PHONE_REGEX.is_match("+1234")); // Too short
216/// ```
217pub static PHONE_REGEX: LazyLock<Regex> =
218 LazyLock::new(|| Regex::new(r"^\+[1-9]\d{9,14}$").expect("valid phone regex"));
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn phone_regex_accepts_valid_numbers() {
226 assert!(PHONE_REGEX.is_match("+14155551234"));
227 assert!(PHONE_REGEX.is_match("+442071234567"));
228 assert!(PHONE_REGEX.is_match("+79991234567"));
229 }
230
231 #[test]
232 fn phone_regex_rejects_invalid_numbers() {
233 assert!(!PHONE_REGEX.is_match("14155551234"));
234 assert!(!PHONE_REGEX.is_match("+1234"));
235 assert!(!PHONE_REGEX.is_match("+0123456789"));
236 assert!(!PHONE_REGEX.is_match("not a phone"));
237 }
238}