Record login for users
Some checks failed
Rust / Test (push) Failing after 4m54s
Rust / Rustfmt (push) Successful in 21s
Rust / Clippy (push) Failing after 1m36s
Rust / Code coverage (push) Successful in 5m4s

This commit is contained in:
Alphonse Paix
2025-10-09 21:05:48 +02:00
parent 45f529902d
commit e02139ff44
14 changed files with 155 additions and 69 deletions

View File

@@ -8,6 +8,7 @@ pub struct PostEntry {
pub title: String,
pub content: String,
pub published_at: DateTime<Utc>,
pub last_modified: Option<DateTime<Utc>>,
}
impl PostEntry {
@@ -15,6 +16,10 @@ impl PostEntry {
self.published_at.format("%B %d, %Y").to_string()
}
// pub fn last_modified(&self) -> String {
// if let Some(last_modified) = self.last_modi
// }
pub fn to_html(&self) -> anyhow::Result<String> {
match markdown::to_html_with_options(&self.content, &markdown::Options::gfm()) {
Ok(content) => Ok(content),

View File

@@ -15,6 +15,8 @@ use axum::{
};
use axum::{http::StatusCode, response::Redirect};
use secrecy::SecretString;
use sqlx::PgPool;
use uuid::Uuid;
#[derive(serde::Deserialize)]
pub struct LoginFormData {
@@ -50,6 +52,9 @@ pub async fn post_login(
tracing::Span::current().record("username", tracing::field::display(&credentials.username));
let (user_id, role) = validate_credentials(credentials, &connection_pool).await?;
tracing::Span::current().record("user_id", tracing::field::display(&user_id));
record_login(&connection_pool, &user_id)
.await
.context("Failed to register new login event.")?;
session.renew().await.context("Failed to renew session.")?;
session
@@ -69,3 +74,11 @@ pub async fn post_login(
headers.insert("HX-Redirect", "/dashboard".parse().unwrap());
Ok((StatusCode::OK, headers).into_response())
}
#[tracing::instrument(name = "Recording new login event", skip_all, fields(user_id = %user_id))]
async fn record_login(connection_pool: &PgPool, user_id: &Uuid) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO user_logins (user_id) VALUES ($1)", user_id)
.execute(connection_pool)
.await?;
Ok(())
}

View File

@@ -17,6 +17,7 @@ use axum::{
extract::State,
response::{Html, IntoResponse, Redirect, Response},
};
use chrono::Utc;
use sqlx::PgPool;
use uuid::Uuid;
use validator::Validate;
@@ -77,7 +78,8 @@ async fn get_posts(
sqlx::query_as!(
PostEntry,
r#"
SELECT p.post_id, p.author_id, u.username AS author, p.title, p.content, p.published_at
SELECT p.post_id, p.author_id, u.username AS author,
p.title, p.content, p.published_at, p.last_modified
FROM posts p
LEFT JOIN users u ON p.author_id = u.user_id
ORDER BY p.published_at DESC
@@ -99,7 +101,8 @@ pub async fn get_posts_page(
sqlx::query_as!(
PostEntry,
r#"
SELECT p.post_id, p.author_id, u.username AS author, p.title, p.content, p.published_at
SELECT p.post_id, p.author_id, u.username AS author,
p.title, p.content, p.published_at, p.last_modified
FROM posts p
LEFT JOIN users u ON p.author_id = u.user_id
ORDER BY p.published_at DESC
@@ -151,10 +154,11 @@ pub async fn update_post(
sqlx::query!(
"
UPDATE posts
SET title = $1, content = $2 WHERE post_id = $3
SET title = $1, content = $2, last_modified = $3 WHERE post_id = $4
",
form.title,
form.content,
Utc::now(),
post_id
)
.execute(&connection_pool)
@@ -257,7 +261,8 @@ async fn get_post_data(
sqlx::query_as!(
PostEntry,
r#"
SELECT p.post_id, p.author_id, u.username AS author, p.title, p.content, p.published_at
SELECT p.post_id, p.author_id, u.username AS author,
p.title, p.content, p.published_at, last_modified
FROM posts p
LEFT JOIN users u ON p.author_id = u.user_id
WHERE p.post_id = $1

View File

@@ -91,7 +91,11 @@ pub async fn update_user(
return Ok(template.into_response());
}
let updated_full_name = form.full_name.trim();
let bio = form.bio.trim();
let bio = {
let bio = form.bio.trim();
if bio.is_empty() { None } else { Some(bio) }
};
sqlx::query!(
"
UPDATE users
@@ -255,13 +259,32 @@ pub async fn user_profile(
let posts = fetch_user_posts(&connection_pool, &user.user_id)
.await
.context("Could not fetch user posts.")?;
let session_username = session
.get_username()
let session_user_id = session
.get_user_id()
.await
.context("Could not fetch session username.")?;
let profile_user_id =
sqlx::query!("SELECT user_id FROM users WHERE username = $1", username)
.fetch_one(&connection_pool)
.await
.context("Could not fetch profile user id.")?
.user_id;
let last_seen = sqlx::query!(
"
SELECT login_time FROM user_logins
WHERE user_id = $1
ORDER BY login_time DESC
",
profile_user_id
)
.fetch_optional(&connection_pool)
.await
.context("Failed to fetch last user login")?
.map(|r| r.login_time);
let template = HtmlTemplate(UserTemplate {
user,
session_username,
session_user_id,
last_seen,
posts,
});
Ok(template.into_response())
@@ -299,7 +322,8 @@ async fn fetch_user_posts(
sqlx::query_as!(
PostEntry,
r#"
SELECT p.author_id, u.username as author, p.post_id, p.title, p.content, p.published_at
SELECT p.author_id, u.username as author,
p.post_id, p.title, p.content, p.published_at, p.last_modified
FROM posts p
INNER JOIN users u ON p.author_id = u.user_id
WHERE p.author_id = $1

View File

@@ -5,6 +5,7 @@ use crate::{
};
use askama::Template;
use axum::response::{Html, IntoResponse};
use chrono::{DateTime, Utc};
use uuid::Uuid;
pub struct HtmlTemplate<T>(pub T);
@@ -25,8 +26,9 @@ where
#[template(path = "user/profile.html")]
pub struct UserTemplate {
pub user: UserEntry,
pub session_username: Option<String>,
pub session_user_id: Option<Uuid>,
pub posts: Vec<PostEntry>,
pub last_seen: Option<DateTime<Utc>>,
}
#[derive(Template)]