admin accounts concept and a barebones admin page as starting point

merging branch feat/admin-accounts
This commit is contained in:
Artemis 2025-01-27 09:58:37 +01:00
parent 8ea8afd418
commit f7da45312a
17 changed files with 197 additions and 6 deletions

View file

@ -37,6 +37,11 @@
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_admin",
"type_info": "Bool"
}
],
"parameters": {
@ -51,7 +56,8 @@
false,
false,
false,
true
true,
false
]
},
"hash": "7609165d94c8f1bea9d535b9b7ad727fd06592973d7f83017292d41acb203be6"

View file

@ -37,6 +37,11 @@
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_admin",
"type_info": "Bool"
}
],
"parameters": {
@ -51,7 +56,8 @@
false,
false,
false,
true
true,
false
]
},
"hash": "9d00617966f8aeebb08de6ad981dc3b8697c65f0b23cea4684f525732d8f6706"

View file

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "select count(id) from users where id != '00000000-0000-0000-0000-000000000000' and enabled is true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "b94df9ffa981c87c32f36a33f91db1d9feda108f88cfae3dee48e4a0a2464223"
}

View file

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "select count(id) from doll_profiles where archived_at is null",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "d7e0e15fc33fcbc2576807014123229bc099c1fa92fef2617fb58ed65a54e480"
}

View file

@ -1,2 +1,4 @@
[default]
secret_key = "8STDFCStGMYGoOq8RJf3JJXsg4p6wZVAph50R3Fbq6U="
[default.databases.dolltags]
url = "postgres://postgres:woofwoof@localhost/dolltags"

View file

