This commit is contained in:
parent
b98beff824
commit
ae36928791
11 changed files with 410 additions and 15 deletions
|
@ -1,3 +1,4 @@
|
|||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
@ -6,6 +7,7 @@ use walkdir::WalkDir;
|
|||
use func::is_video_file;
|
||||
pub use video::Video;
|
||||
mod func;
|
||||
pub mod user;
|
||||
mod video;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -208,3 +210,37 @@ impl Library {
|
|||
videos
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to generate a Model API representation in JSON format.
|
||||
pub trait ToAPI: Sized {
|
||||
/// Generate public API JSON
|
||||
fn api(&self) -> impl std::future::Future<Output = serde_json::Value>;
|
||||
}
|
||||
|
||||
/// Converts a slice of items implementing the `ToAPI` trait into a `Vec` of JSON values.
|
||||
pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec<serde_json::Value> {
|
||||
let mut ret = Vec::with_capacity(items.len());
|
||||
|
||||
for e in items {
|
||||
ret.push(e.api().await);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn to_uuid(id: &str) -> Result<uuid::Uuid, ApiError> {
|
||||
uuid::Uuid::from_str(id).map_err(|_| no_uuid_error())
|
||||
}
|
||||
|
||||
type ApiError = rocket::response::status::BadRequest<serde_json::Value>;
|
||||
type FallibleApiResponse = Result<serde_json::Value, ApiError>;
|
||||
|
||||
pub fn no_uuid_error() -> ApiError {
|
||||
api_error("No valid UUID")
|
||||
}
|
||||
|
||||
pub fn api_error(msg: &str) -> ApiError {
|
||||
rocket::response::status::BadRequest(json!({
|
||||
"error": msg
|
||||
}))
|
||||
}
|
||||
|
|
179
src/library/user.rs
Normal file
179
src/library/user.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use data_encoding::HEXUPPER;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::pages::ToAPI;
|
||||
|
||||
fn gen_token(token_length: usize) -> String {
|
||||
let mut token_bytes = vec![0u8; token_length];
|
||||
|
||||
rand::thread_rng().fill_bytes(&mut token_bytes);
|
||||
|
||||
HEXUPPER.encode(&token_bytes)
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
pub struct UserManager {
|
||||
conn: sqlx::PgPool,
|
||||
}
|
||||
|
||||
impl UserManager {
|
||||
pub fn new(conn: sqlx::PgPool) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
|
||||
/// Find a user by their username
|
||||
pub async fn find(&self, username: &str) -> Option<User> {
|
||||
sqlx::query_as("SELECT * FROM users WHERE username = $1")
|
||||
.bind(username)
|
||||
.fetch_optional(&self.conn)
|
||||
.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(&self, username: &str, password: &str, role: UserRole) -> Option<User> {
|
||||
// Check if a user already exists with the same username
|
||||
if self.find(username).await.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let u = User {
|
||||
username: username.to_string(),
|
||||
password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(),
|
||||
user_role: role,
|
||||
};
|
||||
|
||||
sqlx::query("INSERT INTO users (username, \"password\", user_role) VALUES ($1, $2, $3)")
|
||||
.bind(&u.username)
|
||||
.bind(&u.password)
|
||||
.bind(&u.user_role)
|
||||
.execute(&self.conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Some(u)
|
||||
}
|
||||
|
||||
/// Login a user with the given username and password
|
||||
pub async fn login(&self, username: &str, password: &str) -> Option<(Session, UserRole)> {
|
||||
let u = self.find(username).await?;
|
||||
|
||||
if !u.verify_pw(password) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((u.session(&self.conn).await, u.user_role))
|
||||
}
|
||||
|
||||
/// Find all users in the system
|
||||
pub async fn find_all(&self) -> Vec<User> {
|
||||
sqlx::query_as("SELECT * FROM users")
|
||||
.fetch_all(&self.conn)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn verify(&self, session_id: &str) -> Option<User> {
|
||||
let ses: Option<Session> = sqlx::query_as("SELECT * FROM user_session WHERE id = $1")
|
||||
.bind(uuid::Uuid::from_str(session_id).unwrap_or(uuid::Uuid::nil()))
|
||||
.fetch_optional(&self.conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if ses.is_some() {
|
||||
self.find(&ses.unwrap().user).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Generate a new session token for the user
|
||||
///
|
||||
/// Returns a Session instance containing the generated token and associated user
|
||||
pub async fn session(&self, conn: &sqlx::PgPool) -> Session {
|
||||
sqlx::query_as(
|
||||
"INSERT INTO user_session (token, \"user\") VALUES ($1, $2) RETURNING id, token, \"user\"",
|
||||
)
|
||||
.bind(gen_token(64))
|
||||
.bind(&self.username)
|
||||
.fetch_one(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Check if the user is an admin
|
||||
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
|
||||
pub fn verify_pw(&self, password: &str) -> bool {
|
||||
bcrypt::verify(password, &self.password).unwrap()
|
||||
}
|
||||
|
||||
/// 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, conn: &sqlx::PgPool) -> 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)
|
||||
.fetch_one(conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAPI for User {
|
||||
async fn api(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"username": self.username,
|
||||
"role": self.user_role
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue