diff --git a/Cargo.lock b/Cargo.lock index eb8a803..e405b77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -348,6 +348,7 @@ dependencies = [ "axum", "axum-extra", "bcrypt", + "doctate-common", "dotenvy", "pulldown-cmark", "rand 0.8.6", diff --git a/server/Cargo.toml b/server/Cargo.toml index 66cff4d..54fb013 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true default-run = "doctate-server" [dependencies] +doctate-common = { path = "../doctate-common" } axum = { version = "0.8", features = ["multipart"] } tokio.workspace = true reqwest.workspace = true diff --git a/server/src/auth.rs b/server/src/auth.rs index 20d0318..dbd0071 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -5,6 +5,7 @@ use std::time::Instant; use axum::extract::{FromRef, FromRequestParts}; use axum::http::request::Parts; use axum_extra::extract::cookie::CookieJar; +use doctate_common::API_KEY_HEADER; use tracing::warn; use crate::config::Config; @@ -43,7 +44,7 @@ where let api_key = parts .headers - .get("X-API-Key") + .get(API_KEY_HEADER) .and_then(|v| v.to_str().ok()) .ok_or(AppError::Unauthorized)?; diff --git a/server/src/models.rs b/server/src/models.rs index 522825e..52e3dc2 100644 --- a/server/src/models.rs +++ b/server/src/models.rs @@ -1,15 +1 @@ -use serde::Serialize; - -#[derive(Serialize)] -pub struct AckResponse { - pub case_id: String, - pub recorded_at: String, - pub status: AckStatus, -} - -#[derive(Serialize)] -#[serde(rename_all = "lowercase")] -pub enum AckStatus { - Received, - Gone, -} +pub use doctate_common::ack::{AckResponse, AckStatus}; diff --git a/server/src/routes/case_actions.rs b/server/src/routes/case_actions.rs index 529ec33..80dd2dd 100644 --- a/server/src/routes/case_actions.rs +++ b/server/src/routes/case_actions.rs @@ -10,6 +10,8 @@ use time::OffsetDateTime; use tokio::io::AsyncWriteExt; use tracing::{info, warn}; +use doctate_common::timestamp::{filename_stem_to_recorded_at, now_rfc3339}; + use crate::analyze::{ AnalysisInput, AnalyzeJob, AnalyzeSender, RecordingInput, ANALYSIS_INPUT_FILE, DOCUMENT_FILE, }; @@ -229,16 +231,6 @@ pub(crate) async fn write_input_create_new( Ok(()) } -/// Reverse the filesystem-safe timestamp mapping applied in `upload.rs`: -/// `:` in the time portion was replaced with `-`. Split at `T`, undo hyphens -/// only in the time part to avoid touching date hyphens. -fn filename_stem_to_recorded_at(stem: &str) -> String { - match stem.split_once('T') { - Some((date, time)) => format!("{}T{}", date, time.replace('-', ":")), - None => stem.to_string(), - } -} - /// Read `case_dir/document.md`. Returns `None` if no document exists. pub(crate) async fn read_document(case_dir: &Path) -> Option { tokio::fs::read_to_string(case_dir.join(DOCUMENT_FILE)).await.ok() @@ -295,7 +287,7 @@ pub async fn handle_delete_case( let marker = DeleteMarker { batch: uuid::Uuid::new_v4(), - deleted_at: now_rfc3339()?, + deleted_at: now_rfc3339(), }; write_delete_marker(&case_dir, &marker).await?; @@ -444,28 +436,3 @@ async fn restore_batch(user_root: &Path, batch: uuid::Uuid) -> usize { } count } - -fn now_rfc3339() -> Result { - OffsetDateTime::now_utc() - .format(&Rfc3339) - .map_err(|e| AppError::Internal(format!("format timestamp: {e}"))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn filename_stem_roundtrip() { - assert_eq!( - filename_stem_to_recorded_at("2026-04-15T10-32-00Z"), - "2026-04-15T10:32:00Z" - ); - // Date-only hyphens are preserved. - assert_eq!( - filename_stem_to_recorded_at("2026-04-15T10-32-00.123Z"), - "2026-04-15T10:32:00.123Z" - ); - } - -} diff --git a/server/src/routes/upload.rs b/server/src/routes/upload.rs index 611eaf1..f6f1dc4 100644 --- a/server/src/routes/upload.rs +++ b/server/src/routes/upload.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use axum::extract::{Multipart, State}; use axum::Json; +use doctate_common::timestamp::recorded_at_to_filename_stem; use tokio::sync::mpsc::error::TrySendError; use tracing::{info, warn}; @@ -72,7 +73,7 @@ pub async fn handle_upload( let case_dir = resolve_case_dir(&user.data_dir, &case_id).await?; // Build a filesystem-safe filename from the timestamp (colons → hyphens). - let safe_timestamp = recorded_at.replace(':', "-"); + let safe_timestamp = recorded_at_to_filename_stem(&recorded_at); let file_path = case_dir.join(format!("{safe_timestamp}.m4a")); tokio::fs::write(&file_path, &audio).await?;