✨ based_auth crate
This commit is contained in:
parent
2f2d77ec48
commit
4a477e478e
12 changed files with 83 additions and 526 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -239,6 +239,7 @@ name = "based"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
"based_auth",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
@ -264,6 +265,24 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "based_auth"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://git.hydrar.de/jmarya/based_auth#70c87bfad5ba066220d9c22a0c03668865b2e0ff"
|
||||||
|
dependencies = [
|
||||||
|
"bcrypt",
|
||||||
|
"chrono",
|
||||||
|
"data-encoding",
|
||||||
|
"env_logger 0.10.2",
|
||||||
|
"hex",
|
||||||
|
"log",
|
||||||
|
"owl",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"rocket",
|
||||||
|
"serde",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
based_auth = { git = "https://git.hydrar.de/jmarya/based_auth" }
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
rayon = "1.7.0"
|
rayon = "1.7.0"
|
||||||
|
|
17
src/asset.rs
17
src/asset.rs
|
@ -52,12 +52,15 @@ pub trait AssetRoutes {
|
||||||
|
|
||||||
impl AssetRoutes for rocket::Rocket<Build> {
|
impl AssetRoutes for rocket::Rocket<Build> {
|
||||||
fn mount_assets(self) -> Self {
|
fn mount_assets(self) -> Self {
|
||||||
self.mount("/", routes![
|
self.mount(
|
||||||
crate::asset::htmx_script_route,
|
"/",
|
||||||
crate::asset::flowbite_css,
|
routes![
|
||||||
crate::asset::flowbite_js,
|
crate::asset::htmx_script_route,
|
||||||
crate::asset::material_css,
|
crate::asset::flowbite_css,
|
||||||
crate::asset::material_font
|
crate::asset::flowbite_js,
|
||||||
])
|
crate::asset::material_css,
|
||||||
|
crate::asset::material_font
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
use maud::PreEscaped;
|
|
||||||
use owl::{query, update};
|
|
||||||
|
|
||||||
use super::User;
|
|
||||||
use crate::{auth::Session, ui::prelude::script};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
pub trait CSRF {
|
|
||||||
fn get_csrf(&self) -> impl std::future::Future<Output = uuid::Uuid>;
|
|
||||||
fn verify_csrf(&self, csrf: &str) -> impl std::future::Future<Output = bool>;
|
|
||||||
fn update_csrf(&self) -> impl std::future::Future<Output = PreEscaped<String>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CSRF for User {
|
|
||||||
/// Javascript to update the `value` of an element with id `csrf`.
|
|
||||||
///
|
|
||||||
/// This is useful for htmx requests to update the CSRF token in place.
|
|
||||||
async fn update_csrf(&self) -> PreEscaped<String> {
|
|
||||||
script(&format!(
|
|
||||||
"document.querySelectorAll('.csrf').forEach(element => {{ element.value = '{}'; }});",
|
|
||||||
self.get_csrf().await
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get CSRF Token for the current session
|
|
||||||
async fn get_csrf(&self) -> uuid::Uuid {
|
|
||||||
assert!(!self.session.is_empty());
|
|
||||||
|
|
||||||
let res = query!(|s: &Session| { s.token == self.session });
|
|
||||||
|
|
||||||
res.first().unwrap().read().csrf
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify CSRF and generate a new one
|
|
||||||
async fn verify_csrf(&self, csrf: &str) -> bool {
|
|
||||||
if self.get_csrf().await == uuid::Uuid::from_str(csrf).unwrap_or_default() {
|
|
||||||
let mut res = query!(|s: &Session| { s.token == self.session });
|
|
||||||
update!(&mut res, |s: &mut Session| {
|
|
||||||
s.csrf = uuid::Uuid::new_v4();
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
pub mod csrf;
|
|
||||||
pub mod profile_pic;
|
|
||||||
mod session;
|
|
||||||
mod user;
|
|
||||||
pub use session::Session;
|
|
||||||
pub use session::Sessions;
|
|
||||||
pub use user::APIUser;
|
|
||||||
pub use user::AdminUser;
|
|
||||||
pub use user::MaybeUser;
|
|
||||||
pub use user::User;
|
|
||||||
pub use user::UserAuth;
|
|
||||||
pub use user::UserRole;
|
|
||||||
|
|
||||||
/// A macro to check if a user has admin privileges.
|
|
||||||
///
|
|
||||||
/// This macro checks whether the provided user has admin privileges by calling the `is_admin` method on it.
|
|
||||||
/// If the user is not an admin, it returns a `Forbidden` error with a message indicating the restriction.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `$u` - The user to check.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// The macro does not return a value directly but controls the flow of execution. If the user is not an admin,
|
|
||||||
/// it returns a `Forbidden` error immediately and prevents further execution.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! check_admin {
|
|
||||||
($u:ident) => {
|
|
||||||
if !$u.is_admin() {
|
|
||||||
return Err($crate::request::api::api_error("Forbidden"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
use super::User;
|
|
||||||
use owl::{db::model::file::File, dereference, get, prelude::*, update};
|
|
||||||
pub trait ProfilePic {
|
|
||||||
fn profile_pic(&self) -> impl std::future::Future<Output = Option<Vec<u8>>>;
|
|
||||||
fn set_profile_pic(&self, image: Vec<u8>) -> impl std::future::Future<Output = ()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfilePic for User {
|
|
||||||
/// Get a user's profile picture from the database
|
|
||||||
async fn profile_pic(&self) -> Option<Vec<u8>> {
|
|
||||||
self.profile_picture
|
|
||||||
.as_ref()
|
|
||||||
.map(|x| dereference!(x).read_file(&owl::DB.get().unwrap()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a user's profile picture in the database
|
|
||||||
async fn set_profile_pic(&self, image: Vec<u8>) {
|
|
||||||
let mut target = vec![get!(self.id.clone()).unwrap()];
|
|
||||||
|
|
||||||
let file = File::new(image, None, &owl::DB.get().unwrap());
|
|
||||||
|
|
||||||
update!(&mut target, |u: &mut User| {
|
|
||||||
u.profile_picture = Some(file.reference());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
use chrono::Utc;
|
|
||||||
use owl::{dereference, prelude::*, query, save};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::gen_random;
|
|
||||||
|
|
||||||
use super::{User, UserRole};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[model]
|
|
||||||
pub struct Session {
|
|
||||||
/// The unique ID of the session token
|
|
||||||
pub id: Id,
|
|
||||||
/// The generated session token
|
|
||||||
pub token: String,
|
|
||||||
/// The username associated with the session token
|
|
||||||
pub user: IdRef<User>,
|
|
||||||
/// Session creation time
|
|
||||||
pub created: chrono::DateTime<Utc>,
|
|
||||||
/// Internal CSRF value
|
|
||||||
pub csrf: uuid::Uuid,
|
|
||||||
/// Named session value
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// Kind of session
|
|
||||||
pub kind: SessionKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
|
|
||||||
#[sqlx(type_name = "session_kind", rename_all = "lowercase")]
|
|
||||||
pub enum SessionKind {
|
|
||||||
API,
|
|
||||||
USER,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Sessions {
|
|
||||||
fn from_session(session: String) -> impl std::future::Future<Output = Option<Model<User>>>;
|
|
||||||
fn login(
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
) -> impl std::future::Future<Output = Option<(Model<Session>, UserRole)>>;
|
|
||||||
fn api_key(&self, name: &str) -> impl std::future::Future<Output = Model<Session>>;
|
|
||||||
fn session(&self) -> impl std::future::Future<Output = Model<Session>>;
|
|
||||||
fn list_sessions(&self) -> impl std::future::Future<Output = Vec<Model<Session>>>;
|
|
||||||
fn end_session(&self, id: &uuid::Uuid) -> impl std::future::Future<Output = ()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sessions for User {
|
|
||||||
/// Generate a new API Key session
|
|
||||||
async fn api_key(&self, name: &str) -> Model<Session> {
|
|
||||||
save!(Session {
|
|
||||||
id: Id::new_ulid(),
|
|
||||||
token: gen_random(64),
|
|
||||||
user: self.reference(),
|
|
||||||
created: chrono::Utc::now(),
|
|
||||||
csrf: Uuid::new_v4(),
|
|
||||||
name: Some(name.to_string()),
|
|
||||||
kind: SessionKind::API
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// End a user session
|
|
||||||
async fn end_session(&self, id: &uuid::Uuid) {
|
|
||||||
/* TODO : deletion
|
|
||||||
sqlx::query("DELETE FROM user_session WHERE id = $1 AND \"user\" = $2")
|
|
||||||
.bind(id)
|
|
||||||
.bind(&self.username)
|
|
||||||
.execute(get_pg!())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all sessions for a user
|
|
||||||
async fn list_sessions(&self) -> Vec<Model<Session>> {
|
|
||||||
query!(|ses: &Session| ses.user.to_string() == self.reference().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a user from session ID
|
|
||||||
async fn from_session(session_token: String) -> Option<Model<User>> {
|
|
||||||
let session = query!(|ses: &Session| ses.token == session_token);
|
|
||||||
let session = session.first();
|
|
||||||
if let Some(ses) = session {
|
|
||||||
let mut user = dereference!(ses.read().user);
|
|
||||||
user.write_raw_inline(|u: &mut _| {
|
|
||||||
u.session = session_token.to_string();
|
|
||||||
});
|
|
||||||
return Some(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Login a user with the given username and password
|
|
||||||
async fn login(username: &str, password: &str) -> Option<(Model<Session>, UserRole)> {
|
|
||||||
let u = Self::find(username).await?;
|
|
||||||
let u = u.read();
|
|
||||||
|
|
||||||
if !u.verify_pw(password) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((u.session().await, u.user_role.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a new session token for the user
|
|
||||||
///
|
|
||||||
/// Returns a Session instance containing the generated token and associated user
|
|
||||||
async fn session(&self) -> Model<Session> {
|
|
||||||
save!(Session {
|
|
||||||
id: Id::new_ulid(),
|
|
||||||
token: gen_random(64),
|
|
||||||
user: self.reference(),
|
|
||||||
created: chrono::Utc::now(),
|
|
||||||
csrf: Uuid::new_v4(),
|
|
||||||
name: None,
|
|
||||||
kind: SessionKind::USER
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
252
src/auth/user.rs
252
src/auth/user.rs
|
@ -1,252 +0,0 @@
|
||||||
use owl::{db::model::file::File, get, prelude::*, query, save, update};
|
|
||||||
use rocket::{Request, http::Status, outcome::Outcome, request::FromRequest};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use super::Sessions;
|
|
||||||
use crate::request::api::ToAPI;
|
|
||||||
|
|
||||||
// TODO : 2FA
|
|
||||||
|
|
||||||
/// User
|
|
||||||
///
|
|
||||||
/// # Example:
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
///
|
|
||||||
/// // Needs login
|
|
||||||
/// #[get("/myaccount")]
|
|
||||||
/// pub async fn account_page(ctx: RequestContext, user: User) -> StringResponse {
|
|
||||||
/// ...
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[model]
|
|
||||||
pub struct User {
|
|
||||||
/// The username chosen by the user
|
|
||||||
pub id: Id,
|
|
||||||
/// The hashed password for the user
|
|
||||||
pub password: String,
|
|
||||||
/// The role of the user
|
|
||||||
pub user_role: UserRole,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub session: String,
|
|
||||||
pub profile_picture: Option<IdRef<File>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
|
|
||||||
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
|
|
||||||
pub enum UserRole {
|
|
||||||
/// A regular user with limited permissions
|
|
||||||
Regular,
|
|
||||||
/// An admin user with full system privileges
|
|
||||||
Admin,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
/// Find a user by their username
|
|
||||||
pub async fn find(username: &str) -> Option<Model<Self>> {
|
|
||||||
get!(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new user with the given details
|
|
||||||
///
|
|
||||||
/// Returns an Option containing the created user, or None if a user already exists with the same username
|
|
||||||
pub async fn create(username: String, password: &str, role: UserRole) -> Option<Model<Self>> {
|
|
||||||
// Check if a user already exists with the same username
|
|
||||||
if Self::find(&username).await.is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let u = Self {
|
|
||||||
id: Id::String(username),
|
|
||||||
password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(),
|
|
||||||
user_role: role,
|
|
||||||
profile_picture: None,
|
|
||||||
session: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(save!(u))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change the password of a User
|
|
||||||
///
|
|
||||||
/// Returns a Result indicating whether the password change was successful or not
|
|
||||||
pub async fn passwd(&self, old: &str, new: &str) -> Result<(), ()> {
|
|
||||||
if self.verify_pw(old) {
|
|
||||||
let mut target = vec![get!(self.id.clone()).unwrap()];
|
|
||||||
update!(&mut target, |u: &mut User| {
|
|
||||||
u.password = bcrypt::hash(new, bcrypt::DEFAULT_COST).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find all users in the system
|
|
||||||
#[must_use]
|
|
||||||
pub async fn find_all() -> Vec<Model<Self>> {
|
|
||||||
query!(|_| true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the user is an admin
|
|
||||||
#[must_use]
|
|
||||||
pub const fn is_admin(&self) -> bool {
|
|
||||||
matches!(self.user_role, UserRole::Admin)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify that a provided password matches the hashed password for the user
|
|
||||||
///
|
|
||||||
/// Returns a boolean indicating whether the passwords match or not
|
|
||||||
#[must_use]
|
|
||||||
pub fn verify_pw(&self, password: &str) -> bool {
|
|
||||||
bcrypt::verify(password, &self.password).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToAPI for User {
|
|
||||||
async fn api(&self) -> serde_json::Value {
|
|
||||||
json!({
|
|
||||||
"username": self.id.to_string(),
|
|
||||||
"role": self.user_role
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// extracts a user from a request with `session` cookie
|
|
||||||
async fn extract_user(request: &Request<'_>) -> Option<Model<User>> {
|
|
||||||
if let Some(session_id) = request.cookies().get("session") {
|
|
||||||
if let Some(user) = User::from_session(session_id.value().to_string()).await {
|
|
||||||
return Some(user);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UserAuth(pub Model<User>);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for UserAuth {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
|
|
||||||
if let Some(user) = extract_user(request).await {
|
|
||||||
return Outcome::Success(UserAuth(user));
|
|
||||||
}
|
|
||||||
Outcome::Error((Status::Unauthorized, ()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Struct which extracts a user with session from `Token` HTTP Header.
|
|
||||||
pub struct APIUser(pub Model<User>);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for APIUser {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
|
|
||||||
match request.headers().get_one("token") {
|
|
||||||
Some(key) => {
|
|
||||||
if let Some(user) = User::from_session(key.to_string()).await {
|
|
||||||
return Outcome::Success(APIUser(user));
|
|
||||||
}
|
|
||||||
return Outcome::Error((Status::Unauthorized, ()));
|
|
||||||
}
|
|
||||||
None => Outcome::Error((Status::Unauthorized, ())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maybe User?
|
|
||||||
///
|
|
||||||
/// This struct extracts a user if possible, but also allows anybody.
|
|
||||||
///
|
|
||||||
/// # Example:
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
///
|
|
||||||
/// // Publicly accessable
|
|
||||||
/// #[get("/")]
|
|
||||||
/// pub async fn index(ctx: RequestContext, user: MaybeUser) -> StringResponse {
|
|
||||||
/// match user {
|
|
||||||
/// MaybeUser::User(user) => println!("You are {}", user.username),
|
|
||||||
/// MaybeUser::Anonymous => println!("Who are you?")
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub enum MaybeUser {
|
|
||||||
User(Model<User>),
|
|
||||||
Anonymous,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for MaybeUser {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
|
|
||||||
if let Some(user) = extract_user(request).await {
|
|
||||||
return Outcome::Success(MaybeUser::User(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
Outcome::Success(MaybeUser::Anonymous)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<MaybeUser> for Option<Model<User>> {
|
|
||||||
fn from(value: MaybeUser) -> Self {
|
|
||||||
value.take_user()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MaybeUser {
|
|
||||||
#[must_use]
|
|
||||||
pub const fn user(&self) -> Option<&Model<User>> {
|
|
||||||
match self {
|
|
||||||
MaybeUser::User(user) => Some(user),
|
|
||||||
MaybeUser::Anonymous => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn take_user(self) -> Option<Model<User>> {
|
|
||||||
match self {
|
|
||||||
MaybeUser::User(user) => Some(user),
|
|
||||||
MaybeUser::Anonymous => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Admin User
|
|
||||||
///
|
|
||||||
/// This struct expects an Admin User and returns `Forbidden` otherwise.
|
|
||||||
///
|
|
||||||
/// # Example:
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
///
|
|
||||||
/// // Only admin users can access this route
|
|
||||||
/// #[get("/admin")]
|
|
||||||
/// pub async fn admin_panel(ctx: RequestContext, user: AdminUser) -> StringResponse {
|
|
||||||
/// ...
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub struct AdminUser(pub Model<User>);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for AdminUser {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
|
|
||||||
if let Some(user) = extract_user(request).await {
|
|
||||||
if user.read().is_admin() {
|
|
||||||
return Outcome::Success(AdminUser(user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Outcome::Error((Status::Unauthorized, ()))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,9 @@ use rand::RngCore;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
pub mod asset;
|
pub mod asset;
|
||||||
pub mod auth;
|
pub mod auth {
|
||||||
|
pub use based_auth::*;
|
||||||
|
}
|
||||||
pub mod format;
|
pub mod format;
|
||||||
pub mod ogp;
|
pub mod ogp;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
|
|
|
@ -29,29 +29,32 @@ pub fn Modal<T: UIWidget + 'static, E: UIWidget + 'static, F: FnOnce(String) ->
|
||||||
) -> (String, PreEscaped<String>) {
|
) -> (String, PreEscaped<String>) {
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
(format!("modal-{id}"), html! {
|
(
|
||||||
div id=(format!("modal-{id}")) tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full" {
|
format!("modal-{id}"),
|
||||||
div class="relative p-4 w-full max-w-2xl max-h-full" {
|
html! {
|
||||||
|
div id=(format!("modal-{id}")) tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full" {
|
||||||
|
div class="relative p-4 w-full max-w-2xl max-h-full" {
|
||||||
|
|
||||||
div class="relative bg-white rounded-lg shadow dark:bg-gray-700" {
|
div class="relative bg-white rounded-lg shadow dark:bg-gray-700" {
|
||||||
div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600" {
|
div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600" {
|
||||||
h3 class="text-xl font-semibold text-gray-900 dark:text-white" { (title) }
|
h3 class="text-xl font-semibold text-gray-900 dark:text-white" { (title) }
|
||||||
button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide=(format!("modal-{id}")) {
|
button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide=(format!("modal-{id}")) {
|
||||||
svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" {
|
svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" {
|
||||||
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" {};
|
path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" {};
|
||||||
};
|
};
|
||||||
span class="sr-only" { "Close modal" };
|
span class="sr-only" { "Close modal" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
div class="p-4 md:p-5 space-y-4" {
|
div class="p-4 md:p-5 space-y-4" {
|
||||||
(body)
|
(body)
|
||||||
};
|
};
|
||||||
|
|
||||||
div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600" {
|
div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600" {
|
||||||
(footer(format!("modal-{id}")))
|
(footer(format!("modal-{id}")))
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}};
|
||||||
}};
|
},
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,10 +287,13 @@ pub fn BottomNavigationTile<T: UIWidget + 'static>(
|
||||||
) -> ClassicWidget<LinkWidget> {
|
) -> ClassicWidget<LinkWidget> {
|
||||||
Classic(
|
Classic(
|
||||||
"inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group",
|
"inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group",
|
||||||
Link(reference, html! {
|
Link(
|
||||||
(icon.map(|x| x.render()).unwrap_or_default());
|
reference,
|
||||||
span class="text-sm text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500" { (text) };
|
html! {
|
||||||
}),
|
(icon.map(|x| x.render()).unwrap_or_default());
|
||||||
|
span class="text-sm text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500" { (text) };
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -274,21 +274,25 @@ impl GridElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn span(mut self, value: GridElementValue) -> Self {
|
pub fn span(mut self, value: GridElementValue) -> Self {
|
||||||
self.1.push(format!("{}-span-{}", self.2, match value {
|
self.1.push(format!(
|
||||||
GridElementValue::_1 => "1",
|
"{}-span-{}",
|
||||||
GridElementValue::_2 => "2",
|
self.2,
|
||||||
GridElementValue::_3 => "3",
|
match value {
|
||||||
GridElementValue::_4 => "4",
|
GridElementValue::_1 => "1",
|
||||||
GridElementValue::_5 => "5",
|
GridElementValue::_2 => "2",
|
||||||
GridElementValue::_6 => "6",
|
GridElementValue::_3 => "3",
|
||||||
GridElementValue::_7 => "7",
|
GridElementValue::_4 => "4",
|
||||||
GridElementValue::_8 => "8",
|
GridElementValue::_5 => "5",
|
||||||
GridElementValue::_9 => "9",
|
GridElementValue::_6 => "6",
|
||||||
GridElementValue::_10 => "10",
|
GridElementValue::_7 => "7",
|
||||||
GridElementValue::_11 => "11",
|
GridElementValue::_8 => "8",
|
||||||
GridElementValue::_12 => "12",
|
GridElementValue::_9 => "9",
|
||||||
GridElementValue::Auto => "full",
|
GridElementValue::_10 => "10",
|
||||||
}));
|
GridElementValue::_11 => "11",
|
||||||
|
GridElementValue::_12 => "12",
|
||||||
|
GridElementValue::Auto => "full",
|
||||||
|
}
|
||||||
|
));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue