Split configuration module (#117)

This commit is contained in:
Antoine Gersant 2020-12-18 01:14:24 -08:00 committed by GitHub
parent e5c1d86577
commit 5e065c5e6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1666 additions and 712 deletions

View file

@ -16,18 +16,22 @@
"name": "Collection", "name": "Collection",
"description": "Browsing the music collection" "description": "Browsing the music collection"
}, },
{
"name": "Configuration",
"description": "Managing the polaris installation"
},
{ {
"name": "Last.fm", "name": "Last.fm",
"description": "Integrating with Last.fm" "description": "Integrating with Last.fm"
}, },
{
"name": "Settings",
"description": "Managing the polaris installation"
},
{ {
"name": "Playlists", "name": "Playlists",
"description": "Managing playlists" "description": "Managing playlists"
}, },
{
"name": "Users",
"description": "Managing user accounts"
},
{ {
"name": "Other" "name": "Other"
} }
@ -95,38 +99,13 @@
] ]
} }
}, },
"/settings": { "/config": {
"get": {
"tags": [
"Settings"
],
"summary": "Reads the existing server configuration",
"operationId": "getSettings",
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#components/schemas/Config"
}
}
}
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
},
"put": { "put": {
"tags": [ "tags": [
"Settings" "Configuration"
], ],
"summary": "Overwrites the server configuration", "summary": "Amends the server settings, mount directories and list of users",
"operationId": "getSettings", "operationId": "putConfig",
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@ -150,10 +129,250 @@
] ]
} }
}, },
"/mount_dirs": {
"get": {
"tags": [
"Configuration"
],
"summary": "Reads the existing collection mount directories",
"operationId": "getMountDirs",
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"type": "array",
"$ref": "#components/schemas/MountDir"
}
}
}
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
},
"put": {
"tags": [
"Configuration"
],
"summary": "Replaces the list collection mount directories",
"operationId": "putMountDirs",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "array",
"$ref": "#components/schemas/MountDir"
}
}
}
},
"responses": {
"200": {
"description": "Successful operation"
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
}
},
"/settings": {
"get": {
"tags": [
"Configuration"
],
"summary": "Reads the existing server settings",
"operationId": "getSettings",
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#components/schemas/Settings"
}
}
}
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
},
"put": {
"tags": [
"Configuration"
],
"summary": "Amends the server settings",
"operationId": "putSettings",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#components/schemas/Settings"
}
}
}
},
"responses": {
"200": {
"description": "Successful operation"
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
}
},
"/users": {
"get": {
"tags": [
"Users"
],
"summary": "List existing user accounts",
"operationId": "getUsers",
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
}
},
"/user": {
"post": {
"tags": [
"Users"
],
"summary": "Creates a new user account",
"operationId": "postUser",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#components/schemas/NewUser"
}
}
}
},
"responses": {
"200": {
"description": "Successful operation"
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
}
},
"/user/{name}": {
"put": {
"tags": [
"Users"
],
"summary": "Updates properties of an existing user",
"operationId": "putUserName",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Name of the affected user",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#components/schemas/UserUpdate"
}
}
}
},
"responses": {
"200": {
"description": "Successful operation"
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
},
"delete": {
"tags": [
"Users"
],
"summary": "Deletes an existing user",
"operationId": "deleteUserName",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Name of the affected user",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful operation"
}
},
"security": [
{
"admin_http_header": [],
"admin_cookie": []
}
]
}
},
"/preferences": { "/preferences": {
"get": { "get": {
"tags": [ "tags": [
"Settings" "Users"
], ],
"summary": "Reads the preferences of the current user", "summary": "Reads the preferences of the current user",
"operationId": "getPreferences", "operationId": "getPreferences",
@ -175,12 +394,40 @@
"auth_cookie": [] "auth_cookie": []
} }
] ]
},
"put": {
"tags": [
"Users"
],
"summary": "Saves the preferences of the current user",
"operationId": "putPreferences",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#components/schemas/Preferences"
}
}
}
},
"responses": {
"200": {
"description": "Successful operation"
}
},
"security": [
{
"auth_http_header": [],
"auth_cookie": []
}
]
} }
}, },
"/auth": { "/auth": {
"post": { "post": {
"tags": [ "tags": [
"Other" "Users"
], ],
"summary": "Signs in a user. Response has Set-Cookie headers for the session, username and admin permission of the user.", "summary": "Signs in a user. Response has Set-Cookie headers for the session, username and admin permission of the user.",
"operationId": "postAuth", "operationId": "postAuth",
@ -810,7 +1057,7 @@
} }
} }
}, },
"Config": { "Settings": {
"type": "object", "type": "object",
"properties": { "properties": {
"album_art_pattern": { "album_art_pattern": {
@ -821,18 +1068,6 @@
"type": "integer", "type": "integer",
"example": 3600 "example": 3600
}, },
"mount_dirs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MountPoint"
}
},
"users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ConfigUser"
}
},
"ydns": { "ydns": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -852,22 +1087,72 @@
} }
} }
}, },
"ConfigUser": { "Config": {
"type": "object",
"properties": {
"settings": {
"$ref": "#/components/schemas/Settings"
},
"mount_dirs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MountDir"
}
},
"users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
}
}
},
"User": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"is_admin": {
"type": "boolean"
}
},
"required": [
"name",
"is_admin"
]
},
"NewUser": {
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string"
}, },
"password": { "password": {
"type": "string", "type": "string"
"description": "Always blank when this field appear in a server response"
}, },
"admin": { "is_admin": {
"type": "boolean"
}
},
"required": [
"name",
"password",
"is_admin"
]
},
"UserUpdate": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"is_admin": {
"type": "boolean" "type": "boolean"
} }
} }
}, },
"MountPoint": { "MountDir": {
"type": "object", "type": "object",
"properties": { "properties": {
"source": { "source": {
@ -878,7 +1163,11 @@
"type": "string", "type": "string",
"example": "My Music" "example": "My Music"
} }
} },
"required": [
"source",
"name"
]
}, },
"Preferences": { "Preferences": {
"type": "object", "type": "object",

View file

@ -1,13 +1,5 @@
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error("Missing auth secret")]
AuthSecretNotFound,
#[error("Missing index sleep duration")]
IndexSleepDurationNotFound,
#[error("Missing index album art pattern")]
IndexAlbumArtPatternNotFound,
#[error("Index album art pattern is not a valid regex")]
IndexAlbumArtPatternInvalid,
#[error("Unspecified")] #[error("Unspecified")]
Unspecified, Unspecified,
} }

View file

@ -1,191 +1,83 @@
use diesel;
use diesel::prelude::*;
use regex::Regex;
use std::time::Duration;
use super::*; use super::*;
use crate::app::user; use crate::app::{ddns, settings, user, vfs};
use crate::db::{ddns_config, misc_settings, mount_points, users, DB};
#[derive(Clone)] #[derive(Clone)]
pub struct Manager { pub struct Manager {
pub db: DB, settings_manager: settings::Manager,
user_manager: user::Manager, user_manager: user::Manager,
vfs_manager: vfs::Manager,
ddns_manager: ddns::Manager,
} }
impl Manager { impl Manager {
pub fn new(db: DB, user_manager: user::Manager) -> Self { pub fn new(
Self { db, user_manager } settings_manager: settings::Manager,
user_manager: user::Manager,
vfs_manager: vfs::Manager,
ddns_manager: ddns::Manager,
) -> Self {
Self {
settings_manager,
user_manager,
vfs_manager,
ddns_manager,
}
} }
pub fn get_auth_secret(&self) -> Result<Vec<u8>, Error> { pub fn apply(&self, config: &Config) -> Result<(), Error> {
use self::misc_settings::dsl::*; if let Some(new_settings) = &config.settings {
let connection = self.db.connect()?; self.settings_manager
misc_settings .amend(new_settings)
.select(auth_secret) .map_err(|_| Error::Unspecified)?;
.get_result(&connection)
.map_err(|e| match e {
diesel::result::Error::NotFound => Error::AuthSecretNotFound,
_ => Error::Unspecified,
})
}
pub fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
use self::misc_settings::dsl::*;
let connection = self.db.connect()?;
misc_settings
.select(index_sleep_duration_seconds)
.get_result(&connection)
.map_err(|e| match e {
diesel::result::Error::NotFound => Error::IndexSleepDurationNotFound,
_ => Error::Unspecified,
})
.map(|s: i32| Duration::from_secs(s as u64))
}
pub fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
use self::misc_settings::dsl::*;
let connection = self.db.connect()?;
misc_settings
.select(index_album_art_pattern)
.get_result(&connection)
.map_err(|e| match e {
diesel::result::Error::NotFound => Error::IndexAlbumArtPatternNotFound,
_ => Error::Unspecified,
})
.map(|s: String| format!("(?i){}", s))
.and_then(|s| Regex::new(&s).map_err(|_| Error::IndexAlbumArtPatternInvalid))
}
pub fn read(&self) -> anyhow::Result<Config> {
use self::ddns_config::dsl::*;
use self::misc_settings::dsl::*;
let connection = self.db.connect()?;
let mut config = Config {
album_art_pattern: None,
reindex_every_n_seconds: None,
mount_dirs: None,
users: None,
ydns: None,
};
let (art_pattern, sleep_duration) = misc_settings
.select((index_album_art_pattern, index_sleep_duration_seconds))
.get_result(&connection)?;
config.album_art_pattern = Some(art_pattern);
config.reindex_every_n_seconds = Some(sleep_duration);
let mount_dirs;
{
use self::mount_points::dsl::*;
mount_dirs = mount_points
.select((source, name))
.get_results(&connection)?;
config.mount_dirs = Some(mount_dirs);
} }
let found_users: Vec<(String, i32)> = users::table if let Some(mount_dirs) = &config.mount_dirs {
.select((users::columns::name, users::columns::admin)) self.vfs_manager
.get_results(&connection)?; .set_mount_dirs(&mount_dirs)
config.users = Some( .map_err(|_| Error::Unspecified)?;
found_users
.into_iter()
.map(|(name, admin)| ConfigUser {
name,
password: "".to_owned(),
admin: admin != 0,
})
.collect::<_>(),
);
let ydns = ddns_config
.select((host, username, password))
.get_result(&connection)?;
config.ydns = Some(ydns);
Ok(config)
}
pub fn amend(&self, new_config: &Config) -> anyhow::Result<()> {
let connection = self.db.connect()?;
if let Some(ref mount_dirs) = new_config.mount_dirs {
diesel::delete(mount_points::table).execute(&connection)?;
diesel::insert_into(mount_points::table)
.values(mount_dirs)
.execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
} }
if let Some(ref config_users) = new_config.users { if let Some(ddns_config) = &config.ydns {
let old_usernames: Vec<String> = self.ddns_manager
users::table.select(users::name).get_results(&connection)?; .set_config(&ddns_config)
.map_err(|_| Error::Unspecified)?;
}
if let Some(ref users) = config.users {
let old_users: Vec<user::User> =
self.user_manager.list().map_err(|_| Error::Unspecified)?;
// Delete users that are not in new list // Delete users that are not in new list
let delete_usernames: Vec<String> = old_usernames for old_user in old_users
.iter() .iter()
.cloned() .filter(|old_user| !users.iter().any(|u| u.name == old_user.name))
.filter(|old_name| config_users.iter().find(|u| &u.name == old_name).is_none()) {
.collect::<_>(); self.user_manager
diesel::delete(users::table.filter(users::name.eq_any(&delete_usernames))) .delete(&old_user.name)
.execute(&connection)?; .map_err(|_| Error::Unspecified)?;
}
// Insert new users // Insert new users
let insert_users: Vec<&ConfigUser> = config_users for new_user in users
.iter() .iter()
.filter(|u| { .filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name))
!u.name.is_empty() {
&& !u.password.is_empty()
&& old_usernames
.iter()
.find(|old_name| *old_name == &u.name)
.is_none()
})
.collect::<_>();
for config_user in &insert_users {
self.user_manager self.user_manager
.create_user(&config_user.name, &config_user.password)?; .create(new_user)
.map_err(|_| Error::Unspecified)?;
} }
// Update users // Update users
for user in config_users.iter() { for user in users {
// Update password if provided self.user_manager
if !user.password.is_empty() { .set_password(&user.name, &user.password)
self.user_manager.set_password(&user.name, &user.password)?; .map_err(|_| Error::Unspecified)?;
} self.user_manager
.set_is_admin(&user.name, user.admin)
// Update admin rights .map_err(|_| Error::Unspecified)?;
diesel::update(users::table.filter(users::name.eq(&user.name)))
.set(users::admin.eq(user.admin as i32))
.execute(&connection)?;
} }
} }
if let Some(sleep_duration) = new_config.reindex_every_n_seconds {
diesel::update(misc_settings::table)
.set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration as i32))
.execute(&connection)?;
}
if let Some(ref album_art_pattern) = new_config.album_art_pattern {
diesel::update(misc_settings::table)
.set(misc_settings::index_album_art_pattern.eq(album_art_pattern))
.execute(&connection)?;
}
if let Some(ref ydns) = new_config.ydns {
use self::ddns_config::dsl::*;
diesel::update(ddns_config)
.set((
host.eq(ydns.host.clone()),
username.eq(ydns.username.clone()),
password.eq(ydns.password.clone()),
))
.execute(&connection)?;
}
Ok(()) Ok(())
} }
} }

