rss-reader/src/user.rs

348 lines
9.1 KiB
Rust
Raw Normal View History

2025-02-01 23:01:33 -08:00
use rocket::http::{Cookie, CookieJar, Status};
2025-02-01 20:11:02 -08:00
use rocket::serde::{json::Json, Deserialize, Serialize};
use rocket_db_pools::Connection;
2025-02-02 20:44:00 -08:00
use rocket_dyn_templates::{context, Template};
2025-02-01 20:07:32 -08:00
use uuid::Uuid;
2025-02-01 20:11:02 -08:00
use crate::Db;
2025-02-01 20:07:32 -08:00
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct User {
pub id: Uuid,
pub username: String,
pub password_hash: String,
pub email: Option<String>,
pub display_name: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
2025-02-02 20:44:00 -08:00
pub admin: bool,
2025-02-01 20:07:32 -08:00
}
impl User {
2025-02-01 23:41:23 -08:00
pub fn new(
username: String,
password_hash: String,
email: Option<String>,
display_name: Option<String>,
) -> Self {
2025-02-01 20:07:32 -08:00
User {
id: Uuid::new_v4(),
username,
password_hash,
email,
display_name,
created_at: chrono::Utc::now(),
2025-02-02 20:44:00 -08:00
admin: false,
2025-02-01 20:07:32 -08:00
}
}
}
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct NewUser {
2025-02-02 00:23:02 -08:00
username: String,
password: String,
email: Option<String>,
display_name: Option<String>,
2025-02-01 20:11:02 -08:00
}
2025-02-01 23:01:33 -08:00
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct LoginCredentials {
2025-02-02 00:23:02 -08:00
username: String,
password: String,
2025-02-01 23:01:33 -08:00
}
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct LoginResponse {
2025-02-02 00:23:02 -08:00
user_id: Uuid,
username: String,
2025-02-01 23:01:33 -08:00
}
2025-02-02 20:44:00 -08:00
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct SetupError {
error: String,
}
2025-02-01 20:11:02 -08:00
#[post("/users", data = "<new_user>")]
pub async fn create_user(
mut db: Connection<Db>,
new_user: Json<NewUser>,
) -> Result<Json<User>, Status> {
let new_user = new_user.into_inner();
2025-02-01 23:41:23 -08:00
2025-02-01 20:11:02 -08:00
// Hash the password - we'll use bcrypt
let password_hash = bcrypt::hash(new_user.password.as_bytes(), bcrypt::DEFAULT_COST)
.map_err(|_| Status::InternalServerError)?;
2025-02-01 23:41:23 -08:00
2025-02-01 20:11:02 -08:00
let user = User::new(
new_user.username,
password_hash,
new_user.email,
2025-02-01 23:41:23 -08:00
new_user.display_name,
2025-02-01 20:11:02 -08:00
);
let query = sqlx::query(
2025-02-02 20:44:00 -08:00
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
2025-02-01 20:11:02 -08:00
)
.bind(user.id.to_string())
.bind(user.username.as_str())
.bind(user.password_hash.as_str())
.bind(user.email.as_ref())
.bind(user.display_name.as_ref())
.bind(user.created_at.to_rfc3339())
2025-02-02 20:44:00 -08:00
.bind(false)
2025-02-01 20:11:02 -08:00
.execute(&mut **db).await;
match query {
Ok(_) => Ok(Json(user)),
Err(e) => {
eprintln!("Database error: {}", e);
match e {
sqlx::Error::Database(db_err) if db_err.is_unique_violation() => {
Err(Status::Conflict)
}
_ => Err(Status::InternalServerError),
}
}
}
}
#[get("/users")]
pub async fn get_users(mut db: Connection<Db>) -> Result<Json<Vec<User>>, Status> {
let query = sqlx::query!(
r#"
SELECT
id as "id: String",
username,
password_hash,
email,
display_name,
2025-02-02 20:44:00 -08:00
created_at as "created_at: chrono::DateTime<chrono::Utc>",
admin as "admin: bool"
2025-02-01 20:11:02 -08:00
FROM users
"#
)
.fetch_all(&mut **db)
.await
.map_err(|e| {
eprintln!("Database error: {}", e);
Status::InternalServerError
})?;
// Convert the strings to UUIDs
let users = query
.into_iter()
.map(|row| User {
id: Uuid::parse_str(&row.id).unwrap(),
username: row.username,
password_hash: row.password_hash,
email: row.email,
display_name: row.display_name,
created_at: row.created_at,
2025-02-02 20:44:00 -08:00
admin: row.admin,
2025-02-01 20:11:02 -08:00
})
.collect::<Vec<_>>();
Ok(Json(users))
}
#[delete("/users/<user_id>")]
pub async fn delete_user(mut db: Connection<Db>, user_id: &str) -> Status {
// Validate UUID format
let uuid = match Uuid::parse_str(user_id) {
Ok(uuid) => uuid,
Err(_) => return Status::BadRequest,
};
let query = sqlx::query("DELETE FROM users WHERE id = ?")
.bind(uuid.to_string())
.execute(&mut **db)
.await;
match query {
Ok(result) => {
if result.rows_affected() > 0 {
Status::NoContent
} else {
Status::NotFound
}
}
Err(e) => {
eprintln!("Database error: {}", e);
Status::InternalServerError
}
}
2025-02-01 23:01:33 -08:00
}
#[post("/login", data = "<credentials>")]
pub async fn login(
mut db: Connection<Db>,
credentials: Json<LoginCredentials>,
cookies: &CookieJar<'_>,
) -> Result<Json<LoginResponse>, Status> {
let creds = credentials.into_inner();
2025-02-01 23:41:23 -08:00
2025-02-01 23:01:33 -08:00
// Find user by username
let user = sqlx::query!(
r#"
SELECT
id as "id: String",
username,
password_hash
FROM users
WHERE username = ?
"#,
creds.username
)
.fetch_optional(&mut **db)
.await
.map_err(|e| {
eprintln!("Database error: {}", e);
Status::InternalServerError
})?;
let user = match user {
Some(user) => user,
None => return Err(Status::Unauthorized),
};
// Verify password
let valid = bcrypt::verify(creds.password.as_bytes(), &user.password_hash)
.map_err(|_| Status::InternalServerError)?;
if !valid {
return Err(Status::Unauthorized);
}
// Set session cookie
let user_id = Uuid::parse_str(&user.id).map_err(|_| Status::InternalServerError)?;
cookies.add_private(Cookie::new("user_id", user_id.to_string()));
Ok(Json(LoginResponse {
user_id,
username: user.username,
}))
}
#[post("/logout")]
pub fn logout(cookies: &CookieJar<'_>) -> Status {
2025-02-01 23:41:23 -08:00
cookies.remove_private(Cookie::build("user_id"));
2025-02-01 23:01:33 -08:00
Status::NoContent
}
// Add auth guard
pub struct AuthenticatedUser {
pub user_id: Uuid,
}
#[rocket::async_trait]
impl<'r> rocket::request::FromRequest<'r> for AuthenticatedUser {
type Error = ();
2025-02-01 23:41:23 -08:00
async fn from_request(
request: &'r rocket::Request<'_>,
) -> rocket::request::Outcome<Self, Self::Error> {
2025-02-02 00:36:19 -08:00
use rocket::request::Outcome;
2025-02-01 23:39:32 -08:00
match request.cookies().get_private("user_id") {
Some(cookie) => {
if let Ok(user_id) = Uuid::parse_str(cookie.value()) {
2025-02-02 00:36:19 -08:00
Outcome::Success(AuthenticatedUser { user_id })
2025-02-01 23:39:32 -08:00
} else {
2025-02-02 00:36:19 -08:00
Outcome::Forward(Status::Unauthorized)
2025-02-01 23:39:32 -08:00
}
2025-02-01 23:41:23 -08:00
}
2025-02-02 00:36:19 -08:00
None => Outcome::Forward(Status::Unauthorized),
2025-02-01 23:01:33 -08:00
}
}
2025-02-01 23:41:23 -08:00
}
2025-02-02 20:44:00 -08:00
#[get("/setup")]
pub async fn setup_page(mut db: Connection<Db>) -> Result<Template, Status> {
// Check if any users exist
let count = sqlx::query!("SELECT COUNT(*) as count FROM users")
.fetch_one(&mut **db)
.await
.map_err(|_| Status::InternalServerError)?
.count;
if count > 0 {
// If users exist, redirect to login
Err(Status::SeeOther)
} else {
// Show setup page
Ok(Template::render("setup", context! {}))
}
}
#[post("/setup", data = "<new_user>")]
2025-02-02 21:01:54 -08:00
pub async fn setup(
mut db: Connection<Db>,
new_user: Json<NewUser>,
) -> Result<Status, Json<SetupError>> {
2025-02-02 20:44:00 -08:00
// Check if any users exist
let count = sqlx::query!("SELECT COUNT(*) as count FROM users")
.fetch_one(&mut **db)
.await
.map_err(|e| {
eprintln!("Database error: {}", e);
Json(SetupError {
error: "Internal server error".to_string(),
})
})?
.count;
if count > 0 {
return Err(Json(SetupError {
error: "Setup has already been completed".to_string(),
}));
}
// Hash the password
2025-02-02 21:01:54 -08:00
let password_hash =
bcrypt::hash(new_user.password.as_bytes(), bcrypt::DEFAULT_COST).map_err(|e| {
eprintln!("Password hashing error: {}", e);
Json(SetupError {
error: "Failed to process password".to_string(),
})
})?;
2025-02-02 20:44:00 -08:00
// Create admin user
let user_id = Uuid::new_v4().to_string();
let now = chrono::Utc::now().to_rfc3339();
let result = sqlx::query(
"INSERT INTO users (id, username, password_hash, email, display_name, created_at, admin)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
)
.bind(&user_id)
.bind(&new_user.username)
.bind(&password_hash)
.bind(&new_user.email)
.bind(&new_user.display_name)
.bind(&now)
.bind(true) // This is an admin user
.execute(&mut **db)
.await;
match result {
Ok(_) => Ok(Status::Created),
Err(e) => {
eprintln!("Database error: {}", e);
match e {
2025-02-02 21:01:54 -08:00
sqlx::Error::Database(db_err) if db_err.is_unique_violation() => {
Err(Json(SetupError {
error: "Username already exists".to_string(),
}))
}
2025-02-02 20:44:00 -08:00
_ => Err(Json(SetupError {
error: "Failed to create user".to_string(),
})),
}
}
}
}