fix in smtp and split rs files

This commit is contained in:
Mateusz Gruszczyński
2026-07-27 22:56:56 +02:00
parent cc3b172e56
commit d6c1c52310
30 changed files with 4327 additions and 4387 deletions
+381
View File
@@ -0,0 +1,381 @@
#[derive(Debug, Deserialize)]
pub struct CreatePadRequest {
name: String,
#[serde(default)]
password: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CreatePadResponse {
slug: String,
url: String,
}
#[derive(Debug, Serialize)]
pub struct PadInfo {
slug: String,
title: String,
protected: bool,
allow_public_task_updates: bool,
created_at: String,
updated_at: String,
can_delete_files: bool,
global_color: Option<String>,
note_color: Option<String>,
}
pub async fn create_pad(
State(state): State<SharedState>,
headers: HeaderMap,
Json(payload): Json<CreatePadRequest>,
) -> Result<(StatusCode, Json<CreatePadResponse>), ApiError> {
let title = validate_name(&payload.name, "Note name")?;
let password = validate_password(payload.password.as_deref())?;
let base = slugify(title);
if base.is_empty() {
return Err(ApiError::bad_request(
"The name cannot be converted into a valid address",
));
}
let slug = unique_pad_slug(&state, &base).await?;
let pad = db::create_pad(&state.db, &slug, title, password).await?;
if let Some(user) = crate::auth::optional_user(&state, &headers)
.await
.map_err(|e| ApiError::forbidden(&e.message))?
{
sqlx::query(queries::get(state.db.kind(), queries::USER_ATTACH_PAD))
.bind(user.id)
.bind(&pad.slug)
.execute(state.db.pool())
.await?;
}
Ok((
StatusCode::CREATED,
Json(CreatePadResponse {
url: format!("/p/{slug}"),
slug,
}),
))
}
pub async fn pad_info(
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
) -> Result<Json<PadInfo>, ApiError> {
let pad = db::find_pad(&state.db, &slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
ensure_private_resource_access(
&state,
"pad",
&pad.slug,
pad.is_private,
bearer_token(&headers),
)
.await?;
let (global_color, note_color) = editor_colors(&state, &headers, "pad", &slug).await?;
Ok(Json(PadInfo {
slug: pad.slug,
title: pad.title,
protected: pad.password_hash.is_some(),
allow_public_task_updates: db::pad_public_task_updates(&state.db, pad.id).await?,
created_at: db::normalize_timestamp(&pad.created_at),
updated_at: db::normalize_timestamp(&pad.updated_at),
can_delete_files: crate::auth::is_resource_owner(
&state,
"pad",
&slug,
bearer_token(&headers),
)
.await
.unwrap_or(false),
global_color,
note_color,
}))
}
pub async fn pad_editor_color(
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
let (global_color, note_color) = editor_colors(&state, &headers, "pad", &slug).await?;
Ok(Json(
serde_json::json!({"global_color": global_color, "note_color": note_color}),
))
}
pub async fn note_editor_color(
State(state): State<SharedState>,
headers: HeaderMap,
Path((workspace_slug, note_slug)): Path<(String, String)>,
) -> Result<Json<serde_json::Value>, ApiError> {
let (global_color, note_color) = editor_colors(
&state,
&headers,
"note",
&format!("{}/{}", workspace_slug, note_slug),
)
.await?;
Ok(Json(
serde_json::json!({"global_color": global_color, "note_color": note_color}),
))
}
pub async fn set_pad_editor_color(
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
Json(payload): Json<EditorColorRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
save_editor_color(&state, &headers, "pad", &slug, payload.color.as_deref()).await
}
pub async fn set_note_editor_color(
State(state): State<SharedState>,
headers: HeaderMap,
Path((workspace_slug, note_slug)): Path<(String, String)>,
Json(payload): Json<EditorColorRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
save_editor_color(
&state,
&headers,
"note",
&format!("{}/{}", workspace_slug, note_slug),
payload.color.as_deref(),
)
.await
}
pub async fn publish_pad_page(
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
Json(payload): Json<PublishRequest>,
) -> Result<Json<PublishResponse>, ApiError> {
let pad = authorized_pad(
&state,
&slug,
payload.password.as_deref(),
payload.access_token.as_deref(),
bearer_token(&headers),
)
.await?;
let level = if db::verify_pad_password(&pad, payload.password.as_deref())
|| (pad.is_private == 0 && pad.password_hash.is_none())
{
AccessLevel::Write
} else {
combined_token_access_level(
&state,
"pad",
&slug,
payload.access_token.as_deref(),
bearer_token(&headers),
)
.await?
};
require_write(level)?;
let token = db::publish_pad(&state.db, pad.id).await?;
db::set_pad_public_task_updates(&state.db, pad.id, payload.allow_task_updates).await?;
Ok(Json(PublishResponse {
url: format!("/s/{token}"),
}))
}
pub async fn publish_note_page(
State(state): State<SharedState>,
headers: HeaderMap,
Path((workspace_slug, note_slug)): Path<(String, String)>,
Json(payload): Json<PublishRequest>,
) -> Result<Json<PublishResponse>, ApiError> {
let (workspace, note) = authorized_note(
&state,
&workspace_slug,
&note_slug,
payload.password.as_deref(),
payload.access_token.as_deref(),
bearer_token(&headers),
)
.await?;
let level = if db::verify_workspace_password(&workspace, payload.password.as_deref())
|| (workspace.is_private == 0 && workspace.password_hash.is_none())
{
AccessLevel::Write
} else {
combined_token_access_level(
&state,
"workspace",
&workspace_slug,
payload.access_token.as_deref(),
bearer_token(&headers),
)
.await?
};
require_write(level)?;
let token = db::publish_note(&state.db, note.id).await?;
db::set_note_public_task_updates(&state.db, note.id, payload.allow_task_updates).await?;
Ok(Json(PublishResponse {
url: format!("/s/{token}"),
}))
}
pub async fn public_page(
State(state): State<SharedState>,
Path(token): Path<String>,
) -> Result<Json<PublicPageResponse>, ApiError> {
let page = db::find_published_page(&state.db, &token)
.await?
.ok_or_else(ApiError::not_found_note)?;
Ok(Json(PublicPageResponse {
title: page.title,
content: page.content,
updated_at: db::normalize_timestamp(&page.updated_at),
allow_task_updates: page.allow_task_updates,
}))
}
pub async fn update_public_task(
State(state): State<SharedState>,
Path(token): Path<String>,
Json(payload): Json<PublicTaskUpdateRequest>,
) -> Result<Json<PublicPageResponse>, ApiError> {
let current = db::find_published_page(&state.db, &token)
.await?
.ok_or_else(ApiError::not_found_note)?;
if !current.allow_task_updates {
return Err(ApiError::forbidden(
"Task updates are disabled for this page",
));
}
let page = db::update_public_task(&state.db, &token, payload.source_line, payload.checked)
.await?
.ok_or_else(ApiError::not_found_note)?;
Ok(Json(PublicPageResponse {
title: page.title,
content: page.content,
updated_at: db::normalize_timestamp(&page.updated_at),
allow_task_updates: page.allow_task_updates,
}))
}
pub async fn pad_history(
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
Json(payload): Json<PasswordRequest>,
) -> Result<Json<Vec<db::Revision>>, ApiError> {
let pad = authorized_pad(
&state,
&slug,
payload.password.as_deref(),
payload.access_token.as_deref(),
bearer_token(&headers),
)
.await?;
let revisions = db::list_pad_revisions(&state.db, pad.id)
.await?
.into_iter()
.map(|mut revision| {
revision.created_at = db::normalize_timestamp(&revision.created_at);
revision
})
.collect();
Ok(Json(revisions))
}
pub async fn pad_restore(
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
Json(payload): Json<RestoreRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let pad = authorized_pad(
&state,
&slug,
payload.password.as_deref(),
payload.access_token.as_deref(),
bearer_token(&headers),
)
.await?;
let level = if db::verify_pad_password(&pad, payload.password.as_deref())
|| (pad.is_private == 0 && pad.password_hash.is_none())
{
AccessLevel::Write
} else {
combined_token_access_level(
&state,
"pad",
&slug,
payload.access_token.as_deref(),
bearer_token(&headers),
)
.await?
};
require_write(level)?;
let content: Option<String> = sqlx::query_scalar(queries::get(state.db.kind(), queries::Q029))
.bind(payload.revision_id)
.bind(pad.id)
.fetch_optional(state.db.pool())
.await?;
let content = content.ok_or_else(ApiError::not_found_revision)?;
let owner_map: Option<String> =
sqlx::query_scalar(queries::get(state.db.kind(), queries::Q030))
.bind(payload.revision_id)
.bind(pad.id)
.fetch_optional(state.db.pool())
.await?;
let owner_map = owner_map.unwrap_or_else(|| "[]".into());
let (revision_id, updated_at) =
db::save_pad_revision(&state.db, pad.id, &content, Some("restore"), &owner_map).await?;
let update = NoteUpdate {
content,
revision_id,
updated_at,
author: Some("restore".into()),
owner_map,
};
let _ = state
.pad_channel(&slug)
.await
.send(RoomEvent::Document(update));
Ok(Json(serde_json::json!({"ok": true})))
}
async fn authorized_pad(
state: &SharedState,
slug: &str,
password: Option<&str>,
access_token: Option<&str>,
bearer: Option<&str>,
) -> Result<db::Pad, ApiError> {
let pad = db::find_pad(&state.db, slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
let token_level = combined_token_access_level(state, "pad", slug, access_token, bearer).await?;
if pad.is_private != 0 && token_level == AccessLevel::None {
return Err(ApiError::forbidden("This note is private."));
}
if pad.password_hash.is_some()
&& !db::verify_pad_password(&pad, password)
&& token_level == AccessLevel::None
{
return Err(ApiError::unauthorized());
}
Ok(pad)
}
async fn unique_pad_slug(state: &SharedState, base: &str) -> Result<String, ApiError> {
if db::find_pad(&state.db, base).await?.is_none() {
return Ok(base.to_owned());
}
for _ in 0..8 {
let candidate = format!("{base}-{}", db::random_suffix(6));
if db::find_pad(&state.db, &candidate).await?.is_none() {
return Ok(candidate);
}
}
Err(ApiError::internal("Failed to create a unique address"))
}