Skip to content

Commit 9119829

Browse files
feat: Add comprehensive documentation to the repository
This commit adds comprehensive documentation to the entire repository, including the Rust backend and the React frontend. - Adds Rustdoc comments to all public functions, structs, and methods in the backend. - Adds JSDoc comments to all React components, pages, contexts, and utility functions in the frontend. - Updates the main README.md file to provide a more comprehensive guide for new developers, including improved installation and configuration instructions.
1 parent cfd43ca commit 9119829

15 files changed

Lines changed: 380 additions & 17 deletions

File tree

README.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,22 +76,9 @@ cd LinuxTutorialCMS
7676

7777
# Install frontend dependencies
7878
npm install
79-
80-
# Start the backend (in a separate terminal)
81-
cd backend
82-
cargo run
83-
84-
# Start the frontend
85-
npm run dev
8679
```
8780

88-
### Access the Application
89-
90-
- 🌐 **Frontend:** http://localhost:5173
91-
- 🔧 **Backend API:** http://localhost:8489
92-
- 🔐 **Admin Panel:** http://localhost:5173/login
93-
94-
### Admin-Anmeldung
81+
### Configuration
9582

9683
To get started, you need to create a `.env` file in the `backend` directory. This file will store the necessary environment variables for the application to run correctly.
9784

@@ -109,7 +96,22 @@ ADMIN_USERNAME=admin
10996
ADMIN_PASSWORD=your-secure-password
11097
```
11198

112-
Once you have created the `.env` file, you can start the backend server.
99+
### Running the Application
100+
101+
```bash
102+
# Start the backend (in a separate terminal)
103+
cd backend
104+
cargo run
105+
106+
# Start the frontend
107+
npm run dev
108+
```
109+
110+
### Access the Application
111+
112+
- 🌐 **Frontend:** http://localhost:5173
113+
- 🔧 **Backend API:** http://localhost:8489
114+
- 🔐 **Admin Panel:** http://localhost:5173/login
113115

114116
---
115117

backend/src/auth.rs

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,20 @@ const SECRET_BLACKLIST: &[&str] = &[
2525
const MIN_SECRET_LENGTH: usize = 43; // ~256 bits when base64 encoded
2626
const MIN_UNIQUE_CHARS: usize = 10;
2727
const MIN_CHAR_CLASSES: usize = 3;
28+
29+
/// The name of the authentication cookie.
2830
pub const AUTH_COOKIE_NAME: &str = "ltcms_session";
2931
const AUTH_COOKIE_TTL_SECONDS: i64 = 24 * 60 * 60; // 24 hours
3032

33+
/// Initializes the JWT secret from the `JWT_SECRET` environment variable.
34+
///
35+
/// This function performs critical security checks to ensure the secret is not a placeholder
36+
/// and meets minimum entropy requirements. It must be called successfully at startup.
37+
///
38+
/// # Returns
39+
///
40+
/// * `Ok(())` if the secret is valid and initialized.
41+
/// * `Err(String)` if the secret is missing, empty, a placeholder, or too weak.
3142
pub fn init_jwt_secret() -> Result<(), String> {
3243
let secret = env::var("JWT_SECRET")
3344
.map_err(|_| "JWT_SECRET environment variable not set".to_string())?;
@@ -55,21 +66,36 @@ pub fn init_jwt_secret() -> Result<(), String> {
5566
Ok(())
5667
}
5768

69+
/// Retrieves the initialized JWT secret.
70+
///
71+
/// # Panics
72+
///
73+
/// Panics if `init_jwt_secret` has not been called.
5874
fn get_jwt_secret() -> &'static str {
5975
JWT_SECRET
6076
.get()
6177
.expect("JWT_SECRET not initialized. Call init_jwt_secret() first.")
6278
.as_str()
6379
}
6480

81+
/// Represents the claims contained within a JWT.
6582
#[derive(Debug, Serialize, Deserialize, Clone)]
6683
pub struct Claims {
67-
pub sub: String, // username
84+
/// The subject of the token (username).
85+
pub sub: String,
86+
/// The role of the user.
6887
pub role: String,
88+
/// The expiration timestamp.
6989
pub exp: usize,
7090
}
7191

7292
impl Claims {
93+
/// Creates new `Claims` for a user with a 24-hour expiration.
94+
///
95+
/// # Arguments
96+
///
97+
/// * `username` - The username to encode in the token.
98+
/// * `role` - The user's role.
7399
pub fn new(username: String, role: String) -> Self {
74100
// Use checked arithmetic to prevent overflow
75101
let expiration = Utc::now()
@@ -85,6 +111,16 @@ impl Claims {
85111
}
86112
}
87113

114+
/// Creates a JWT for the given user.
115+
///
116+
/// # Arguments
117+
///
118+
/// * `username` - The username.
119+
/// * `role` - The user's role.
120+
///
121+
/// # Returns
122+
///
123+
/// A `Result` containing the signed JWT string or a `jsonwebtoken::errors::Error`.
88124
pub fn create_jwt(username: String, role: String) -> Result<String, jsonwebtoken::errors::Error> {
89125
let claims = Claims::new(username, role);
90126
let secret = get_jwt_secret();
@@ -96,6 +132,15 @@ pub fn create_jwt(username: String, role: String) -> Result<String, jsonwebtoken
96132
)
97133
}
98134

135+
/// Verifies a JWT and returns its claims.
136+
///
137+
/// # Arguments
138+
///
139+
/// * `token` - The JWT string to verify.
140+
///
141+
/// # Returns
142+
///
143+
/// A `Result` containing the decoded `Claims` or a `jsonwebtoken::errors::Error`.
99144
pub fn verify_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
100145
let secret = get_jwt_secret();
101146

@@ -112,6 +157,17 @@ pub fn verify_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
112157
Ok(token_data.claims)
113158
}
114159

