Compare commits
4 Commits
d083f8ae3d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a66661932 | |||
| b30a6a102b | |||
| 38a3aa6bbf | |||
| 3135a15cb3 |
@@ -4,3 +4,7 @@ target/
|
|||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
.cargo/
|
||||||
|
.sqlx/
|
||||||
|
docs/
|
||||||
|
dev.db
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,7 +8,9 @@
|
|||||||
.env.prod
|
.env.prod
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
|
*db-shm
|
||||||
|
*db-wal
|
||||||
|
|
||||||
.worktrees/
|
.worktrees/
|
||||||
.superpowers/
|
.superpowers/
|
||||||
docs/
|
docs/
|
||||||
|
|||||||
@@ -3,7 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Movies Diary</title>
|
<title>{{ ctx.page_title }}</title>
|
||||||
|
<meta name="description" content="A personal movie diary — track what you watch, rate and review films.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:site_name" content="Movies Diary">
|
||||||
|
<meta property="og:title" content="{{ ctx.page_title }}">
|
||||||
|
<meta property="og:url" content="{{ ctx.canonical_url }}">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="{{ ctx.page_title }}">
|
||||||
|
<link rel="canonical" href="{{ ctx.canonical_url }}">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub allow_registration: bool,
|
pub allow_registration: bool,
|
||||||
|
pub base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
@@ -8,6 +9,8 @@ impl AppConfig {
|
|||||||
let allow_registration = std::env::var("ALLOW_REGISTRATION")
|
let allow_registration = std::env::var("ALLOW_REGISTRATION")
|
||||||
.map(|v| v == "true" || v == "1")
|
.map(|v| v == "true" || v == "1")
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
Self { allow_registration }
|
let base_url = std::env::var("BASE_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||||
|
Self { allow_registration, base_url }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ pub struct HtmlPageContext {
|
|||||||
pub user_id: Option<Uuid>,
|
pub user_id: Option<Uuid>,
|
||||||
pub register_enabled: bool,
|
pub register_enabled: bool,
|
||||||
pub rss_url: String,
|
pub rss_url: String,
|
||||||
|
pub page_title: String,
|
||||||
|
pub canonical_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HtmlPageContext {
|
impl HtmlPageContext {
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(PanicAuth),
|
auth_service: Arc::new(PanicAuth),
|
||||||
password_hasher: Arc::new(PanicHasher),
|
password_hasher: Arc::new(PanicHasher),
|
||||||
user_repository: Arc::new(PanicUserRepo),
|
user_repository: Arc::new(PanicUserRepo),
|
||||||
config: AppConfig { allow_registration: false },
|
config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(PanicAuth),
|
auth_service: Arc::new(PanicAuth),
|
||||||
password_hasher: Arc::new(PanicHasher),
|
password_hasher: Arc::new(PanicHasher),
|
||||||
user_repository: Arc::new(PanicUserRepo),
|
user_repository: Arc::new(PanicUserRepo),
|
||||||
config: application::config::AppConfig { allow_registration: false },
|
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(PanicRenderer),
|
html_renderer: Arc::new(PanicRenderer),
|
||||||
rss_renderer: Arc::new(PanicRssRenderer),
|
rss_renderer: Arc::new(PanicRssRenderer),
|
||||||
@@ -282,7 +282,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(PanicAuth2),
|
auth_service: Arc::new(PanicAuth2),
|
||||||
password_hasher: Arc::new(PanicHasher2),
|
password_hasher: Arc::new(PanicHasher2),
|
||||||
user_repository: Arc::new(PanicUserRepo2),
|
user_repository: Arc::new(PanicUserRepo2),
|
||||||
config: application::config::AppConfig { allow_registration: false },
|
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(PanicRenderer2),
|
html_renderer: Arc::new(PanicRenderer2),
|
||||||
rss_renderer: Arc::new(PanicRssRenderer2),
|
rss_renderer: Arc::new(PanicRssRenderer2),
|
||||||
@@ -341,7 +341,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(RejectingAuth),
|
auth_service: Arc::new(RejectingAuth),
|
||||||
password_hasher: Arc::new(PanicHasher3),
|
password_hasher: Arc::new(PanicHasher3),
|
||||||
user_repository: Arc::new(PanicUserRepo3),
|
user_repository: Arc::new(PanicUserRepo3),
|
||||||
config: application::config::AppConfig { allow_registration: false },
|
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(PanicRenderer3),
|
html_renderer: Arc::new(PanicRenderer3),
|
||||||
rss_renderer: Arc::new(PanicRssRenderer3),
|
rss_renderer: Arc::new(PanicRssRenderer3),
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ pub mod html {
|
|||||||
user_id: uuid,
|
user_id: uuid,
|
||||||
register_enabled: state.app_ctx.config.allow_registration,
|
register_enabled: state.app_ctx.config.allow_registration,
|
||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
|
page_title: "Movies Diary".to_string(),
|
||||||
|
canonical_url: state.app_ctx.config.base_url.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +76,8 @@ pub mod html {
|
|||||||
user_id: None,
|
user_id: None,
|
||||||
register_enabled: state.app_ctx.config.allow_registration,
|
register_enabled: state.app_ctx.config.allow_registration,
|
||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
|
page_title: "Login — Movies Diary".to_string(),
|
||||||
|
canonical_url: format!("{}/login", state.app_ctx.config.base_url),
|
||||||
};
|
};
|
||||||
let html = state
|
let html = state
|
||||||
.html_renderer
|
.html_renderer
|
||||||
@@ -125,6 +129,8 @@ pub mod html {
|
|||||||
user_id: None,
|
user_id: None,
|
||||||
register_enabled: true,
|
register_enabled: true,
|
||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
|
page_title: "Register — Movies Diary".to_string(),
|
||||||
|
canonical_url: format!("{}/register", state.app_ctx.config.base_url),
|
||||||
};
|
};
|
||||||
let html = state
|
let html = state
|
||||||
.html_renderer
|
.html_renderer
|
||||||
@@ -175,7 +181,9 @@ pub mod html {
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<ErrorQuery>,
|
Query(params): Query<ErrorQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let ctx = build_page_context(&state, Some(user_id)).await;
|
let mut ctx = build_page_context(&state, Some(user_id)).await;
|
||||||
|
ctx.page_title = "Log a Review — Movies Diary".to_string();
|
||||||
|
ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url);
|
||||||
let html = state
|
let html = state
|
||||||
.html_renderer
|
.html_renderer
|
||||||
.render_new_review_page(NewReviewPageData {
|
.render_new_review_page(NewReviewPageData {
|
||||||
@@ -262,7 +270,9 @@ pub mod html {
|
|||||||
OptionalCookieUser(user_id): OptionalCookieUser,
|
OptionalCookieUser(user_id): OptionalCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let ctx = build_page_context(&state, user_id).await;
|
let mut ctx = build_page_context(&state, user_id).await;
|
||||||
|
ctx.page_title = "Members — Movies Diary".to_string();
|
||||||
|
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
||||||
match application::use_cases::get_users::execute(&state.app_ctx, application::queries::GetUsersQuery).await {
|
match application::use_cases::get_users::execute(&state.app_ctx, application::queries::GetUsersQuery).await {
|
||||||
Ok(users) => {
|
Ok(users) => {
|
||||||
let data = application::ports::UsersPageData { ctx, users };
|
let data = application::ports::UsersPageData { ctx, users };
|
||||||
@@ -293,6 +303,11 @@ pub mod html {
|
|||||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let display_name = profile_user.email().value()
|
||||||
|
.split('@').next().unwrap_or("User");
|
||||||
|
ctx.page_title = format!("{}'s Diary — Movies Diary", display_name);
|
||||||
|
ctx.canonical_url = format!("{}/users/{}", state.app_ctx.config.base_url, profile_user_uuid);
|
||||||
|
|
||||||
let query = application::queries::GetUserProfileQuery {
|
let query = application::queries::GetUserProfileQuery {
|
||||||
user_id: profile_user_uuid,
|
user_id: profile_user_uuid,
|
||||||
view: view.clone(),
|
view: view.clone(),
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let app = routes::build_router(state);
|
let app = routes::build_router(state);
|
||||||
|
|
||||||
let listener = TcpListener::bind("0.0.0.0:3000").await?;
|
let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||||
tracing::info!("Listening on 0.0.0.0:3000");
|
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
|
let addr = format!("{}:{}", host, port);
|
||||||
|
let listener = TcpListener::bind(&addr).await?;
|
||||||
|
tracing::info!("Listening on {}", addr);
|
||||||
axum::serve(listener, app).await?;
|
axum::serve(listener, app).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -48,7 +51,9 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
|
|||||||
let database_url = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?;
|
let database_url = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?;
|
||||||
let opts = SqliteConnectOptions::from_str(&database_url)
|
let opts = SqliteConnectOptions::from_str(&database_url)
|
||||||
.context("Invalid DATABASE_URL")?
|
.context("Invalid DATABASE_URL")?
|
||||||
.create_if_missing(true);
|
.create_if_missing(true)
|
||||||
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||||
|
.busy_timeout(std::time::Duration::from_secs(5));
|
||||||
let pool = SqlitePool::connect_with(opts)
|
let pool = SqlitePool::connect_with(opts)
|
||||||
.await
|
.await
|
||||||
.context("Failed to connect to SQLite database")?;
|
.context("Failed to connect to SQLite database")?;
|
||||||
|
|||||||
@@ -35,14 +35,15 @@ impl RateLimiter {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_secs()
|
.as_secs()
|
||||||
/ 60;
|
/ 60;
|
||||||
let prev = self.window.load(Ordering::Relaxed);
|
let prev = self.window.load(Ordering::Acquire);
|
||||||
if now != prev {
|
if now != prev {
|
||||||
self.window.store(now, Ordering::Relaxed);
|
// compare_exchange ensures only one thread wins the window reset
|
||||||
self.count.store(1, Ordering::Relaxed);
|
if self.window.compare_exchange(prev, now, Ordering::AcqRel, Ordering::Relaxed).is_ok() {
|
||||||
true
|
self.count.store(1, Ordering::Release);
|
||||||
} else {
|
return true;
|
||||||
self.count.fetch_add(1, Ordering::Relaxed) + 1 <= self.limit
|
}
|
||||||
}
|
}
|
||||||
|
self.count.fetch_add(1, Ordering::Relaxed) + 1 <= self.limit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ async fn test_app() -> Router {
|
|||||||
auth_service: Arc::new(PanicAuth),
|
auth_service: Arc::new(PanicAuth),
|
||||||
password_hasher: Arc::new(PanicHasher),
|
password_hasher: Arc::new(PanicHasher),
|
||||||
user_repository: Arc::new(NobodyUserRepo),
|
user_repository: Arc::new(NobodyUserRepo),
|
||||||
config: AppConfig { allow_registration: false },
|
config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(AskamaHtmlRenderer::new()),
|
html_renderer: Arc::new(AskamaHtmlRenderer::new()),
|
||||||
rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),
|
rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),
|
||||||
|
|||||||
@@ -34,6 +34,21 @@ body {
|
|||||||
background: url("/static/background.avif") center / cover no-repeat fixed;
|
background: url("/static/background.avif") center / cover no-repeat fixed;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|||||||
Reference in New Issue
Block a user