Skip to content

Commit f9ceef6

Browse files
committed
feat: Implement tutorial management system with backend API, data models, search functionality, and frontend integration.
1 parent 7d37950 commit f9ceef6

6 files changed

Lines changed: 43 additions & 7 deletions

File tree

backend/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ hmac = "0.12"
3838
subtle = "2.5"
3939

4040
[dependencies.home]
41-
version = "=0.5.12"
41+
version = "=0.5.9"
4242

4343
[dependencies.base64ct]
4444
version = "=1.8.0"

backend/src/handlers/search.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fn sanitize_fts_query(raw: &str) -> Result<String, String> {
5757
// Keep only safe characters for FTS5 queries
5858
let sanitized: String = token
5959
.chars()
60-
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '*' | '-' | '_' | '.' | '+' | '#' | '@'))
60+
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '*' | '-' | '_' | '.' | '+' | '#' | '@' | '/' | ':' | '(' | ')' | '[' | ']'))
6161
.collect();
6262
if sanitized.is_empty() {
6363
None

backend/src/handlers/tutorials.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub(crate) fn validate_tutorial_id(id: &str) -> Result<(), String> {
4343
}
4444

4545
// Ensure only safe characters for database and URL usage
46-
if !id.chars().all(|c| c.is_alphanumeric() || c == '-') {
46+
if !id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') {
4747
return Err("Tutorial ID contains invalid characters".to_string());
4848
}
4949
Ok(())
@@ -308,7 +308,38 @@ pub async fn create_tutorial(
308308
return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e })));
309309
}
310310

311-
let id = Uuid::new_v4().to_string();
311+
let id = if let Some(custom_id) = &payload.id {
312+
let trimmed = custom_id.trim();
313+
if let Err(e) = validate_tutorial_id(trimmed) {
314+
return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e })));
315+
}
316+
// Check for collision
317+
let exists: Option<(i64,)> = sqlx::query_as("SELECT 1 FROM tutorials WHERE id = ?")
318+
.bind(trimmed)
319+
.fetch_optional(&pool)
320+
.await
321+
.map_err(|e| {
322+
tracing::error!("Database error checking ID existence: {}", e);
323+
(
324+
StatusCode::INTERNAL_SERVER_ERROR,
325+
Json(ErrorResponse {
326+
error: "Failed to create tutorial".to_string(),
327+
}),
328+
)
329+
})?;
330+
331+
if exists.is_some() {
332+
return Err((
333+
StatusCode::CONFLICT,
334+
Json(ErrorResponse {
335+
error: "Tutorial ID already exists".to_string(),
336+
}),
337+
));
338+
}
339+
trimmed.to_string()
340+
} else {
341+
Uuid::new_v4().to_string()
342+
};
312343
let sanitized_topics = sanitize_topics(&payload.topics)
313344
.map_err(|e| (StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e })))?;
314345
let topics_json = serde_json::to_string(&sanitized_topics).map_err(|e| {

backend/src/models.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ pub struct CreateTutorialRequest {
7474
pub color: String,
7575

7676
pub topics: Vec<String>,
77-
77+
7878
pub content: String,
79+
80+
pub id: Option<String>,
7981
}
8082

8183
#[derive(Debug, Deserialize)]

src/api/client.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export const getApiBaseUrl = () => {
1717
}
1818
}
1919
}
20+
if (typeof process !== 'undefined' && process.env && process.env.API_BASE_URL) {
21+
return process.env.API_BASE_URL.replace(/\/+$/, '')
22+
}
2023
return typeof window !== 'undefined' ? '/api' : 'http://localhost:8489/api'
2124
}
2225
const API_BASE_URL = getApiBaseUrl()
@@ -189,7 +192,7 @@ class ApiClient {
189192
}
190193
const contentType = response.headers.get('content-type') || ''
191194
let payload
192-
if (contentType.includes('application/json')) {
195+
if (contentType.toLowerCase().includes('application/json')) {
193196
try {
194197
payload = await response.json()
195198
} catch (parseError) {

src/components/Comments.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAuth } from '../context/AuthContext';
44
import { api } from '../api/client';
55
import PropTypes from 'prop-types';
66

7-
const VALID_TUTORIAL_ID = /^[a-zA-Z0-9_-]+$/;
7+
const VALID_TUTORIAL_ID = /^[a-zA-Z0-9_.-]+$/;
88
const COMMENTS_PER_PAGE = 20;
99

1010
const Comments = ({ tutorialId }) => {

0 commit comments

Comments
 (0)