User profile and admin privileges
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::telemetry::spawn_blocking_with_tracing;
|
||||
use anyhow::Context;
|
||||
use argon2::{
|
||||
@@ -138,6 +140,15 @@ pub enum Role {
|
||||
Writer,
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Role::Admin => write!(f, "admin"),
|
||||
Role::Writer => write!(f, "writer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthenticatedUser {
|
||||
pub user_id: Uuid,
|
||||
|
||||
@@ -2,8 +2,10 @@ mod new_subscriber;
|
||||
mod post;
|
||||
mod subscriber_email;
|
||||
mod subscribers;
|
||||
mod user;
|
||||
|
||||
pub use new_subscriber::NewSubscriber;
|
||||
pub use post::PostEntry;
|
||||
pub use subscriber_email::SubscriberEmail;
|
||||
pub use subscribers::SubscriberEntry;
|
||||
pub use user::UserEntry;
|
||||
|
||||
@@ -3,14 +3,13 @@ use uuid::Uuid;
|
||||
|
||||
pub struct PostEntry {
|
||||
pub post_id: Uuid,
|
||||
pub author: Option<String>,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub published_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl PostEntry {
|
||||
#[allow(dead_code)]
|
||||
pub fn formatted_date(&self) -> String {
|
||||
self.published_at.format("%B %d, %Y").to_string()
|
||||
}
|
||||
|
||||
23
src/domain/user.rs
Normal file
23
src/domain/user.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::authentication::Role;
|
||||
|
||||
pub struct UserEntry {
|
||||
pub user_id: Uuid,
|
||||
pub username: String,
|
||||
pub role: Role,
|
||||
pub full_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub member_since: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl UserEntry {
|
||||
pub fn formatted_date(&self) -> String {
|
||||
self.member_since.format("%B %d, %Y").to_string()
|
||||
}
|
||||
|
||||
pub fn is_admin(&self) -> bool {
|
||||
matches!(self.role, Role::Admin)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod posts;
|
||||
mod subscriptions;
|
||||
mod subscriptions_confirm;
|
||||
mod unsubscribe;
|
||||
mod users;
|
||||
|
||||
pub use admin::*;
|
||||
use askama::Template;
|
||||
@@ -24,6 +25,7 @@ use serde::de::DeserializeOwned;
|
||||
pub use subscriptions::*;
|
||||
pub use subscriptions_confirm::*;
|
||||
pub use unsubscribe::*;
|
||||
pub use users::*;
|
||||
|
||||
use crate::{
|
||||
authentication::AuthError,
|
||||
|
||||
82
src/routes/users.rs
Normal file
82
src/routes/users.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use crate::{
|
||||
authentication::Role,
|
||||
domain::{PostEntry, UserEntry},
|
||||
routes::{AppError, not_found_html},
|
||||
startup::AppState,
|
||||
templates::{HtmlTemplate, UserTemplate},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ProfilePath {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Fetching user data", skip(connection_pool))]
|
||||
pub async fn user_profile(
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
Path(ProfilePath { username }): Path<ProfilePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
match fetch_user_data(&connection_pool, &username)
|
||||
.await
|
||||
.context("Failed to fetch user data.")?
|
||||
{
|
||||
Some(user) => {
|
||||
let posts = fetch_user_posts(&connection_pool, &user.user_id)
|
||||
.await
|
||||
.context("Could not fetch user posts.")?;
|
||||
let template = HtmlTemplate(UserTemplate { user, posts });
|
||||
Ok(template.into_response())
|
||||
}
|
||||
None => {
|
||||
tracing::error!(username = %username, "user not found");
|
||||
Ok(not_found_html().into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Fetching user profile", skip_all)]
|
||||
async fn fetch_user_data(
|
||||
connection_pool: &PgPool,
|
||||
username: &str,
|
||||
) -> Result<Option<UserEntry>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
UserEntry,
|
||||
r#"
|
||||
SELECT user_id, username, full_name, role as "role: Role", member_since, bio
|
||||
FROM users
|
||||
WHERE username = $1
|
||||
"#,
|
||||
username
|
||||
)
|
||||
.fetch_optional(connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Fetching user posts", skip_all)]
|
||||
async fn fetch_user_posts(
|
||||
connection_pool: &PgPool,
|
||||
user_id: &Uuid,
|
||||
) -> Result<Vec<PostEntry>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
PostEntry,
|
||||
r#"
|
||||
SELECT u.username as author, p.post_id, p.title, p.content, p.published_at
|
||||
FROM posts p
|
||||
INNER JOIN users u ON p.author_id = u.user_id
|
||||
WHERE p.author_id = $1
|
||||
ORDER BY p.published_at DESC
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_all(connection_pool)
|
||||
.await
|
||||
}
|
||||
@@ -108,6 +108,7 @@ pub fn app(
|
||||
.route("/posts", get(list_posts))
|
||||
.route("/posts/load_more", get(load_more))
|
||||
.route("/posts/{post_id}", get(see_post))
|
||||
.route("/users/{username}", get(user_profile))
|
||||
.route("/favicon.ico", get(favicon))
|
||||
.nest("/admin", auth_routes)
|
||||
.nest_service("/assets", ServeDir::new("assets"))
|
||||
|
||||
@@ -21,7 +21,7 @@ where
|
||||
)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.pretty()
|
||||
.compact()
|
||||
.with_writer(sink)
|
||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
authentication::AuthenticatedUser,
|
||||
domain::{PostEntry, SubscriberEntry},
|
||||
domain::{PostEntry, SubscriberEntry, UserEntry},
|
||||
routes::{AppError, DashboardStats},
|
||||
};
|
||||
use askama::Template;
|
||||
@@ -21,6 +21,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user/profile.html")]
|
||||
pub struct UserTemplate {
|
||||
pub user: UserEntry,
|
||||
pub posts: Vec<PostEntry>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "message.html")]
|
||||
pub struct MessageTemplate {
|
||||
|
||||
Reference in New Issue
Block a user