diff --git a/src/handler/documents.rs b/src/handler/documents.rs index 498eaba..e6908ee 100644 --- a/src/handler/documents.rs +++ b/src/handler/documents.rs @@ -1,7 +1,7 @@ -use axum::{response::Redirect, Form}; +use axum::{extract::Path, response::Redirect, Form}; use axum_login::AuthSession; -use crate::{models::ModelPermission, prelude::*}; +use crate::{handler::internal_server_error, models::{Document, ModelPermission, ModelType, Permission}, prelude::*}; pub async fn documents_page( State(ctx): State, @@ -24,3 +24,103 @@ async fn render_documents_page(ctx: Context, user: crate::entity::user::Model) - ctx.render_resp("documents/list_documents.html", values) } + +pub async fn create_document_page( + State(ctx): State, + auth_session: AuthSession, +) -> Response { + let user = match auth_session.user { + Some(user) => user, + None => return Redirect::to("/login").into_response(), + }; + + let projects = ModelPermission::user_projects(&ctx.kv_handles, user.id).unwrap_or_default(); + + let values = context! { + user => user, + projects => projects, + }; + ctx.render_resp("documents/create_document.html", values) +} + +#[derive(Debug, Deserialize)] +pub struct CreateDocumentSubmission { + project_id: Uuid, + title: String, +} + +pub async fn create_document_submit( + State(ctx): State, + auth_session: AuthSession, + form: Form, +) -> Response { + let user = match auth_session.user { + Some(user) => user, + None => return Redirect::to("/login").into_response(), + }; + + let project = match ModelPermission::user_project(&ctx.kv_handles, user.id, form.project_id) { + Ok(Some(project)) => project, + Ok(None) => return Redirect::to("/documents/create").into_response(), + Err(err) => { + error!(?err, "failed to access kv store"); + return Redirect::to("/documents/create").into_response(); + } + }; + + let document = Document { + id: Uuid::now_v7(), + project_id: project.id, + title: form.title.to_owned(), + content: "".to_owned(), + }; + + if let Err(err) = document.save(&ctx.kv_handles) { + error!(?err, "failed to save document"); + return internal_server_error(); + } + info!(?document, "document created"); + + let permission = ModelPermission { + user_id: user.id, + model_type: ModelType::Document, + role: Permission::Admin, + model_id: document.id, + }; + + if let Err(err) = permission.add(&ctx.kv_handles) { + error!(?err, "failed to save new project permission"); + return internal_server_error(); + } + + + + Redirect::to("/documents").into_response() +} + +pub async fn edit_document_page( + State(ctx): State, + auth_session: AuthSession, + Path((id,)): Path<(Uuid,)>, +) -> Response { + let user = match auth_session.user { + Some(user) => user, + None => return Redirect::to("/login").into_response(), + }; + + let document = match ModelPermission::user_document(&ctx.kv_handles, user.id, id) { + Ok(Some(document)) => document, + Ok(None) => return Redirect::to("/documents").into_response(), + Err(err) => { + error!(?err, "failed to load document"); + return internal_server_error(); + } + }; + + let values = context! { + user => user, + document => document, + }; + + ctx.render_resp("documents/edit_document.html", values) +} diff --git a/src/handler/projects.rs b/src/handler/projects.rs index 60d953e..14baa30 100644 --- a/src/handler/projects.rs +++ b/src/handler/projects.rs @@ -62,7 +62,7 @@ pub async fn create_project_submit( }; let project = Project { - id: Uuid::new_v4(), + id: Uuid::now_v7(), owner_id: user.id, name: form.name.clone(), diff --git a/src/models.rs b/src/models.rs index 8e57f32..60d2893 100644 --- a/src/models.rs +++ b/src/models.rs @@ -140,6 +140,20 @@ impl ModelPermission { Ok(projects) } + pub fn user_project(kv_handle: &KvHandle, user_id: i32, project_id: Uuid) -> Result> { + let key = format!("{}:{}:{}:{}", user_id, ModelType::Project, Permission::Admin, project_id); + let value = kv_handle.permissions.get(key)?; + + match value { + Some(value) => { + let permission: ModelPermission = bincode::deserialize(&value)?; + let project = Project::load(kv_handle, permission.model_id)?; + Ok(project) + } + None => Ok(None), + } + } + pub fn user_documents(kv_handle: &KvHandle, user_id: i32) -> Result> { let prefix = format!("{}:{}:", user_id, ModelType::Document); @@ -149,13 +163,28 @@ impl ModelPermission { let permission: ModelPermission = bincode::deserialize(&value)?; ids.push(permission.model_id); } + dbg!(&ids); let documents: Vec = ids .into_iter() .filter_map(|id| Document::load(kv_handle, id).ok().flatten()) .collect(); + dbg!(&documents); Ok(documents) } + pub fn user_document(kv_handle: &KvHandle, user_id: i32, document_id: Uuid) -> Result> { + let key = format!("{}:{}:{}:{}", user_id, ModelType::Document, Permission::Admin, document_id); + let value = kv_handle.permissions.get(key)?; + + match value { + Some(value) => { + let permission: ModelPermission = bincode::deserialize(&value)?; + let document = Document::load(kv_handle, permission.model_id)?; + Ok(document) + } + None => Ok(None), + } + } } diff --git a/src/server.rs b/src/server.rs index 7ac8b47..309e23e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -10,7 +10,7 @@ use tower_sessions::SessionManagerLayer; use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore}; use tracing::Level; -use crate::{config::CommandLineOptions, context::Context, handler::{documents::documents_page, home::home_page, login::logout, login_page, login_submit, projects::{create_project_page, create_project_submit, projects_page}}, kv::KvHandle, logging::setup_logging, templates::make_template_loader}; +use crate::{config::CommandLineOptions, context::Context, handler::{documents::{create_document_page, create_document_submit, documents_page, edit_document_page}, home::home_page, login::logout, login_page, login_submit, projects::{create_project_page, create_project_submit, projects_page}}, kv::KvHandle, logging::setup_logging, templates::make_template_loader}; pub async fn run() -> Result<()> { dotenvy::dotenv()?; @@ -47,6 +47,9 @@ pub async fn run() -> Result<()> { .route("/projects/new", get(create_project_page)) .route("/projects/new", post(create_project_submit)) .route("/documents", get(documents_page)) + .route("/documents/new", get(create_document_page)) + .route("/documents/new", post(create_document_submit)) + .route("/documents/edit/:id", get(edit_document_page)) .layer(trace_layer) .layer(session_layer) .layer(auth_layer) diff --git a/static/style.css b/static/style.css index 69a048a..e4653d8 100644 --- a/static/style.css +++ b/static/style.css @@ -1132,6 +1132,40 @@ select { justify-content: flex-end; } +.select { + display: inline-flex; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 3rem; + min-height: 3rem; + padding-left: 1rem; + padding-right: 2.5rem; + font-size: 0.875rem; + line-height: 1.25rem; + line-height: 2; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); + background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), + linear-gradient(135deg, currentColor 50%, transparent 50%); + background-position: calc(100% - 20px) calc(1px + 50%), + calc(100% - 16.1px) calc(1px + 50%); + background-size: 4px 4px, + 4px 4px; + background-repeat: no-repeat; +} + +.select[multiple] { + height: auto; +} + .btm-nav > * .label { font-size: 1rem; line-height: 1.5rem; @@ -1519,6 +1553,50 @@ select { } } +.select:focus { + box-shadow: none; + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.select-disabled, + .select:disabled, + .select[disabled] { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + color: var(--fallback-bc,oklch(var(--bc)/0.4)); +} + +.select-disabled::-moz-placeholder, .select:disabled::-moz-placeholder, .select[disabled]::-moz-placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.select-disabled::placeholder, + .select:disabled::placeholder, + .select[disabled]::placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.select-multiple, + .select[multiple], + .select[size].select:not([size="1"]) { + background-image: none; + padding-right: 1rem; +} + +[dir="rtl"] .select { + background-position: calc(0% + 12px) calc(1px + 50%), + calc(0% + 16px) calc(1px + 50%); +} + @keyframes skeleton { from { background-position: 150%; diff --git a/templates/documents/create_document.html b/templates/documents/create_document.html index e69de29..2862390 100644 --- a/templates/documents/create_document.html +++ b/templates/documents/create_document.html @@ -0,0 +1,44 @@ + + +{% include "head.html" %} + + +
+ {% set current_page = "projects" %} + {% include "components/sidebar.html" %} + +
+ + +
+
+ + + + + +
+
+ +
+ + + + + diff --git a/templates/documents/list_documents.html b/templates/documents/list_documents.html index dbd9a70..3a05171 100644 --- a/templates/documents/list_documents.html +++ b/templates/documents/list_documents.html @@ -22,14 +22,13 @@
-
{% for document in documents %}

{{ document.name }}

- + Open
@@ -37,6 +36,7 @@ {% endfor %}
+