Files
gpodder-rs/src/admin.rs
Matthew Pomes 59058207f4
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 22s
Switch to git dep so build system will work
2026-07-02 21:59:01 -05:00

175 lines
4.5 KiB
Rust

use std::convert::Infallible;
use argon2::Argon2;
use either::Either;
use password_hash::{PasswordHasher, SaltString, rand_core::OsRng};
use rocket::{
FromForm, Request, Route, TypedError, async_trait,
form::Form,
get,
outcome::{Outcome, try_outcome},
post,
request::FromRequest,
response::Redirect,
routes,
serde::{Deserialize, Serialize},
trace::info,
uri,
};
use rocket_db_pools::Connection;
use rocket_dyn_templates::{Template, context};
use crate::{
Db, SqlError,
auth::{self, AdminUser, BasicAuth},
};
#[get("/", rank = 1)]
fn dash(auth: Option<BasicAuth>) -> Template {
info!("user: {:?}", auth.as_ref().map(|a| a.username()));
Template::render(
"dash",
context! {
username: auth.as_ref().map(|a| a.username()),
admin: auth.as_ref().map(|a| a.is_admin()).unwrap_or(false),
mode: "dark",
},
)
}
#[get("/login")]
fn login(auth: Option<BasicAuth>) -> Either<Template, Redirect> {
if auth.is_some() {
Either::Right(Redirect::temporary(uri!(dash)))
} else {
Either::Left(Template::render(
"login",
context! {
mode: "dark",
},
))
}
}
#[get("/account")]
fn account(auth: BasicAuth) -> Template {
Template::render(
"account",
context! {
username: auth.username(),
admin: auth.is_admin(),
mode: "dark",
},
)
}
#[get("/admin")]
async fn admin(auth: AdminUser, mut db: Connection<Db>) -> Result<Template, SqlError> {
let users = sqlx::query!("SELECT * from users")
.fetch_all(&mut **db)
.await?;
Ok(Template::render(
"admin",
context! {
username: auth.username(),
admin: auth.is_admin(),
users: users.into_iter().filter(|r| r.name != auth.username()).map(|r| r.name).collect::<Vec<_>>(),
mode: "dark",
},
))
}
struct InitialSetup;
#[derive(Debug, TypedError)]
pub enum InitialSetupError {
#[error(status = 500)]
InternalError,
#[error(status = 500)]
DbError(#[error(source)] SqlError),
}
impl From<SqlError> for InitialSetupError {
fn from(value: SqlError) -> Self {
Self::DbError(value)
}
}
impl From<sqlx::Error> for InitialSetupError {
fn from(value: sqlx::Error) -> Self {
Self::DbError(value.into())
}
}
#[derive(Debug, TypedError)]
struct Empty;
impl From<Infallible> for Empty {
fn from(value: Infallible) -> Self {
match value {}
}
}
#[async_trait]
impl<'r> FromRequest<'r> for InitialSetup {
type Forward = Empty;
type Error = InitialSetupError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error, Self::Forward> {
// TODO: cache result in managed state?
let mut db = try_outcome!(
req.guard::<Connection<Db>>()
.await
.map_error(|_| InitialSetupError::InternalError)
);
let user_count = match sqlx::query!("SELECT count(name) as count from users")
.fetch_one(&mut **db)
.await
{
Ok(v) => v,
Err(e) => return Outcome::Error(e.into()),
};
if user_count.count == 0 {
Outcome::Success(Self)
} else {
Outcome::Forward(Empty)
}
}
}
#[get("/")]
fn initial_setup(_initial: InitialSetup) -> Template {
Template::render("initial_setup", context! {})
}
#[derive(Debug, Serialize, Deserialize, FromForm)]
#[serde(crate = "rocket::serde")]
pub struct InitialSetupOptions<'a> {
admin_username: &'a str,
admin_password: &'a str,
}
#[post("/initial_setup", data = "<opts>")]
async fn initial_complete(
_initial: InitialSetup,
opts: Form<InitialSetupOptions<'_>>,
mut db: Connection<Db>,
) -> Result<Redirect, SqlError> {
let salt = SaltString::generate(&mut OsRng);
let pw_hash = Argon2::default()
.hash_password(opts.admin_password.as_bytes(), salt.as_salt())
.expect("Failed to hash password");
sqlx::query("INSERT INTO users (name, role, password) VALUES (?1, \"admin\", ?2)")
.bind(opts.admin_username)
.bind(pw_hash.to_string())
.execute(&mut **db)
.await?;
Ok(Redirect::to("/"))
}
pub fn routes() -> Vec<Route> {
routes![
dash,
login,
auth::login_form,
auth::update_password,
account,
admin,
initial_setup,
initial_complete,
]
}