View file

@ -1,9 +1,8 @@
use crate::app::{ddns, vfs}; use serde::Deserialize;
use core::ops::Deref;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::io::Read; use std::io::Read;
use std::path::{self, PathBuf}; use std::path;
use crate::app::{ddns, settings, user, vfs};
mod error; mod error;
mod manager; mod manager;
@ -13,20 +12,12 @@ mod test;
pub use error::*; pub use error::*;
pub use manager::*; pub use manager::*;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Default, Deserialize)]
pub struct ConfigUser {
pub name: String,
pub password: String,
pub admin: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub album_art_pattern: Option<String>, pub settings: Option<settings::NewSettings>,
pub reindex_every_n_seconds: Option<i32>, pub mount_dirs: Option<Vec<vfs::MountDir>>,
pub mount_dirs: Option<Vec<vfs::MountPoint>>,
pub users: Option<Vec<ConfigUser>>,
pub ydns: Option<ddns::Config>, pub ydns: Option<ddns::Config>,
pub users: Option<Vec<user::NewUser>>,
} }
impl Config { impl Config {
@ -34,36 +25,7 @@ impl Config {
let mut config_file = std::fs::File::open(path)?; let mut config_file = std::fs::File::open(path)?;
let mut config_file_content = String::new(); let mut config_file_content = String::new();
config_file.read_to_string(&mut config_file_content)?; config_file.read_to_string(&mut config_file_content)?;
let mut config = toml::de::from_str::<Config>(&config_file_content)?; let config = toml::de::from_str::<Self>(&config_file_content)?;
config.clean_paths()?;
Ok(config) Ok(config)
} }
fn clean_paths(&mut self) -> anyhow::Result<()> {
if let Some(ref mut mount_dirs) = self.mount_dirs {
for mount_dir in mount_dirs {
match Self::clean_path_string(&mount_dir.source).to_str() {
Some(p) => mount_dir.source = p.to_owned(),
_ => anyhow::bail!("Bad mount directory path"),
}
}
}
Ok(())
}
fn clean_path_string(path_string: &str) -> PathBuf {
let separator_regex = Regex::new(r"\\|/").unwrap();
let mut correct_separator = String::new();
correct_separator.push(path::MAIN_SEPARATOR);
let path_string = separator_regex.replace_all(path_string, correct_separator.as_str());
path::Path::new(path_string.deref()).iter().collect()
}
}
#[derive(Debug, Queryable)]
pub struct MiscSettings {
id: i32,
pub auth_secret: Vec<u8>,
pub index_sleep_duration_seconds: i32,
pub index_album_art_pattern: String,
} }

View file

