new functions
This commit is contained in:
+254
-25
@@ -130,6 +130,13 @@ pub struct NoteInfo {
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
can_delete_files: bool,
|
||||
global_color: Option<String>,
|
||||
note_color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EditorColorRequest {
|
||||
color: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_workspace(
|
||||
@@ -234,8 +241,17 @@ pub async fn create_note(
|
||||
.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?
|
||||
{
|
||||
AccessLevel::Write
|
||||
} else {
|
||||
combined_token_access_level(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
require_write(level)?;
|
||||
let title = validate_name(&payload.name, "Note name")?;
|
||||
@@ -276,6 +292,90 @@ pub async fn create_note(
|
||||
))
|
||||
}
|
||||
|
||||
fn clean_editor_color(value: Option<&str>) -> Result<Option<String>, ApiError> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
let value = value.trim();
|
||||
if value.len() == 7
|
||||
&& value.starts_with('#')
|
||||
&& value[1..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
{
|
||||
Ok(Some(value.to_ascii_lowercase()))
|
||||
} else {
|
||||
Err(ApiError::bad_request("Invalid editor color"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn editor_colors(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
) -> Result<(Option<String>, Option<String>), ApiError> {
|
||||
let Some(user) = crate::auth::optional_user(state, headers)
|
||||
.await
|
||||
.map_err(|e| ApiError::forbidden(&e.message))?
|
||||
else {
|
||||
return Ok((None, None));
|
||||
};
|
||||
let global: Option<String> = sqlx::query_scalar(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_EDITOR_COLOR_BY_USER,
|
||||
))
|
||||
.bind(user.id)
|
||||
.fetch_one(state.db.pool())
|
||||
.await?;
|
||||
let note: Option<String> = sqlx::query_scalar(queries::get(
|
||||
state.db.kind(),
|
||||
queries::RESOURCE_COLOR_BY_USER,
|
||||
))
|
||||
.bind(user.id)
|
||||
.bind(kind)
|
||||
.bind(slug)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await?;
|
||||
Ok((global, note))
|
||||
}
|
||||
|
||||
async fn save_editor_color(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
color: Option<&str>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = crate::auth::optional_user(state, headers)
|
||||
.await
|
||||
.map_err(|e| ApiError::forbidden(&e.message))?
|
||||
.ok_or_else(|| ApiError::forbidden("Log in to save note colors"))?;
|
||||
let color = clean_editor_color(color)?;
|
||||
let mut tx = state.db.pool().begin().await?;
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::RESOURCE_COLOR_DELETE,
|
||||
))
|
||||
.bind(user.id)
|
||||
.bind(kind)
|
||||
.bind(slug)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if let Some(value) = color.as_deref() {
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::RESOURCE_COLOR_INSERT,
|
||||
))
|
||||
.bind(user.id)
|
||||
.bind(kind)
|
||||
.bind(slug)
|
||||
.bind(value)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(Json(serde_json::json!({"color": color})))
|
||||
}
|
||||
|
||||
pub async fn note_info(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
@@ -295,6 +395,8 @@ pub async fn note_info(
|
||||
let note = db::find_note(&state.db, workspace.id, ¬e_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
let color_slug = format!("{}/{}", workspace_slug, note_slug);
|
||||
let (global_color, note_color) = editor_colors(&state, &headers, "note", &color_slug).await?;
|
||||
|
||||
Ok(Json(NoteInfo {
|
||||
workspace_slug: workspace.slug,
|
||||
@@ -327,6 +429,8 @@ pub async fn note_info(
|
||||
.unwrap_or(false);
|
||||
workspace_owner || note_owner
|
||||
},
|
||||
global_color,
|
||||
note_color,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -374,8 +478,17 @@ pub async fn restore(
|
||||
.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?
|
||||
{
|
||||
AccessLevel::Write
|
||||
} else {
|
||||
combined_token_access_level(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_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::Q028))
|
||||
@@ -407,7 +520,6 @@ pub async fn restore(
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum AccessLevel {
|
||||
None,
|
||||
@@ -513,9 +625,8 @@ pub async fn authorized_workspace(
|
||||
let workspace = db::find_workspace(&state.db, slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_workspace)?;
|
||||
let token_level = combined_token_access_level(
|
||||
state, "workspace", slug, access_token, bearer,
|
||||
).await?;
|
||||
let token_level =
|
||||
combined_token_access_level(state, "workspace", slug, access_token, bearer).await?;
|
||||
if workspace.is_private != 0 && token_level == AccessLevel::None {
|
||||
return Err(ApiError::forbidden("This workspace is private."));
|
||||
}
|
||||
@@ -536,7 +647,8 @@ async fn authorized_note(
|
||||
access_token: Option<&str>,
|
||||
bearer: Option<&str>,
|
||||
) -> Result<(db::Workspace, db::Note), ApiError> {
|
||||
let workspace = authorized_workspace(state, workspace_slug, password, access_token, bearer).await?;
|
||||
let workspace =
|
||||
authorized_workspace(state, workspace_slug, password, access_token, bearer).await?;
|
||||
let note = db::find_note(&state.db, workspace.id, note_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
@@ -644,6 +756,8 @@ pub struct PadInfo {
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
can_delete_files: bool,
|
||||
global_color: Option<String>,
|
||||
note_color: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_pad(
|
||||
@@ -696,6 +810,7 @@ pub async fn pad_info(
|
||||
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,
|
||||
@@ -711,9 +826,64 @@ pub async fn pad_info(
|
||||
)
|
||||
.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,
|
||||
@@ -730,8 +900,17 @@ pub async fn publish_pad_page(
|
||||
.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?
|
||||
{
|
||||
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?;
|
||||
@@ -758,8 +937,17 @@ pub async fn publish_note_page(
|
||||
.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?
|
||||
{
|
||||
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?;
|
||||
@@ -849,8 +1037,17 @@ pub async fn pad_restore(
|
||||
.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?
|
||||
{
|
||||
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))
|
||||
@@ -892,9 +1089,7 @@ async fn authorized_pad(
|
||||
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?;
|
||||
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."));
|
||||
}
|
||||
@@ -961,11 +1156,27 @@ pub async fn upload_pad_file(
|
||||
file = Some((filename, bytes.to_vec()));
|
||||
}
|
||||
}
|
||||
let pad = authorized_pad(&state, &slug, password.as_deref(), access_token.as_deref(), bearer_token(&headers)).await?;
|
||||
let pad = authorized_pad(
|
||||
&state,
|
||||
&slug,
|
||||
password.as_deref(),
|
||||
access_token.as_deref(),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_pad_password(&pad, password.as_deref())
|
||||
|| (pad.is_private == 0 && pad.password_hash.is_none())
|
||||
{ AccessLevel::Write } else {
|
||||
combined_token_access_level(&state, "pad", &slug, access_token.as_deref(), bearer_token(&headers)).await?
|
||||
{
|
||||
AccessLevel::Write
|
||||
} else {
|
||||
combined_token_access_level(
|
||||
&state,
|
||||
"pad",
|
||||
&slug,
|
||||
access_token.as_deref(),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
require_write(level)?;
|
||||
let (original, bytes) = file.ok_or_else(|| ApiError::bad_request("No file provided"))?;
|
||||
@@ -1119,8 +1330,17 @@ pub async fn upload_note_file(
|
||||
|
||||
let level = if db::verify_workspace_password(&workspace, password.as_deref())
|
||||
|| (workspace.is_private == 0 && workspace.password_hash.is_none())
|
||||
{ AccessLevel::Write } else {
|
||||
combined_token_access_level(&state, "workspace", &workspace_slug, access_token.as_deref(), bearer_token(&headers)).await?
|
||||
{
|
||||
AccessLevel::Write
|
||||
} else {
|
||||
combined_token_access_level(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
access_token.as_deref(),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
require_write(level)?;
|
||||
let (original, bytes) = file.ok_or_else(|| ApiError::bad_request("No file provided"))?;
|
||||
@@ -1178,8 +1398,17 @@ pub async fn delete_note(
|
||||
|
||||
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?
|
||||
{
|
||||
AccessLevel::Write
|
||||
} else {
|
||||
combined_token_access_level(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
require_write(level)?;
|
||||
if note.protected {
|
||||
|
||||
@@ -109,6 +109,10 @@ pub fn router(
|
||||
.route("/api/pads", post(api::create_pad))
|
||||
.route("/api/pads/{slug}", get(api::pad_info))
|
||||
.route("/api/pads/{slug}/history", post(api::pad_history))
|
||||
.route(
|
||||
"/api/pads/{slug}/editor-color",
|
||||
get(api::pad_editor_color).post(api::set_pad_editor_color),
|
||||
)
|
||||
.route("/api/pads/{slug}/publish", post(api::publish_pad_page))
|
||||
.route("/api/pads/{slug}/restore", post(api::pad_restore))
|
||||
.route(
|
||||
@@ -133,6 +137,10 @@ pub fn router(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}",
|
||||
get(api::note_info).delete(api::delete_note),
|
||||
)
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}/editor-color",
|
||||
get(api::note_editor_color).post(api::set_note_editor_color),
|
||||
)
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}/publish",
|
||||
post(api::publish_note_page),
|
||||
|
||||
+304
-91
@@ -77,15 +77,25 @@ pub struct ResetConfirmRequest {
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProfileUpdateRequest {
|
||||
#[serde(default)] nickname: Option<String>,
|
||||
#[serde(default)] new_email: Option<String>,
|
||||
#[serde(default)] new_password: Option<String>,
|
||||
#[serde(default)] password: String,
|
||||
#[serde(default)]
|
||||
nickname: Option<String>,
|
||||
#[serde(default)]
|
||||
new_email: Option<String>,
|
||||
#[serde(default)]
|
||||
new_password: Option<String>,
|
||||
#[serde(default)]
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
editor_color: Option<String>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteAccountRequest { password: String }
|
||||
pub struct DeleteAccountRequest {
|
||||
password: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct AccountActionConfirmRequest { token: String }
|
||||
pub struct AccountActionConfirmRequest {
|
||||
token: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResourceActionRequest {
|
||||
kind: String,
|
||||
@@ -163,6 +173,7 @@ pub struct SessionResponse {
|
||||
directory_display_name: Option<String>,
|
||||
directory_organization: Option<String>,
|
||||
suggested_nickname: Option<String>,
|
||||
editor_color: Option<String>,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
pub struct IdentityResponse {
|
||||
@@ -367,18 +378,23 @@ pub async fn login(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn resend_confirmation(
|
||||
State(state): State<SharedState>,
|
||||
Json(req): Json<ResendConfirmationRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
if !state.account_confirmation_required {
|
||||
return Err(AuthError::bad_request("Account confirmation is not enabled."));
|
||||
return Err(AuthError::bad_request(
|
||||
"Account confirmation is not enabled.",
|
||||
));
|
||||
}
|
||||
let smtp = state.smtp.as_ref().ok_or_else(|| AuthError::service_unavailable("SMTP is not configured."))?;
|
||||
let smtp = state
|
||||
.smtp
|
||||
.as_ref()
|
||||
.ok_or_else(|| AuthError::service_unavailable("SMTP is not configured."))?;
|
||||
let email = validate_email(&req.email)?;
|
||||
let user = find_user_by_email(&state, &email).await?
|
||||
.ok_or_else(|| AuthError::bad_request("No unconfirmed account exists for this e-mail address."))?;
|
||||
let user = find_user_by_email(&state, &email).await?.ok_or_else(|| {
|
||||
AuthError::bad_request("No unconfirmed account exists for this e-mail address.")
|
||||
})?;
|
||||
if user.confirmed_at.is_some() {
|
||||
return Err(AuthError::bad_request("This account is already confirmed."));
|
||||
}
|
||||
@@ -386,25 +402,47 @@ pub async fn resend_confirmation(
|
||||
state.db.kind(),
|
||||
queries::AUTH_LATEST_CONFIRMATION_CREATED_AT,
|
||||
))
|
||||
.bind(user.id).fetch_optional(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
.bind(user.id)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
if let Some(value) = last_created {
|
||||
if let Ok(created) = chrono::DateTime::parse_from_rfc3339(&value) {
|
||||
let available = created.with_timezone(&Utc) + Duration::minutes(10);
|
||||
if available > Utc::now() {
|
||||
let seconds = (available - Utc::now()).num_seconds().max(1);
|
||||
return Err(AuthError::bad_request(&format!("A new confirmation e-mail can be sent in {} minute(s).", (seconds + 59) / 60)));
|
||||
return Err(AuthError::bad_request(&format!(
|
||||
"A new confirmation e-mail can be sent in {} minute(s).",
|
||||
(seconds + 59) / 60
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER))
|
||||
.bind(user.id).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER,
|
||||
))
|
||||
.bind(user.id)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
let token = random_confirmation_token();
|
||||
let token_hash = hash_token(&token);
|
||||
let expires_at = (Utc::now() + Duration::hours(24)).to_rfc3339();
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_INSERT_CONFIRMATION_TOKEN))
|
||||
.bind(token_hash).bind(user.id).bind(expires_at).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_INSERT_CONFIRMATION_TOKEN,
|
||||
))
|
||||
.bind(token_hash)
|
||||
.bind(user.id)
|
||||
.bind(expires_at)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
send_registration_email(smtp, &user, Some(&token)).await?;
|
||||
Ok(Json(serde_json::json!({"ok":true,"message":"A new confirmation e-mail has been sent."})))
|
||||
Ok(Json(
|
||||
serde_json::json!({"ok":true,"message":"A new confirmation e-mail has been sent."}),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn confirm_account(
|
||||
@@ -504,7 +542,10 @@ async fn directory_profile_metadata(
|
||||
|
||||
fn suggested_directory_nickname(display_name: Option<&str>, email: &str) -> Option<String> {
|
||||
if let Some(display_name) = display_name {
|
||||
let words: Vec<&str> = display_name.split_whitespace().filter(|word| !word.is_empty()).collect();
|
||||
let words: Vec<&str> = display_name
|
||||
.split_whitespace()
|
||||
.filter(|word| !word.is_empty())
|
||||
.collect();
|
||||
if words.len() >= 2 {
|
||||
let first = words.first().copied().unwrap_or_default();
|
||||
let last = words.last().copied().unwrap_or_default();
|
||||
@@ -540,6 +581,14 @@ pub async fn me(
|
||||
.map_err(AuthError::database)?;
|
||||
let (directory_managed, directory_display_name, directory_organization, suggested_nickname) =
|
||||
directory_profile_metadata(&state, &user).await?;
|
||||
let editor_color: Option<String> = sqlx::query_scalar(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_EDITOR_COLOR_BY_USER,
|
||||
))
|
||||
.bind(user.id)
|
||||
.fetch_one(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
Ok(Json(SessionResponse {
|
||||
token: token.into(),
|
||||
nickname: user.nickname,
|
||||
@@ -549,6 +598,7 @@ pub async fn me(
|
||||
directory_display_name,
|
||||
directory_organization,
|
||||
suggested_nickname,
|
||||
editor_color,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -586,38 +636,34 @@ pub async fn update_profile(
|
||||
if normalize(&nickname) != normalize(&user.nickname)
|
||||
&& find_user_by_nickname(&state, &nickname).await?.is_some()
|
||||
{
|
||||
return Err(AuthError::conflict(
|
||||
"This nickname is already registered.",
|
||||
));
|
||||
return Err(AuthError::conflict("This nickname is already registered."));
|
||||
}
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_UPDATE_NICKNAME,
|
||||
))
|
||||
.bind(&nickname)
|
||||
.bind(normalize(&nickname))
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.bind(user.id)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
}
|
||||
|
||||
let mut email_pending = false;
|
||||
if !directory_managed {
|
||||
if let Some(value) = req.new_password.as_deref().filter(|value| !value.is_empty()) {
|
||||
validate_password(value)?;
|
||||
let hash = hash_password(value)?;
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_UPDATE_PASSWORD,
|
||||
))
|
||||
.bind(hash)
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_UPDATE_NICKNAME))
|
||||
.bind(&nickname)
|
||||
.bind(normalize(&nickname))
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.bind(user.id)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
}
|
||||
|
||||
let mut email_pending = false;
|
||||
if !directory_managed {
|
||||
if let Some(value) = req
|
||||
.new_password
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
validate_password(value)?;
|
||||
let hash = hash_password(value)?;
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_UPDATE_PASSWORD))
|
||||
.bind(hash)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.bind(user.id)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
}
|
||||
|
||||
if let Some(value) = req
|
||||
@@ -632,18 +678,34 @@ pub async fn update_profile(
|
||||
"This e-mail address is already registered.",
|
||||
));
|
||||
}
|
||||
let smtp = state.smtp.as_ref().ok_or_else(|| {
|
||||
AuthError::service_unavailable("SMTP is not configured.")
|
||||
})?;
|
||||
let smtp = state
|
||||
.smtp
|
||||
.as_ref()
|
||||
.ok_or_else(|| AuthError::service_unavailable("SMTP is not configured."))?;
|
||||
create_account_action(&state, &user, "email", Some(&email), smtp).await?;
|
||||
email_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = req.editor_color.as_deref() {
|
||||
let color = validate_editor_color(value)?;
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_UPDATE_EDITOR_COLOR,
|
||||
))
|
||||
.bind(color)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.bind(user.id)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"ok": true,
|
||||
"nickname": nickname,
|
||||
"editor_color": req.editor_color.as_deref(),
|
||||
"email_pending": email_pending,
|
||||
"message": if email_pending {
|
||||
"Profile updated. Confirm the new e-mail address using the link sent to it."
|
||||
@@ -654,61 +716,190 @@ pub async fn update_profile(
|
||||
}
|
||||
|
||||
pub async fn request_account_deletion(
|
||||
State(state): State<SharedState>, headers: HeaderMap, Json(req): Json<DeleteAccountRequest>,
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<DeleteAccountRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
if state.ldap.is_some() { return Err(AuthError::forbidden("LDAP accounts cannot be deleted here.")); }
|
||||
if state.ldap.is_some() {
|
||||
return Err(AuthError::forbidden(
|
||||
"LDAP accounts cannot be deleted here.",
|
||||
));
|
||||
}
|
||||
let user = require_user(&state, &headers).await?;
|
||||
if !verify_password(&user.password_hash, &req.password) { return Err(AuthError::unauthorized("The current password is incorrect.")); }
|
||||
let smtp = state.smtp.as_ref().ok_or_else(|| AuthError::service_unavailable("SMTP is not configured."))?;
|
||||
if !verify_password(&user.password_hash, &req.password) {
|
||||
return Err(AuthError::unauthorized(
|
||||
"The current password is incorrect.",
|
||||
));
|
||||
}
|
||||
let smtp = state
|
||||
.smtp
|
||||
.as_ref()
|
||||
.ok_or_else(|| AuthError::service_unavailable("SMTP is not configured."))?;
|
||||
create_account_action(&state, &user, "delete", None, smtp).await?;
|
||||
Ok(Json(serde_json::json!({"ok":true,"message":"A confirmation link has been sent to your e-mail address."})))
|
||||
Ok(Json(
|
||||
serde_json::json!({"ok":true,"message":"A confirmation link has been sent to your e-mail address."}),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn confirm_account_action(
|
||||
State(state): State<SharedState>, Json(req): Json<AccountActionConfirmRequest>,
|
||||
State(state): State<SharedState>,
|
||||
Json(req): Json<AccountActionConfirmRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
let now = Utc::now(); let hash = hash_token(req.token.trim());
|
||||
let row: Option<(i64,String,Option<String>,String,Option<String>)> = sqlx::query_as(queries::get(state.db.kind(), queries::AUTH_ACCOUNT_ACTION_BY_TOKEN))
|
||||
.bind(&hash).fetch_optional(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
let (user_id, action, payload, expires_at, used_at) = row.ok_or_else(|| AuthError::bad_request("The confirmation link is invalid or has expired."))?;
|
||||
let expires = chrono::DateTime::parse_from_rfc3339(&expires_at).map_err(|_| AuthError::bad_request("The confirmation link is invalid or has expired."))?.with_timezone(&Utc);
|
||||
if used_at.is_some() || expires <= now { return Err(AuthError::bad_request("The confirmation link is invalid or has expired.")); }
|
||||
let mut tx=state.db.pool().begin().await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_CONSUME_ACCOUNT_ACTION))
|
||||
.bind(now.to_rfc3339()).bind(&hash).execute(&mut *tx).await.map_err(AuthError::database)?;
|
||||
let now = Utc::now();
|
||||
let hash = hash_token(req.token.trim());
|
||||
let row: Option<(i64, String, Option<String>, String, Option<String>)> = sqlx::query_as(
|
||||
queries::get(state.db.kind(), queries::AUTH_ACCOUNT_ACTION_BY_TOKEN),
|
||||
)
|
||||
.bind(&hash)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
let (user_id, action, payload, expires_at, used_at) = row.ok_or_else(|| {
|
||||
AuthError::bad_request("The confirmation link is invalid or has expired.")
|
||||
})?;
|
||||
let expires = chrono::DateTime::parse_from_rfc3339(&expires_at)
|
||||
.map_err(|_| AuthError::bad_request("The confirmation link is invalid or has expired."))?
|
||||
.with_timezone(&Utc);
|
||||
if used_at.is_some() || expires <= now {
|
||||
return Err(AuthError::bad_request(
|
||||
"The confirmation link is invalid or has expired.",
|
||||
));
|
||||
}
|
||||
let mut tx = state.db.pool().begin().await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_CONSUME_ACCOUNT_ACTION,
|
||||
))
|
||||
.bind(now.to_rfc3339())
|
||||
.bind(&hash)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
let message = if action == "email" {
|
||||
let email=payload.ok_or_else(|| AuthError::internal("Missing e-mail change payload."))?;
|
||||
if find_user_by_email(&state,&email).await?.is_some() { return Err(AuthError::conflict("This e-mail address is already registered.")); }
|
||||
let email = payload.ok_or_else(|| AuthError::internal("Missing e-mail change payload."))?;
|
||||
if find_user_by_email(&state, &email).await?.is_some() {
|
||||
return Err(AuthError::conflict(
|
||||
"This e-mail address is already registered.",
|
||||
));
|
||||
}
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_UPDATE_EMAIL))
|
||||
.bind(&email).bind(normalize(&email)).bind(now.to_rfc3339()).bind(user_id).execute(&mut *tx).await.map_err(AuthError::database)?;
|
||||
.bind(&email)
|
||||
.bind(normalize(&email))
|
||||
.bind(now.to_rfc3339())
|
||||
.bind(user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
"E-mail address changed."
|
||||
} else if action == "delete" {
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_USER)).bind(user_id).execute(&mut *tx).await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_USER))
|
||||
.bind(user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
"Account deleted."
|
||||
} else { return Err(AuthError::bad_request("Unknown account action.")); };
|
||||
} else {
|
||||
return Err(AuthError::bad_request("Unknown account action."));
|
||||
};
|
||||
tx.commit().await.map_err(AuthError::database)?;
|
||||
Ok(Json(serde_json::json!({"ok":true,"message":message})))
|
||||
}
|
||||
|
||||
async fn create_account_action(state:&SharedState,user:&User,action:&str,payload:Option<&str>,smtp:&SmtpConfig)->Result<(),AuthError>{
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_ACCOUNT_ACTIONS))
|
||||
.bind(user.id).bind(action).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
let token=random_token(); let expires=(Utc::now()+Duration::hours(1)).to_rfc3339();
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_INSERT_ACCOUNT_ACTION))
|
||||
.bind(hash_token(&token)).bind(user.id).bind(action).bind(payload).bind(expires).bind(Utc::now().to_rfc3339()).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
send_account_action(smtp,user,action,payload,&token).await
|
||||
async fn create_account_action(
|
||||
state: &SharedState,
|
||||
user: &User,
|
||||
action: &str,
|
||||
payload: Option<&str>,
|
||||
smtp: &SmtpConfig,
|
||||
) -> Result<(), AuthError> {
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_DELETE_ACCOUNT_ACTIONS,
|
||||
))
|
||||
.bind(user.id)
|
||||
.bind(action)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
let token = random_token();
|
||||
let expires = (Utc::now() + Duration::hours(1)).to_rfc3339();
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_INSERT_ACCOUNT_ACTION,
|
||||
))
|
||||
.bind(hash_token(&token))
|
||||
.bind(user.id)
|
||||
.bind(action)
|
||||
.bind(payload)
|
||||
.bind(expires)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
send_account_action(smtp, user, action, payload, &token).await
|
||||
}
|
||||
|
||||
async fn send_account_action(smtp:&SmtpConfig,user:&User,action:&str,payload:Option<&str>,token:&str)->Result<(),AuthError>{
|
||||
let site=smtp.public_url.trim_end_matches('/'); let url=format!("{site}/?account_action_token={token}");
|
||||
let sender=smtp.from.parse::<Mailbox>().map_err(|_|AuthError::internal("Invalid SMTP_FROM."))?;
|
||||
let target=if action=="email" { payload.unwrap_or(&user.email) } else { &user.email };
|
||||
let recipient=target.parse::<Mailbox>().map_err(|_|AuthError::internal("Invalid recipient address."))?;
|
||||
let (subject,title,copy)=if action=="email" {("Confirm your new RustPad e-mail","Confirm e-mail change","Confirm the new e-mail address within one hour.")} else {("Confirm RustPad account deletion","Confirm account deletion","Confirm permanent account deletion within one hour.")};
|
||||
let text=format!("Hello {},\n\n{}\n{}\n\nIf you did not request this, ignore this message.",user.nickname,copy,url);
|
||||
let html=format!(r#"<!doctype html><html lang="en"><body style="margin:0;padding:24px;background:#f4f4f5;font-family:Arial,sans-serif;color:#18181b"><div style="max-width:560px;margin:0 auto;padding:24px;background:#fff;border-radius:10px"><h1 style="margin-top:0;font-size:22px">{}</h1><p>Hello {},</p><p>{}</p><p><a href="{}" style="display:inline-block;padding:11px 18px;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px">Confirm action</a></p><p style="font-size:13px;overflow-wrap:anywhere"><a href="{}">{}</a></p><p>If you did not request this, ignore this message.</p></div></body></html>"#,title,user.nickname,copy,url,url,url);
|
||||
let message=Message::builder().from(sender).to(recipient).subject(subject).multipart(MultiPart::alternative().singlepart(SinglePart::builder().header(ContentType::TEXT_PLAIN).body(text)).singlepart(SinglePart::builder().header(ContentType::TEXT_HTML).body(html))).map_err(|_|AuthError::internal("Failed to build account confirmation e-mail."))?;
|
||||
send_message(smtp,message,"account action e-mail").await
|
||||
async fn send_account_action(
|
||||
smtp: &SmtpConfig,
|
||||
user: &User,
|
||||
action: &str,
|
||||
payload: Option<&str>,
|
||||
token: &str,
|
||||
) -> Result<(), AuthError> {
|
||||
let site = smtp.public_url.trim_end_matches('/');
|
||||
let url = format!("{site}/?account_action_token={token}");
|
||||
let sender = smtp
|
||||
.from
|
||||
.parse::<Mailbox>()
|
||||
.map_err(|_| AuthError::internal("Invalid SMTP_FROM."))?;
|
||||
let target = if action == "email" {
|
||||
payload.unwrap_or(&user.email)
|
||||
} else {
|
||||
&user.email
|
||||
};
|
||||
let recipient = target
|
||||
.parse::<Mailbox>()
|
||||
.map_err(|_| AuthError::internal("Invalid recipient address."))?;
|
||||
let (subject, title, copy) = if action == "email" {
|
||||
(
|
||||
"Confirm your new RustPad e-mail",
|
||||
"Confirm e-mail change",
|
||||
"Confirm the new e-mail address within one hour.",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Confirm RustPad account deletion",
|
||||
"Confirm account deletion",
|
||||
"Confirm permanent account deletion within one hour.",
|
||||
)
|
||||
};
|
||||
let text = format!(
|
||||
"Hello {},\n\n{}\n{}\n\nIf you did not request this, ignore this message.",
|
||||
user.nickname, copy, url
|
||||
);
|
||||
let html = format!(
|
||||
r#"<!doctype html><html lang="en"><body style="margin:0;padding:24px;background:#f4f4f5;font-family:Arial,sans-serif;color:#18181b"><div style="max-width:560px;margin:0 auto;padding:24px;background:#fff;border-radius:10px"><h1 style="margin-top:0;font-size:22px">{}</h1><p>Hello {},</p><p>{}</p><p><a href="{}" style="display:inline-block;padding:11px 18px;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px">Confirm action</a></p><p style="font-size:13px;overflow-wrap:anywhere"><a href="{}">{}</a></p><p>If you did not request this, ignore this message.</p></div></body></html>"#,
|
||||
title, user.nickname, copy, url, url, url
|
||||
);
|
||||
let message = Message::builder()
|
||||
.from(sender)
|
||||
.to(recipient)
|
||||
.subject(subject)
|
||||
.multipart(
|
||||
MultiPart::alternative()
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(text),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html),
|
||||
),
|
||||
)
|
||||
.map_err(|_| AuthError::internal("Failed to build account confirmation e-mail."))?;
|
||||
send_message(smtp, message, "account action e-mail").await
|
||||
}
|
||||
|
||||
pub async fn resources(
|
||||
@@ -1476,12 +1667,13 @@ pub async fn confirm_reset(
|
||||
pub async fn user_from_token(state: &SharedState, token: &str) -> Result<Option<User>, AuthError> {
|
||||
let now = Utc::now();
|
||||
let now_rfc3339 = now.to_rfc3339();
|
||||
let user = sqlx::query_as::<_, User>(queries::get(state.db.kind(), queries::AUTH_USER_BY_SESSION))
|
||||
.bind(token)
|
||||
.bind(&now_rfc3339)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
let user =
|
||||
sqlx::query_as::<_, User>(queries::get(state.db.kind(), queries::AUTH_USER_BY_SESSION))
|
||||
.bind(token)
|
||||
.bind(&now_rfc3339)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
|
||||
if let Some(user) = user {
|
||||
let expires_at = (now + Duration::days(state.user_session_ttl_days)).to_rfc3339();
|
||||
@@ -1553,6 +1745,14 @@ async fn create_session(state: &SharedState, user: &User) -> Result<SessionRespo
|
||||
debug!(user_id = user.id, expires_at = %expires_at, "authentication session created");
|
||||
let (directory_managed, directory_display_name, directory_organization, suggested_nickname) =
|
||||
directory_profile_metadata(state, user).await?;
|
||||
let editor_color: Option<String> = sqlx::query_scalar(queries::get(
|
||||
state.db.kind(),
|
||||
queries::AUTH_EDITOR_COLOR_BY_USER,
|
||||
))
|
||||
.bind(user.id)
|
||||
.fetch_one(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
Ok(SessionResponse {
|
||||
token,
|
||||
nickname: user.nickname.clone(),
|
||||
@@ -1562,6 +1762,7 @@ async fn create_session(state: &SharedState, user: &User) -> Result<SessionRespo
|
||||
directory_display_name,
|
||||
directory_organization,
|
||||
suggested_nickname,
|
||||
editor_color,
|
||||
})
|
||||
}
|
||||
async fn find_user_by_nickname(
|
||||
@@ -1600,6 +1801,18 @@ async fn find_user_by_email(state: &SharedState, email: &str) -> Result<Option<U
|
||||
.await
|
||||
.map_err(AuthError::database)
|
||||
}
|
||||
fn validate_editor_color(value: &str) -> Result<String, AuthError> {
|
||||
let value = value.trim();
|
||||
if value.len() == 7
|
||||
&& value.starts_with('#')
|
||||
&& value[1..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
{
|
||||
Ok(value.to_ascii_lowercase())
|
||||
} else {
|
||||
Err(AuthError::bad_request("Invalid editor color."))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_nickname(v: &str) -> Result<String, AuthError> {
|
||||
let v = v.trim();
|
||||
if v.is_empty() || v.chars().count() > MAX_NICKNAME {
|
||||
|
||||
+20
-7
@@ -17,22 +17,35 @@ pub const POSTGRES_PAD_REVISION_LAST_INSERT_ID: &str =
|
||||
|
||||
// Authentication queries.
|
||||
pub const AUTH_LATEST_CONFIRMATION_CREATED_AT: &str = "SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1";
|
||||
pub const AUTH_UPDATE_NICKNAME: &str = "UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?";
|
||||
pub const AUTH_UPDATE_NICKNAME: &str =
|
||||
"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?";
|
||||
pub const AUTH_UPDATE_EDITOR_COLOR: &str =
|
||||
"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?";
|
||||
pub const AUTH_EDITOR_COLOR_BY_USER: &str = "SELECT editor_color FROM users WHERE id = ?";
|
||||
pub const RESOURCE_COLOR_BY_USER: &str = "SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?";
|
||||
pub const RESOURCE_COLOR_DELETE: &str = "DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?";
|
||||
pub const RESOURCE_COLOR_INSERT: &str = "INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)";
|
||||
pub const AUTH_ACCOUNT_ACTION_BY_TOKEN: &str = "SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = ?";
|
||||
pub const AUTH_CONSUME_ACCOUNT_ACTION: &str = "UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL";
|
||||
pub const AUTH_UPDATE_EMAIL: &str = "UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?";
|
||||
pub const AUTH_DELETE_ACCOUNT_ACTIONS: &str = "DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?";
|
||||
pub const AUTH_CONSUME_ACCOUNT_ACTION: &str =
|
||||
"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL";
|
||||
pub const AUTH_UPDATE_EMAIL: &str =
|
||||
"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?";
|
||||
pub const AUTH_DELETE_ACCOUNT_ACTIONS: &str =
|
||||
"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?";
|
||||
pub const AUTH_INSERT_ACCOUNT_ACTION: &str = "INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
pub const AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS: &str = "DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?";
|
||||
pub const AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS: &str =
|
||||
"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?";
|
||||
pub const AUTH_INSERT_USER: &str = "INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
pub const AUTH_INSERT_DIRECTORY_USER: &str = "INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
pub const AUTH_UPDATE_DIRECTORY_USER: &str = "UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?";
|
||||
pub const AUTH_DIRECTORY_PROFILE_BY_USER: &str = "SELECT auth_provider, directory_display_name FROM users WHERE id = ?";
|
||||
pub const AUTH_DIRECTORY_PROFILE_BY_USER: &str =
|
||||
"SELECT auth_provider, directory_display_name FROM users WHERE id = ?";
|
||||
pub const AUTH_USER_BY_EXTERNAL_ID: &str = "SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE auth_provider = ? AND external_id = ?";
|
||||
pub const AUTH_DELETE_USER: &str = "DELETE FROM users WHERE id = ?";
|
||||
pub const AUTH_SESSION_EXPIRES_AT: &str = "SELECT expires_at FROM user_sessions WHERE token = ?";
|
||||
pub const AUTH_DELETE_SESSION_BY_TOKEN: &str = "DELETE FROM user_sessions WHERE token = ?";
|
||||
pub const AUTH_REFRESH_SESSION: &str = "UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?";
|
||||
pub const AUTH_REFRESH_SESSION: &str =
|
||||
"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?";
|
||||
pub const AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER: &str =
|
||||
"DELETE FROM account_confirmation_tokens WHERE user_id = ?";
|
||||
pub const AUTH_INSERT_CONFIRMATION_TOKEN: &str =
|
||||
|
||||
Reference in New Issue
Block a user