init
This commit is contained in:
commit
70c87bfad5
10 changed files with 4165 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
3636
Cargo.lock
generated
Normal file
3636
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "based_auth"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
hex = "0.4.3"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
log = "0.4.20"
|
||||||
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
data-encoding = "2.6.0"
|
||||||
|
bcrypt = "0.16.0"
|
||||||
|
owl = { git = "https://git.hydrar.de/red/owl" }
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# based_auth
|
||||||
|
based_auth provides basic auth primitives.
|
47
src/csrf.rs
Normal file
47
src/csrf.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use owl::{query, update};
|
||||||
|
|
||||||
|
use crate::Session;
|
||||||
|
|
||||||
|
use super::User;
|
||||||
|
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 = 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) -> String {
|
||||||
|
format!(
|
||||||
|
"<script>document.querySelectorAll('.csrf').forEach(element => {{ element.value = '{}'; }});</script>",
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
44
src/lib.rs
Normal file
44
src/lib.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#![feature(const_vec_string_slice)]
|
||||||
|
use data_encoding::HEXUPPER;
|
||||||
|
use rand::RngCore;
|
||||||
|
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_random(token_length: usize) -> String {
|
||||||
|
let mut token_bytes = vec![0u8; token_length];
|
||||||
|
|
||||||
|
rand::thread_rng().fill_bytes(&mut token_bytes);
|
||||||
|
|
||||||
|
HEXUPPER.encode(&token_bytes)
|
||||||
|
}
|
32
src/mod.rs
Normal file
32
src/mod.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
26
src/profile_pic.rs
Normal file
26
src/profile_pic.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
120
src/session.rs
Normal file
120
src/session.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
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)]
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
240
src/user.rs
Normal file
240
src/user.rs
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
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 super::Sessions;
|
||||||
|
|
||||||
|
// 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)]
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, ()))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue