Compare commits

...

4 Commits

Author SHA1 Message Date
Greg Shuflin
3ab595abd2 Demo notice 2025-02-03 16:01:34 -08:00
Greg Shuflin
4c44767301 display demo mode notice 2025-02-03 15:53:28 -08:00
Greg Shuflin
2b632f0a93 Add demo feed 2025-02-03 15:46:28 -08:00
Greg Shuflin
4cce902a21 Moving code around (broken) 2025-02-03 04:29:16 -08:00
5 changed files with 97 additions and 37 deletions

View File

@ -2,9 +2,12 @@ use chrono;
use sqlx; use sqlx;
use uuid::Uuid; use uuid::Uuid;
use crate::feeds::Feed;
pub async fn setup_demo_data(pool: &sqlx::SqlitePool) { pub async fn setup_demo_data(pool: &sqlx::SqlitePool) {
// Create admin user // Create admin user
let admin_id = Uuid::new_v4().to_string(); let admin_id = Uuid::new_v4();
let admin_id_str = admin_id.to_string();
let admin_hash = bcrypt::hash("admin", bcrypt::DEFAULT_COST).unwrap(); let admin_hash = bcrypt::hash("admin", bcrypt::DEFAULT_COST).unwrap();
let now = chrono::Utc::now().to_rfc3339(); let now = chrono::Utc::now().to_rfc3339();
@ -12,7 +15,7 @@ pub async fn setup_demo_data(pool: &sqlx::SqlitePool) {
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin) "INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
) )
.bind(&admin_id) .bind(&admin_id_str)
.bind("admin") .bind("admin")
.bind(&admin_hash) .bind(&admin_hash)
.bind(Option::<String>::None) .bind(Option::<String>::None)
@ -24,14 +27,15 @@ pub async fn setup_demo_data(pool: &sqlx::SqlitePool) {
.expect("Failed to create admin user"); .expect("Failed to create admin user");
// Create demo user // Create demo user
let demo_id = Uuid::new_v4().to_string(); let demo_id = Uuid::new_v4();
let demo_id_str = demo_id.to_string();
let demo_hash = bcrypt::hash("demo", bcrypt::DEFAULT_COST).unwrap(); let demo_hash = bcrypt::hash("demo", bcrypt::DEFAULT_COST).unwrap();
sqlx::query( sqlx::query(
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin) "INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
) )
.bind(&demo_id) .bind(&demo_id_str)
.bind("demo") .bind("demo")
.bind(&demo_hash) .bind(&demo_hash)
.bind(Option::<String>::None) .bind(Option::<String>::None)
@ -42,5 +46,30 @@ pub async fn setup_demo_data(pool: &sqlx::SqlitePool) {
.await .await
.expect("Failed to create demo user"); .expect("Failed to create demo user");
let feed = Feed::new(
"BBC News".to_string(),
"https://feeds.bbci.co.uk/news/world/us_and_canada/rss.xml"
.parse()
.unwrap(),
demo_id,
);
// TODO: This insert logic is substantially the same as Feed::write_to_database.
// Should find a way to unify these two code paths to avoid duplication.
sqlx::query(
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))",
)
.bind(feed.feed_id.to_string())
.bind(&feed.name)
.bind(feed.url.as_str())
.bind(feed.user_id.to_string())
.bind(feed.added_time.to_rfc3339())
.bind(feed.last_checked_time.to_rfc3339())
.bind("[]") // empty categorization array as JSON
.execute(pool)
.await
.expect("Failed to create demo feed");
println!("Successfully set up demo data"); println!("Successfully set up demo data");
} }

View File

@ -12,13 +12,13 @@ use crate::Db;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct Feed { pub struct Feed {
feed_id: Uuid, pub feed_id: Uuid,
name: String, pub name: String,
url: Url, pub url: Url,
user_id: Uuid, pub user_id: Uuid,
added_time: chrono::DateTime<chrono::Utc>, pub added_time: chrono::DateTime<chrono::Utc>,
last_checked_time: chrono::DateTime<chrono::Utc>, pub last_checked_time: chrono::DateTime<chrono::Utc>,
categorization: Vec<String>, pub categorization: Vec<String>,
} }
impl Feed { impl Feed {
@ -34,6 +34,30 @@ impl Feed {
categorization: Vec::new(), categorization: Vec::new(),
} }
} }
pub async fn write_to_database(&self, mut db: Connection<Db>) -> Result<(), sqlx::Error> {
// Convert categorization to JSON value
let categorization_json = serde::json::to_value(&self.categorization).map_err(|e| {
eprintln!("Failed to serialize categorization: {}", e);
sqlx::Error::Decode(Box::new(e))
})?;
sqlx::query(
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))",
)
.bind(self.feed_id.to_string())
.bind(&self.name)
.bind(self.url.as_str())
.bind(self.user_id.to_string())
.bind(self.added_time.to_rfc3339())
.bind(self.last_checked_time.to_rfc3339())
.bind(&categorization_json.to_string())
.execute(&mut **db)
.await?;
Ok(())
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -46,7 +70,7 @@ pub struct NewFeed {
#[post("/feeds", data = "<new_feed>")] #[post("/feeds", data = "<new_feed>")]
pub async fn create_feed( pub async fn create_feed(
mut db: Connection<Db>, db: Connection<Db>,
new_feed: Json<NewFeed>, new_feed: Json<NewFeed>,
user: AuthenticatedUser, user: AuthenticatedUser,
) -> Result<Json<Feed>, Status> { ) -> Result<Json<Feed>, Status> {
@ -73,27 +97,7 @@ pub async fn create_feed(
let mut feed = Feed::new(name, new_feed.url, user.user_id); let mut feed = Feed::new(name, new_feed.url, user.user_id);
feed.categorization = new_feed.categorization; feed.categorization = new_feed.categorization;
// Convert categorization to JSON value match feed.write_to_database(db).await {
let categorization_json = serde::json::to_value(&feed.categorization).map_err(|e| {
eprintln!("Failed to serialize categorization: {}", e);
Status::InternalServerError
})?;
let query = sqlx::query(
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))",
)
.bind(feed.feed_id.to_string())
.bind(&feed.name)
.bind(feed.url.as_str())
.bind(feed.user_id.to_string())
.bind(feed.added_time.to_rfc3339())
.bind(feed.last_checked_time.to_rfc3339())
.bind(&categorization_json.to_string())
.execute(&mut **db)
.await;
match query {
Ok(_) => Ok(Json(feed)), Ok(_) => Ok(Json(feed)),
Err(e) => { Err(e) => {
eprintln!("Database error: {}", e); eprintln!("Database error: {}", e);

View File

@ -57,8 +57,8 @@ async fn index_redirect(mut db: Connection<Db>) -> Redirect {
} }
#[get("/login")] #[get("/login")]
fn login() -> Template { fn login(demo_mode: &State<bool>) -> Template {
Template::render("login", context! {}) Template::render("login", context! { demo_mode: *demo_mode })
} }
// Run migrations and setup demo data if needed // Run migrations and setup demo data if needed
@ -100,7 +100,6 @@ fn rocket() -> _ {
}; };
let figment = rocket::Config::figment().merge(("databases.rss_data.url", db_url)); let figment = rocket::Config::figment().merge(("databases.rss_data.url", db_url));
let demo = args.demo;
rocket::custom(figment) rocket::custom(figment)
.mount( .mount(
@ -126,7 +125,8 @@ fn rocket() -> _ {
.mount("/static", FileServer::from("static")) .mount("/static", FileServer::from("static"))
.attach(Template::fairing()) .attach(Template::fairing())
.attach(Db::init()) .attach(Db::init())
.manage(args.demo)
.attach(AdHoc::try_on_ignite("DB Setup", move |rocket| async move { .attach(AdHoc::try_on_ignite("DB Setup", move |rocket| async move {
setup_database(demo, rocket).await setup_database(args.demo, rocket).await
})) }))
} }

View File

@ -535,3 +535,21 @@ button:disabled {
color: var(--text-color); color: var(--text-color);
line-height: 1.5; line-height: 1.5;
} }
.demo-notice {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.demo-notice p {
margin: 0.5rem 0;
}
.demo-notice pre {
padding: 0.1rem 0.4rem;
border-radius: 2px;
font-family: monospace;
}

View File

@ -9,6 +9,15 @@
<div class="login-container"> <div class="login-container">
<form class="login-form" id="loginForm"> <form class="login-form" id="loginForm">
<h1>Login</h1> <h1>Login</h1>
{% if demo_mode %}
<div class="demo-notice">
<p>RSS Reader is currently running in demo mode. You can log in with these credentials:</p>
Username: <pre style="display: inline">demo</pre>, Password: <pre style="display: inline">demo</pre>
<p>Note: Any data you create will not be persisted after the application restarts.</p>
</div>
{% endif %}
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" id="username" name="username" required> <input type="text" id="username" name="username" required>