@ -300,7 +300,7 @@ input#ident {
text-align: center;
}
.profile .ident,
.ident,
.handler {
text-transform: uppercase;
font-size: .8em;

View file

@ -0,0 +1,2 @@
alter table users
add column is_admin boolean not null default false;

View file

@ -2,10 +2,12 @@ use std::str::FromStr;
use rocket::{
http::{CookieJar, Status},
outcome::try_outcome,
request::{FromRequest, Outcome},
response::Redirect,
Request,
};
use rocket_dyn_templates::{context, Template};
use uuid::Uuid;
use crate::db::{
@ -88,8 +90,33 @@ impl<'a> FromRequest<'a> for User {
}
}
/// A specialization of User as a [`FromRequest`] guard to only trigger when the user is logged in,
/// checked as okay in DB, and is marked as an admin user.
#[derive(Debug)]
pub struct Admin(pub User);
#[rocket::async_trait]
impl<'a> FromRequest<'a> for Admin {
type Error = SessionInternalFailure;
async fn from_request(req: &'a Request<'_>) -> Outcome<Self, Self::Error> {
let user = try_outcome!(req.guard::<User>().await);
if user.is_admin {
Outcome::Success(Admin(user))
} else {
Outcome::Forward(Status::Forbidden)
}
}
}
#[catch(401)]
pub fn unauthorized(req: &Request) -> Redirect {
let next = req.uri().to_string();
Redirect::to(uri!(crate::routes::form::accounts::show_login(Some(&next))))
}
#[catch(403)]
pub fn forbidden(_: &Request) -> Template {
Template::render("error/forbidden", context! {})
}

21
src/db/admin.rs Normal file
View file

@ -0,0 +1,21 @@
use super::schema::{ServiceStatus, TrxHook};
/// Aggregates info on the current service status, incl. accounts and tags
pub async fn get_service_status(trx: &mut TrxHook<'_>) -> sqlx::Result<ServiceStatus> {
let active_accounts_count =
sqlx::query_scalar!("select count(id) from users where id != '00000000-0000-0000-0000-000000000000' and enabled is true")
.fetch_one(&mut **trx)
.await?
.unwrap_or(0);
let active_tags_count =
sqlx::query_scalar!("select count(id) from doll_profiles where archived_at is null")
.fetch_one(&mut **trx)
.await?
.unwrap_or(0);
Ok(ServiceStatus {
active_accounts_count,
active_tags_count,
})
}

View file

@ -1,3 +1,4 @@
pub mod admin;
pub mod doll;
pub mod migrate;
pub mod schema;

View file

@ -95,4 +95,13 @@ pub struct User {
pub email: Option<String>,
pub enabled: bool,
pub is_admin: bool,
}
/// The service status aggregate
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct ServiceStatus {
pub active_accounts_count: i64,
pub active_tags_count: i64,
}

View file

@ -11,7 +11,7 @@ use rocket::fairing::AdHoc;
use rocket::fs::{relative, FileServer};
use rocket_db_pools::Database;
use routes::form::accounts;
use routes::{account, error_handlers, form, public};
use routes::{account, admin, error_handlers, form, public};
pub mod auth;
pub mod db;
@ -50,7 +50,11 @@ fn rocket() -> _ {
.attach(AdHoc::try_on_ignite("SQLx migrations", run_migrations))
.register(
"/",
catchers![error_handlers::not_found, session::unauthorized],
catchers![
error_handlers::not_found,
session::unauthorized,
session::forbidden
],
)
.mount("/assets", FileServer::from(assets_path))
.mount(
@ -71,6 +75,7 @@ fn rocket() -> _ {
account::export_data,
],
)
.mount("/admin", routes![admin::index,])
.mount(
"/",
routes![

View file

@ -10,7 +10,7 @@ use rocket::{
use rocket_dyn_templates::{tera::try_get_value, Template};
use serde_json::{to_value, Value};
use crate::{auth::session::Session, ids};
use crate::{auth::session::Session, db::schema::User, ids};
pub fn init_templates() -> impl Fairing {
Template::custom(|engines| {
@ -32,10 +32,23 @@ pub fn init_templates() -> impl Fairing {
pub struct CommonTemplateState {
/// true if the user is logged in (doesn't check the DB, instead uses [`Session`])
pub logged_in: bool,
/// true if the user is an admin (defaults to false, call [`CommonTemplateState::for_user`] to add the additional data)
pub is_admin: bool,
/// feature flag - disables the UI for it since it's not implemeted yet.
pub forgot_password_implemented: bool,
}
impl CommonTemplateState {
/// Populates the state struct with the additional knowledge provided by the [`User`] passed in parameter,
/// such as if the current context is an admin-privileges-enabled one (which need a DB lookup).
pub fn for_user(self, user: &User) -> Self {
CommonTemplateState {
is_admin: user.is_admin,
..self
}
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for CommonTemplateState {
type Error = ();
@ -45,6 +58,7 @@ impl<'r> FromRequest<'r> for CommonTemplateState {
Outcome::Success(CommonTemplateState {
logged_in: session_state.0.is_some(),
is_admin: false,
forgot_password_implemented: false,
})
}

26
src/routes/admin.rs Normal file
View file

@ -0,0 +1,26 @@
use rocket_dyn_templates::{context, Template};
use sqlx::Acquire;
use crate::{
auth::session::Admin,
db::{admin, schema::DollTagsDb},
pages::CommonTemplateState,
};
use super::error_handlers::PageResult;
#[get("/")]
pub async fn index(meta: CommonTemplateState, mut db: DollTagsDb, user: Admin) -> PageResult {
let mut trx = db.begin().await?;
let service_status = admin::get_service_status(&mut trx).await?;
trx.commit().await?;
Ok(Template::render(
"admin/index",
context! {
meta: meta.for_user(&user.0),
service_status,
},
)
.into())
}

View file

@ -1,4 +1,5 @@
pub mod account;
pub mod admin;
pub mod error_handlers;
pub mod form;
pub mod public;

View file

@ -0,0 +1,25 @@
{% extends "base" %}
{% block title %}Admin panel - {% endblock title %}
{% block main %}
<aside>
<p class="subnav">
<a href="/account">Back to account</a>
<a href="/logout">Log out</a>
</p>
</aside>
<section>
<h2>Service status</h2>
<div class="dual-fields raised center">
<div>
<p class="ident">Active accounts</p>
<p class="b">{{service_status.active_accounts_count}}</p>
</div>
<div>
<p class="ident">Active tag profiles</p>
<p class="b">{{service_status.active_tags_count}}</p>
</div>
</div>
</section>
{% endblock main %}

View file

@ -0,0 +1,6 @@
{% extends "base" %}
{% block title %}Admin page access forbidden - {% endblock title %}
{% block main %}
<h1>This page can only be accessed by an admin, which you don't seem to be, sorry.</h1>
<p>You can go <a href="/account">back to your account</a> or <a href="/logout">log out</a> if you wish.</p>
{% endblock main %}