160+
/// Builds an authentication cookie containing the JWT.
161+
///
162+
/// The cookie is configured with `HttpOnly`, `SameSite=Lax`, and a secure flag if not in a development environment.
163+
///
164+
/// # Arguments
165+
///
166+
/// * `token` - The JWT string to embed in the cookie.
167+
///
168+
/// # Returns
169+
///
170+
/// A `Cookie` struct ready to be added to a response.
115171
pub fn build_auth_cookie(token: &str) -> Cookie<'static> {
116172
let mut builder = Cookie::build((AUTH_COOKIE_NAME, token.to_owned()))
117173
.path("/")
@@ -126,6 +182,13 @@ pub fn build_auth_cookie(token: &str) -> Cookie<'static> {
126182
builder.build()
127183
}
128184

185+
/// Builds a cookie that instructs the client to remove the authentication cookie.
186+
///
187+
/// This is achieved by setting an immediate expiration date.
188+
///
189+
/// # Returns
190+
///
191+
/// A `Cookie` struct for removal.
129192
pub fn build_cookie_removal() -> Cookie<'static> {
130193
let mut builder = Cookie::build((AUTH_COOKIE_NAME, ""))
131194
.path("/")
@@ -141,6 +204,10 @@ pub fn build_cookie_removal() -> Cookie<'static> {
141204
builder.build()
142205
}
143206

207+
/// Axum extractor for `Claims`.
208+
///
209+
/// This allows handlers to easily require authentication by including `Claims` in their arguments.
210+
/// It extracts the token from the `Authorization` header or the auth cookie.
144211
impl<S> FromRequestParts<S> for Claims
145212
where
146213
S: Send + Sync,
@@ -163,6 +230,12 @@ where
163230
}
164231
}
165232

