fix in smtp and split rs files
This commit is contained in:
@@ -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,
|
||||
¬e_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"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user