Compare commits
3 Commits
e55aa1153d
...
feature/wi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14edcf4bf6 | ||
|
|
9fbeb0afa5 | ||
|
|
7f98a79c29 |
181
TASK_LOG.md
181
TASK_LOG.md
@@ -3,7 +3,7 @@
|
|||||||
## Session Overview
|
## Session Overview
|
||||||
**Date:** August 10, 2025
|
**Date:** August 10, 2025
|
||||||
**Duration:** ~1 hour
|
**Duration:** ~1 hour
|
||||||
**Total Tasks Completed:** 8
|
**Total Tasks Completed:** 18
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -97,8 +97,143 @@
|
|||||||
**Request:** "can you create a log with a summary of each task i have asked you to perform with a timestamp"
|
**Request:** "can you create a log with a summary of each task i have asked you to perform with a timestamp"
|
||||||
**Actions:**
|
**Actions:**
|
||||||
- Created this comprehensive task log
|
- Created this comprehensive task log
|
||||||
- Documented all changes, reasoning, and outcomes
|
|
||||||
- **Result:** Complete audit trail of development session
|
---
|
||||||
|
|
||||||
|
### 9. Fix Missing /wiki Prefix on Links
|
||||||
|
**Time:** ~1:00 AM
|
||||||
|
**Request:** "/wiki is not prepended on some of the links"
|
||||||
|
**Actions:**
|
||||||
|
- Identified issue in markdown link processing (`src/markdown/mod.rs:72-96`)
|
||||||
|
- Fixed relative links missing `/wiki/` prefix in page content
|
||||||
|
- Modified link processing logic to add `/wiki/` prefix to relative links that don't start with `http`, `/`, or `#`
|
||||||
|
- Added test case to verify fix works correctly
|
||||||
|
- **Result:** Relative markdown links like `[Page](page-name)` now correctly become `/wiki/page-name`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Add Toggleable Filetree Title
|
||||||
|
**Time:** ~1:05 AM
|
||||||
|
**Request:** "make the filetree title 'Filetree' and make it clickable and toggle the showing the contents"
|
||||||
|
**Actions:**
|
||||||
|
- Updated both folder and wiki page templates in `src/server/mod.rs`
|
||||||
|
- Changed "Files in this folder" heading to clickable "📁 Filetree" with cursor pointer styling
|
||||||
|
- Added JavaScript toggle functionality to show/hide file tree contents
|
||||||
|
- Icon changes between 📁 (open) and 📂 (closed) states
|
||||||
|
- Applied to both `render_folder_page()` and `render_wiki_page()` functions
|
||||||
|
- **Result:** File tree now has a collapsible interface with visual feedback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Move Filetree Above Article Content
|
||||||
|
**Time:** ~1:10 AM
|
||||||
|
**Request:** "move the filetree above the article"
|
||||||
|
**Actions:**
|
||||||
|
- Updated folder page template in `src/server/mod.rs` to move filetree outside and above the `<article>` tag
|
||||||
|
- Updated wiki page template to move filetree above article content
|
||||||
|
- Restructured HTML layout: `<main>` now contains separate `<div>` for filetree followed by `<article>`
|
||||||
|
- Maintained all existing toggle functionality and styling
|
||||||
|
- **Result:** Filetree now appears above the main article content on both page types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. Style Header Navigation in One Row
|
||||||
|
**Time:** ~1:15 AM
|
||||||
|
**Request:** "put Home, search and logout all in one row"
|
||||||
|
**Actions:**
|
||||||
|
- Added CSS styling to both folder and wiki page templates in `src/server/mod.rs`
|
||||||
|
- Implemented flexbox layout for navigation bar with `display: flex`
|
||||||
|
- Used `justify-content: space-between` to spread elements across the full width
|
||||||
|
- Added padding, borders, and proper spacing for visual organization
|
||||||
|
- Escaped CSS braces properly for Rust format strings (using `{{` and `}}`)
|
||||||
|
- Applied consistent styling to both `render_folder_page()` and `render_wiki_page()` functions
|
||||||
|
- **Result:** Header now displays Home link, search input, and auth section in a clean horizontal row
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Redesign Navigation as Right-Side Column
|
||||||
|
**Time:** ~1:20 AM
|
||||||
|
**Request:** "changed my mind. put home, search and login in a column on the right, with a border around the box"
|
||||||
|
**Actions:**
|
||||||
|
- Completely redesigned navigation layout from horizontal to vertical column
|
||||||
|
- Changed nav from header bar to fixed-position box on the right side
|
||||||
|
- Added border styling: 2px solid border, rounded corners, light background
|
||||||
|
- Implemented box-shadow for depth and visual separation
|
||||||
|
- Set navigation to `position: fixed` at top-right (20px from edges)
|
||||||
|
- Adjusted main content with right margin to avoid nav overlap
|
||||||
|
- Removed unnecessary div wrappers for cleaner HTML structure
|
||||||
|
- Applied changes to both folder and wiki page templates
|
||||||
|
- **Result:** Navigation now appears as an elegant bordered box in the top-right corner with vertical layout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. Change Font to Serif & Default Filetree Closed
|
||||||
|
**Time:** ~1:25 AM
|
||||||
|
**Request:** "change back to serif font and default filetree to be closed"
|
||||||
|
**Actions:**
|
||||||
|
- Changed `font-family` from `Arial, sans-serif` to `serif` in both templates
|
||||||
|
- Updated filetree to start in closed state by default:
|
||||||
|
- Changed initial icon from 📁 to 📂 (closed folder icon)
|
||||||
|
- Added `style="display: none;"` to filetree content div
|
||||||
|
- Modified JavaScript to not load filetree content immediately
|
||||||
|
- Updated toggle functionality to lazy-load content on first open
|
||||||
|
- Applied lazy loading: filetree content now only loads when user first clicks to open it
|
||||||
|
- Maintained all existing toggle functionality while improving performance
|
||||||
|
- **Result:** Site now uses serif font and filetree starts collapsed, loading content only when needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. Remove Navigation Box Shadow & Background
|
||||||
|
**Time:** ~1:30 AM
|
||||||
|
**Request:** "take out the drop shadow on the box, and the background color"
|
||||||
|
**Actions:**
|
||||||
|
- Removed `box-shadow: 0 2px 5px rgba(0,0,0,0.1);` from navigation styling
|
||||||
|
- Removed `background-color: #f9f9f9;` from navigation styling
|
||||||
|
- Applied changes to both folder and wiki page templates
|
||||||
|
- Maintained all other styling: border, border-radius, positioning, and layout
|
||||||
|
- **Result:** Navigation box now has a cleaner, more minimal appearance with just the border outline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. Remove Sans-Serif Fonts & Rounded Corners
|
||||||
|
**Time:** ~1:35 AM
|
||||||
|
**Request:** "remove all sans-serif, even from the search box. remove the rounded corners from tha page as well"
|
||||||
|
**Actions:**
|
||||||
|
- Explicitly set `font-family: serif` for navigation links and search input
|
||||||
|
- Removed `border-radius: 8px` from navigation box styling
|
||||||
|
- Removed `border-radius: 4px` from search input styling
|
||||||
|
- Ensured all UI elements consistently use serif fonts
|
||||||
|
- Applied changes to both folder and wiki page templates
|
||||||
|
- **Result:** Complete serif typography throughout the interface with sharp, angular styling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 17. Open Filetree by Default on Folder Pages
|
||||||
|
**Time:** ~1:40 AM
|
||||||
|
**Request:** "if the page is a folder, then we should open the filetree by default"
|
||||||
|
**Actions:**
|
||||||
|
- Modified folder page template to show filetree open by default:
|
||||||
|
- Changed filetree icon from 📂 (closed) to 📁 (open)
|
||||||
|
- Removed `style="display: none;"` from filetree content div
|
||||||
|
- Updated JavaScript to load filetree content immediately on page load
|
||||||
|
- Simplified toggle functionality for folder pages (removed lazy loading logic)
|
||||||
|
- Wiki pages remain unchanged - still start with closed filetree
|
||||||
|
- **Result:** Folder pages now display file tree immediately for better navigation, while wiki pages maintain minimalist closed state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. Add Markdown Table Support
|
||||||
|
**Time:** ~1:45 AM
|
||||||
|
**Request:** "create a new branch for this next feature: let's try and render markdown tables"
|
||||||
|
**Actions:**
|
||||||
|
- Created new branch `markdown-tables` for the feature
|
||||||
|
- Modified `src/markdown/mod.rs` to enable table support:
|
||||||
|
- Added `Options` import from pulldown-cmark
|
||||||
|
- Updated parser to use `Parser::new_ext()` with `Options::ENABLE_TABLES`
|
||||||
|
- Added comprehensive test case for table rendering verification
|
||||||
|
- Verified compilation with `cargo check` - all changes compile successfully
|
||||||
|
- Tables now support standard markdown table syntax with headers and data rows
|
||||||
|
- **Result:** Markdown tables now render properly with full HTML table structure (`<table>`, `<thead>`, `<tbody>`, etc.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -386,3 +521,43 @@
|
|||||||
4. JavaScript API calls use Authorization header via `authenticatedFetch()`
|
4. JavaScript API calls use Authorization header via `authenticatedFetch()`
|
||||||
- **Result:** Working JWT authentication with unstyled HTML forms, supporting both browser navigation and API calls
|
- **Result:** Working JWT authentication with unstyled HTML forms, supporting both browser navigation and API calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 25. Implement Wiki Page Editing Functionality
|
||||||
|
**Time:** August 15, 2025
|
||||||
|
**Request:** "Let's add an editing functionality. Make a new branch for this or a small batch of kittens will die!"
|
||||||
|
**Actions:**
|
||||||
|
- Created new branch `feature/wiki-editing` for development
|
||||||
|
- **1. Added Edit Button to UI (only visible when logged in):**
|
||||||
|
- Modified `render_wiki_page()` in `src/server/mod.rs` to include edit button in navigation
|
||||||
|
- Added JavaScript to show/hide edit button based on JWT authentication status
|
||||||
|
- Button redirects to `/edit/<path>` when clicked
|
||||||
|
- **2. Created /edit Route Handler:**
|
||||||
|
- Added `/edit/*path` route to serve raw markdown content for editing
|
||||||
|
- Implemented `edit_page_handler()` with authentication and permission checking
|
||||||
|
- Returns raw markdown content from WikiService for editing
|
||||||
|
- **3. Built Multi-line Text Editor Page:**
|
||||||
|
- Created `render_edit_page()` function with full HTML template
|
||||||
|
- Features no-frills textarea editor preserving all whitespace
|
||||||
|
- Added Save and Cancel buttons with proper styling
|
||||||
|
- Auto-focus on editor for immediate typing
|
||||||
|
- **4. Implemented PUT API Endpoint:**
|
||||||
|
- Added PUT handler to `/api/wiki/*path` route for saving edited content
|
||||||
|
- Implemented `save_wiki_handler()` with authentication and permission validation
|
||||||
|
- Uses existing `WikiService.save_page()` method for file operations
|
||||||
|
- **5. Added Git Commit Functionality:**
|
||||||
|
- Created `git_commit_changes()` helper function
|
||||||
|
- Automatically stages and commits edited files if git repository exists
|
||||||
|
- Includes commit message with editor username: "Update {path}\n\nEdited by: {username}"
|
||||||
|
- Fails silently if no git repository present (as requested)
|
||||||
|
- **6. Helper Functions:**
|
||||||
|
- Added `get_user_from_headers()` to extract user from JWT tokens
|
||||||
|
- Made `WikiService.can_edit_page()` method public for permission checking
|
||||||
|
- **Technical Implementation:**
|
||||||
|
- Uses existing authentication system (JWT tokens)
|
||||||
|
- Respects user role permissions (Admin/Editor can edit, Viewer cannot)
|
||||||
|
- Handles both Bearer token headers and auth_token cookies
|
||||||
|
- Proper URL encoding/decoding for nested paths
|
||||||
|
- JavaScript fetch API with proper error handling
|
||||||
|
- **Result:** Complete editing workflow allowing authenticated users to edit any wiki page with automatic git versioning
|
||||||
|
|
||||||
|
|||||||
@@ -269,18 +269,25 @@ impl AuthService {
|
|||||||
&Validation::default(),
|
&Validation::default(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
tracing::debug!("JWT contains user ID: {}", token_data.claims.sub);
|
||||||
|
|
||||||
// Get user from database to ensure they're still active
|
// Get user from database to ensure they're still active
|
||||||
|
let user_id_str = &token_data.claims.sub;
|
||||||
|
tracing::debug!("Querying with user ID string: {}", user_id_str);
|
||||||
|
|
||||||
let user_row = sqlx::query(
|
let user_row = sqlx::query(
|
||||||
"SELECT id, username, email, password_hash, role, provider, provider_id, created_at, last_login, is_active
|
"SELECT id, username, email, password_hash, role, provider, provider_id, created_at, last_login, is_active
|
||||||
FROM users WHERE id = ? AND is_active = true"
|
FROM users WHERE id = ? AND is_active = 1"
|
||||||
)
|
)
|
||||||
.bind(Uuid::parse_str(&token_data.claims.sub)?)
|
.bind(user_id_str)
|
||||||
.fetch_optional(&self.db)
|
.fetch_optional(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
tracing::debug!("Query returned {} rows", if user_row.is_some() { 1 } else { 0 });
|
||||||
|
|
||||||
if let Some(row) = user_row {
|
if let Some(row) = user_row {
|
||||||
let user = User {
|
let user = User {
|
||||||
id: row.get("id"),
|
id: Uuid::parse_str(&row.get::<String, _>("id")).unwrap_or_else(|_| Uuid::new_v4()),
|
||||||
username: row.get("username"),
|
username: row.get("username"),
|
||||||
email: row.get("email"),
|
email: row.get("email"),
|
||||||
password_hash: row.get("password_hash"),
|
password_hash: row.get("password_hash"),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use pulldown_cmark::{html, Event, Parser, Tag, TagEnd};
|
use pulldown_cmark::{html, Event, Options, Parser, Tag, TagEnd};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -66,7 +66,9 @@ impl MarkdownRenderer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Parse markdown to HTML, processing links to .md files
|
// Parse markdown to HTML, processing links to .md files
|
||||||
let parser = Parser::new(&processed_content);
|
let mut options = Options::empty();
|
||||||
|
options.insert(Options::ENABLE_TABLES);
|
||||||
|
let parser = Parser::new_ext(&processed_content, options);
|
||||||
let processed_events = parser.map(|event| {
|
let processed_events = parser.map(|event| {
|
||||||
match event {
|
match event {
|
||||||
Event::Start(Tag::Link { link_type, dest_url, title, id }) => {
|
Event::Start(Tag::Link { link_type, dest_url, title, id }) => {
|
||||||
@@ -81,6 +83,15 @@ impl MarkdownRenderer {
|
|||||||
title,
|
title,
|
||||||
id
|
id
|
||||||
})
|
})
|
||||||
|
} else if !dest_url.starts_with("http") && !dest_url.starts_with("/") && !dest_url.starts_with("#") {
|
||||||
|
// This looks like a relative wiki link, prepend /wiki/
|
||||||
|
let wiki_url = format!("/wiki/{}", dest_url);
|
||||||
|
Event::Start(Tag::Link {
|
||||||
|
link_type,
|
||||||
|
dest_url: wiki_url.into(),
|
||||||
|
title,
|
||||||
|
id
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Event::Start(Tag::Link { link_type, dest_url, title, id })
|
Event::Start(Tag::Link { link_type, dest_url, title, id })
|
||||||
}
|
}
|
||||||
@@ -210,4 +221,35 @@ This is the page content."#;
|
|||||||
assert_eq!(frontmatter.get("author"), Some(&"John Doe".to_string()));
|
assert_eq!(frontmatter.get("author"), Some(&"John Doe".to_string()));
|
||||||
assert!(remaining.contains("# Main Content"));
|
assert!(remaining.contains("# Main Content"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relative_links_get_wiki_prefix() {
|
||||||
|
let renderer = MarkdownRenderer::new();
|
||||||
|
let content = "This is a [relative link](page-name) and an [external link](https://example.com) and a [root link](/root).";
|
||||||
|
let result = renderer.render(content).unwrap();
|
||||||
|
|
||||||
|
// Relative links should get /wiki/ prefix
|
||||||
|
assert!(result.html.contains(r#"<a href="/wiki/page-name">"#));
|
||||||
|
// External links should remain unchanged
|
||||||
|
assert!(result.html.contains(r#"<a href="https://example.com">"#));
|
||||||
|
// Root links should remain unchanged
|
||||||
|
assert!(result.html.contains(r#"<a href="/root">"#));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_markdown_tables() {
|
||||||
|
let renderer = MarkdownRenderer::new();
|
||||||
|
let content = r#"| Name | Age | City |
|
||||||
|
|------|-----|------|
|
||||||
|
| John | 25 | NYC |
|
||||||
|
| Jane | 30 | LA |"#;
|
||||||
|
let result = renderer.render(content).unwrap();
|
||||||
|
|
||||||
|
// Check that table elements are present
|
||||||
|
assert!(result.html.contains("<table>"));
|
||||||
|
assert!(result.html.contains("<thead>"));
|
||||||
|
assert!(result.html.contains("<tbody>"));
|
||||||
|
assert!(result.html.contains("<th>Name</th>"));
|
||||||
|
assert!(result.html.contains("<td>John</td>"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,8 @@ async fn create_app(state: AppState) -> Router {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(index_handler))
|
.route("/", get(index_handler))
|
||||||
.route("/wiki/*path", get(wiki_page_handler))
|
.route("/wiki/*path", get(wiki_page_handler))
|
||||||
.route("/api/wiki/*path", get(api_wiki_handler))
|
.route("/edit/*path", get(edit_page_handler))
|
||||||
|
.route("/api/wiki/*path", get(api_wiki_handler).put(save_wiki_handler))
|
||||||
.route("/api/search", get(search_handler))
|
.route("/api/search", get(search_handler))
|
||||||
.route("/api/folder-files", get(folder_files_handler))
|
.route("/api/folder-files", get(folder_files_handler))
|
||||||
.route("/auth/login", get(login_form_handler).post(login_handler))
|
.route("/auth/login", get(login_form_handler).post(login_handler))
|
||||||
@@ -320,6 +321,117 @@ async fn github_callback_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn edit_page_handler(
|
||||||
|
Path(path): Path<String>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
// Remove leading slash from captured path
|
||||||
|
let clean_path = path.strip_prefix('/').unwrap_or(&path);
|
||||||
|
let decoded_path = urlencoding::decode(clean_path).unwrap_or_else(|_| clean_path.into());
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
tracing::info!("Edit page request for path: {}", decoded_path);
|
||||||
|
if !is_user_authenticated(&headers, &state).await {
|
||||||
|
tracing::warn!("User not authenticated for edit page: {}", decoded_path);
|
||||||
|
return Html(render_login_required_page()).into_response();
|
||||||
|
}
|
||||||
|
tracing::info!("User authenticated for edit page: {}", decoded_path);
|
||||||
|
|
||||||
|
// Get user from token for permission checking (simplified approach)
|
||||||
|
let user = if let Some(cookie_header) = headers.get("Cookie") {
|
||||||
|
if let Ok(cookie_str) = cookie_header.to_str() {
|
||||||
|
tracing::info!("Found cookies: {}", cookie_str);
|
||||||
|
let mut found_user = None;
|
||||||
|
for cookie in cookie_str.split(';') {
|
||||||
|
let cookie = cookie.trim();
|
||||||
|
if let Some(token) = cookie.strip_prefix("auth_token=") {
|
||||||
|
tracing::debug!("Found auth_token cookie, verifying...");
|
||||||
|
match state.auth.verify_token(token).await {
|
||||||
|
Ok(Some(user)) => {
|
||||||
|
tracing::info!("Cookie token verified successfully for user: {}", user.username);
|
||||||
|
found_user = Some(user);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::warn!("Cookie token is valid JWT but user not found in database");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Cookie token verification failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found_user
|
||||||
|
} else {
|
||||||
|
tracing::error!("Could not parse cookie header");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::error!("No Cookie header found in request");
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = match user {
|
||||||
|
Some(user) => {
|
||||||
|
tracing::info!("Successfully got user from cookie: {}", user.username);
|
||||||
|
user
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
tracing::error!("Failed to get user from cookie despite authentication check passing");
|
||||||
|
return Html(render_login_required_page()).into_response();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if user can edit this page
|
||||||
|
match state.wiki.can_edit_page(&decoded_path, &user).await {
|
||||||
|
Ok(true) => {},
|
||||||
|
Ok(false) => return (StatusCode::FORBIDDEN, "Permission denied").into_response(),
|
||||||
|
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Error checking permissions").into_response(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get raw markdown content
|
||||||
|
let raw_content = match state.wiki.get_page(&decoded_path).await {
|
||||||
|
Ok(Some(page)) => page.content,
|
||||||
|
Ok(None) => String::new(), // New page
|
||||||
|
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Error loading page").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Html(render_edit_page(&decoded_path, &raw_content)).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SavePageRequest {
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_wiki_handler(
|
||||||
|
Path(path): Path<String>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<SavePageRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Remove leading slash from captured path
|
||||||
|
let clean_path = path.strip_prefix('/').unwrap_or(&path);
|
||||||
|
let decoded_path = urlencoding::decode(clean_path).unwrap_or_else(|_| clean_path.into());
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
let user = match get_user_from_headers(&headers, &state).await {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return StatusCode::UNAUTHORIZED.into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the page
|
||||||
|
match state.wiki.save_page(&decoded_path, &req.content, &user).await {
|
||||||
|
Ok(_) => {
|
||||||
|
// Try to commit to git if possible
|
||||||
|
let _ = git_commit_changes(&state, &decoded_path, &user).await;
|
||||||
|
StatusCode::OK.into_response()
|
||||||
|
}
|
||||||
|
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn render_folder_page(folder_path: &str, state: &AppState) -> String {
|
async fn render_folder_page(folder_path: &str, state: &AppState) -> String {
|
||||||
let folder_name = if folder_path.is_empty() {
|
let folder_name = if folder_path.is_empty() {
|
||||||
"Wiki Root"
|
"Wiki Root"
|
||||||
@@ -333,28 +445,64 @@ async fn render_folder_page(folder_path: &str, state: &AppState) -> String {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{}</title>
|
<title>{}</title>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
nav {{
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
z-index: 1000;
|
||||||
|
}}
|
||||||
|
nav a {{
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
padding: 5px 0;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
nav input {{
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
width: 150px;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
#auth-section {{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}}
|
||||||
|
main {{
|
||||||
|
margin-right: 220px;
|
||||||
|
padding: 20px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<div>
|
<input type="text" id="search" placeholder="Search wiki...">
|
||||||
<input type="text" id="search" placeholder="Search wiki...">
|
|
||||||
</div>
|
|
||||||
<div id="auth-section">
|
<div id="auth-section">
|
||||||
<a href="/auth/login" id="auth-link">Login</a>
|
<a href="/auth/login" id="auth-link">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
<div>
|
||||||
|
<h3 id="filetree-toggle" style="cursor: pointer; user-select: none;">📁 Filetree</h3>
|
||||||
|
<div id="filetree">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<h1>{}</h1>
|
<h1>{}</h1>
|
||||||
<div>
|
|
||||||
<h3>Files in this folder</h3>
|
|
||||||
<div id="filetree">
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<script src="/static/js/script.js"></script>
|
<script src="/static/js/script.js"></script>
|
||||||
@@ -376,7 +524,7 @@ async fn render_folder_page(folder_path: &str, state: &AppState) -> String {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}};
|
}};
|
||||||
}}
|
}}
|
||||||
// Load filetree for current folder
|
// Load filetree immediately for folder pages
|
||||||
loadFolderFiles('{}');
|
loadFolderFiles('{}');
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -420,6 +568,20 @@ async fn render_folder_page(folder_path: &str, state: &AppState) -> String {
|
|||||||
|
|
||||||
container.innerHTML = list;
|
container.innerHTML = list;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// Add filetree toggle functionality
|
||||||
|
const filetreeToggle = document.getElementById('filetree-toggle');
|
||||||
|
const filetreeContent = document.getElementById('filetree');
|
||||||
|
|
||||||
|
filetreeToggle.addEventListener('click', function() {{
|
||||||
|
if (filetreeContent.style.display === 'none') {{
|
||||||
|
filetreeContent.style.display = 'block';
|
||||||
|
filetreeToggle.textContent = '📁 Filetree';
|
||||||
|
}} else {{
|
||||||
|
filetreeContent.style.display = 'none';
|
||||||
|
filetreeToggle.textContent = '📂 Filetree';
|
||||||
|
}}
|
||||||
|
}});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"#,
|
</html>"#,
|
||||||
@@ -440,31 +602,68 @@ async fn render_wiki_page(page: &WikiPage, state: &AppState) -> String {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{}</title>
|
<title>{}</title>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
nav {{
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
z-index: 1000;
|
||||||
|
}}
|
||||||
|
nav a {{
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
padding: 5px 0;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
nav input {{
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
width: 150px;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
#auth-section {{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}}
|
||||||
|
main {{
|
||||||
|
margin-right: 220px;
|
||||||
|
padding: 20px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<div>
|
<input type="text" id="search" placeholder="Search wiki...">
|
||||||
<input type="text" id="search" placeholder="Search wiki...">
|
|
||||||
</div>
|
|
||||||
<div id="auth-section">
|
<div id="auth-section">
|
||||||
<a href="/auth/login" id="auth-link">Login</a>
|
<a href="/auth/login" id="auth-link">Login</a>
|
||||||
|
<button id="edit-button" style="display: none; margin-top: 5px; padding: 5px 10px; border: 1px solid #ccc; background: white; cursor: pointer; font-family: serif;">Edit</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
<div>
|
||||||
|
<h3 id="filetree-toggle" style="cursor: pointer; user-select: none;">📂 Filetree</h3>
|
||||||
|
<div id="filetree" style="display: none;">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<h1>{}</h1>
|
<h1>{}</h1>
|
||||||
<div>
|
<div>
|
||||||
{}
|
{}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h3>Files in this folder</h3>
|
|
||||||
<div id="filetree">
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h3>Backlinks</h3>
|
<h3>Backlinks</h3>
|
||||||
<div>
|
<div>
|
||||||
@@ -480,6 +679,7 @@ async fn render_wiki_page(page: &WikiPage, state: &AppState) -> String {
|
|||||||
const token = localStorage.getItem('obswiki_token');
|
const token = localStorage.getItem('obswiki_token');
|
||||||
const cookieToken = getCookie('auth_token');
|
const cookieToken = getCookie('auth_token');
|
||||||
const authLink = document.getElementById('auth-link');
|
const authLink = document.getElementById('auth-link');
|
||||||
|
const editButton = document.getElementById('edit-button');
|
||||||
|
|
||||||
if (token || cookieToken) {{
|
if (token || cookieToken) {{
|
||||||
authLink.textContent = 'Logout';
|
authLink.textContent = 'Logout';
|
||||||
@@ -491,9 +691,15 @@ async fn render_wiki_page(page: &WikiPage, state: &AppState) -> String {
|
|||||||
document.cookie = 'auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
document.cookie = 'auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
// Show edit button for logged in users
|
||||||
|
editButton.style.display = 'block';
|
||||||
|
editButton.onclick = function() {{
|
||||||
|
const currentPath = window.location.pathname.replace('/wiki/', '') || 'index';
|
||||||
|
window.location.href = '/edit/' + currentPath;
|
||||||
|
}};
|
||||||
}}
|
}}
|
||||||
// Load filetree for current folder
|
// Don't load filetree initially - load on first toggle
|
||||||
loadCurrentFolderFiles();
|
|
||||||
}});
|
}});
|
||||||
|
|
||||||
function getCookie(name) {{
|
function getCookie(name) {{
|
||||||
@@ -539,6 +745,26 @@ async fn render_wiki_page(page: &WikiPage, state: &AppState) -> String {
|
|||||||
|
|
||||||
container.innerHTML = list;
|
container.innerHTML = list;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// Add filetree toggle functionality
|
||||||
|
const filetreeToggle = document.getElementById('filetree-toggle');
|
||||||
|
const filetreeContent = document.getElementById('filetree');
|
||||||
|
let filetreeLoaded = false;
|
||||||
|
|
||||||
|
filetreeToggle.addEventListener('click', function() {{
|
||||||
|
if (filetreeContent.style.display === 'none') {{
|
||||||
|
filetreeContent.style.display = 'block';
|
||||||
|
filetreeToggle.textContent = '📁 Filetree';
|
||||||
|
// Load filetree content on first open
|
||||||
|
if (!filetreeLoaded) {{
|
||||||
|
loadCurrentFolderFiles();
|
||||||
|
filetreeLoaded = true;
|
||||||
|
}}
|
||||||
|
}} else {{
|
||||||
|
filetreeContent.style.display = 'none';
|
||||||
|
filetreeToggle.textContent = '📂 Filetree';
|
||||||
|
}}
|
||||||
|
}});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"#,
|
</html>"#,
|
||||||
@@ -725,6 +951,96 @@ async fn is_user_authenticated(headers: &HeaderMap, state: &AppState) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_user_from_headers(headers: &HeaderMap, state: &AppState) -> Option<User> {
|
||||||
|
tracing::debug!("Getting user from headers");
|
||||||
|
|
||||||
|
// Check Authorization header first
|
||||||
|
if let Some(auth_header) = headers.get("Authorization") {
|
||||||
|
if let Ok(auth_str) = auth_header.to_str() {
|
||||||
|
tracing::debug!("Found Authorization header: {}", auth_str);
|
||||||
|
if let Some(token) = auth_str.strip_prefix("Bearer ") {
|
||||||
|
tracing::debug!("Extracted Bearer token, verifying...");
|
||||||
|
match state.auth.verify_token(token).await {
|
||||||
|
Ok(Some(user)) => {
|
||||||
|
tracing::info!("Bearer token verified successfully for user: {}", user.username);
|
||||||
|
return Some(user);
|
||||||
|
},
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::warn!("Bearer token verification returned None");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Bearer token verification failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::debug!("No Authorization header found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check cookies for token
|
||||||
|
if let Some(cookie_header) = headers.get("Cookie") {
|
||||||
|
if let Ok(cookie_str) = cookie_header.to_str() {
|
||||||
|
tracing::debug!("Found Cookie header: {}", cookie_str);
|
||||||
|
for cookie in cookie_str.split(';') {
|
||||||
|
let cookie = cookie.trim();
|
||||||
|
if let Some(token) = cookie.strip_prefix("auth_token=") {
|
||||||
|
tracing::debug!("Found auth_token cookie, verifying...");
|
||||||
|
match state.auth.verify_token(token).await {
|
||||||
|
Ok(Some(user)) => {
|
||||||
|
tracing::info!("Cookie token verified successfully for user: {}", user.username);
|
||||||
|
return Some(user);
|
||||||
|
},
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::warn!("Cookie token verification returned None");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Cookie token verification failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::debug!("No Cookie header found");
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::error!("Failed to get user from any authentication method");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn git_commit_changes(state: &AppState, path: &str, user: &User) -> Result<()> {
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
let wiki_path = state.wiki.get_wiki_path();
|
||||||
|
|
||||||
|
// Check if this is a git repository
|
||||||
|
let git_dir = wiki_path.join(".git");
|
||||||
|
if !git_dir.exists() {
|
||||||
|
// Fail silently as requested
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage the changed file
|
||||||
|
let file_path = format!("{}.md", path);
|
||||||
|
let _output = Command::new("git")
|
||||||
|
.current_dir(wiki_path)
|
||||||
|
.args(&["add", &file_path])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create commit message
|
||||||
|
let commit_message = format!("Update {}\n\nEdited by: {}", path, user.username);
|
||||||
|
|
||||||
|
// Commit the changes
|
||||||
|
let _output = Command::new("git")
|
||||||
|
.current_dir(wiki_path)
|
||||||
|
.args(&["commit", "-m", &commit_message])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct FolderFile {
|
struct FolderFile {
|
||||||
@@ -846,9 +1162,7 @@ fn render_login_required_page() -> String {
|
|||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<div>
|
<input type="text" id="search" placeholder="Search wiki...">
|
||||||
<input type="text" id="search" placeholder="Search wiki...">
|
|
||||||
</div>
|
|
||||||
<div id="auth-section">
|
<div id="auth-section">
|
||||||
<a href="/auth/login" id="auth-link">Login</a>
|
<a href="/auth/login" id="auth-link">Login</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -974,3 +1288,150 @@ fn render_login_page() -> String {
|
|||||||
</html>"#
|
</html>"#
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_edit_page(path: &str, content: &str) -> String {
|
||||||
|
// Escape content for HTML textarea (only need to escape < > & and ")
|
||||||
|
let html_escaped_content = content
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\"", """);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Edit: {}</title>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
nav {{
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
z-index: 1000;
|
||||||
|
}}
|
||||||
|
nav a {{
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
padding: 5px 0;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
main {{
|
||||||
|
margin-right: 220px;
|
||||||
|
padding: 20px;
|
||||||
|
}}
|
||||||
|
#editor {{
|
||||||
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
}}
|
||||||
|
.button-group {{
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}}
|
||||||
|
.button-group button {{
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: serif;
|
||||||
|
}}
|
||||||
|
.button-group button:hover {{
|
||||||
|
background: #f0f0f0;
|
||||||
|
}}
|
||||||
|
.save-btn {{
|
||||||
|
background: #28a745 !important;
|
||||||
|
color: white !important;
|
||||||
|
}}
|
||||||
|
.cancel-btn {{
|
||||||
|
background: #dc3545 !important;
|
||||||
|
color: white !important;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href="/wiki/{}">← Back to Page</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<h1>Edit: {}</h1>
|
||||||
|
<textarea id="editor" placeholder="Enter your markdown content here...">{}</textarea>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="save-btn" onclick="savePage()">Save</button>
|
||||||
|
<button class="cancel-btn" onclick="cancelEdit()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
function savePage() {{
|
||||||
|
const content = document.getElementById('editor').value;
|
||||||
|
const path = '{}';
|
||||||
|
|
||||||
|
// Get token from localStorage or cookies
|
||||||
|
const token = localStorage.getItem('obswiki_token') || getCookie('auth_token');
|
||||||
|
|
||||||
|
if (!token) {{
|
||||||
|
alert('You must be logged in to save pages');
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
fetch('/api/wiki/' + path, {{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {{
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + token
|
||||||
|
}},
|
||||||
|
body: JSON.stringify({{ content: content }})
|
||||||
|
}})
|
||||||
|
.then(response => {{
|
||||||
|
if (response.ok) {{
|
||||||
|
window.location.href = '/wiki/' + path;
|
||||||
|
}} else if (response.status === 401) {{
|
||||||
|
alert('Your session has expired. Please log in again.');
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}} else {{
|
||||||
|
alert('Failed to save page. Please try again.');
|
||||||
|
}}
|
||||||
|
}})
|
||||||
|
.catch(error => {{
|
||||||
|
alert('Error saving page: ' + error.message);
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
|
||||||
|
function cancelEdit() {{
|
||||||
|
const path = '{}';
|
||||||
|
window.location.href = '/wiki/' + path;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function getCookie(name) {{
|
||||||
|
const value = '; ' + document.cookie;
|
||||||
|
const parts = value.split('; ' + name + '=');
|
||||||
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Auto-focus the editor
|
||||||
|
document.getElementById('editor').focus();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"#,
|
||||||
|
path, path, path, html_escaped_content, path, path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ impl WikiService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn can_edit_page(&self, path: &str, user: &User) -> Result<bool> {
|
pub async fn can_edit_page(&self, path: &str, user: &User) -> Result<bool> {
|
||||||
// Check both role-based permissions and path-specific access rules
|
// Check both role-based permissions and path-specific access rules
|
||||||
if !matches!(user.role, UserRole::Admin | UserRole::Editor) {
|
if !matches!(user.role, UserRole::Admin | UserRole::Editor) {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user