Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
8d3e6508a4 |
14 changed files with 781 additions and 817 deletions
956
Cargo.lock
generated
956
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -4,14 +4,13 @@ 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"
|
||||||
regex = "1.9.5"
|
regex = "1.9.5"
|
||||||
ring = "0.16.20"
|
ring = "0.16.20"
|
||||||
walkdir = "2.4.0"
|
walkdir = "2.4.0"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
|
@ -21,13 +20,12 @@ serde_json = "1.0.111"
|
||||||
tokio = { version = "1.35.1", features = ["full"] }
|
tokio = { version = "1.35.1", features = ["full"] }
|
||||||
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
||||||
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "derive", "uuid", "chrono", "json"] }
|
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "derive", "uuid", "chrono", "json"] }
|
||||||
maud = "0.26.0"
|
maud = "0.27.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
data-encoding = "2.6.0"
|
data-encoding = "2.6.0"
|
||||||
bcrypt = "0.16.0"
|
bcrypt = "0.16.0"
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
async-stream = "0.3.6"
|
async-stream = "0.3.6"
|
||||||
owl = { git = "https://git.hydrar.de/red/owl" }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
reqwest = { version = "0.11", features = ["blocking"] }
|
reqwest = { version = "0.11", features = ["blocking"] }
|
||||||
|
|
17
src/asset.rs
17
src/asset.rs
|
@ -52,15 +52,12 @@ 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(
|
self.mount("/", routes![
|
||||||
"/",
|
crate::asset::htmx_script_route,
|
||||||
routes![
|
crate::asset::flowbite_css,
|
||||||
crate::asset::htmx_script_route,
|
crate::asset::flowbite_js,
|
||||||
crate::asset::flowbite_css,
|
crate::asset::material_css,
|
||||||
crate::asset::flowbite_js,
|
crate::asset::material_font
|
||||||
crate::asset::material_css,
|
])
|
||||||
crate::asset::material_font
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
src/auth/auth.sql
Normal file
24
src/auth/auth.sql
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
CREATE TYPE user_role AS ENUM ('regular', 'admin');
|
||||||
|
CREATE TYPE session_kind AS ENUM ('api', 'user');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
username VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||||
|
"password" text NOT NULL,
|
||||||
|
user_role user_role NOT NULL DEFAULT 'regular'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_session (
|
||||||
|
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
token text NOT NULL,
|
||||||
|
"user" varchar(255) NOT NULL,
|
||||||
|
"created" timestamptz NOT NULL DEFAULT NOW(),
|
||||||
|
"csrf" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"name" varchar(255),
|
||||||
|
kind session_kind NOT NULL DEFAULT 'user',
|
||||||
|
FOREIGN KEY("user") REFERENCES users(username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_profile_pic (
|
||||||
|
username VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||||
|
"image" bytea NOT NULL
|
||||||
|
);
|
49
src/auth/csrf.rs
Normal file
49
src/auth/csrf.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use maud::PreEscaped;
|
||||||
|
|
||||||
|
use super::User;
|
||||||
|
use crate::{get_pg, 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 {
|
||||||
|
let res: (uuid::Uuid,) = sqlx::query_as("SELECT csrf FROM user_session WHERE token = $1")
|
||||||
|
.bind(&self.session)
|
||||||
|
.fetch_one(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
res.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
||||||
|
sqlx::query("UPDATE user_session SET csrf = gen_random_uuid() WHERE token = $1")
|
||||||
|
.bind(&self.session)
|
||||||
|
.execute(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
31
src/auth/mod.rs
Normal file
31
src/auth/mod.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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::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"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
31
src/auth/profile_pic.rs
Normal file
31
src/auth/profile_pic.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::get_pg;
|
||||||
|
|
||||||
|
use super::User;
|
||||||
|
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>> {
|
||||||
|
let res: Option<(Vec<u8>,)> =
|
||||||
|
sqlx::query_as("SELECT image FROM user_profile_pic WHERE username = $1;")
|
||||||
|
.bind(&self.username)
|
||||||
|
.fetch_optional(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
res.map(|x| x.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a user's profile picture in the database
|
||||||
|
async fn set_profile_pic(&self, image: Vec<u8>) {
|
||||||
|
sqlx::query("INSERT INTO user_profile_pic (username, image) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET image = EXCLUDED.image",
|
||||||
|
)
|
||||||
|
.bind(&self.username)
|
||||||
|
.bind(&image)
|
||||||
|
.execute(get_pg!())
|
||||||
|
.await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
118
src/auth/session.rs
Normal file
118
src/auth/session.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
use crate::{gen_random, get_pg};
|
||||||
|
|
||||||
|
use super::{User, UserRole};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct Session {
|
||||||
|
/// The unique ID of the session token
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
/// The generated session token
|
||||||
|
pub token: String,
|
||||||
|
/// The username associated with the session token
|
||||||
|
pub user: String,
|
||||||
|
/// Session creation time
|
||||||
|
pub created: chrono::DateTime<Utc>,
|
||||||
|
/// Internal CSRF value
|
||||||
|
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<User>>;
|
||||||
|
fn login(
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> impl std::future::Future<Output = Option<(Session, UserRole)>>;
|
||||||
|
fn api_key(&self, name: &str) -> impl std::future::Future<Output = Session>;
|
||||||
|
fn session(&self) -> impl std::future::Future<Output = Session>;
|
||||||
|
fn list_sessions(&self) -> impl std::future::Future<Output = Vec<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) -> Session {
|
||||||
|
sqlx::query_as(
|
||||||
|
"INSERT INTO user_session (token, \"user\", kind, name) VALUES ($1, $2, $3, $4) RETURNING *",
|
||||||
|
)
|
||||||
|
.bind(gen_random(64))
|
||||||
|
.bind(&self.username)
|
||||||
|
.bind(SessionKind::API)
|
||||||
|
.bind(name)
|
||||||
|
.fetch_one(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End a user session
|
||||||
|
async fn end_session(&self, id: &uuid::Uuid) {
|
||||||
|
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<Session> {
|
||||||
|
sqlx::query_as("SELECT * FROM user_session WHERE \"user\" = $1")
|
||||||
|
.bind(&self.username)
|
||||||
|
.fetch_all(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a user from session ID
|
||||||
|
async fn from_session(session: String) -> Option<User> {
|
||||||
|
let user: Option<Self> = sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(&session).fetch_optional(get_pg!()).await.unwrap();
|
||||||
|
|
||||||
|
if let Some(mut user) = user {
|
||||||
|
user.session = session;
|
||||||
|
return Some(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login a user with the given username and password
|
||||||
|
async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> {
|
||||||
|
let u = Self::find(username).await?;
|
||||||
|
|
||||||
|
if !u.verify_pw(password) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((u.session().await, u.user_role))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a new session token for the user
|
||||||
|
///
|
||||||
|
/// Returns a Session instance containing the generated token and associated user
|
||||||
|
async fn session(&self) -> Session {
|
||||||
|
sqlx::query_as(
|
||||||
|
"INSERT INTO user_session (token, \"user\", kind) VALUES ($1, $2, $3) RETURNING *",
|
||||||
|
)
|
||||||
|
.bind(gen_random(64))
|
||||||
|
.bind(&self.username)
|
||||||
|
.bind(SessionKind::USER)
|
||||||
|
.fetch_one(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
264
src/auth/user.rs
Normal file
264
src/auth/user.rs
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
use rocket::{Request, http::Status, outcome::Outcome, request::FromRequest};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
use super::Sessions;
|
||||||
|
use crate::{get_pg, 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, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct User {
|
||||||
|
/// The username chosen by the user
|
||||||
|
pub username: String,
|
||||||
|
/// The hashed password for the user
|
||||||
|
pub password: String,
|
||||||
|
/// The role of the user
|
||||||
|
pub user_role: UserRole,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub session: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Self> {
|
||||||
|
sqlx::query_as("SELECT * FROM users WHERE username = $1")
|
||||||
|
.bind(username)
|
||||||
|
.fetch_optional(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Self> {
|
||||||
|
// Check if a user already exists with the same username
|
||||||
|
if Self::find(&username).await.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let u = Self {
|
||||||
|
username,
|
||||||
|
password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(),
|
||||||
|
user_role: role,
|
||||||
|
session: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO users (username, \"password\", user_role) VALUES ($1, $2, $3)")
|
||||||
|
.bind(&u.username)
|
||||||
|
.bind(&u.password)
|
||||||
|
.bind(&u.user_role)
|
||||||
|
.execute(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Some(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) {
|
||||||
|
sqlx::query("UPDATE users SET \"password\" = $1 WHERE username = $2;")
|
||||||
|
.bind(bcrypt::hash(new, bcrypt::DEFAULT_COST).unwrap())
|
||||||
|
.bind(&self.username)
|
||||||
|
.execute(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all users in the system
|
||||||
|
#[must_use]
|
||||||
|
pub async fn find_all() -> Vec<Self> {
|
||||||
|
sqlx::query_as("SELECT * FROM users")
|
||||||
|
.fetch_all(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.username,
|
||||||
|
"role": self.user_role
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// extracts a user from a request with `session` cookie
|
||||||
|
async fn extract_user(request: &Request<'_>) -> Option<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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for User {
|
||||||
|
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(user);
|
||||||
|
}
|
||||||
|
Outcome::Error((Status::Unauthorized, ()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Struct which extracts a user with session from `Token` HTTP Header.
|
||||||
|
pub struct APIUser(pub 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(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<User> {
|
||||||
|
fn from(value: MaybeUser) -> Self {
|
||||||
|
value.take_user()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaybeUser {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn user(&self) -> Option<&User> {
|
||||||
|
match self {
|
||||||
|
MaybeUser::User(user) => Some(user),
|
||||||
|
MaybeUser::Anonymous => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn take_user(self) -> Option<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 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.is_admin() {
|
||||||
|
return Outcome::Success(AdminUser(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Outcome::Error((Status::Unauthorized, ()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,7 @@ 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;
|
||||||
|
@ -171,7 +169,6 @@ impl DatabaseType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO : fix errors on conflict -> ignore already inserted ones
|
|
||||||
pub async fn batch_insert(table: &str, values: &[String], entries: Vec<Vec<DatabaseType>>) {
|
pub async fn batch_insert(table: &str, values: &[String], entries: Vec<Vec<DatabaseType>>) {
|
||||||
assert_eq!(values.len(), entries.first().unwrap().len());
|
assert_eq!(values.len(), entries.first().unwrap().len());
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,11 @@ use rocket::response::Responder;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::io::Seek;
|
use std::io::Seek;
|
||||||
|
use std::os::unix::fs::FileExt;
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
|
// TODO: Implement file based response
|
||||||
|
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
raw: Option<Vec<u8>>,
|
raw: Option<Vec<u8>>,
|
||||||
|
|
|
@ -29,32 +29,29 @@ 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! {
|
||||||
format!("modal-{id}"),
|
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" {
|
||||||
html! {
|
div class="relative p-4 w-full max-w-2xl max-h-full" {
|
||||||
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" {
|
|
||||||
(body)
|
|
||||||
};
|
|
||||||
|
|
||||||
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}")))
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}};
|
|
||||||
},
|
div class="p-4 md:p-5 space-y-4" {
|
||||||
)
|
(body)
|
||||||
|
};
|
||||||
|
|
||||||
|
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}")))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}};
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,13 +287,10 @@ 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(
|
Link(reference, html! {
|
||||||
reference,
|
(icon.map(|x| x.render()).unwrap_or_default());
|
||||||
html! {
|
span class="text-sm text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500" { (text) };
|
||||||
(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,25 +274,21 @@ impl GridElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn span(mut self, value: GridElementValue) -> Self {
|
pub fn span(mut self, value: GridElementValue) -> Self {
|
||||||
self.1.push(format!(
|
self.1.push(format!("{}-span-{}", self.2, match value {
|
||||||
"{}-span-{}",
|
GridElementValue::_1 => "1",
|
||||||
self.2,
|
GridElementValue::_2 => "2",
|
||||||
match value {
|
GridElementValue::_3 => "3",
|
||||||
GridElementValue::_1 => "1",
|
GridElementValue::_4 => "4",
|
||||||
GridElementValue::_2 => "2",
|
GridElementValue::_5 => "5",
|
||||||
GridElementValue::_3 => "3",
|
GridElementValue::_6 => "6",
|
||||||
GridElementValue::_4 => "4",
|
GridElementValue::_7 => "7",
|
||||||
GridElementValue::_5 => "5",
|
GridElementValue::_8 => "8",
|
||||||
GridElementValue::_6 => "6",
|
GridElementValue::_9 => "9",
|
||||||
GridElementValue::_7 => "7",
|
GridElementValue::_10 => "10",
|
||||||
GridElementValue::_8 => "8",
|
GridElementValue::_11 => "11",
|
||||||
GridElementValue::_9 => "9",
|
GridElementValue::_12 => "12",
|
||||||
GridElementValue::_10 => "10",
|
GridElementValue::Auto => "full",
|
||||||
GridElementValue::_11 => "11",
|
}));
|
||||||
GridElementValue::_12 => "12",
|
|
||||||
GridElementValue::Auto => "full",
|
|
||||||
}
|
|
||||||
));
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue