Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Changelog

## v0.1.3

### Search
- Search now has a cleaner page layout and clearer result cards.
- It handles posts, usernames, mentions, and hashtags more consistently.
- Bad or unusual search input no longer sends people to an error page.
- Search results can be liked or opened without leaving the search flow.

### Profiles and Media
- Profile pictures now use thumbnails in compact places, which keeps feeds faster and cleaner.
- Full-size profile pages still show the original image.
- The database now tracks thumbnail paths for stored media.

### Admin
- The admin media jobs view is shorter and easier to read.
- It now shows the most useful job status details and recent failures at a glance.
- Admins can upload a custom favicon and reset back to the built-in one.

### Login and Registration
- Login now shows specific errors for missing accounts, wrong passwords, and unavailable accounts.
- Registration now shows a clear message when a username is already taken.

### Threads
- Thread pages no longer show an extra generic thread header.
- The root post no longer links back to itself, which makes navigation less confusing.
94 changes: 94 additions & 0 deletions src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ use crate::auth;
use crate::config::Settings;
use crate::db::SqlitePool;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MediaJobsReport {
pub total: i64,
pub pending: i64,
pub running: i64,
pub succeeded: i64,
pub failed: i64,
pub newest_pending_age_seconds: Option<i64>,
pub oldest_pending_age_seconds: Option<i64>,
pub recent_failures: Vec<MediaJobFailure>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaJobFailure {
pub id: i64,
pub media_id: Option<i64>,
pub media_path: Option<String>,
pub job_kind: Option<String>,
pub age_seconds: Option<i64>,
pub error_summary: String,
}

pub async fn create_admin(
pool: &SqlitePool,
settings: &Settings,
Expand Down Expand Up @@ -152,3 +174,75 @@ pub async fn recent_media_jobs(pool: &SqlitePool) -> anyhow::Result<Vec<(i64, St
})
.await
}

pub async fn media_jobs_report(pool: &SqlitePool) -> anyhow::Result<MediaJobsReport> {
pool.call(|conn| {
let (total, pending, running, succeeded, failed, newest_pending_age, oldest_pending_age) =
conn.query_row(
r"
SELECT
COUNT(*),
COALESCE(SUM(CASE WHEN status IN ('pending', 'queued') THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN status IN ('succeeded', 'success', 'converted') THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN status IN ('failed', 'error', 'fallback') THEN 1 ELSE 0 END), 0),
MIN(CASE WHEN status IN ('pending', 'queued') THEN CAST(strftime('%s', 'now') - strftime('%s', created_at) AS INTEGER) END),
MAX(CASE WHEN status IN ('pending', 'queued') THEN CAST(strftime('%s', 'now') - strftime('%s', created_at) AS INTEGER) END)
FROM media_jobs
",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
row.get(6)?,
))
},
)?;

let mut stmt = conn.prepare(
r"
SELECT
j.id,
j.media_id,
COALESCE(NULLIF(m.public_path, ''), NULLIF(m.original_filename, ''), NULLIF(m.stored_path, '')),
NULLIF(m.media_kind, ''),
CAST(strftime('%s', 'now') - strftime('%s', COALESCE(j.finished_at, j.created_at)) AS INTEGER),
j.stderr_summary
FROM media_jobs j
LEFT JOIN media m ON m.id = j.media_id
WHERE j.status IN ('failed', 'error', 'fallback')
ORDER BY j.id DESC
LIMIT 5
",
)?;
let recent_failures = stmt
.query_map([], |row| {
Ok(MediaJobFailure {
id: row.get(0)?,
media_id: row.get(1)?,
media_path: row.get(2)?,
job_kind: row.get(3)?,
age_seconds: row.get(4)?,
error_summary: row.get(5)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;

Ok(MediaJobsReport {
total,
pending,
running,
succeeded,
failed,
newest_pending_age_seconds: newest_pending_age,
oldest_pending_age_seconds: oldest_pending_age,
recent_failures,
})
})
.await
}
21 changes: 15 additions & 6 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ pub struct Session {
pub csrf_token: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoginFailure {
NoAccount,
InvalidPassword,
UnavailableAccount,
}

pub const USERNAME_TAKEN_MESSAGE: &str = "username is already taken";

pub async fn register_user(
pool: &SqlitePool,
settings: &Settings,
Expand All @@ -47,7 +56,7 @@ pub async fn register_user(
.optional()?
.is_some();
if existing {
anyhow::bail!("username is already taken");
anyhow::bail!(USERNAME_TAKEN_MESSAGE);
}
conn.execute(
"INSERT INTO users (username, normalized_username, password_hash, display_name, is_admin) VALUES (?, ?, ?, ?, ?)",
Expand All @@ -62,7 +71,7 @@ pub async fn login(
pool: &SqlitePool,
username: &str,
password: &str,
) -> anyhow::Result<Option<Session>> {
) -> anyhow::Result<Result<Session, LoginFailure>> {
let normalized = username.trim().to_ascii_lowercase();
let row = pool
.call(move |conn| {
Expand All @@ -83,15 +92,15 @@ pub async fn login(
})
.await?;
let Some(row) = row else {
return Ok(None);
return Ok(Err(LoginFailure::NoAccount));
};
if row.2 != 0 || row.3 != 0 {
return Ok(None);
return Ok(Err(LoginFailure::UnavailableAccount));
}
if !verify_password(password, &row.1)? {
return Ok(None);
return Ok(Err(LoginFailure::InvalidPassword));
}
create_session(pool, row.0).await.map(Some)
create_session(pool, row.0).await.map(Ok)
}

pub async fn create_session(pool: &SqlitePool, user_id: i64) -> anyhow::Result<Session> {
Expand Down
11 changes: 10 additions & 1 deletion src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ pub async fn migrate(pool: &Db) -> anyhow::Result<()> {
tx.execute_batch(MIGRATION_2)?;
tx.execute("INSERT INTO schema_migrations (version) VALUES (2)", [])?;
}
if applied.unwrap_or(0) < 3 {
tx.execute_batch(MIGRATION_3)?;
tx.execute("INSERT INTO schema_migrations (version) VALUES (3)", [])?;
}
tx.commit()?;
Ok(())
})
Expand Down Expand Up @@ -279,6 +283,11 @@ const MIGRATION_2: &str = r#"
ALTER TABLE sessions ADD COLUMN previous_csrf_token_hash TEXT;
"#;

const MIGRATION_3: &str = r#"
ALTER TABLE media ADD COLUMN thumbnail_path TEXT;
ALTER TABLE media ADD COLUMN thumbnail_public_path TEXT;
"#;

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -320,7 +329,7 @@ mod tests {
.await
.expect("settings");

assert_eq!(versions, 2);
assert_eq!(versions, 3);
assert_eq!(foreign_keys, 1);
assert_eq!(journal_mode, "wal");
assert!(busy_timeout >= 5_000);
Expand Down
Loading
Loading