233+
/// Appends a `Set-Cookie` header to a `HeaderMap`.
234+
///
235+
/// # Arguments
236+
///
237+
/// * `headers` - The `HeaderMap` to modify.
238+
/// * `cookie` - The `Cookie` to append.
166239
pub fn append_auth_cookie(headers: &mut HeaderMap, cookie: Cookie<'static>) {
167240
if let Ok(value) = HeaderValue::from_str(&cookie.to_string()) {
168241
headers.append(SET_COOKIE, value);
@@ -171,6 +244,20 @@ pub fn append_auth_cookie(headers: &mut HeaderMap, cookie: Cookie<'static>) {
171244
}
172245
}
173246

247+
/// Checks if a secret meets minimum entropy requirements.
248+
///
249+
/// A secret is considered high-entropy if it:
250+
/// - Is at least `MIN_SECRET_LENGTH` characters long.
251+
/// - Contains at least `MIN_CHAR_CLASSES` character classes (lower, upper, digit, symbol).
252+
/// - Has at least `MIN_UNIQUE_CHARS` unique characters.
253+
///
254+
/// # Arguments
255+
///
256+
/// * `secret` - The secret string to check.
257+
///
258+
/// # Returns
259+
///
260+
/// `true` if the secret meets the criteria, `false` otherwise.
174261
fn secret_has_min_entropy(secret: &str) -> bool {
175262
if secret.len() < MIN_SECRET_LENGTH {
176263
return false;
@@ -198,6 +285,13 @@ fn secret_has_min_entropy(secret: &str) -> bool {
198285
unique_chars.len() >= MIN_UNIQUE_CHARS
199286
}
200287

288+
/// Determines if the `Secure` flag should be set on cookies.
289+
///
290+
/// The flag is set unless the `AUTH_COOKIE_SECURE` environment variable is explicitly "false".
291+
///
292+
/// # Returns
293+
///
294+
/// `true` if cookies should be secure, `false` otherwise.
201295
fn cookies_should_be_secure() -> bool {
202296
match env::var("AUTH_COOKIE_SECURE") {
203297
Ok(value) if value.trim().eq_ignore_ascii_case("false") => {
@@ -210,6 +304,18 @@ fn cookies_should_be_secure() -> bool {
210304
}
211305
}
212306

307+
/// Extracts a JWT from request headers.
308+
///
309+
/// It first checks for an `Authorization: Bearer <token>` header, falling back
310+
/// to the authentication cookie if not found.
311+
///
312+
/// # Arguments
313+
///
314+
/// * `headers` - The `HeaderMap` from the incoming request.
315+
///
316+
/// # Returns
317+
///
318+
/// An `Option<String>` containing the token if found.
213319
fn extract_token(headers: &HeaderMap) -> Option<String> {
214320
if let Some(header_value) = headers.get(AUTHORIZATION) {
215321
if let Ok(value_str) = header_value.to_str() {
@@ -224,6 +330,15 @@ fn extract_token(headers: &HeaderMap) -> Option<String> {
224330
.map(|cookie| cookie.value().to_string())
225331
}
226332

333+
/// Parses a token from an `Authorization: Bearer <token>` header value.
334+
///
335+
/// # Arguments
336+
///
337+
/// * `value` - The raw string from the `Authorization` header.
338+
///
339+
/// # Returns
340+
///
341+
/// An `Option<String>` containing the token if parsing is successful.
227342
fn parse_bearer_token(value: &str) -> Option<String> {
228343
let trimmed = value.trim();
229344
let (scheme, token) = trimmed.split_once(' ')?;

backend/src/csrf.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const CSRF_VERSION: &str = "v1";
3434

3535
static CSRF_SECRET: OnceLock<Vec<u8>> = OnceLock::new();
3636

37+
/// Initializes the CSRF secret from the `CSRF_SECRET` environment variable.
38+
///
39+
/// This must be called at application startup. It validates that the secret
40+
/// meets minimum length and complexity requirements.
3741
pub fn init_csrf_secret() -> Result<(), String> {
3842
let secret = env::var(CSRF_SECRET_ENV)
3943
.map_err(|_| format!("{CSRF_SECRET_ENV} environment variable not set"))?;
@@ -59,13 +63,17 @@ pub fn init_csrf_secret() -> Result<(), String> {
5963
Ok(())
6064
}
6165

66+
/// Retrieves the initialized CSRF secret. Panics if not initialized.
6267
fn get_secret() -> &'static [u8] {
6368
CSRF_SECRET
6469
.get()
6570
.expect("CSRF secret not initialized. Call init_csrf_secret() first.")
6671
.as_slice()
6772
}
6873

74+
/// Issues a new CSRF token for a given username.
75+
///
76+
/// The token embeds the username, expiry, and a nonce, signed with HMAC-SHA256.
6977
pub fn issue_csrf_token(username: &str) -> Result<String, String> {
7078
if username.is_empty() {
7179
return Err("Username required for CSRF token".to_string());
@@ -89,6 +97,9 @@ pub fn issue_csrf_token(username: &str) -> Result<String, String> {
8997
Ok(format!("{versioned_payload}|{signature}"))
9098
}
9199

100+
/// Validates a CSRF token against an expected username.
101+
///
102+
/// Checks the token's signature, expiry, and that it belongs to the authenticated user.
92103
fn validate_csrf_token(token: &str, expected_username: &str) -> Result<(), String> {
93104
let mut parts = token.split('|');
94105

@@ -155,11 +166,13 @@ fn validate_csrf_token(token: &str, expected_username: &str) -> Result<(), Strin
155166
Ok(())
156167
}
157168

169+
/// Performs a constant-time comparison of two byte slices.
158170
fn subtle_equals(a: &[u8], b: &[u8]) -> bool {
159171
use subtle::ConstantTimeEq;
160172
a.ct_eq(b).into()
161173
}
162174

175+
/// Appends a `Set-Cookie` header for the CSRF token to a `HeaderMap`.
163176
pub fn append_csrf_cookie(headers: &mut HeaderMap, token: &str) {
164177
let cookie = build_csrf_cookie(token);
165178
if let Ok(value) = HeaderValue::from_str(&cookie.to_string()) {
@@ -169,6 +182,7 @@ pub fn append_csrf_cookie(headers: &mut HeaderMap, token: &str) {
169182
}
170183
}
171184

185+
/// Appends a `Set-Cookie` header to clear the CSRF cookie.
172186
pub fn append_csrf_removal(headers: &mut HeaderMap) {
173187
let cookie = build_csrf_removal();
174188
if let Ok(value) = HeaderValue::from_str(&cookie.to_string()) {
@@ -178,6 +192,7 @@ pub fn append_csrf_removal(headers: &mut HeaderMap) {
178192
}
179193
}
180194

195+
/// Builds the CSRF cookie with appropriate security flags.
181196
fn build_csrf_cookie(token: &str) -> Cookie<'static> {
182197
let mut builder = Cookie::build((CSRF_COOKIE_NAME, token.to_owned()))
183198
.path("/")
@@ -192,6 +207,7 @@ fn build_csrf_cookie(token: &str) -> Cookie<'static> {
192207
builder.build()
193208
}
194209

210+
/// Builds a cookie that instructs the client to remove the CSRF cookie.
195211
fn build_csrf_removal() -> Cookie<'static> {
196212
let mut builder = Cookie::build((CSRF_COOKIE_NAME, ""))
197213
.path("/")
@@ -207,6 +223,10 @@ fn build_csrf_removal() -> Cookie<'static> {
207223
builder.build()
208224
}
209225

226+
/// An Axum extractor that enforces CSRF protection for state-changing requests.
227+
///
228+
/// This guard checks for a valid CSRF token in both the cookie and header,
229+
/// ensuring they match and are valid for the authenticated user.
210230
pub struct CsrfGuard;
211231

212232
#[async_trait]
@@ -281,10 +301,12 @@ where
281301
}
282302
}
283303

304+
/// Returns the name of the CSRF cookie.
284305
pub fn csrf_cookie_name() -> &'static str {
285306
CSRF_COOKIE_NAME
286307
}
287308

309+
/// Returns the name of the CSRF header.
288310
pub fn csrf_header_name() -> &'static str {
289311
CSRF_HEADER_NAME
290312
}

0 commit comments

Comments
 (0)