@ -1,10 +1,9 @@
use diesel::prelude::*;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use super::*; use super::*;
use crate::app::{user, vfs}; use crate::app::{settings, user, vfs};
use crate::db::{users, DB}; use crate::db::DB;
use crate::test_name; use crate::test_name;
#[cfg(test)] #[cfg(test)]
@ -22,249 +21,131 @@ fn get_test_db(name: &str) -> DB {
} }
#[test] #[test]
fn test_amend() { fn apply_saves_misc_settings() {
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let user_manager = user::Manager::new(db.clone());
let config_manager = Manager::new(db, user_manager); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone());
let initial_config = Config { let config_manager = Manager::new(
album_art_pattern: Some("file\\.png".into()), settings_manager.clone(),
reindex_every_n_seconds: Some(123), user_manager.clone(),
mount_dirs: Some(vec![vfs::MountPoint { vfs_manager.clone(),
source: "C:\\Music".into(), ddns_manager.clone(),
name: "root".into(), );
}]),
users: Some(vec![ConfigUser {
name: "Teddy🐻".into(),
password: "Tasty🍖".into(),
admin: false,
}]),
ydns: None,
};
let new_config = Config { let new_config = Config {
album_art_pattern: Some("🖼️\\.jpg".into()), settings: Some(settings::NewSettings {
reindex_every_n_seconds: None, album_art_pattern: Some("🖼️\\.jpg".into()),
mount_dirs: Some(vec![vfs::MountPoint { reindex_every_n_seconds: Some(100),
..Default::default()
}),
..Default::default()
};
config_manager.apply(&new_config).unwrap();
let settings = settings_manager.read().unwrap();
let new_settings = new_config.settings.unwrap();
assert_eq!(
settings.album_art_pattern,
new_settings.album_art_pattern.unwrap()
);
assert_eq!(
settings.reindex_every_n_seconds,
new_settings.reindex_every_n_seconds.unwrap()
);
}
#[test]
fn apply_saves_mount_points() {
let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone());
let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone());
let config_manager = Manager::new(
settings_manager.clone(),
user_manager.clone(),
vfs_manager.clone(),
ddns_manager.clone(),
);
let new_config = Config {
mount_dirs: Some(vec![vfs::MountDir {
source: "/home/music".into(), source: "/home/music".into(),
name: "🎵📁".into(), name: "🎵📁".into(),
}]), }]),
users: Some(vec![ConfigUser { ..Default::default()
name: "Kermit🐸".into(), };
password: "🐞🐞".into(),
admin: false, config_manager.apply(&new_config).unwrap();
}]), let actual_mount_dirs: Vec<vfs::MountDir> = vfs_manager.mount_dirs().unwrap();
assert_eq!(actual_mount_dirs, new_config.mount_dirs.unwrap());
}
#[test]
fn apply_saves_ddns_settings() {
use crate::app::ddns;
let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone());
let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone());
let config_manager = Manager::new(
settings_manager.clone(),
user_manager.clone(),
vfs_manager.clone(),
ddns_manager.clone(),
);
let new_config = Config {
ydns: Some(ddns::Config { ydns: Some(ddns::Config {
host: "🐸🐸🐸.ydns.eu".into(), host: "🐸🐸🐸.ydns.eu".into(),
username: "kfr🐸g".into(), username: "kfr🐸g".into(),
password: "tasty🐞".into(), password: "tasty🐞".into(),
}), }),
..Default::default()
}; };
let mut expected_config = new_config.clone(); config_manager.apply(&new_config).unwrap();
expected_config.reindex_every_n_seconds = initial_config.reindex_every_n_seconds; let actual_ddns = ddns_manager.config().unwrap();
if let Some(ref mut users) = expected_config.users { assert_eq!(actual_ddns, new_config.ydns.unwrap());
users[0].password = "".into();
}
config_manager.amend(&initial_config).unwrap();
config_manager.amend(&new_config).unwrap();
let db_config = config_manager.read().unwrap();
assert_eq!(db_config, expected_config);
} }
#[test] #[test]
fn test_amend_preserve_password_hashes() { fn apply_can_toggle_admin() {
use self::users::dsl::*;
let db = get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let user_manager = user::Manager::new(db.clone());
let config_manager = Manager::new(db.clone(), user_manager); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone());
let initial_hash: String; let config_manager = Manager::new(
let new_hash: String; settings_manager.clone(),
user_manager.clone(),
vfs_manager.clone(),
ddns_manager.clone(),
);
let initial_config = Config { let initial_config = Config {
album_art_pattern: None, users: Some(vec![user::NewUser {
reindex_every_n_seconds: None, name: "Walter".into(),
mount_dirs: None,
users: Some(vec![ConfigUser {
name: "Teddy🐻".into(),
password: "Tasty🍖".into(),
admin: false,
}]),
ydns: None,
};
config_manager.amend(&initial_config).unwrap();
{
let connection = db.connect().unwrap();
initial_hash = users
.select(password_hash)
.filter(name.eq("Teddy🐻"))
.get_result(&connection)
.unwrap();
}
let new_config = Config {
album_art_pattern: None,
reindex_every_n_seconds: None,
mount_dirs: None,
users: Some(vec![
ConfigUser {
name: "Kermit🐸".into(),
password: "tasty🐞".into(),
admin: false,
},
ConfigUser {
name: "Teddy🐻".into(),
password: "".into(),
admin: false,
},
]),
ydns: None,
};
config_manager.amend(&new_config).unwrap();
{
let connection = db.connect().unwrap();
new_hash = users
.select(password_hash)
.filter(name.eq("Teddy🐻"))
.get_result(&connection)
.unwrap();
}
assert_eq!(new_hash, initial_hash);
}
#[test]
fn test_amend_ignore_blank_users() {
use self::users::dsl::*;
let db = get_test_db(&test_name!());
let user_manager = user::Manager::new(db.clone());
let config_manager = Manager::new(db.clone(), user_manager);
{
let config = Config {
album_art_pattern: None,
reindex_every_n_seconds: None,
mount_dirs: None,
users: Some(vec![ConfigUser {
name: "".into(),
password: "Tasty🍖".into(),
admin: false,
}]),
ydns: None,
};
config_manager.amend(&config).unwrap();
let connection = db.connect().unwrap();
let user_count: i64 = users.count().get_result(&connection).unwrap();
assert_eq!(user_count, 0);
}
{
let config = Config {
album_art_pattern: None,
reindex_every_n_seconds: None,
mount_dirs: None,
users: Some(vec![ConfigUser {
name: "Teddy🐻".into(),
password: "".into(),
admin: false,
}]),
ydns: None,
};
config_manager.amend(&config).unwrap();
let connection = db.connect().unwrap();
let user_count: i64 = users.count().get_result(&connection).unwrap();
assert_eq!(user_count, 0);
}
}
#[test]
fn test_toggle_admin() {
use self::users::dsl::*;
let db = get_test_db(&test_name!());
let user_manager = user::Manager::new(db.clone());
let config_manager = Manager::new(db.clone(), user_manager);
let initial_config = Config {
album_art_pattern: None,
reindex_every_n_seconds: None,
mount_dirs: None,
users: Some(vec![ConfigUser {
name: "Teddy🐻".into(),
password: "Tasty🍖".into(), password: "Tasty🍖".into(),
admin: true, admin: true,
}]), }]),
ydns: None, ..Default::default()
}; };
config_manager.amend(&initial_config).unwrap(); config_manager.apply(&initial_config).unwrap();
assert!(user_manager.list().unwrap()[0].is_admin());
{
let connection = db.connect().unwrap();
let is_admin: i32 = users.select(admin).get_result(&connection).unwrap();
assert_eq!(is_admin, 1);
}
let new_config = Config { let new_config = Config {
album_art_pattern: None, users: Some(vec![user::NewUser {
reindex_every_n_seconds: None, name: "Walter".into(),
mount_dirs: None, password: "Tasty🍖".into(),
users: Some(vec![ConfigUser {
name: "Teddy🐻".into(),
password: "".into(),
admin: false, admin: false,
}]), }]),
ydns: None, ..Default::default()
}; };
config_manager.amend(&new_config).unwrap(); config_manager.apply(&new_config).unwrap();
assert!(!user_manager.list().unwrap()[0].is_admin());
{
let connection = db.connect().unwrap();
let is_admin: i32 = users.select(admin).get_result(&connection).unwrap();
assert_eq!(is_admin, 0);
}
}
#[test]
fn test_clean_path_string() {
let mut correct_path = path::PathBuf::new();
if cfg!(target_os = "windows") {
correct_path.push("C:\\");
} else {
correct_path.push("/usr");
}
correct_path.push("some");
correct_path.push("path");
if cfg!(target_os = "windows") {
assert_eq!(correct_path, Config::clean_path_string(r#"C:/some/path"#));
assert_eq!(correct_path, Config::clean_path_string(r#"C:\some\path"#));
assert_eq!(correct_path, Config::clean_path_string(r#"C:\some\path\"#));
assert_eq!(
correct_path,
Config::clean_path_string(r#"C:\some\path\\\\"#)
);
assert_eq!(correct_path, Config::clean_path_string(r#"C:\some/path//"#));
} else {
assert_eq!(correct_path, Config::clean_path_string(r#"/usr/some/path"#));
assert_eq!(correct_path, Config::clean_path_string(r#"/usr\some\path"#));
assert_eq!(
correct_path,
Config::clean_path_string(r#"/usr\some\path\"#)
);
assert_eq!(
correct_path,
Config::clean_path_string(r#"/usr\some\path\\\\"#)
);
assert_eq!(
correct_path,
Config::clean_path_string(r#"/usr\some/path//"#)
);
}
} }

View file

@ -10,6 +10,7 @@ use crate::db::DB;
const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/"; const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/";
#[derive(Clone)]
pub struct Manager { pub struct Manager {
db: DB, db: DB,
} }
@ -20,7 +21,7 @@ impl Manager {
} }
fn update_my_ip(&self) -> Result<()> { fn update_my_ip(&self) -> Result<()> {
let config = self.get_config()?; let config = self.config()?;
if config.host.is_empty() || config.username.is_empty() { if config.host.is_empty() || config.username.is_empty() {
info!("Skipping DDNS update because credentials are missing"); info!("Skipping DDNS update because credentials are missing");
return Ok(()); return Ok(());
@ -41,7 +42,7 @@ impl Manager {
Ok(()) Ok(())
} }
fn get_config(&self) -> Result<Config> { pub fn config(&self) -> Result<Config> {
use crate::db::ddns_config::dsl::*; use crate::db::ddns_config::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
Ok(ddns_config Ok(ddns_config
@ -49,7 +50,27 @@ impl Manager {
.get_result(&connection)?) .get_result(&connection)?)
} }
pub fn run(&self) { pub fn set_config(&self, new_config: &Config) -> Result<()> {
use crate::db::ddns_config::dsl::*;
let connection = self.db.connect()?;
diesel::update(ddns_config)
.set((
host.eq(&new_config.host),
username.eq(&new_config.username),
password.eq(&new_config.password),
))
.execute(&connection)?;
Ok(())
}
pub fn begin_periodic_updates(&self) {
let cloned = self.clone();
std::thread::spawn(move || {
cloned.run();
});
}
fn run(&self) {
loop { loop {
if let Err(e) = self.update_my_ip() { if let Err(e) = self.update_my_ip() {
error!("Dynamic DNS update error: {:?}", e); error!("Dynamic DNS update error: {:?}", e);

View file

@ -3,7 +3,7 @@ use log::error;
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration; use std::time::Duration;
use crate::app::{config, vfs}; use crate::app::{settings, vfs};
use crate::db::DB; use crate::db::DB;
mod metadata; mod metadata;
@ -21,16 +21,16 @@ pub use self::update::*;
pub struct Index { pub struct Index {
db: DB, db: DB,
vfs_manager: vfs::Manager, vfs_manager: vfs::Manager,
config_manager: config::Manager, settings_manager: settings::Manager,
pending_reindex: Arc<(Mutex<bool>, Condvar)>, pending_reindex: Arc<(Mutex<bool>, Condvar)>,
} }
impl Index { impl Index {
pub fn new(db: DB, vfs_manager: vfs::Manager, config_manager: config::Manager) -> Self { pub fn new(db: DB, vfs_manager: vfs::Manager, settings_manager: settings::Manager) -> Self {
let index = Self { let index = Self {
db, db,
vfs_manager, vfs_manager,
config_manager, settings_manager,
pending_reindex: Arc::new((Mutex::new(false), Condvar::new())), pending_reindex: Arc::new((Mutex::new(false), Condvar::new())),
}; };
@ -76,7 +76,7 @@ impl Index {
loop { loop {
self.trigger_reindex(); self.trigger_reindex();
let sleep_duration = self let sleep_duration = self
.config_manager .settings_manager
.get_index_sleep_duration() .get_index_sleep_duration()
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
error!("Could not retrieve index sleep duration: {}", e); error!("Could not retrieve index sleep duration: {}", e);

View file

@ -2,16 +2,15 @@ use diesel::prelude::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::*; use super::*;
use crate::app::{config, user, vfs}; use crate::app::{index::Index, settings, vfs};
use crate::db::{self, directories, songs}; use crate::db::{self, directories, songs};
use crate::test_name; use crate::test_name;
fn get_context(test_name: &str) -> (db::DB, Index) { fn get_context(test_name: &str) -> (db::DB, Index) {
let db = db::get_test_db(test_name); let db = db::get_test_db(test_name);
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let config_manager = config::Manager::new(db.clone(), user_manager); let index = Index::new(db.clone(), vfs_manager, settings_manager);
let index = Index::new(db.clone(), vfs_manager, config_manager);
(db, index) (db, index)
} }

View file

@ -18,7 +18,7 @@ impl Index {
let start = time::Instant::now(); let start = time::Instant::now();
info!("Beginning library index update"); info!("Beginning library index update");
let album_art_pattern = self.config_manager.get_index_album_art_pattern()?; let album_art_pattern = self.settings_manager.get_index_album_art_pattern()?;
let cleaner = Cleaner::new(self.db.clone(), self.vfs_manager.clone()); let cleaner = Cleaner::new(self.db.clone(), self.vfs_manager.clone());
cleaner.clean()?; cleaner.clean()?;
@ -38,9 +38,9 @@ impl Index {
let vfs = self.vfs_manager.get_vfs()?; let vfs = self.vfs_manager.get_vfs()?;
let traverser_thread = std::thread::spawn(move || { let traverser_thread = std::thread::spawn(move || {
let mount_points = vfs.get_mount_points(); let mounts = vfs.mounts();
let traverser = Traverser::new(collect_sender); let traverser = Traverser::new(collect_sender);
traverser.traverse(mount_points.values().map(|p| p.clone()).collect()); traverser.traverse(mounts.iter().map(|p| p.source.clone()).collect());
}); });
if let Err(e) = traverser_thread.join() { if let Err(e) = traverser_thread.join() {

View file

@ -3,6 +3,7 @@ pub mod ddns;
pub mod index; pub mod index;
pub mod lastfm; pub mod lastfm;
pub mod playlist; pub mod playlist;
pub mod settings;
pub mod thumbnail; pub mod thumbnail;
pub mod user; pub mod user;
pub mod vfs; pub mod vfs;

View file

@ -2,7 +2,7 @@ use core::clone::Clone;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::*; use super::*;
use crate::app::{config, index::Index, user, vfs}; use crate::app::{index::Index, settings, vfs};
use crate::db; use crate::db;
use crate::test_name; use crate::test_name;
@ -58,9 +58,8 @@ fn test_delete_playlist() {
fn test_fill_playlist() { fn test_fill_playlist() {
let db = db::get_test_db(&test_name!()); let db = db::get_test_db(&test_name!());
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone());
let config_manager = config::Manager::new(db.clone(), user_manager); let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager);
let index = Index::new(db.clone(), vfs_manager.clone(), config_manager);
let manager = Manager::new(db, vfs_manager); let manager = Manager::new(db, vfs_manager);
index.update().unwrap(); index.update().unwrap();

21
src/app/settings/error.rs Normal file
View file

@ -0,0 +1,21 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Missing auth secret")]
AuthSecretNotFound,
#[error("Auth secret does not have the expected format")]
InvalidAuthSecret,
#[error("Missing index sleep duration")]
IndexSleepDurationNotFound,
#[error("Missing index album art pattern")]
IndexAlbumArtPatternNotFound,
#[error("Index album art pattern is not a valid regex")]
IndexAlbumArtPatternInvalid,
#[error("Unspecified")]
Unspecified,
}
impl From<anyhow::Error> for Error {
fn from(_: anyhow::Error) -> Self {
Error::Unspecified
}
}

View file

@ -0,0 +1,97 @@
use diesel;
use diesel::prelude::*;
use regex::Regex;
use std::convert::TryInto;
use std::time::Duration;
use super::*;
use crate::db::{misc_settings, DB};
#[derive(Clone)]
pub struct Manager {
pub db: DB,
}
impl Manager {
pub fn new(db: DB) -> Self {
Self { db }
}
pub fn get_auth_secret(&self) -> Result<AuthSecret, Error> {
use self::misc_settings::dsl::*;
let connection = self.db.connect()?;
let secret: Vec<u8> = misc_settings
.select(auth_secret)
.get_result(&connection)
.map_err(|e| match e {
diesel::result::Error::NotFound => Error::AuthSecretNotFound,
_ => Error::Unspecified,
})?;
secret
.try_into()
.map_err(|_| Error::InvalidAuthSecret)
.map(|key| AuthSecret { key })
}
pub fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
use self::misc_settings::dsl::*;
let connection = self.db.connect()?;
misc_settings
.select(index_sleep_duration_seconds)
.get_result(&connection)
.map_err(|e| match e {
diesel::result::Error::NotFound => Error::IndexSleepDurationNotFound,
_ => Error::Unspecified,
})
.map(|s: i32| Duration::from_secs(s as u64))
}
pub fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
use self::misc_settings::dsl::*;
let connection = self.db.connect()?;
misc_settings
.select(index_album_art_pattern)
.get_result(&connection)
.map_err(|e| match e {
diesel::result::Error::NotFound => Error::IndexAlbumArtPatternNotFound,
_ => Error::Unspecified,
})
.and_then(|s: String| {
Regex::new(&format!("(?i){}", &s)).map_err(|_| Error::IndexAlbumArtPatternInvalid)
})
}
pub fn read(&self) -> Result<Settings, Error> {
let connection = self.db.connect()?;
let misc: MiscSettings = misc_settings::table
.get_result(&connection)
.map_err(|_| Error::Unspecified)?;
Ok(Settings {
auth_secret: misc.auth_secret,
album_art_pattern: misc.index_album_art_pattern,
reindex_every_n_seconds: misc.index_sleep_duration_seconds,
})
}
pub fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> {
let connection = self.db.connect()?;
if let Some(sleep_duration) = new_settings.reindex_every_n_seconds {
diesel::update(misc_settings::table)
.set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration as i32))
.execute(&connection)
.map_err(|_| Error::Unspecified)?;
}
if let Some(ref album_art_pattern) = new_settings.album_art_pattern {
diesel::update(misc_settings::table)
.set(misc_settings::index_album_art_pattern.eq(album_art_pattern))
.execute(&connection)
.map_err(|_| Error::Unspecified)?;
}
Ok(())
}
}

33
src/app/settings/mod.rs Normal file
View file

@ -0,0 +1,33 @@
use serde::Deserialize;
mod error;
mod manager;
pub use error::*;
pub use manager::*;
#[derive(Clone)]
pub struct AuthSecret {
pub key: [u8; 32],
}
#[derive(Debug, Queryable)]
struct MiscSettings {
id: i32,
auth_secret: Vec<u8>,
index_sleep_duration_seconds: i32,
index_album_art_pattern: String,
}
#[derive(Debug)]
pub struct Settings {
auth_secret: Vec<u8>,
pub reindex_every_n_seconds: i32,
pub album_art_pattern: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct NewSettings {
pub reindex_every_n_seconds: Option<i32>,
pub album_art_pattern: Option<String>,
}

View file

@ -1,7 +1,11 @@
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
#[error("Cannot use empty username")]
EmptyUsername,
#[error("Cannot use empty password")] #[error("Cannot use empty password")]
EmptyPassword, EmptyPassword,
#[error("Username does not exist")]
IncorrectUsername,
#[error("Unspecified")] #[error("Unspecified")]
Unspecified, Unspecified,
} }

View file

@ -17,17 +17,19 @@ impl Manager {
Self { db } Self { db }
} }
pub fn create_user(&self, username: &str, password: &str) -> Result<(), Error> { pub fn create(&self, new_user: &NewUser) -> Result<(), Error> {
if password.is_empty() { if new_user.name.is_empty() {
return Err(Error::EmptyPassword); return Err(Error::EmptyUsername);
} }
let password_hash = hash_password(password)?;
let password_hash = hash_password(&new_user.password)?;
let connection = self.db.connect()?; let connection = self.db.connect()?;
let new_user = User { let new_user = User {
name: username.to_owned(), name: new_user.name.to_owned(),
password_hash, password_hash,
admin: 0, admin: new_user.admin as i32,
}; };
diesel::insert_into(users::table) diesel::insert_into(users::table)
.values(&new_user) .values(&new_user)
.execute(&connection) .execute(&connection)
@ -35,17 +37,37 @@ impl Manager {
Ok(()) Ok(())
} }
pub fn set_password(&self, username: &str, password: &str) -> Result<(), Error> { pub fn delete(&self, username: &str) -> Result<(), Error> {
let password_hash = hash_password(password)?; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
diesel::update(users::table.filter(users::name.eq(username))) diesel::delete(users.filter(name.eq(username)))
.set(users::password_hash.eq(password_hash))
.execute(&connection) .execute(&connection)
.map_err(|_| Error::Unspecified)?; .map_err(|_| Error::Unspecified)?;
Ok(()) Ok(())
} }
pub fn auth(&self, username: &str, password: &str) -> anyhow::Result<bool> { pub fn set_password(&self, username: &str, password: &str) -> Result<(), Error> {
let hash = hash_password(password)?;
let connection = self.db.connect()?;
use crate::db::users::dsl::*;
diesel::update(users.filter(name.eq(username)))
.set(password_hash.eq(hash))
.execute(&connection)
.map_err(|_| Error::Unspecified)?;
Ok(())
}
pub fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> {
use crate::db::users::dsl::*;
let connection = self.db.connect()?;
diesel::update(users.filter(name.eq(username)))
.set(admin.eq(is_admin as i32))
.execute(&connection)
.map_err(|_| Error::Unspecified)?;
Ok(())
}
pub fn login(&self, username: &str, password: &str) -> Result<bool, Error> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
match users match users
@ -53,15 +75,14 @@ impl Manager {
.filter(name.eq(username)) .filter(name.eq(username))
.get_result(&connection) .get_result(&connection)
{ {
Err(diesel::result::Error::NotFound) => Ok(false), Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername),
Ok(hash) => { Ok(hash) => {
let hash: String = hash; let hash: String = hash;
Ok(verify_password(&hash, password)) Ok(verify_password(&hash, password))
} }
Err(e) => Err(e.into()), Err(_) => Err(Error::Unspecified),
} }
} }
pub fn count(&self) -> anyhow::Result<i64> { pub fn count(&self) -> anyhow::Result<i64> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
@ -69,13 +90,23 @@ impl Manager {
Ok(count) Ok(count)
} }
pub fn exists(&self, username: &str) -> anyhow::Result<bool> { pub fn list(&self) -> Result<Vec<User>, Error> {
use crate::db::users::dsl::*;
let connection = self.db.connect()?;
users
.select((name, password_hash, admin))
.get_results(&connection)
.map_err(|_| Error::Unspecified)
}
pub fn exists(&self, username: &str) -> Result<bool, Error> {
use crate::db::users::dsl::*; use crate::db::users::dsl::*;
let connection = self.db.connect()?; let connection = self.db.connect()?;
let results: Vec<String> = users let results: Vec<String> = users
.select(name) .select(name)
.filter(name.eq(username)) .filter(name.eq(username))
.get_results(&connection)?; .get_results(&connection)
.map_err(|_| Error::Unspecified)?;
Ok(results.len() > 0) Ok(results.len() > 0)
} }
@ -134,11 +165,11 @@ impl Manager {
} }
} }
fn hash_password(password: &str) -> anyhow::Result<String> { fn hash_password(password: &str) -> Result<String, Error> {
match pbkdf2::pbkdf2_simple(password, HASH_ITERATIONS) { if password.is_empty() {
Ok(hash) => Ok(hash), return Err(Error::EmptyPassword);
Err(e) => Err(e.into()),
} }
pbkdf2::pbkdf2_simple(password, HASH_ITERATIONS).map_err(|_| Error::Unspecified)
} }
fn verify_password(password_hash: &str, attempted_password: &str) -> bool { fn verify_password(password_hash: &str, attempted_password: &str) -> bool {

View file

@ -1,3 +1,5 @@
use serde::Deserialize;
use crate::db::users; use crate::db::users;
mod error; mod error;
@ -17,3 +19,16 @@ pub struct User {
pub password_hash: String, pub password_hash: String,
pub admin: i32, pub admin: i32,
} }
impl User {
pub fn is_admin(&self) -> bool {
self.admin != 0
}
}
#[derive(Debug, Deserialize)]
pub struct NewUser {
pub name: String,
pub password: String,
pub admin: bool,
}

View file

@ -1,11 +1,92 @@
use super::*; use super::*;
use crate::db; use crate::db::DB;
use crate::test_name; use crate::test_name;
#[cfg(test)]
pub fn get_test_db(name: &str) -> DB {
let mut db_path = std::path::PathBuf::new();
db_path.push("test-output");
std::fs::create_dir_all(&db_path).unwrap();
db_path.push(name);
if db_path.exists() {
std::fs::remove_file(&db_path).unwrap();
}
DB::new(&db_path).unwrap()
}
#[test] #[test]
fn test_preferences_read_write() { fn create_delete_user_golden_path() {
let db = db::get_test_db(&test_name!()); let db = get_test_db(&test_name!());
let manager = Manager::new(db); let user_manager = Manager::new(db);
let new_user = NewUser {
name: "Walter".to_owned(),
password: "super_secret!".to_owned(),
admin: false,
};
assert_eq!(user_manager.list().unwrap().len(), 0);
user_manager.create(&new_user).unwrap();
assert_eq!(user_manager.list().unwrap().len(), 1);
user_manager.delete(&new_user.name).unwrap();
assert_eq!(user_manager.list().unwrap().len(), 0);
}
#[test]
fn cannot_create_user_with_blank_username() {
let db = get_test_db(&test_name!());
let user_manager = Manager::new(db);
let new_user = NewUser {
name: "".to_owned(),
password: "super_secret!".to_owned(),
admin: false,
};
assert_eq!(
user_manager.create(&new_user).unwrap_err(),
Error::EmptyUsername
);
}
#[test]
fn cannot_create_user_with_blank_password() {
let db = get_test_db(&test_name!());
let user_manager = Manager::new(db);
let new_user = NewUser {
name: "Walter".to_owned(),
password: "".to_owned(),
admin: false,
};
assert_eq!(
user_manager.create(&new_user).unwrap_err(),
Error::EmptyPassword
);
}
#[test]
fn cannot_create_duplicate_user() {
let db = get_test_db(&test_name!());
let user_manager = Manager::new(db);
let new_user = NewUser {
name: "Walter".to_owned(),
password: "super_secret!".to_owned(),
admin: false,
};
user_manager.create(&new_user).unwrap();
user_manager.create(&new_user).unwrap_err();
}
#[test]
fn can_read_write_preferences() {
let db = get_test_db(&test_name!());
let user_manager = Manager::new(db);
let new_preferences = Preferences { let new_preferences = Preferences {
web_theme_base: Some("very-dark-theme".to_owned()), web_theme_base: Some("very-dark-theme".to_owned()),
@ -13,12 +94,17 @@ fn test_preferences_read_write() {
lastfm_username: None, lastfm_username: None,
}; };
manager.create_user("Walter", "super_secret!").unwrap(); let new_user = NewUser {
name: "Walter".to_owned(),
password: "super_secret!".to_owned(),
admin: false,
};
user_manager.create(&new_user).unwrap();
manager user_manager
.write_preferences("Walter", &new_preferences) .write_preferences("Walter", &new_preferences)
.unwrap(); .unwrap();
let read_preferences = manager.read_preferences("Walter").unwrap(); let read_preferences = user_manager.read_preferences("Walter").unwrap();
assert_eq!(new_preferences, read_preferences); assert_eq!(new_preferences, read_preferences);
} }

View file

@ -1,6 +1,5 @@
use anyhow::*; use anyhow::*;
use diesel::prelude::*; use diesel::prelude::*;
use std::path::Path;
use super::*; use super::*;
use crate::db::mount_points; use crate::db::mount_points;
@ -17,15 +16,27 @@ impl Manager {
} }
pub fn get_vfs(&self) -> Result<VFS> { pub fn get_vfs(&self) -> Result<VFS> {
let mount_dirs = self.mount_dirs()?;
let mounts = mount_dirs.into_iter().map(|p| p.into()).collect();
Ok(VFS::new(mounts))
}
pub fn mount_dirs(&self) -> Result<Vec<MountDir>> {
use self::mount_points::dsl::*; use self::mount_points::dsl::*;
let mut vfs = VFS::new();
let connection = self.db.connect()?; let connection = self.db.connect()?;
let points: Vec<MountPoint> = mount_points let mount_dirs: Vec<MountDir> = mount_points
.select((source, name)) .select((source, name))
.get_results(&connection)?; .get_results(&connection)?;
for point in points { Ok(mount_dirs)
vfs.mount(&Path::new(&point.source), &point.name)?; }
}
Ok(vfs) pub fn set_mount_dirs(&self, mount_dirs: &Vec<MountDir>) -> Result<()> {
use self::mount_points::dsl::*;
let connection = self.db.connect()?;
diesel::delete(mount_points).execute(&connection)?;
diesel::insert_into(mount_points)
.values(mount_dirs)
.execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
Ok(())
} }
} }

View file

@ -1,8 +1,8 @@
use anyhow::*; use anyhow::*;
use core::ops::Deref;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::path::{self, Path, PathBuf};
use std::path::Path;
use std::path::PathBuf;
use crate::db::mount_points; use crate::db::mount_points;
@ -14,32 +14,44 @@ pub use manager::*;
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)] #[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)]
#[table_name = "mount_points"] #[table_name = "mount_points"]
pub struct MountPoint { pub struct MountDir {
pub source: String, pub source: String,
pub name: String, pub name: String,
} }
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Mount {
pub source: PathBuf,
pub name: String,
}
impl From<MountDir> for Mount {
fn from(m: MountDir) -> Self {
let separator_regex = Regex::new(r"\\|/").unwrap();
let mut correct_separator = String::new();
correct_separator.push(path::MAIN_SEPARATOR);
let path_string = separator_regex.replace_all(&m.source, correct_separator.as_str());
let source = PathBuf::from(path_string.deref());
Self {
name: m.name,
source: source,
}
}
}
pub struct VFS { pub struct VFS {
mount_points: HashMap<String, PathBuf>, mounts: Vec<Mount>,
} }
impl VFS { impl VFS {
pub fn new() -> VFS { pub fn new(mounts: Vec<Mount>) -> VFS {
VFS { VFS { mounts }
mount_points: HashMap::new(),
}
}
pub fn mount(&mut self, real_path: &Path, name: &str) -> Result<()> {
self.mount_points
.insert(name.to_owned(), real_path.to_path_buf());
Ok(())
} }
pub fn real_to_virtual<P: AsRef<Path>>(&self, real_path: P) -> Result<PathBuf> { pub fn real_to_virtual<P: AsRef<Path>>(&self, real_path: P) -> Result<PathBuf> {
for (name, target) in &self.mount_points { for mount in &self.mounts {
if let Ok(p) = real_path.as_ref().strip_prefix(target) { if let Ok(p) = real_path.as_ref().strip_prefix(&mount.source) {
let mount_path = Path::new(&name); let mount_path = Path::new(&mount.name);
return if p.components().count() == 0 { return if p.components().count() == 0 {
Ok(mount_path.to_path_buf()) Ok(mount_path.to_path_buf())
} else { } else {
@ -51,20 +63,20 @@ impl VFS {
} }
pub fn virtual_to_real<P: AsRef<Path>>(&self, virtual_path: P) -> Result<PathBuf> { pub fn virtual_to_real<P: AsRef<Path>>(&self, virtual_path: P) -> Result<PathBuf> {
for (name, target) in &self.mount_points { for mount in &self.mounts {
let mount_path = Path::new(&name); let mount_path = Path::new(&mount.name);
if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) { if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) {
return if p.components().count() == 0 { return if p.components().count() == 0 {
Ok(target.clone()) Ok(mount.source.clone())
} else { } else {
Ok(target.join(p)) Ok(mount.source.join(p))
}; };
} }
} }
bail!("Virtual path has no match in VFS") bail!("Virtual path has no match in VFS")
} }
pub fn get_mount_points(&self) -> &HashMap<String, PathBuf> { pub fn mounts(&self) -> &Vec<Mount> {
&self.mount_points &self.mounts
} }
} }

View file

@ -4,8 +4,10 @@ use super::*;
#[test] #[test]
fn test_virtual_to_real() { fn test_virtual_to_real() {
let mut vfs = VFS::new(); let vfs = VFS::new(vec![Mount {
vfs.mount(Path::new("test_dir"), "root").unwrap(); name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let mut correct_path = PathBuf::new(); let mut correct_path = PathBuf::new();
correct_path.push("test_dir"); correct_path.push("test_dir");
@ -23,8 +25,10 @@ fn test_virtual_to_real() {
#[test] #[test]
fn test_virtual_to_real_no_trail() { fn test_virtual_to_real_no_trail() {
let mut vfs = VFS::new(); let vfs = VFS::new(vec![Mount {
vfs.mount(Path::new("test_dir"), "root").unwrap(); name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let correct_path = Path::new("test_dir"); let correct_path = Path::new("test_dir");
let found_path = vfs.virtual_to_real(Path::new("root")).unwrap(); let found_path = vfs.virtual_to_real(Path::new("root")).unwrap();
assert!(found_path.to_str() == correct_path.to_str()); assert!(found_path.to_str() == correct_path.to_str());
@ -32,8 +36,10 @@ fn test_virtual_to_real_no_trail() {
#[test] #[test]
fn test_real_to_virtual() { fn test_real_to_virtual() {
let mut vfs = VFS::new(); let vfs = VFS::new(vec![Mount {
vfs.mount(Path::new("test_dir"), "root").unwrap(); name: "root".to_owned(),
source: Path::new("test_dir").to_owned(),
}]);
let mut correct_path = PathBuf::new(); let mut correct_path = PathBuf::new();
correct_path.push("root"); correct_path.push("root");
@ -48,3 +54,42 @@ fn test_real_to_virtual() {
let found_path = vfs.real_to_virtual(real_path.as_path()).unwrap(); let found_path = vfs.real_to_virtual(real_path.as_path()).unwrap();
assert!(found_path == correct_path); assert!(found_path == correct_path);
} }
#[test]
fn test_clean_path_string() {
let mut correct_path = path::PathBuf::new();
if cfg!(target_os = "windows") {
correct_path.push("C:\\");
} else {
correct_path.push("/usr");
}
correct_path.push("some");
correct_path.push("path");
let tests = if cfg!(target_os = "windows") {
vec![
r#"C:/some/path"#,
r#"C:\some\path"#,
r#"C:\some\path\"#,
r#"C:\some\path\\\\"#,
r#"C:\some/path//"#,
]
} else {
vec![
r#"/usr/some/path"#,
r#"/usr\some\path"#,
r#"/usr\some\path\"#,
r#"/usr\some\path\\\\"#,
r#"/usr\some/path//"#,
]
};
for test in tests {
let mount_dir = MountDir {
source: test.to_owned(),
name: "name".to_owned(),
};
let mount: Mount = mount_dir.into();
assert_eq!(mount.source, correct_path);
}
}

View file

@ -89,9 +89,7 @@ impl DB {
#[cfg(test)] #[cfg(test)]
pub fn get_test_db(name: &str) -> DB { pub fn get_test_db(name: &str) -> DB {
use crate::app::{config, user}; use crate::app::{config, ddns, settings, user, vfs};
let config_path = Path::new("test-data/config.toml");
let config = config::Config::from_path(&config_path).unwrap();
let mut db_path = std::path::PathBuf::new(); let mut db_path = std::path::PathBuf::new();
db_path.push("test-output"); db_path.push("test-output");
@ -103,10 +101,16 @@ pub fn get_test_db(name: &str) -> DB {
} }
let db = DB::new(&db_path).unwrap(); let db = DB::new(&db_path).unwrap();
let settings_manager = settings::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let user_manager = user::Manager::new(db.clone());
let config_manager = config::Manager::new(db.clone(), user_manager); let vfs_manager = vfs::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone());
let config_manager =
config::Manager::new(settings_manager, user_manager, vfs_manager, ddns_manager);
config_manager.amend(&config).unwrap(); let config_path = Path::new("test-data/config.toml");
let config = config::Config::from_path(&config_path).unwrap();
config_manager.apply(&config).unwrap();
db db
} }

View file

@ -148,10 +148,7 @@ fn main() -> Result<()> {
context.index.begin_periodic_updates(); context.index.begin_periodic_updates();
// Start DDNS updates // Start DDNS updates
let ddns_manager = app::ddns::Manager::new(context.db.clone()); context.ddns_manager.begin_periodic_updates();
std::thread::spawn(move || {
ddns_manager.run();
});
// Start server // Start server
info!("Starting up server"); info!("Starting up server");

View file

@ -19,12 +19,11 @@ use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::pin::Pin; use std::pin::Pin;
use std::str; use std::str;
use time::Duration;
use crate::app::{ use crate::app::{
config, config, ddns,
index::{self, Index}, index::{self, Index},
lastfm, playlist, thumbnail, user, vfs, lastfm, playlist, settings, thumbnail, user, vfs,
}; };
use crate::service::{dto, error::*}; use crate::service::{dto, error::*};
@ -34,8 +33,17 @@ pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone {
cfg.app_data(JsonConfig::default().limit(4 * megabyte)) // 4MB cfg.app_data(JsonConfig::default().limit(4 * megabyte)) // 4MB
.service(version) .service(version)
.service(initial_setup) .service(initial_setup)
.service(apply_config)
.service(get_settings) .service(get_settings)
.service(put_settings) .service(put_settings)
.service(list_mount_dirs)
.service(put_mount_dirs)
.service(get_ddns_config)
.service(put_ddns_config)
.service(list_users)
.service(create_user)
.service(update_user)
.service(delete_user)
.service(get_preferences) .service(get_preferences)
.service(put_preferences) .service(put_preferences)
.service(trigger_index) .service(trigger_index)
@ -65,6 +73,9 @@ impl ResponseError for APIError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED, APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED,
APIError::EmptyUsername => StatusCode::BAD_REQUEST,
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
APIError::OwnAdminPrivilegeRemoval => StatusCode::CONFLICT, APIError::OwnAdminPrivilegeRemoval => StatusCode::CONFLICT,
APIError::AudioFileIOError => StatusCode::NOT_FOUND, APIError::AudioFileIOError => StatusCode::NOT_FOUND,
APIError::ThumbnailFileIOError => StatusCode::NOT_FOUND, APIError::ThumbnailFileIOError => StatusCode::NOT_FOUND,
@ -190,7 +201,7 @@ impl FromRequest for Auth {
.map(|s| s.as_ref()) .map(|s| s.as_ref())
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
let auth_result = block(move || user_manager.auth(&username, &password)).await?; let auth_result = block(move || user_manager.login(&username, &password)).await?;
if auth_result { if auth_result {
Ok(Auth { Ok(Auth {
username: auth.user_id().to_string(), username: auth.user_id().to_string(),
@ -294,15 +305,13 @@ fn add_auth_cookies<T>(
username: &str, username: &str,
is_admin: bool, is_admin: bool,
) -> Result<(), HttpError> { ) -> Result<(), HttpError> {
let duration = Duration::days(1);
let mut cookies = cookies.clone(); let mut cookies = cookies.clone();
cookies.add_signed( cookies.add_signed(
Cookie::build(dto::COOKIE_SESSION, username.to_owned()) Cookie::build(dto::COOKIE_SESSION, username.to_owned())
.same_site(cookie::SameSite::Lax) .same_site(cookie::SameSite::Lax)
.http_only(true) .http_only(true)
.max_age(duration) .permanent()
.finish(), .finish(),
); );
@ -310,7 +319,7 @@ fn add_auth_cookies<T>(
Cookie::build(dto::COOKIE_USERNAME, username.to_owned()) Cookie::build(dto::COOKIE_USERNAME, username.to_owned())
.same_site(cookie::SameSite::Lax) .same_site(cookie::SameSite::Lax)
.http_only(false) .http_only(false)
.max_age(duration) .permanent()
.path("/") .path("/")
.finish(), .finish(),
); );
@ -319,7 +328,7 @@ fn add_auth_cookies<T>(
Cookie::build(dto::COOKIE_ADMIN, format!("{}", is_admin)) Cookie::build(dto::COOKIE_ADMIN, format!("{}", is_admin))
.same_site(cookie::SameSite::Lax) .same_site(cookie::SameSite::Lax)
.http_only(false) .http_only(false)
.max_age(duration) .permanent()
.path("/") .path("/")
.finish(), .finish(),
); );
@ -360,42 +369,150 @@ async fn initial_setup(
user_manager: Data<user::Manager>, user_manager: Data<user::Manager>,
) -> Result<Json<dto::InitialSetup>, APIError> { ) -> Result<Json<dto::InitialSetup>, APIError> {
let initial_setup = block(move || -> Result<dto::InitialSetup, APIError> { let initial_setup = block(move || -> Result<dto::InitialSetup, APIError> {
let user_count = user_manager.count()?; let users = user_manager.list()?;
let has_any_admin = users.iter().any(|u| u.is_admin());
Ok(dto::InitialSetup { Ok(dto::InitialSetup {
has_any_users: user_count > 0, has_any_users: has_any_admin,
}) })
}) })
.await?; .await?;
Ok(Json(initial_setup)) Ok(Json(initial_setup))
} }
#[put("/config")]
async fn apply_config(
_admin_rights: AdminRights,
config_manager: Data<config::Manager>,
config: Json<dto::Config>,
) -> Result<HttpResponse, APIError> {
block(move || config_manager.apply(&config.to_owned().into())).await?;
Ok(HttpResponse::new(StatusCode::OK))
}
#[get("/settings")] #[get("/settings")]
async fn get_settings( async fn get_settings(
config_manager: Data<config::Manager>, settings_manager: Data<settings::Manager>,
_admin_rights: AdminRights, _admin_rights: AdminRights,
) -> Result<Json<config::Config>, APIError> { ) -> Result<Json<dto::Settings>, APIError> {
let config = block(move || config_manager.read()).await?; let settings = block(move || settings_manager.read()).await?;
Ok(Json(config)) Ok(Json(settings.into()))
} }
#[put("/settings")] #[put("/settings")]
async fn put_settings( async fn put_settings(
admin_rights: AdminRights, _admin_rights: AdminRights,
config_manager: Data<config::Manager>, settings_manager: Data<settings::Manager>,
config: Json<config::Config>, new_settings: Json<dto::NewSettings>,
) -> Result<HttpResponse, APIError> {
block(move || settings_manager.amend(&new_settings.to_owned().into())).await?;
Ok(HttpResponse::new(StatusCode::OK))
}
#[get("/mount_dirs")]
async fn list_mount_dirs(
vfs_manager: Data<vfs::Manager>,
_admin_rights: AdminRights,
) -> Result<Json<Vec<dto::MountDir>>, APIError> {
let mount_dirs = block(move || vfs_manager.mount_dirs()).await?;
let mount_dirs = mount_dirs.into_iter().map(|m| m.into()).collect();
Ok(Json(mount_dirs))
}
#[put("/mount_dirs")]
async fn put_mount_dirs(
_admin_rights: AdminRights,
vfs_manager: Data<vfs::Manager>,
new_mount_dirs: Json<Vec<dto::MountDir>>,
) -> Result<HttpResponse, APIError> {
let new_mount_dirs = new_mount_dirs
.to_owned()
.into_iter()
.map(|m| m.into())
.collect();
block(move || vfs_manager.set_mount_dirs(&new_mount_dirs)).await?;
Ok(HttpResponse::new(StatusCode::OK))
}
#[get("/ddns")]
async fn get_ddns_config(
ddns_manager: Data<ddns::Manager>,
_admin_rights: AdminRights,
) -> Result<Json<dto::DDNSConfig>, APIError> {
let ddns_config = block(move || ddns_manager.config()).await?;
Ok(Json(ddns_config.into()))
}
#[put("/ddns")]
async fn put_ddns_config(
_admin_rights: AdminRights,
ddns_manager: Data<ddns::Manager>,
new_ddns_config: Json<dto::DDNSConfig>,
) -> Result<HttpResponse, APIError> {
block(move || ddns_manager.set_config(&new_ddns_config.to_owned().into())).await?;
Ok(HttpResponse::new(StatusCode::OK))
}
#[get("/users")]
async fn list_users(
user_manager: Data<user::Manager>,
_admin_rights: AdminRights,
) -> Result<Json<Vec<dto::User>>, APIError> {
let users = block(move || user_manager.list()).await?;
let users = users.into_iter().map(|u| u.into()).collect();
Ok(Json(users))
}
#[post("/user")]
async fn create_user(
user_manager: Data<user::Manager>,
_admin_rights: AdminRights,
new_user: Json<dto::NewUser>,
) -> Result<HttpResponse, APIError> {
let new_user = new_user.to_owned().into();
block(move || user_manager.create(&new_user)).await?;
Ok(HttpResponse::new(StatusCode::OK))
}
#[put("/user/{name}")]
async fn update_user(
user_manager: Data<user::Manager>,
admin_rights: AdminRights,
name: web::Path<String>,
user_update: Json<dto::UserUpdate>,
) -> Result<HttpResponse, APIError> { ) -> Result<HttpResponse, APIError> {
// Do not let users remove their own admin rights
if let Some(auth) = &admin_rights.auth { if let Some(auth) = &admin_rights.auth {
if let Some(users) = &config.users { if auth.username == name.as_str() {
for user in users { if user_update.new_is_admin == Some(false) {
if auth.username == user.name && !user.admin { return Err(APIError::OwnAdminPrivilegeRemoval);
return Err(APIError::OwnAdminPrivilegeRemoval);
}
} }
} }
} }
block(move || config_manager.amend(&config)).await?; block(move || -> Result<(), APIError> {
if let Some(password) = &user_update.new_password {
user_manager.set_password(&name, password)?;
}
if let Some(is_admin) = &user_update.new_is_admin {
user_manager.set_is_admin(&name, *is_admin)?;
}
Ok(())
})
.await?;
Ok(HttpResponse::new(StatusCode::OK))
}
#[delete("/user/{name}")]
async fn delete_user(
user_manager: Data<user::Manager>,
admin_rights: AdminRights,
name: web::Path<String>,
) -> Result<HttpResponse, APIError> {
if let Some(auth) = &admin_rights.auth {
if auth.username == name.as_str() {
return Err(APIError::DeletingOwnAccount);
}
}
block(move || user_manager.delete(&name)).await?;
Ok(HttpResponse::new(StatusCode::OK)) Ok(HttpResponse::new(StatusCode::OK))
} }
@ -435,7 +552,7 @@ async fn login(
) -> Result<HttpResponse, APIError> { ) -> Result<HttpResponse, APIError> {
let username = credentials.username.clone(); let username = credentials.username.clone();
let is_admin = block(move || { let is_admin = block(move || {
if !user_manager.auth(&credentials.username, &credentials.password)? { if !user_manager.login(&credentials.username, &credentials.password)? {
return Err(APIError::IncorrectCredentials); return Err(APIError::IncorrectCredentials);
} }
user_manager user_manager

View file

@ -16,11 +16,13 @@ pub mod test;
pub fn make_config(context: service::Context) -> impl FnOnce(&mut ServiceConfig) + Clone { pub fn make_config(context: service::Context) -> impl FnOnce(&mut ServiceConfig) + Clone {
move |cfg: &mut ServiceConfig| { move |cfg: &mut ServiceConfig| {
let encryption_key = cookie::Key::derive_from(&context.auth_secret[..]); let encryption_key = cookie::Key::derive_from(&context.auth_secret.key[..]);
cfg.app_data(web::Data::new(context.index)) cfg.app_data(web::Data::new(context.index))
.app_data(web::Data::new(context.config_manager)) .app_data(web::Data::new(context.config_manager))
.app_data(web::Data::new(context.ddns_manager))
.app_data(web::Data::new(context.lastfm_manager)) .app_data(web::Data::new(context.lastfm_manager))
.app_data(web::Data::new(context.playlist_manager)) .app_data(web::Data::new(context.playlist_manager))
.app_data(web::Data::new(context.settings_manager))
.app_data(web::Data::new(context.thumbnail_manager)) .app_data(web::Data::new(context.thumbnail_manager))
.app_data(web::Data::new(context.user_manager)) .app_data(web::Data::new(context.user_manager))
.app_data(web::Data::new(context.vfs_manager)) .app_data(web::Data::new(context.vfs_manager))

View file

@ -50,7 +50,8 @@ impl ActixTestService {
Method::PUT => self.server.put(url), Method::PUT => self.server.put(url),
Method::DELETE => self.server.delete(url), Method::DELETE => self.server.delete(url),
_ => unimplemented!(), _ => unimplemented!(),
}; }
.timeout(std::time::Duration::from_secs(30));
for (name, value) in request.headers() { for (name, value) in request.headers() {
actix_request = actix_request.set_header(name, value.clone()); actix_request = actix_request.set_header(name, value.clone());

View file

@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub const API_MAJOR_VERSION: i32 = 5; use crate::app::{config, ddns, settings, user, vfs};
pub const API_MAJOR_VERSION: i32 = 6;
pub const API_MINOR_VERSION: i32 = 0; pub const API_MINOR_VERSION: i32 = 0;
pub const COOKIE_SESSION: &str = "session"; pub const COOKIE_SESSION: &str = "session";
pub const COOKIE_USERNAME: &str = "username"; pub const COOKIE_USERNAME: &str = "username";
@ -44,4 +46,144 @@ pub struct LastFMLink {
pub content: String, pub content: String,
} }
// TODO: Config, Preferences, CollectionFile, Song and Directory should have dto types #[derive(Serialize, Deserialize)]
pub struct User {
pub name: String,
pub is_admin: bool,
}
impl From<user::User> for User {
fn from(u: user::User) -> Self {
Self {
name: u.name,
is_admin: u.admin != 0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct NewUser {
pub name: String,
pub password: String,
pub admin: bool,
}
impl From<NewUser> for user::NewUser {
fn from(u: NewUser) -> Self {
Self {
name: u.name,
password: u.password,
admin: u.admin,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct UserUpdate {
pub new_password: Option<String>,
pub new_is_admin: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct DDNSConfig {
pub host: String,
pub username: String,
pub password: String,
}
impl From<DDNSConfig> for ddns::Config {
fn from(c: DDNSConfig) -> Self {
Self {
host: c.host,
username: c.username,
password: c.password,
}
}
}
impl From<ddns::Config> for DDNSConfig {
fn from(c: ddns::Config) -> Self {
Self {
host: c.host,
username: c.username,
password: c.password,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct MountDir {
pub source: String,
pub name: String,
}
impl From<MountDir> for vfs::MountDir {
fn from(m: MountDir) -> Self {
Self {
name: m.name,
source: m.source,
}
}
}
impl From<vfs::MountDir> for MountDir {
fn from(m: vfs::MountDir) -> Self {
Self {
name: m.name,
source: m.source,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub settings: Option<NewSettings>,
pub users: Option<Vec<NewUser>>,
pub mount_dirs: Option<Vec<MountDir>>,
pub ydns: Option<DDNSConfig>,
}
impl From<Config> for config::Config {
fn from(s: Config) -> Self {
Self {
settings: s.settings.map(|s| s.into()),
mount_dirs: s
.mount_dirs
.map(|v| v.into_iter().map(|m| m.into()).collect()),
users: s.users.map(|v| v.into_iter().map(|u| u.into()).collect()),
ydns: s.ydns.map(|c| c.into()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct NewSettings {
pub album_art_pattern: Option<String>,
pub reindex_every_n_seconds: Option<i32>,
}
impl From<NewSettings> for settings::NewSettings {
fn from(s: NewSettings) -> Self {
Self {
album_art_pattern: s.album_art_pattern,
reindex_every_n_seconds: s.reindex_every_n_seconds,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Settings {
pub album_art_pattern: String,
pub reindex_every_n_seconds: i32,
}
impl From<settings::Settings> for Settings {
fn from(s: settings::Settings) -> Self {
Self {
album_art_pattern: s.album_art_pattern,
reindex_every_n_seconds: s.reindex_every_n_seconds,
}
}
}
// TODO: Preferences, CollectionFile, Song and Directory should have dto types

View file

@ -1,13 +1,19 @@
use thiserror::Error; use thiserror::Error;
use crate::app::index::QueryError; use crate::app::index::QueryError;
use crate::app::playlist; use crate::app::{config, playlist, settings, user};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum APIError { pub enum APIError {
#[error("Incorrect Credentials")] #[error("Incorrect Credentials")]
IncorrectCredentials, IncorrectCredentials,
#[error("Cannot remove own admin privilege")] #[error("EmptyUsername")]
EmptyUsername,
#[error("EmptyPassword")]
EmptyPassword,
#[error("Cannot delete your own account")]
DeletingOwnAccount,
#[error("Cannot remove your own admin privilege")]
OwnAdminPrivilegeRemoval, OwnAdminPrivilegeRemoval,
#[error("Audio file could not be opened")] #[error("Audio file could not be opened")]
AudioFileIOError, AudioFileIOError,
@ -35,6 +41,14 @@ impl From<anyhow::Error> for APIError {
} }
} }
impl From<config::Error> for APIError {
fn from(error: config::Error) -> APIError {
match error {
config::Error::Unspecified => APIError::Unspecified,
}
}
}
impl From<playlist::Error> for APIError { impl From<playlist::Error> for APIError {
fn from(error: playlist::Error) -> APIError { fn from(error: playlist::Error) -> APIError {
match error { match error {
@ -53,3 +67,27 @@ impl From<QueryError> for APIError {
} }
} }
} }
impl From<settings::Error> for APIError {
fn from(error: settings::Error) -> APIError {
match error {
settings::Error::AuthSecretNotFound => APIError::Unspecified,
settings::Error::InvalidAuthSecret => APIError::Unspecified,
settings::Error::IndexSleepDurationNotFound => APIError::Unspecified,
settings::Error::IndexAlbumArtPatternNotFound => APIError::Unspecified,
settings::Error::IndexAlbumArtPatternInvalid => APIError::Unspecified,
settings::Error::Unspecified => APIError::Unspecified,
}
}
}
impl From<user::Error> for APIError {
fn from(error: user::Error) -> APIError {
match error {
user::Error::EmptyUsername => APIError::EmptyUsername,
user::Error::EmptyPassword => APIError::EmptyPassword,
user::Error::IncorrectUsername => APIError::IncorrectCredentials,
user::Error::Unspecified => APIError::Unspecified,
}
}
}

View file

@ -1,7 +1,7 @@
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use crate::app::{config, index::Index, lastfm, playlist, thumbnail, user, vfs}; use crate::app::{config, ddns, index::Index, lastfm, playlist, settings, thumbnail, user, vfs};
use crate::db::DB; use crate::db::DB;
mod dto; mod dto;
@ -16,7 +16,7 @@ pub use actix::*;
#[derive(Clone)] #[derive(Clone)]
pub struct Context { pub struct Context {
pub port: u16, pub port: u16,
pub auth_secret: Vec<u8>, pub auth_secret: settings::AuthSecret,
pub web_dir_path: PathBuf, pub web_dir_path: PathBuf,
pub swagger_dir_path: PathBuf, pub swagger_dir_path: PathBuf,
pub web_url: String, pub web_url: String,
@ -25,8 +25,10 @@ pub struct Context {
pub db: DB, pub db: DB,
pub index: Index, pub index: Index,
pub config_manager: config::Manager, pub config_manager: config::Manager,
pub ddns_manager: ddns::Manager,
pub lastfm_manager: lastfm::Manager, pub lastfm_manager: lastfm::Manager,
pub playlist_manager: playlist::Manager, pub playlist_manager: playlist::Manager,
pub settings_manager: settings::Manager,
pub thumbnail_manager: thumbnail::Manager, pub thumbnail_manager: thumbnail::Manager,
pub user_manager: user::Manager, pub user_manager: user::Manager,
pub vfs_manager: vfs::Manager, pub vfs_manager: vfs::Manager,
@ -81,18 +83,26 @@ impl ContextBuilder {
thumbnails_dir_path.push("thumbnails"); thumbnails_dir_path.push("thumbnails");
let vfs_manager = vfs::Manager::new(db.clone()); let vfs_manager = vfs::Manager::new(db.clone());
let settings_manager = settings::Manager::new(db.clone());
let ddns_manager = ddns::Manager::new(db.clone());
let user_manager = user::Manager::new(db.clone()); let user_manager = user::Manager::new(db.clone());
let config_manager = config::Manager::new(db.clone(), user_manager.clone()); let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
let index = Index::new(db.clone(), vfs_manager.clone(), config_manager.clone()); let config_manager = config::Manager::new(
settings_manager.clone(),
user_manager.clone(),
vfs_manager.clone(),
ddns_manager.clone(),
);
let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone()); let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone());
let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path); let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path);
let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone()); let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone());
if let Some(config_path) = self.config_file_path { if let Some(config_path) = self.config_file_path {
let config = config::Config::from_path(&config_path)?; let config = config::Config::from_path(&config_path)?;
config_manager.amend(&config)?; config_manager.apply(&config)?;
} }
let auth_secret = config_manager.get_auth_secret()?;
let auth_secret = settings_manager.get_auth_secret()?;
Ok(Context { Ok(Context {
port: self.port.unwrap_or(5050), port: self.port.unwrap_or(5050),
@ -104,8 +114,10 @@ impl ContextBuilder {
swagger_dir_path, swagger_dir_path,
index, index,
config_manager, config_manager,
ddns_manager,
lastfm_manager, lastfm_manager,
playlist_manager, playlist_manager,
settings_manager,
thumbnail_manager, thumbnail_manager,
user_manager, user_manager,
vfs_manager, vfs_manager,

View file

@ -1,32 +1,42 @@
use cookie::Cookie; use cookie::Cookie;
use headers::{self, HeaderMapExt}; use headers::{self, HeaderMapExt};
use http::{Response, StatusCode}; use http::{Response, StatusCode};
use time::Duration;
use crate::service::dto; use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name; use crate::test_name;
fn validate_cookies<T>(response: &Response<T>) { fn validate_added_cookies<T>(response: &Response<T>) {
let twenty_years = Duration::days(365 * 20);
let cookies: Vec<Cookie> = response let cookies: Vec<Cookie> = response
.headers() .headers()
.get_all(http::header::SET_COOKIE) .get_all(http::header::SET_COOKIE)
.iter() .iter()
.map(|c| Cookie::parse(c.to_str().unwrap()).unwrap()) .map(|c| Cookie::parse(c.to_str().unwrap()).unwrap())
.collect(); .collect();
let session = cookies let session = cookies
.iter() .iter()
.find_map(|c| { .find(|c| c.name() == dto::COOKIE_SESSION)
if c.name() == dto::COOKIE_SESSION {
Some(c.value())
} else {
None
}
})
.unwrap(); .unwrap();
assert_ne!(session, TEST_USERNAME); assert_ne!(session.value(), TEST_USERNAME);
assert_ne!(session, TEST_USERNAME_ADMIN); assert!(session.max_age().unwrap() >= twenty_years);
assert!(cookies.iter().any(|c| c.name() == dto::COOKIE_USERNAME));
assert!(cookies.iter().any(|c| c.name() == dto::COOKIE_ADMIN)); let username = cookies
.iter()
.find(|c| c.name() == dto::COOKIE_USERNAME)
.unwrap();
assert_eq!(username.value(), TEST_USERNAME);
assert!(session.max_age().unwrap() >= twenty_years);
let is_admin = cookies
.iter()
.find(|c| c.name() == dto::COOKIE_ADMIN)
.unwrap();
assert_eq!(is_admin.value(), false.to_string());
assert!(session.max_age().unwrap() >= twenty_years);
} }
fn validate_no_cookies<T>(response: &Response<T>) { fn validate_no_cookies<T>(response: &Response<T>) {
@ -70,7 +80,7 @@ fn test_login_golden_path() {
let response = service.fetch(&request); let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
validate_cookies(&response); validate_added_cookies(&response);
} }
#[test] #[test]
@ -124,5 +134,5 @@ fn test_authentication_via_http_header_golden_path() {
let response = service.fetch(&request); let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
validate_cookies(&response); validate_added_cookies(&response);
} }

63
src/service/test/ddns.rs Normal file
View file

@ -0,0 +1,63 @@
use http::StatusCode;
use crate::service::dto;
use crate::service::test::{protocol, ServiceType, TestService};
use crate::test_name;
#[test]
fn test_get_ddns_config_requires_admin() {
let mut service = ServiceType::new(&test_name!());
let request = protocol::get_ddns_config();
service.complete_initial_setup();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
service.login();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_get_ddns_config_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login_admin();
let request = protocol::get_ddns_config();
let response = service.fetch_json::<_, dto::DDNSConfig>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_put_ddns_config_requires_admin() {
let mut service = ServiceType::new(&test_name!());
let request = protocol::put_ddns_config(dto::DDNSConfig {
host: "test".to_owned(),
username: "test".to_owned(),
password: "test".to_owned(),
});
service.complete_initial_setup();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
service.login();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_put_ddns_config_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login_admin();
let request = protocol::put_ddns_config(dto::DDNSConfig {
host: "test".to_owned(),
username: "test".to_owned(),
password: "test".to_owned(),
});
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}

View file

@ -10,15 +10,17 @@ pub mod protocol;
mod admin; mod admin;
mod auth; mod auth;
mod collection; mod collection;
mod ddns;
mod lastfm; mod lastfm;
mod media; mod media;
mod playlist; mod playlist;
mod preferences;
mod settings; mod settings;
mod swagger; mod swagger;
mod user;
mod web; mod web;
use crate::app::{config, index, vfs}; use crate::app::index;
use crate::service::dto;
use crate::service::test::constants::*; use crate::service::test::constants::*;
pub use crate::service::actix::test::ServiceType; pub use crate::service::actix::test::ServiceType;
@ -36,28 +38,26 @@ pub trait TestService {
) -> Response<U>; ) -> Response<U>;
fn complete_initial_setup(&mut self) { fn complete_initial_setup(&mut self) {
let configuration = config::Config { let configuration = dto::Config {
album_art_pattern: None,
reindex_every_n_seconds: None,
ydns: None,
users: Some(vec![ users: Some(vec![
config::ConfigUser { dto::NewUser {
name: TEST_USERNAME_ADMIN.into(), name: TEST_USERNAME_ADMIN.into(),
password: TEST_PASSWORD_ADMIN.into(), password: TEST_PASSWORD_ADMIN.into(),
admin: true, admin: true,
}, },
config::ConfigUser { dto::NewUser {
name: TEST_USERNAME.into(), name: TEST_USERNAME.into(),
password: TEST_PASSWORD.into(), password: TEST_PASSWORD.into(),
admin: false, admin: false,
}, },
]), ]),
mount_dirs: Some(vec![vfs::MountPoint { mount_dirs: Some(vec![dto::MountDir {
name: TEST_MOUNT_NAME.into(), name: TEST_MOUNT_NAME.into(),
source: TEST_MOUNT_SOURCE.into(), source: TEST_MOUNT_SOURCE.into(),
}]), }]),
..Default::default()
}; };
let request = protocol::put_settings(configuration); let request = protocol::apply_config(configuration);
let response = self.fetch(&request); let response = self.fetch(&request);
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }

View file

@ -1,43 +0,0 @@
use http::StatusCode;
use crate::app::user;
use crate::service::test::{protocol, ServiceType, TestService};
use crate::test_name;
#[test]
fn test_get_preferences_requires_auth() {
let mut service = ServiceType::new(&test_name!());
let request = protocol::get_preferences();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_get_preferences_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login();
let request = protocol::get_preferences();
let response = service.fetch_json::<_, user::Preferences>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_put_preferences_requires_auth() {
let mut service = ServiceType::new(&test_name!());
let request = protocol::put_preferences(user::Preferences::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_put_preferences_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login();
let request = protocol::put_preferences(user::Preferences::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}

View file

@ -2,7 +2,7 @@ use http::{method::Method, Request};
use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use std::path::Path; use std::path::Path;
use crate::app::{config, user}; use crate::app::user;
use crate::service::dto; use crate::service::dto;
pub fn web_index() -> Request<()> { pub fn web_index() -> Request<()> {
@ -49,6 +49,14 @@ pub fn login(username: &str, password: &str) -> Request<dto::AuthCredentials> {
.unwrap() .unwrap()
} }
pub fn apply_config(config: dto::Config) -> Request<dto::Config> {
Request::builder()
.method(Method::PUT)
.uri("/api/config")
.body(config)
.unwrap()
}
pub fn get_settings() -> Request<()> { pub fn get_settings() -> Request<()> {
Request::builder() Request::builder()
.method(Method::GET) .method(Method::GET)
@ -57,11 +65,59 @@ pub fn get_settings() -> Request<()> {
.unwrap() .unwrap()
} }
pub fn put_settings(configuration: config::Config) -> Request<config::Config> { pub fn put_settings(settings: dto::NewSettings) -> Request<dto::NewSettings> {
Request::builder() Request::builder()
.method(Method::PUT) .method(Method::PUT)
.uri("/api/settings") .uri("/api/settings")
.body(configuration) .body(settings)
.unwrap()
}
pub fn get_ddns_config() -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/ddns")
.body(())
.unwrap()
}
pub fn put_ddns_config(ddns_config: dto::DDNSConfig) -> Request<dto::DDNSConfig> {
Request::builder()
.method(Method::PUT)
.uri("/api/ddns")
.body(ddns_config)
.unwrap()
}
pub fn list_users() -> Request<()> {
Request::builder()
.method(Method::GET)
.uri("/api/users")
.body(())
.unwrap()
}
pub fn create_user(new_user: dto::NewUser) -> Request<dto::NewUser> {
Request::builder()
.method(Method::POST)
.uri("/api/user")
.body(new_user)
.unwrap()
}
pub fn update_user(username: &str, user_update: dto::UserUpdate) -> Request<dto::UserUpdate> {
Request::builder()
.method(Method::PUT)
.uri(format!("/api/user/{}", username))
.body(user_update)
.unwrap()
}
pub fn delete_user(username: &str) -> Request<()> {
Request::builder()
.method(Method::DELETE)
.uri(format!("/api/user/{}", username))
.body(())
.unwrap() .unwrap()
} }

View file

@ -1,7 +1,7 @@
use http::StatusCode; use http::StatusCode;
use crate::app::config; use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::service::test::{protocol, ServiceType, TestService};
use crate::test_name; use crate::test_name;
#[test] #[test]
@ -32,7 +32,7 @@ fn test_get_settings_golden_path() {
service.login_admin(); service.login_admin();
let request = protocol::get_settings(); let request = protocol::get_settings();
let response = service.fetch_json::<_, config::Config>(&request); let response = service.fetch_json::<_, dto::Settings>(&request);
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }
@ -40,7 +40,7 @@ fn test_get_settings_golden_path() {
fn test_put_settings_requires_auth() { fn test_put_settings_requires_auth() {
let mut service = ServiceType::new(&test_name!()); let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup(); service.complete_initial_setup();
let request = protocol::put_settings(config::Config::default()); let request = protocol::put_settings(dto::NewSettings::default());
let response = service.fetch(&request); let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED); assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
} }
@ -50,7 +50,7 @@ fn test_put_settings_requires_admin() {
let mut service = ServiceType::new(&test_name!()); let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup(); service.complete_initial_setup();
service.login(); service.login();
let request = protocol::put_settings(config::Config::default()); let request = protocol::put_settings(dto::NewSettings::default());
let response = service.fetch(&request); let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN); assert_eq!(response.status(), StatusCode::FORBIDDEN);
} }
@ -61,24 +61,7 @@ fn test_put_settings_golden_path() {
service.complete_initial_setup(); service.complete_initial_setup();
service.login_admin(); service.login_admin();
let request = protocol::put_settings(config::Config::default()); let request = protocol::put_settings(dto::NewSettings::default());
let response = service.fetch(&request); let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }
#[test]
fn test_put_settings_cannot_unadmin_self() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login_admin();
let mut configuration = config::Config::default();
configuration.users = Some(vec![config::ConfigUser {
name: TEST_USERNAME_ADMIN.into(),
password: "".into(),
admin: false,
}]);
let request = protocol::put_settings(configuration);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::CONFLICT);
}

180
src/service/test/user.rs Normal file
View file

@ -0,0 +1,180 @@
use http::StatusCode;
use std::default::Default;
use crate::app::user;
use crate::service::dto;
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name;
#[test]
fn list_users_requires_admin() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::list_users();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
service.login();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn list_users_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login_admin();
let request = protocol::list_users();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn create_user_requires_admin() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::create_user(dto::NewUser {
name: "Walter".into(),
password: "secret".into(),
admin: false,
});
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
service.login();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn create_user_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login_admin();
let new_user = dto::NewUser {
name: "Walter".into(),
password: "secret".into(),
admin: false,
};
let request = protocol::create_user(new_user);
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn update_user_requires_admin() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::update_user("Walter", dto::UserUpdate::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
service.login();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn update_user_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::update_user("Walter", dto::UserUpdate::default());
service.login_admin();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn update_user_cannot_unadmin_self() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::update_user(
TEST_USERNAME_ADMIN,
dto::UserUpdate {
new_is_admin: Some(false),
..Default::default()
},
);
service.login_admin();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::CONFLICT);
}
#[test]
fn delete_user_requires_admin() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::delete_user("Walter");
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
service.login();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn delete_user_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::delete_user("Walter");
service.login_admin();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn delete_user_cannot_delete_self() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
let request = protocol::delete_user(TEST_USERNAME_ADMIN);
service.login_admin();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::CONFLICT);
}
#[test]
fn get_preferences_requires_auth() {
let mut service = ServiceType::new(&test_name!());
let request = protocol::get_preferences();
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn get_preferences_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login();
let request = protocol::get_preferences();
let response = service.fetch_json::<_, user::Preferences>(&request);
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn put_preferences_requires_auth() {
let mut service = ServiceType::new(&test_name!());
let request = protocol::put_preferences(user::Preferences::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn put_preferences_golden_path() {
let mut service = ServiceType::new(&test_name!());
service.complete_initial_setup();
service.login();
let request = protocol::put_preferences(user::Preferences::default());
let response = service.fetch(&request);
assert_eq!(response.status(), StatusCode::OK);
}

View file

@ -1,3 +1,4 @@
[settings]
album_art_pattern = '^Folder\.(png|jpg|jpeg)$' album_art_pattern = '^Folder\.(png|jpg|jpeg)$'
[[mount_dirs]] [[mount_dirs]]