From f33268497425e7d608fe5c9d5211513daaa74fd0 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sun, 23 Apr 2023 15:43:39 +0200 Subject: [PATCH 01/26] Added device and client parametrization. --- src/gpodder/device/device_controller.rs | 55 +++++++++++++++++++++++++ src/gpodder/device/dto/device_post.rs | 6 +++ src/gpodder/device/dto/mod.rs | 1 + src/gpodder/device/mod.rs | 2 + src/gpodder/mod.rs | 3 ++ src/gpodder/parametrization.rs | 37 +++++++++++++++++ src/main.rs | 5 +++ src/models/device.rs | 45 ++++++++++++++++++++ src/models/mod.rs | 1 + src/models/user.rs | 8 ++++ src/schema.rs | 11 +++++ 11 files changed, 174 insertions(+) create mode 100644 src/gpodder/device/device_controller.rs create mode 100644 src/gpodder/device/dto/device_post.rs create mode 100644 src/gpodder/device/dto/mod.rs create mode 100644 src/gpodder/device/mod.rs create mode 100644 src/gpodder/mod.rs create mode 100644 src/gpodder/parametrization.rs create mode 100644 src/models/device.rs diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs new file mode 100644 index 00000000..0b304366 --- /dev/null +++ b/src/gpodder/device/device_controller.rs @@ -0,0 +1,55 @@ +use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use crate::gpodder::device::dto::device_post::DevicePost; +use crate::models::device::Device; +use actix_web::{post, get}; +use actix_web::web::Data; +use crate::controllers::user_controller::get_user; +use crate::controllers::watch_time_controller::get_username; +use crate::DbPool; +use crate::models::user::User; + +#[post("/devices/{username}/{deviceid}.json")] +pub async fn post_device( + query: web::Path<(String, String)>, + device_post: web::Json, + conn: Data, + rq: HttpRequest +) -> impl Responder { + + let username = User::get_gpodder_req_header(&rq); + + if username.is_err(){ + return HttpResponse::Unauthorized().finish(); + } + let username = username.unwrap(); + + if query.clone().0 != username { + return HttpResponse::Unauthorized().finish(); + } + + let username = query.clone().0; + let deviceid = query.clone().1; + + let device = Device::new(device_post.into_inner(), deviceid, username); + + let result = device.save(&mut conn.get().unwrap()).unwrap(); + + HttpResponse::Ok().json(result) +} + +#[get("/devices/{username}.json")] +pub async fn get_devices_of_user(query: web::Path, conn: Data, rq: HttpRequest) -> impl Responder { + let username = get_username(rq); + if username.is_err(){ + return username.err().unwrap() + } + + + let username = query.clone(); + + if query.clone() != username { + return HttpResponse::Unauthorized().finish(); + } + let devices = Device::get_devices_of_user(&mut conn.get().unwrap(), username).unwrap(); + HttpResponse::Ok().json(devices) +} \ No newline at end of file diff --git a/src/gpodder/device/dto/device_post.rs b/src/gpodder/device/dto/device_post.rs new file mode 100644 index 00000000..a8f863fd --- /dev/null +++ b/src/gpodder/device/dto/device_post.rs @@ -0,0 +1,6 @@ +#[derive(Serialize, Deserialize, Clone)] +pub struct DevicePost{ + pub caption: String, + #[serde(rename = "type")] + pub kind: String +} \ No newline at end of file diff --git a/src/gpodder/device/dto/mod.rs b/src/gpodder/device/dto/mod.rs new file mode 100644 index 00000000..5e6d2b5d --- /dev/null +++ b/src/gpodder/device/dto/mod.rs @@ -0,0 +1 @@ +pub mod device_post; \ No newline at end of file diff --git a/src/gpodder/device/mod.rs b/src/gpodder/device/mod.rs new file mode 100644 index 00000000..a06a51c2 --- /dev/null +++ b/src/gpodder/device/mod.rs @@ -0,0 +1,2 @@ +pub mod device_controller; +pub mod dto; \ No newline at end of file diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs new file mode 100644 index 00000000..b74826e2 --- /dev/null +++ b/src/gpodder/mod.rs @@ -0,0 +1,3 @@ +pub mod routes; +pub mod device; +pub mod parametrization; \ No newline at end of file diff --git a/src/gpodder/parametrization.rs b/src/gpodder/parametrization.rs new file mode 100644 index 00000000..3455b09e --- /dev/null +++ b/src/gpodder/parametrization.rs @@ -0,0 +1,37 @@ +use std::sync::Mutex; +use actix_web::{HttpResponse, Responder}; +use actix_web::web::Data; +use crate::mutex::LockResultExt; +use crate::service::environment_service::EnvironmentService; +use actix_web::get; + +#[derive(Serialize, Deserialize)] +pub struct ClientParametrization{ + mygpo: BaseURL, + #[serde(rename = "mygpo-feedservice")] + mygpo_feedservice: BaseURL, + update_timeout: i32, +} + +#[derive(Serialize, Deserialize)] +pub struct BaseURL{ + #[serde(rename = "baseurl")] + base_url: String +} + +#[get("/clientconfig.json")] +pub async fn get_client_parametrization(environment_service: Data>) + ->impl Responder { + let env_service = environment_service.lock().ignore_poison(); + let answer = ClientParametrization { + mygpo_feedservice: BaseURL { + base_url: env_service.clone().server_url + }, + mygpo: BaseURL { + base_url: env_service.clone().server_url + "rss" + }, + update_timeout: 604800 + }; + + HttpResponse::Ok().json(answer) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 80fdbc1e..30ef033e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,8 @@ mod db; mod models; mod service; use crate::db::DB; +use crate::gpodder::parametrization::get_client_parametrization; +use crate::gpodder::routes::get_gpodder_api; use crate::models::oidc_model::{CustomJwk, CustomJwkSet}; use crate::models::user::User; use crate::models::web_socket_message::Lobby; @@ -84,6 +86,7 @@ mod config; pub mod utils; pub mod mutex; mod exception; +mod gpodder; type DbPool = Pool>; @@ -331,6 +334,7 @@ async fn main() -> std::io::Result<()> { App::new() .service(redirect("/", var("SUB_DIRECTORY").unwrap()+"/ui/")) + .service(get_gpodder_api(pool.clone())) .service(get_global_scope(pool.clone())) .app_data(Data::new(chat_server.clone())) .app_data(Data::new(Mutex::new(podcast_episode_service.clone()))) @@ -372,6 +376,7 @@ pub fn get_global_scope(pool1: Pool>) -> Sco web::scope(&base_path) + .service(get_client_parametrization) .service(proxy_podcast) .service(get_ui_config()) .service(Files::new("/podcasts", "podcasts")) diff --git a/src/models/device.rs b/src/models/device.rs new file mode 100644 index 00000000..d168c62c --- /dev/null +++ b/src/models/device.rs @@ -0,0 +1,45 @@ +use diesel::{Queryable, QueryableByName, RunQueryDsl, Insertable, SqliteConnection}; +use diesel::associations::HasTable; +use utoipa::ToSchema; +use crate::gpodder::device::dto::device_post::DevicePost; +use crate::schema::devices; +use diesel::QueryDsl; +use diesel::ExpressionMethods; + +#[derive(Serialize, Deserialize, Queryable,Insertable, QueryableByName, Clone, ToSchema)] +#[diesel(table_name=devices)] +pub struct Device { + #[diesel(deserialize_as = i32)] + pub id: Option, + pub deviceid: String, + pub kind: String, + pub name: String, + pub username: String +} + +impl Device { + + pub fn new(device_post: DevicePost, device_id: String, username: String) -> Device { + Device{ + id: None, + deviceid:device_id, + kind: device_post.kind, + name: device_post.caption, + username, + } + } + + pub fn save(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::devices::dsl::*; + diesel::insert_into(devices) + .values(self) + .get_result(conn) + } + + pub fn get_devices_of_user(conn: &mut SqliteConnection, username_to_insert: String) -> + Result, diesel::result::Error> { + use crate::schema::devices::dsl::*; + devices.filter(username.eq(username_to_insert)) + .load::(conn) + } +} \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 6bd389a4..4d57c0f8 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -10,3 +10,4 @@ pub mod oidc_model; pub mod user; pub mod invite; pub mod favorites; +pub mod device; diff --git a/src/models/user.rs b/src/models/user.rs index 60517630..1ac79bfe 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -141,6 +141,14 @@ impl User{ Ok(None) } + pub fn get_gpodder_req_header(req: &actix_web::HttpRequest) -> Result{ + let auth_header = req.headers().get(USERNAME); + if auth_header.is_none() { + return Err(Error::new(std::io::ErrorKind::Other, "Username not found")); + } + return Ok(auth_header.unwrap().to_str().unwrap().parse().unwrap()) + } + pub fn check_if_admin_or_uploader(username: &Option, conn: &mut SqliteConnection) -> Option { diff --git a/src/schema.rs b/src/schema.rs index 12f4c7b2..a7acc751 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,15 @@ // @generated automatically by Diesel CLI. +diesel::table! { + devices (id) { + id -> Integer, + deviceid -> Text, + kind -> Text, + name -> Text, + username -> Text, + } +} + diesel::table! { favorites (username, podcast_id) { username -> Text, @@ -103,6 +113,7 @@ diesel::joinable!(podcast_episodes -> podcasts (podcast_id)); diesel::joinable!(podcast_history_items -> podcasts (podcast_id)); diesel::allow_tables_to_appear_in_same_query!( + devices, favorites, invites, notifications, From 467139a16081f23ed55160bbf2965dc54ac18840 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sun, 23 Apr 2023 21:22:40 +0200 Subject: [PATCH 02/26] Added device and auth. --- Cargo.lock | 170 ++++++++++++++++++++++++ Cargo.toml | 2 + src/gpodder/auth/auth.rs | 57 ++++++++ src/gpodder/auth/mod.rs | 1 + src/gpodder/device/device_controller.rs | 14 +- src/gpodder/mod.rs | 3 +- src/main.rs | 44 +++--- 7 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 src/gpodder/auth/auth.rs create mode 100644 src/gpodder/auth/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 76dd5196..fde4a851 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,6 +168,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da8b818ae1f11049a4d218975345fe8e56ce5a5f92c11f972abcff5ff80e87" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "async-trait", + "derive_more", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-tls" version = "3.0.3" @@ -301,6 +318,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.6" @@ -357,6 +409,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + [[package]] name = "askama_escape" version = "0.10.3" @@ -386,6 +444,17 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "atom_syndication" version = "0.12.1" @@ -446,6 +515,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.21.0" @@ -552,6 +627,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clokwerk" version = "0.4.0" @@ -583,7 +668,14 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", "percent-encoding", + "rand", + "sha2 0.10.6", + "subtle", "time", "version_check", ] @@ -672,9 +764,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "cxx" version = "1.0.94" @@ -850,6 +952,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.4", "crypto-common", + "subtle", ] [[package]] @@ -1122,6 +1225,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "h2" version = "0.3.17" @@ -1193,6 +1306,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "http" version = "0.2.9" @@ -1327,6 +1458,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1784,6 +1924,7 @@ version = "0.1.0" dependencies = [ "actix", "actix-files", + "actix-session", "actix-web", "actix-web-actors", "actix-web-httpauth", @@ -1800,6 +1941,7 @@ dependencies = [ "frankenstein", "fs_extra", "futures", + "futures-util", "jsonwebtoken", "libsqlite3-sys", "log", @@ -1822,6 +1964,18 @@ dependencies = [ "xml-builder", ] +[[package]] +name = "polyval" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2364,6 +2518,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.109" @@ -2646,6 +2806,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index acda77ba..7ce48cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,11 @@ uuid = {version="1.3.1", features = ["v4", "serde"]} libsqlite3-sys = {version = "0.25.2", features = ["bundled"]} diesel_migrations = "2.0.0" actix-files = "0.6.2" +actix-session = {version="0.7.2",features=["cookie-session"]} actix-web = {version="4.3.0", features=["rustls"]} jsonwebtoken = {version="8.2.0"} log = "0.4.17" +futures-util = "0.3.28" opml = "1.1.5" rand = "0.8.5" env_logger = "0.10.0" diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs new file mode 100644 index 00000000..e4e20255 --- /dev/null +++ b/src/gpodder/auth/auth.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; +use std::sync::Mutex; +use actix_web::dev::ServiceRequest; +use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use actix_web::web::Data; +use sha256::digest; +use crate::{DbPool, extract_basic_auth, validator}; +use crate::models::user::User; +use actix_web::{post}; +use utoipa::openapi::HeaderBuilder; +use uuid::Uuid; +use crate::mutex::LockResultExt; +use crate::service::environment_service::EnvironmentService; +use actix_session::Session; +use awc::cookie::Cookie; + +#[post("/auth/{username}/login.json")] +pub async fn login(username:web::Path, rq: HttpRequest, conn:Data, + env_service: Data>, session:Session) + ->impl +Responder { + let authorization = rq.headers().get("Authorization").unwrap().to_str().unwrap(); + let unwrapped_username = username.into_inner(); + let (username_basic, password) = basic_auth_login(authorization.to_string()); + let env = env_service.lock().ignore_poison(); + if username_basic != unwrapped_username { + return HttpResponse::Unauthorized().finish(); + } + + if unwrapped_username == env.username && password == env.password { + return HttpResponse::Ok().finish(); + } else { + match User::find_by_username(&unwrapped_username, &mut conn.get().unwrap()) { + Some(user) => { + if user.clone().password.unwrap()== digest(password) { + let token = Uuid::new_v4().to_string(); + session.insert(token.clone(),user.clone().username).expect("TODO: panic \ + message"); + let user_cookie = Cookie::new("sessionid", token); + HttpResponse::Ok().cookie(user_cookie).finish() + } else { + HttpResponse::Unauthorized().finish() + } + } + None => { + return HttpResponse::Unauthorized().finish() + } + } + } +} + +pub fn basic_auth_login(rq: String) -> (String, String) { + let (u,p) = extract_basic_auth(rq.as_str()); + + return (u.to_string(),p.to_string()) +} + diff --git a/src/gpodder/auth/mod.rs b/src/gpodder/auth/mod.rs new file mode 100644 index 00000000..5696e21f --- /dev/null +++ b/src/gpodder/auth/mod.rs @@ -0,0 +1 @@ +pub mod auth; \ No newline at end of file diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 0b304366..16271922 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -1,8 +1,10 @@ +use actix_session::{Session, SessionExt}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use crate::gpodder::device::dto::device_post::DevicePost; use crate::models::device::Device; use actix_web::{post, get}; use actix_web::web::Data; +use serde::de::Unexpected::Str; use crate::controllers::user_controller::get_user; use crate::controllers::watch_time_controller::get_username; use crate::DbPool; @@ -13,20 +15,18 @@ pub async fn post_device( query: web::Path<(String, String)>, device_post: web::Json, conn: Data, + session:Session, rq: HttpRequest ) -> impl Responder { - let username = User::get_gpodder_req_header(&rq); + let headers = rq.get_session(); - if username.is_err(){ - return HttpResponse::Unauthorized().finish(); - } - let username = username.unwrap(); - if query.clone().0 != username { + let username:Option = session.get("test").unwrap(); + + if username.is_none() { return HttpResponse::Unauthorized().finish(); } - let username = query.clone().0; let deviceid = query.clone().1; diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index b74826e2..77614a86 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -1,3 +1,4 @@ pub mod routes; pub mod device; -pub mod parametrization; \ No newline at end of file +pub mod parametrization; +pub mod auth; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 30ef033e..fba655a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,12 +14,13 @@ use actix_web::dev::{fn_service, ServiceFactory, ServiceRequest, ServiceResponse use actix_web::error::ErrorUnauthorized; use actix_web::middleware::{Condition, Logger}; use actix_web::web::{redirect, Data}; -use actix_web::{web, App, Error, HttpResponse, HttpServer, Responder, Scope}; +use actix_web::{web, App, Error, HttpResponse, HttpServer, Responder, Scope, HttpRequest}; use actix_web_httpauth::extractors::basic::BasicAuth; use clokwerk::{Scheduler, TimeUnits}; use std::sync::{Mutex}; use std::time::Duration; use std::{env, thread}; +use std::collections::HashMap; use std::env::var; use std::io::Read; use std::str::FromStr; @@ -91,34 +92,27 @@ mod gpodder; type DbPool = Pool>; async fn validator( - mut req: ServiceRequest, - _credentials: BasicAuth, db: DbPool + mut req: ServiceRequest, db: DbPool ) -> Result { - let authorization = req.headers().get("Authorization").unwrap().to_str(); + let headers = req.headers().clone(); + let authorization = headers.get("Authorization").unwrap().to_str(); match authorization { Ok(auth) => { - let auth = auth.to_string(); - let auth = auth.split(" ").collect::>(); - let auth = auth[1]; - let auth = general_purpose::STANDARD.decode(auth).unwrap(); - let auth = String::from_utf8(auth).unwrap(); - let auth = auth.split(":").collect::>(); - let username = auth[0]; - let password = auth[1]; + let (username, password) = extract_basic_auth(auth); let env = EnvironmentService::new(); // Check if user is admin if username == env.username && password == env.password { req.headers_mut().append(HeaderName::from_str(USERNAME).unwrap(), - HeaderValue::from_str(username).unwrap()); + HeaderValue::from_str(username.as_str()).unwrap()); return Ok(req); } else { - match User::find_by_username(username, &mut db.get().unwrap()) { + match User::find_by_username(username.as_str(), &mut db.get().unwrap()) { Some(user) => { if user.password.unwrap()== digest(password) { req.headers_mut().append(HeaderName::from_str(USERNAME).unwrap(), - HeaderValue::from_str(username).unwrap()); + HeaderValue::from_str(username.as_str()).unwrap()); Ok(req) } else { Err((ErrorUnauthorized(ERROR_LOGIN_MESSAGE), req)) @@ -136,6 +130,19 @@ async fn validator( } } +pub fn extract_basic_auth(auth: &str) -> (String, String) { + let auth = auth.to_string(); + let auth = auth.split(" ").collect::>(); + let auth = auth[1]; + let auth = general_purpose::STANDARD.decode(auth).unwrap(); + let auth = String::from_utf8(auth).unwrap(); + let auth = auth.split(":").collect::>(); + let username = auth[0]; + let password = auth[1]; + (username.to_string(), password.to_string()) +} + + async fn validate_oidc_token(mut rq: ServiceRequest, bearer: BearerAuth, mut jwk_service: JWKService, pool: Pool>) ->Result { @@ -262,7 +269,7 @@ async fn main() -> std::io::Result<()> { let environment_service = EnvironmentService::new(); let notification_service = NotificationService::new(); let settings_service = SettingsService::new(); - + let session_ids:HashMap = HashMap::new(); let lobby = Lobby::default(); let pool = init_db_pool(&get_database_url()).await.expect("Failed to connect to database"); @@ -345,8 +352,9 @@ async fn main() -> std::io::Result<()> { .app_data(Data::new(Mutex::new(environment_service.clone()))) .app_data(Data::new(Mutex::new(notification_service.clone()))) .app_data(Data::new(Mutex::new(settings_service.clone()))) + .app_data(Data::new(Mutex::new(session_ids.clone()))) .app_data(Data::new(pool.clone())) - .wrap(Condition::new(false,Logger::default())) + .wrap(Condition::new(true,Logger::default())) }) .bind(("0.0.0.0", 8000))? .run() @@ -393,7 +401,7 @@ fn get_private_api(db: Pool>) -> Scope Date: Sun, 23 Apr 2023 21:25:27 +0200 Subject: [PATCH 03/26] Add missing routes. --- .../2023-04-23-115251_gpodder_api/down.sql | 2 ++ .../2023-04-23-115251_gpodder_api/up.sql | 9 +++++ src/gpodder/routes.rs | 35 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 migrations/2023-04-23-115251_gpodder_api/down.sql create mode 100644 migrations/2023-04-23-115251_gpodder_api/up.sql create mode 100644 src/gpodder/routes.rs diff --git a/migrations/2023-04-23-115251_gpodder_api/down.sql b/migrations/2023-04-23-115251_gpodder_api/down.sql new file mode 100644 index 00000000..db50bf27 --- /dev/null +++ b/migrations/2023-04-23-115251_gpodder_api/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE devices; \ No newline at end of file diff --git a/migrations/2023-04-23-115251_gpodder_api/up.sql b/migrations/2023-04-23-115251_gpodder_api/up.sql new file mode 100644 index 00000000..ba3ba7c1 --- /dev/null +++ b/migrations/2023-04-23-115251_gpodder_api/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE devices( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + deviceid VARCHAR(255) NOT NULL, + kind TEXT CHECK(kind IN ('desktop', 'laptop', 'server', 'other')) NOT NULL, + name VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL, + FOREIGN KEY (username) REFERENCES users(username) +); \ No newline at end of file diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs new file mode 100644 index 00000000..85225032 --- /dev/null +++ b/src/gpodder/routes.rs @@ -0,0 +1,35 @@ +use actix_session::SessionMiddleware; +use actix_session::storage::CookieSessionStore; +use actix_web::{Either, Error, Handler, HttpRequest, HttpResponse, Scope, web}; +use actix_web::body::{BoxBody, EitherBody}; +use actix_web::dev::{Service, ServiceFactory, ServiceRequest, ServiceResponse}; +use actix_web::error::ErrorUnauthorized; +use actix_web::http::header::HeaderMap; +use actix_web_httpauth::middleware::HttpAuthentication; +use awc::cookie::Key; +use futures::TryFutureExt; +use futures_util::FutureExt; +use crate::config::dbconfig::establish_connection; +use crate::gpodder::device::device_controller::{get_devices_of_user, post_device}; +use crate::{DbPool, extract_basic_auth, validator}; +use crate::constants::constants::ERROR_LOGIN_MESSAGE; +use crate::gpodder::auth::auth::login; +use crate::gpodder::parametrization::get_client_parametrization; + +pub fn get_gpodder_api(pool: DbPool) ->Scope{ + + web::scope("/api/2") + .service(login) + .service(get_authenticated_api(pool.clone())) + +} + + +pub fn get_authenticated_api(pool: DbPool) ->Scope>{ + let secret_key = Key::generate(); + web::scope("") + .wrap(SessionMiddleware::new(CookieSessionStore::default(),secret_key)) + .service(post_device) + .service(get_devices_of_user) +} \ No newline at end of file From 50951f9fb8e28da3b5915be29c1fb27e926a4352 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:41:14 +0200 Subject: [PATCH 04/26] Added session. --- .../2023-04-23-115251_gpodder_api/up.sql | 12 ++++++++-- src/gpodder/auth/auth.rs | 13 ++++++----- src/gpodder/device/device_controller.rs | 22 +++++++++++-------- src/gpodder/routes.rs | 10 ++++----- src/models/mod.rs | 1 + src/models/session.rs | 11 ++++++++++ src/schema.rs | 9 ++++++++ 7 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 src/models/session.rs diff --git a/migrations/2023-04-23-115251_gpodder_api/up.sql b/migrations/2023-04-23-115251_gpodder_api/up.sql index ba3ba7c1..6370c758 100644 --- a/migrations/2023-04-23-115251_gpodder_api/up.sql +++ b/migrations/2023-04-23-115251_gpodder_api/up.sql @@ -2,8 +2,16 @@ CREATE TABLE devices( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, deviceid VARCHAR(255) NOT NULL, - kind TEXT CHECK(kind IN ('desktop', 'laptop', 'server', 'other')) NOT NULL, + kind TEXT CHECK(kind IN ('desktop', 'laptop', 'server','mobile', 'other')) NOT NULL, name VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) -); \ No newline at end of file +); + + +CREATE TABLE sessions( + username VARCHAR(255) NOT NULL, + session_id VARCHAR(255) NOT NULL, + expires DATETIME NOT NULL, + PRIMARY KEY (username, session_id) +) \ No newline at end of file diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs index e4e20255..77ae9b3b 100644 --- a/src/gpodder/auth/auth.rs +++ b/src/gpodder/auth/auth.rs @@ -12,11 +12,12 @@ use uuid::Uuid; use crate::mutex::LockResultExt; use crate::service::environment_service::EnvironmentService; use actix_session::Session; -use awc::cookie::Cookie; +use awc::cookie::{Cookie, SameSite}; #[post("/auth/{username}/login.json")] pub async fn login(username:web::Path, rq: HttpRequest, conn:Data, - env_service: Data>, session:Session) + env_service: Data>, session:Data>>) ->impl Responder { let authorization = rq.headers().get("Authorization").unwrap().to_str().unwrap(); @@ -34,9 +35,11 @@ Responder { Some(user) => { if user.clone().password.unwrap()== digest(password) { let token = Uuid::new_v4().to_string(); - session.insert(token.clone(),user.clone().username).expect("TODO: panic \ - message"); - let user_cookie = Cookie::new("sessionid", token); + session.lock().ignore_poison().insert(token.clone(), user.username); + let user_cookie = Cookie::build("sessionid", token) + .http_only(true).secure + (false).same_site + (SameSite::Strict).path("/api").finish(); HttpResponse::Ok().cookie(user_cookie).finish() } else { HttpResponse::Unauthorized().finish() diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 16271922..78c4ef45 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -1,3 +1,6 @@ +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::Mutex; use actix_session::{Session, SessionExt}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use crate::gpodder::device::dto::device_post::DevicePost; @@ -9,24 +12,25 @@ use crate::controllers::user_controller::get_user; use crate::controllers::watch_time_controller::get_username; use crate::DbPool; use crate::models::user::User; +use crate::mutex::LockResultExt; #[post("/devices/{username}/{deviceid}.json")] pub async fn post_device( query: web::Path<(String, String)>, device_post: web::Json, conn: Data, - session:Session, - rq: HttpRequest -) -> impl Responder { + session:Data>>, + rq: HttpRequest) -> impl Responder { + let sessions = session.lock().ignore_poison(); + let username = rq.cookie("sessionid").unwrap().value().to_string(); - let headers = rq.get_session(); + sessions.keys().for_each(|key| { + println!("key: {}", key); + }); + println!("username: {}", username); + let username:Option<&String> = sessions.get(&*username); - let username:Option = session.get("test").unwrap(); - - if username.is_none() { - return HttpResponse::Unauthorized().finish(); - } let username = query.clone().0; let deviceid = query.clone().1; diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index 85225032..9b5dd4a8 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -16,20 +16,20 @@ use crate::constants::constants::ERROR_LOGIN_MESSAGE; use crate::gpodder::auth::auth::login; use crate::gpodder::parametrization::get_client_parametrization; -pub fn get_gpodder_api(pool: DbPool) ->Scope{ +pub fn get_gpodder_api(pool: DbPool) ->Scope>{ + let secret_key = Key::generate(); web::scope("/api/2") + .wrap(SessionMiddleware::new(CookieSessionStore::default(),secret_key)) .service(login) .service(get_authenticated_api(pool.clone())) } -pub fn get_authenticated_api(pool: DbPool) ->Scope>{ - let secret_key = Key::generate(); +pub fn get_authenticated_api(pool: DbPool) ->Scope{ web::scope("") - .wrap(SessionMiddleware::new(CookieSessionStore::default(),secret_key)) .service(post_device) .service(get_devices_of_user) } \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 4d57c0f8..4c8a217e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,3 +11,4 @@ pub mod user; pub mod invite; pub mod favorites; pub mod device; +pub mod session; diff --git a/src/models/session.rs b/src/models/session.rs new file mode 100644 index 00000000..acda75d8 --- /dev/null +++ b/src/models/session.rs @@ -0,0 +1,11 @@ +use chrono::NaiveDateTime; +use diesel::{Insertable, Queryable}; +use utoipa::ToSchema; +use crate::schema::sessions; + +#[derive(Queryable, Insertable, Clone, ToSchema, PartialEq)] +pub struct Session{ + pub username: String, + pub session_id: String, + pub expires: NaiveDateTime +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs index a7acc751..820e9a49 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -87,6 +87,14 @@ diesel::table! { } } +diesel::table! { + sessions (username, session_id) { + username -> Text, + session_id -> Text, + expires -> Timestamp, + } +} + diesel::table! { settings (id) { id -> Integer, @@ -120,6 +128,7 @@ diesel::allow_tables_to_appear_in_same_query!( podcast_episodes, podcast_history_items, podcasts, + sessions, settings, users, ); From 32ef5f7d6946c951d86c32c8d8d89886198c0311 Mon Sep 17 00:00:00 2001 From: "samuel1998.schwanzer" Date: Mon, 24 Apr 2023 10:39:33 +0200 Subject: [PATCH 05/26] Added better error logging and fixed build. --- src/gpodder/device/device_controller.rs | 13 +------------ src/gpodder/routes.rs | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 78c4ef45..7188f36c 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -18,18 +18,7 @@ use crate::mutex::LockResultExt; pub async fn post_device( query: web::Path<(String, String)>, device_post: web::Json, - conn: Data, - session:Data>>, - rq: HttpRequest) -> impl Responder { - let sessions = session.lock().ignore_poison(); - let username = rq.cookie("sessionid").unwrap().value().to_string(); - - sessions.keys().for_each(|key| { - println!("key: {}", key); - }); - - println!("username: {}", username); - let username:Option<&String> = sessions.get(&*username); + conn: Data) -> impl Responder { let username = query.clone().0; let deviceid = query.clone().1; diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index 9b5dd4a8..1a6e2c82 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -8,7 +8,10 @@ use actix_web::http::header::HeaderMap; use actix_web_httpauth::middleware::HttpAuthentication; use awc::cookie::Key; use futures::TryFutureExt; +use futures_util::future::LocalBoxFuture; use futures_util::FutureExt; +use serde_json::json; +use utoipa::openapi::security::Scopes; use crate::config::dbconfig::establish_connection; use crate::gpodder::device::device_controller::{get_devices_of_user, post_device}; use crate::{DbPool, extract_basic_auth, validator}; @@ -28,8 +31,23 @@ pub fn get_gpodder_api(pool: DbPool) ->ScopeScope{ +pub fn get_authenticated_api(pool: DbPool) ->actix_web::Scope>{ web::scope("") + .wrap_fn(|rq, srv|{ + let srv1 = srv; + let res = rq.cookie("sessionid").clone(); + async move { + if res.is_none(){ + let mut resp = rq.into_response(HttpResponse::Unauthorized().finish()); + return Ok(resp); + } + let fut = srv1.call(rq).await; + + Ok(fut.unwrap()) + } + }) .service(post_device) .service(get_devices_of_user) -} \ No newline at end of file +} + + From d5e36c1ced58d360404e46ad7adbd93981ff95bd Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Mon, 24 Apr 2023 19:36:28 +0200 Subject: [PATCH 06/26] Added subscriptions. --- LICENSE | 14 ++--- .../2023-04-23-115251_gpodder_api/down.sql | 4 +- .../2023-04-23-115251_gpodder_api/up.sql | 13 ++++- src/gpodder/mod.rs | 3 +- src/gpodder/routes.rs | 13 ----- src/gpodder/subscription/mod.rs | 1 + src/gpodder/subscription/subscriptions.rs | 27 ++++++++++ src/models/device_subscription.rs | 51 +++++++++++++++++++ src/models/mod.rs | 3 ++ src/models/subscription.rs | 41 +++++++++++++++ .../subscription_changes_from_client.rs | 4 ++ src/schema.rs | 11 ++++ src/service/mod.rs | 1 + src/service/subscription.rs | 20 ++++++++ 14 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 src/gpodder/subscription/mod.rs create mode 100644 src/gpodder/subscription/subscriptions.rs create mode 100644 src/models/device_subscription.rs create mode 100644 src/models/subscription.rs create mode 100644 src/models/subscription_changes_from_client.rs create mode 100644 src/service/subscription.rs diff --git a/LICENSE b/LICENSE index 4e382b2f..f67c4985 100644 --- a/LICENSE +++ b/LICENSE @@ -13,7 +13,7 @@ the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common + Other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or @@ -30,7 +30,7 @@ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, - and conversions to other media types. + and conversions to Other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a @@ -39,7 +39,7 @@ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications + editorial revisions, annotations, elaborations, or Other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, @@ -91,7 +91,7 @@ modifications, and in Source or Object form, provided that You meet the following conditions: - (a) You must give any other recipients of the Work or + (a) You must give any Other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices @@ -159,16 +159,16 @@ RESULT of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor + Other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this + or Other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, + of any Other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. diff --git a/migrations/2023-04-23-115251_gpodder_api/down.sql b/migrations/2023-04-23-115251_gpodder_api/down.sql index db50bf27..b9b18d1e 100644 --- a/migrations/2023-04-23-115251_gpodder_api/down.sql +++ b/migrations/2023-04-23-115251_gpodder_api/down.sql @@ -1,2 +1,4 @@ -- This file should undo anything in `up.sql` -DROP TABLE devices; \ No newline at end of file +DROP TABLE devices; +DROP TABLE sessions; +DROP TABLE subscriptions; \ No newline at end of file diff --git a/migrations/2023-04-23-115251_gpodder_api/up.sql b/migrations/2023-04-23-115251_gpodder_api/up.sql index 6370c758..97d4e23b 100644 --- a/migrations/2023-04-23-115251_gpodder_api/up.sql +++ b/migrations/2023-04-23-115251_gpodder_api/up.sql @@ -2,7 +2,7 @@ CREATE TABLE devices( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, deviceid VARCHAR(255) NOT NULL, - kind TEXT CHECK(kind IN ('desktop', 'laptop', 'server','mobile', 'other')) NOT NULL, + kind TEXT CHECK(kind IN ('desktop', 'laptop', 'server','mobile', 'Other')) NOT NULL, name VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) @@ -14,4 +14,13 @@ CREATE TABLE sessions( session_id VARCHAR(255) NOT NULL, expires DATETIME NOT NULL, PRIMARY KEY (username, session_id) -) \ No newline at end of file +); + +CREATE TABLE subscriptions( + username TEXT NOT NULL, + device TEXT NOT NULL, + podcast_id INTEGER NOT NULL, + created Datetime NOT NULL, + deleted Datetime, + PRIMARY KEY (username, device, podcast_id) +); \ No newline at end of file diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index 77614a86..800dd261 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -1,4 +1,5 @@ pub mod routes; pub mod device; pub mod parametrization; -pub mod auth; \ No newline at end of file +pub mod auth; +mod subscription; \ No newline at end of file diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index 1a6e2c82..96440785 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -33,19 +33,6 @@ pub fn get_gpodder_api(pool: DbPool) ->Scopeactix_web::Scope>{ web::scope("") - .wrap_fn(|rq, srv|{ - let srv1 = srv; - let res = rq.cookie("sessionid").clone(); - async move { - if res.is_none(){ - let mut resp = rq.into_response(HttpResponse::Unauthorized().finish()); - return Ok(resp); - } - let fut = srv1.call(rq).await; - - Ok(fut.unwrap()) - } - }) .service(post_device) .service(get_devices_of_user) } diff --git a/src/gpodder/subscription/mod.rs b/src/gpodder/subscription/mod.rs new file mode 100644 index 00000000..a76e0c49 --- /dev/null +++ b/src/gpodder/subscription/mod.rs @@ -0,0 +1 @@ +mod subscriptions; \ No newline at end of file diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs new file mode 100644 index 00000000..0c3897a9 --- /dev/null +++ b/src/gpodder/subscription/subscriptions.rs @@ -0,0 +1,27 @@ +use actix_web::{HttpResponse, Responder, web}; +use actix_web::get; +use actix_web::web::Data; +use chrono::NaiveDateTime; +use crate::DbPool; +use crate::models::subscription::SubscriptionChangesToClient; + +#[derive(Deserialize, Serialize)] +pub struct SubscriptionUpdateRequest{ + pub since: NaiveDateTime +} + +#[get("/subscriptions/{username}/{deviceid}.json")] +pub async fn get_subscriptions(paths: web::Path<(String, String)>, + query:web::Query, conn: Data) -> impl +Responder { + let username = paths.clone().0; + let deviceid = paths.clone().1; + + let res = SubscriptionChangesToClient::get_device_subscriptions(&deviceid, &username,query + .since, + &mut *conn.get().unwrap()).await; + match res { + Ok(res) => HttpResponse::Ok().json(res), + Err(e) => HttpResponse::InternalServerError().finish() + } +} \ No newline at end of file diff --git a/src/models/device_subscription.rs b/src/models/device_subscription.rs new file mode 100644 index 00000000..e35a5567 --- /dev/null +++ b/src/models/device_subscription.rs @@ -0,0 +1,51 @@ +use diesel::{RunQueryDsl}; + +// only for http +pub struct DeviceSubscription { + pub id: String, + pub caption: String, + pub r#type: DeviceType, + pub subscriptions: u32, +} + +// only for http +pub struct DeviceUpdateEvent{ + pub caption: Option, + pub r#type: Option +} + +pub enum DeviceType { + Mobile, + Desktop, + Laptop, + Server, + Other, + Web, + Unknown, +} + +impl DeviceType { + pub fn from_string(s: &str) -> DeviceType { + match s { + "mobile" => DeviceType::Mobile, + "desktop" => DeviceType::Desktop, + "laptop" => DeviceType::Laptop, + "server" => DeviceType::Server, + "other" => DeviceType::Other, + "web" => DeviceType::Web, + _ => DeviceType::Unknown, + } + } + + pub fn to_string(&self) -> String { + match self { + DeviceType::Mobile => "mobile".to_string(), + DeviceType::Desktop => "desktop".to_string(), + DeviceType::Laptop => "laptop".to_string(), + DeviceType::Server => "server".to_string(), + DeviceType::Other => "other".to_string(), + DeviceType::Web => "web".to_string(), + DeviceType::Unknown => "unknown".to_string(), + } + } +} \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 4c8a217e..7ec3261b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -12,3 +12,6 @@ pub mod invite; pub mod favorites; pub mod device; pub mod session; +pub mod subscription; +pub mod subscription_changes_from_client; +pub mod device_subscription; diff --git a/src/models/subscription.rs b/src/models/subscription.rs new file mode 100644 index 00000000..e061c3d3 --- /dev/null +++ b/src/models/subscription.rs @@ -0,0 +1,41 @@ +use std::io::Error; +use std::ops::Deref; +use chrono::{NaiveDateTime, Utc}; +use diesel::{QueryDsl, RunQueryDsl, sql_query, SqliteConnection}; +use crate::models::itunes_models::Podcast; +use crate::schema::subscriptions::dsl::subscriptions; +use crate::service::subscription::Subscription; + +#[derive(Debug, Serialize)] +pub struct SubscriptionChangesToClient { + pub add: Vec, + pub remove: Vec, + pub timestamp: NaiveDateTime, +} + + +impl SubscriptionChangesToClient { + pub async fn get_device_subscriptions(device_id: &str, username: &str, since: NaiveDateTime, + conn: &mut SqliteConnection)-> Result{ + + let res = sql_query("SELECT * FROM subscriptions INNER JOIN podcasts ON subscriptions\ + .podcast_id = podcasts.id WHERE subscriptions.device = ? AND (subscriptions.created > ? OR \ + subscriptions.deleted > ? AND username = ?") + .bind::(device_id) + .bind::(since) + .bind::(username) + .load::<(Subscription, Podcast)>(conn) + .expect("Error loading subscriptions"); + + let (deleted_subscriptions,created_subscriptions):(Vec<(Subscription, Podcast)>, + Vec<(Subscription, Podcast)> ) = res + .into_iter() + .partition(|c| c.0.deleted.is_none()); + + Ok(SubscriptionChangesToClient{ + add: created_subscriptions.into_iter().map(|c| c.1.rssfeed).collect(), + remove: deleted_subscriptions.into_iter().map(|c| c.1.rssfeed).collect(), + timestamp: Utc::now().naive_utc() + }) + } +} \ No newline at end of file diff --git a/src/models/subscription_changes_from_client.rs b/src/models/subscription_changes_from_client.rs new file mode 100644 index 00000000..b7163869 --- /dev/null +++ b/src/models/subscription_changes_from_client.rs @@ -0,0 +1,4 @@ +pub struct SubscriptionChangesFromClient { + pub add: Vec, + pub remove: Vec, +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs index 820e9a49..bf517170 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -105,6 +105,16 @@ diesel::table! { } } +diesel::table! { + subscriptions (username, device, podcast_id) { + username -> Text, + device -> Text, + podcast_id -> Integer, + created -> Timestamp, + deleted -> Nullable, + } +} + diesel::table! { users (id) { id -> Integer, @@ -130,5 +140,6 @@ diesel::allow_tables_to_appear_in_same_query!( podcasts, sessions, settings, + subscriptions, users, ); diff --git a/src/service/mod.rs b/src/service/mod.rs index 5a279c2b..ee887162 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -12,3 +12,4 @@ pub mod notification_service; pub mod settings_service; pub mod telegram_api; pub mod user_management_service; +pub mod subscription; diff --git a/src/service/subscription.rs b/src/service/subscription.rs new file mode 100644 index 00000000..148b0723 --- /dev/null +++ b/src/service/subscription.rs @@ -0,0 +1,20 @@ +use utoipa::ToSchema; +use serde::{Deserialize, Serialize}; +use diesel::{Insertable, Queryable, QueryableByName}; +use crate::schema::subscriptions; +use chrono::NaiveDateTime; +use diesel::sql_types::{Integer, Text, Nullable, Timestamp}; +#[derive(Debug, Serialize, Deserialize,QueryableByName, Queryable,Insertable, Clone, ToSchema)] +pub struct Subscription{ + #[diesel(sql_type = Text)] + pub username: String, + #[diesel(sql_type = Text)] + pub device:String, + #[diesel(sql_type = Integer)] + pub podcast_id: i32, + #[diesel(sql_type = Timestamp)] + pub created: NaiveDateTime, + #[diesel(sql_type = Nullable)] + pub deleted: Option +} + From 55a194e0c4ee197a963ae5b002b8dd3fe9d41d7a Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Mon, 24 Apr 2023 21:21:39 +0200 Subject: [PATCH 07/26] Added first succesful sync in PodFetch. --- src/gpodder/episodes/episodes.rs | 0 src/gpodder/episodes/mod.rs | 18 ++++ src/gpodder/mod.rs | 3 +- src/gpodder/routes.rs | 5 + src/gpodder/subscription/mod.rs | 2 +- src/gpodder/subscription/subscriptions.rs | 44 +++++++- src/models/itunes_models.rs | 14 +++ src/models/subscription.rs | 117 +++++++++++++++++++--- src/service/subscription.rs | 19 ---- src/utils/mod.rs | 1 + src/utils/time.rs | 7 ++ 11 files changed, 192 insertions(+), 38 deletions(-) create mode 100644 src/gpodder/episodes/episodes.rs create mode 100644 src/gpodder/episodes/mod.rs create mode 100644 src/utils/time.rs diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/gpodder/episodes/mod.rs b/src/gpodder/episodes/mod.rs new file mode 100644 index 00000000..e614fce4 --- /dev/null +++ b/src/gpodder/episodes/mod.rs @@ -0,0 +1,18 @@ +use actix_web::{HttpResponse, Responder}; + +pub mod episodes; +use actix_web::get; + +#[derive(Serialize, Deserialize)] +pub struct EpisodeActionResponse{ + actions: Vec, + timestamp: i64 +} + +#[get("/episodes/{username}.json")] +pub async fn get_episode_actions() -> impl Responder { + HttpResponse::Ok().json(EpisodeActionResponse{ + actions: vec![], + timestamp: 0 + }) +} \ No newline at end of file diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index 800dd261..c24bede3 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -2,4 +2,5 @@ pub mod routes; pub mod device; pub mod parametrization; pub mod auth; -mod subscription; \ No newline at end of file +pub mod subscription; +mod episodes; \ No newline at end of file diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index 96440785..e7249a6b 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -17,7 +17,9 @@ use crate::gpodder::device::device_controller::{get_devices_of_user, post_device use crate::{DbPool, extract_basic_auth, validator}; use crate::constants::constants::ERROR_LOGIN_MESSAGE; use crate::gpodder::auth::auth::login; +use crate::gpodder::episodes::get_episode_actions; use crate::gpodder::parametrization::get_client_parametrization; +use crate::gpodder::subscription::subscriptions::{get_subscriptions, upload_subscription_changes}; pub fn get_gpodder_api(pool: DbPool) ->Scope>{ @@ -35,6 +37,9 @@ pub fn get_authenticated_api(pool: DbPool) ->actix_web::Scope, + pub remove: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct SubscriptionPostResponse { + pub timestamp: i64, + pub update_urls: Vec } #[get("/subscriptions/{username}/{deviceid}.json")] pub async fn get_subscriptions(paths: web::Path<(String, String)>, - query:web::Query, conn: Data) -> impl + query:web::Query, conn: Data) -> impl Responder { let username = paths.clone().0; let deviceid = paths.clone().1; @@ -20,8 +33,29 @@ Responder { let res = SubscriptionChangesToClient::get_device_subscriptions(&deviceid, &username,query .since, &mut *conn.get().unwrap()).await; + + println!("res: {:?}", res); match res { - Ok(res) => HttpResponse::Ok().json(res), + Ok(res) => { + HttpResponse::Ok().json(res) + }, Err(e) => HttpResponse::InternalServerError().finish() } +} + +#[post("/subscriptions/{username}/{deviceid}.json")] +pub async fn upload_subscription_changes(upload_request: web::Json, + paths: web::Path<(String, String)>, conn: Data)->impl Responder{ + let username = paths.clone().0; + let deviceid = paths.clone().1; + + SubscriptionChangesToClient::update_subscriptions(&deviceid, &username, + upload_request, + &mut *conn.get().unwrap()).await.expect + ("TODO: panic message"); + + HttpResponse::Ok().json(SubscriptionPostResponse{ + update_urls: vec![], + timestamp: get_current_timestamp() + }) } \ No newline at end of file diff --git a/src/models/itunes_models.rs b/src/models/itunes_models.rs index e044dd71..5403b7ed 100644 --- a/src/models/itunes_models.rs +++ b/src/models/itunes_models.rs @@ -1,8 +1,11 @@ use crate::schema::*; use chrono::NaiveDateTime; use diesel::prelude::{Queryable, Identifiable, Selectable, QueryableByName}; +use diesel::{RunQueryDsl, SqliteConnection}; use utoipa::ToSchema; use diesel::sql_types::{Integer, Text, Nullable, Bool, Timestamp}; +use diesel::QueryDsl; +use diesel::ExpressionMethods; #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -84,6 +87,17 @@ pub struct Podcast { pub directory_name:String } + +impl Podcast{ + pub fn get_by_rss_feed(rssfeed_i: &str, conn: &mut SqliteConnection) -> Result { + use crate::schema::podcasts::dsl::*; + podcasts + .filter(rssfeed.eq(rssfeed_i)) + .first::(conn) + } +} + #[derive(Serialize, Deserialize)] pub struct PodcastDto { pub(crate) id: i32, diff --git a/src/models/subscription.rs b/src/models/subscription.rs index e061c3d3..7b1bf6b6 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -1,29 +1,67 @@ use std::io::Error; use std::ops::Deref; +use std::time::SystemTime; +use actix_web::web; use chrono::{NaiveDateTime, Utc}; -use diesel::{QueryDsl, RunQueryDsl, sql_query, SqliteConnection}; +use diesel::{BoolExpressionMethods, QueryDsl, RunQueryDsl, sql_query, SqliteConnection}; use crate::models::itunes_models::Podcast; -use crate::schema::subscriptions::dsl::subscriptions; -use crate::service::subscription::Subscription; +use crate::gpodder::subscription::subscriptions::SubscriptionUpdateRequest; +use diesel::ExpressionMethods; + +use utoipa::ToSchema; +use serde::{Deserialize, Serialize}; +use diesel::{Insertable, Queryable, QueryableByName, AsChangeset}; +use diesel::sql_types::{Integer, Text, Nullable, Timestamp}; +use crate::schema::subscriptions; +use crate::utils::time::get_current_timestamp; + +#[derive(Debug, Serialize, Deserialize,QueryableByName, Queryable,AsChangeset,Insertable, Clone, +ToSchema)] +#[changeset_options(treat_none_as_null="true")] +pub struct Subscription{ + #[diesel(sql_type = Text)] + pub username: String, + #[diesel(sql_type = Text)] + pub device:String, + #[diesel(sql_type = Integer)] + pub podcast_id: i32, + #[diesel(sql_type = Timestamp)] + pub created: NaiveDateTime, + #[diesel(sql_type = Nullable)] + pub deleted: Option +} + +impl Subscription{ + pub fn new(username: String, device: String, podcast_id: i32) -> Self{ + Self{ + username, + device, + podcast_id, + created: Utc::now().naive_utc(), + deleted: None + } + } +} + #[derive(Debug, Serialize)] pub struct SubscriptionChangesToClient { pub add: Vec, pub remove: Vec, - pub timestamp: NaiveDateTime, + pub timestamp: i64, } impl SubscriptionChangesToClient { - pub async fn get_device_subscriptions(device_id: &str, username: &str, since: NaiveDateTime, - conn: &mut SqliteConnection)-> Result{ - + pub async fn get_device_subscriptions(device_id: &str, username: &str, since: i32, + conn: &mut SqliteConnection) -> Result{ + let since = NaiveDateTime::from_timestamp_opt(since as i64, 0).unwrap(); let res = sql_query("SELECT * FROM subscriptions INNER JOIN podcasts ON subscriptions\ .podcast_id = podcasts.id WHERE subscriptions.device = ? AND (subscriptions.created > ? OR \ - subscriptions.deleted > ? AND username = ?") - .bind::(device_id) - .bind::(since) - .bind::(username) + subscriptions.deleted > ?) AND username = ?") + .bind::(device_id) + .bind::(since) + .bind::(username) .load::<(Subscription, Podcast)>(conn) .expect("Error loading subscriptions"); @@ -35,7 +73,62 @@ impl SubscriptionChangesToClient { Ok(SubscriptionChangesToClient{ add: created_subscriptions.into_iter().map(|c| c.1.rssfeed).collect(), remove: deleted_subscriptions.into_iter().map(|c| c.1.rssfeed).collect(), - timestamp: Utc::now().naive_utc() + timestamp: get_current_timestamp() }) } + + pub async fn update_subscriptions(device_id: &str, username: &str, upload_request: + web::Json, conn: &mut SqliteConnection)-> Result, Error>{ + use crate::schema::subscriptions::dsl as dsl_types; + use crate::schema::subscriptions::dsl::subscriptions; + + let res = sql_query("SELECT * FROM subscriptions INNER JOIN podcasts ON subscriptions\ + .podcast_id = podcasts.id WHERE subscriptions.device = ? AND username = ?") + .bind::(device_id) + .bind::(username) + .load::<(Subscription, Podcast)>(conn).unwrap(); + + // Add subscriptions + upload_request.clone().add.iter().for_each(|c| { + let podcast = Podcast::get_by_rss_feed(c, conn).unwrap(); + let subscription = Subscription::new(username.to_string(), device_id.to_string(), podcast.id); + + let option_sub = res.iter().find(|&x| x.0.username == subscription.username&& x.0.device == subscription.device && x.0.podcast_id == subscription.podcast_id); + match option_sub { + Some(_) => { + diesel::update(subscriptions.filter(dsl_types::username.eq(&subscription + .username).and(dsl_types::device.eq(&subscription.device)).and + (dsl_types::podcast_id.eq(&subscription.podcast_id)))) + .set(dsl_types::deleted.eq(None::)) + .execute(conn).unwrap(); + }, + None => { + diesel::insert_into(subscriptions).values(&subscription).execute(conn) + .unwrap(); + } + } + }); + upload_request.clone().remove.iter().for_each(|c|{ + let podcast = Podcast::get_by_rss_feed(c, conn).unwrap(); + let subscription = Subscription::new(username.to_string(), device_id.to_string(), podcast.id); + + let option_sub = res.iter().find(|&x| x.0.username == subscription.username&& x.0.device == subscription.device && x.0.podcast_id == subscription.podcast_id); + + match option_sub { + Some(_) => { + diesel::update(subscriptions.filter(dsl_types::username.eq(&subscription + .username).and(dsl_types::device.eq(&subscription.device)).and + (dsl_types::podcast_id.eq(&subscription.podcast_id)))) + .set(dsl_types::deleted.eq(Some(Utc::now().naive_utc()))) + .execute(conn).unwrap(); + }, + None => { + diesel::insert_into(subscriptions).values(&subscription).execute(conn) + .unwrap(); + } + } + }); + + Ok(upload_request.clone().add) + } } \ No newline at end of file diff --git a/src/service/subscription.rs b/src/service/subscription.rs index 148b0723..8b137891 100644 --- a/src/service/subscription.rs +++ b/src/service/subscription.rs @@ -1,20 +1 @@ -use utoipa::ToSchema; -use serde::{Deserialize, Serialize}; -use diesel::{Insertable, Queryable, QueryableByName}; -use crate::schema::subscriptions; -use chrono::NaiveDateTime; -use diesel::sql_types::{Integer, Text, Nullable, Timestamp}; -#[derive(Debug, Serialize, Deserialize,QueryableByName, Queryable,Insertable, Clone, ToSchema)] -pub struct Subscription{ - #[diesel(sql_type = Text)] - pub username: String, - #[diesel(sql_type = Text)] - pub device:String, - #[diesel(sql_type = Integer)] - pub podcast_id: i32, - #[diesel(sql_type = Timestamp)] - pub created: NaiveDateTime, - #[diesel(sql_type = Nullable)] - pub deleted: Option -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 528f57af..44947433 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod podcast_builder; mod rss_extension; pub mod do_retry; +pub mod time; diff --git a/src/utils/time.rs b/src/utils/time.rs new file mode 100644 index 00000000..1398c1fa --- /dev/null +++ b/src/utils/time.rs @@ -0,0 +1,7 @@ +use std::time::SystemTime; + +pub fn get_current_timestamp()->i64{ + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|duration| duration.as_secs() as i64).unwrap() +} \ No newline at end of file From 8f14acb408a27e7908fc78085b709363a7006f4c Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Tue, 25 Apr 2023 16:54:04 +0200 Subject: [PATCH 08/26] Added subscriptions and episodes. --- .../2023-04-23-115251_gpodder_api/down.sql | 3 +- .../2023-04-23-115251_gpodder_api/up.sql | 26 ++- src/db.rs | 15 ++ src/gpodder/episodes/episodes.rs | 67 ++++++++ src/gpodder/episodes/mod.rs | 17 -- src/gpodder/routes.rs | 3 +- src/gpodder/subscription/subscriptions.rs | 2 +- src/models/episode.rs | 149 ++++++++++++++++++ src/models/mod.rs | 1 + src/models/subscription.rs | 9 +- src/schema.rs | 30 +++- src/service/environment_service.rs | 1 + 12 files changed, 299 insertions(+), 24 deletions(-) create mode 100644 src/models/episode.rs diff --git a/migrations/2023-04-23-115251_gpodder_api/down.sql b/migrations/2023-04-23-115251_gpodder_api/down.sql index b9b18d1e..458919d6 100644 --- a/migrations/2023-04-23-115251_gpodder_api/down.sql +++ b/migrations/2023-04-23-115251_gpodder_api/down.sql @@ -1,4 +1,5 @@ -- This file should undo anything in `up.sql` DROP TABLE devices; DROP TABLE sessions; -DROP TABLE subscriptions; \ No newline at end of file +DROP TABLE subscriptions; +DROP TABLE subscription_devices; \ No newline at end of file diff --git a/migrations/2023-04-23-115251_gpodder_api/up.sql b/migrations/2023-04-23-115251_gpodder_api/up.sql index 97d4e23b..04c5c839 100644 --- a/migrations/2023-04-23-115251_gpodder_api/up.sql +++ b/migrations/2023-04-23-115251_gpodder_api/up.sql @@ -17,10 +17,34 @@ CREATE TABLE sessions( ); CREATE TABLE subscriptions( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username TEXT NOT NULL, device TEXT NOT NULL, podcast_id INTEGER NOT NULL, created Datetime NOT NULL, deleted Datetime, - PRIMARY KEY (username, device, podcast_id) + UNIQUE (username, device, podcast_id) +); + + +CREATE TABLE subscription_devices( + subscription_id INTEGER NOT NULL, + device_id INTEGER NOT NULL, + foreign key(subscription_id) references subscriptions(id), + foreign key(device_id) references devices(id), + PRIMARY KEY (subscription_id, device_id) +); + +CREATE TABLE episodes( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + username VARCHAR(255) NOT NULL, + device VARCHAR(255) NOT NULL, + podcast VARCHAR(255) NOT NULL, + episode VARCHAR(255) NOT NULL, + timestamp DATETIME NOT NULL, + guid VARCHAR(255), + action VARCHAR(255) NOT NULL, + started INTEGER, + position INTEGER, + total INTEGER ); \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index 337dda3e..d0a0e963 100644 --- a/src/db.rs +++ b/src/db.rs @@ -136,6 +136,21 @@ impl DB { Ok(found_podcast_episode) } + pub fn query_podcast_episode_by_url( + conn: &mut SqliteConnection, + podcas_episode_url_to_be_found: &str, + ) -> Result, String> { + use crate::schema::podcast_episodes::dsl::*; + + let found_podcast_episode = podcast_episodes + .filter(url.like("%".to_owned()+podcas_episode_url_to_be_found+"%")) + .first::(conn) + .optional() + .expect("Error loading podcast by id"); + + Ok(found_podcast_episode) + } + pub fn get_podcast_by_track_id(conn: &mut SqliteConnection, podcast_id: i32) -> Result, String> { use crate::schema::podcasts::directory_id; use crate::schema::podcasts::dsl::podcasts; diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index e69de29b..77fef4ea 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -0,0 +1,67 @@ +use std::ops::Deref; +use actix_web::{HttpResponse, Responder, web}; + +use actix_web::{get,post}; +use actix_web::web::Data; +use crate::db::DB; +use crate::DbPool; +use crate::models::episode::{Episode, EpisodeAction, EpisodeDto}; +use crate::models::models::PodcastWatchedPostModel; +use std::borrow::Borrow; +use crate::utils::time::get_current_timestamp; + +#[derive(Serialize, Deserialize)] +pub struct EpisodeActionResponse{ + actions: Vec, + timestamp: i64 +} + +#[get("/episodes/{username}.json")] +pub async fn get_episode_actions(username: web::Path, pool: Data) -> impl Responder { + let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap()).await; + println!("actions: {:?}", actions); + HttpResponse::Ok().json(EpisodeActionResponse{ + actions, + timestamp: get_current_timestamp() + }) +} + + +#[post("/episodes/{username}.json")] +pub async fn upload_episode_actions(username: web::Path, podcast_episode: +web::Json>, conn: Data) -> impl +Responder { + + podcast_episode.iter().for_each(|episode| { + let episode = Episode::convert_to_episode(episode, username.clone()); + Episode::insert_episode(episode.borrow(), &mut *conn.get().unwrap()).expect("TODO: panic message"); + + if EpisodeAction::from_string(&episode.clone().action) == EpisodeAction::Play{ + let mut episode_url = episode.clone().episode; + let mut test = episode.episode.split("?"); + let res = test.next(); + if res.is_some(){ + episode_url = res.unwrap().parse().unwrap() + } + let podcast_episode = DB::query_podcast_episode_by_url(&mut *conn.get().unwrap(), + &*episode_url); + println!("Tres {:?}",podcast_episode.clone().unwrap()); + if podcast_episode.clone().unwrap().is_none(){ + return; + } + + let model = PodcastWatchedPostModel{ + podcast_episode_id: podcast_episode.clone().unwrap().unwrap().episode_id, + time: episode.position.unwrap() as i32, + }; + DB::log_watchtime(&mut *conn.get().unwrap(), model, "admin".to_string()) + .expect("TODO: panic message"); + println!("episode: {:?}", episode); + } + + }); + HttpResponse::Ok().json(EpisodeActionResponse{ + actions: vec![], + timestamp: 0 + }) +} \ No newline at end of file diff --git a/src/gpodder/episodes/mod.rs b/src/gpodder/episodes/mod.rs index e614fce4..e8ea3a47 100644 --- a/src/gpodder/episodes/mod.rs +++ b/src/gpodder/episodes/mod.rs @@ -1,18 +1 @@ -use actix_web::{HttpResponse, Responder}; - pub mod episodes; -use actix_web::get; - -#[derive(Serialize, Deserialize)] -pub struct EpisodeActionResponse{ - actions: Vec, - timestamp: i64 -} - -#[get("/episodes/{username}.json")] -pub async fn get_episode_actions() -> impl Responder { - HttpResponse::Ok().json(EpisodeActionResponse{ - actions: vec![], - timestamp: 0 - }) -} \ No newline at end of file diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index e7249a6b..2cc057c9 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -17,7 +17,7 @@ use crate::gpodder::device::device_controller::{get_devices_of_user, post_device use crate::{DbPool, extract_basic_auth, validator}; use crate::constants::constants::ERROR_LOGIN_MESSAGE; use crate::gpodder::auth::auth::login; -use crate::gpodder::episodes::get_episode_actions; +use crate::gpodder::episodes::episodes::{get_episode_actions, upload_episode_actions}; use crate::gpodder::parametrization::get_client_parametrization; use crate::gpodder::subscription::subscriptions::{get_subscriptions, upload_subscription_changes}; @@ -40,6 +40,7 @@ pub fn get_authenticated_api(pool: DbPool) ->actix_web::Scope, pub remove: Vec, diff --git a/src/models/episode.rs b/src/models/episode.rs new file mode 100644 index 00000000..bef747bb --- /dev/null +++ b/src/models/episode.rs @@ -0,0 +1,149 @@ +use std::ops::Deref; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use diesel::{Queryable, QueryableByName, Insertable, SqliteConnection, RunQueryDsl, QueryDsl}; +use crate::schema::episodes; +use utoipa::ToSchema; +use diesel::sql_types::{Integer, Text, Nullable, Timestamp}; +use diesel::ExpressionMethods; + +#[derive(Serialize, Deserialize, Debug,Queryable, QueryableByName,Insertable, Clone, ToSchema)] +pub struct Episode{ + #[diesel(sql_type = Integer)] + pub id: i32, + #[diesel(sql_type = Text)] + pub username: String, + #[diesel(sql_type = Text)] + pub device: String, + #[diesel(sql_type = Text)] + pub podcast: String, + #[diesel(sql_type = Text)] + pub episode: String, + #[diesel(sql_type = Timestamp)] + pub timestamp: NaiveDateTime, + #[diesel(sql_type = Nullable)] + pub guid: Option, + #[diesel(sql_type = Text)] + pub action: String, + #[diesel(sql_type = Nullable)] + pub started:Option, + #[diesel(sql_type = Nullable)] + pub position:Option, + #[diesel(sql_type = Nullable)] + pub total:Option, +} + + +impl Episode{ + pub fn insert_episode(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::episodes::dsl::*; + diesel::insert_into(episodes) + .values(( + username.eq(&self.username), + device.eq(&self.device), + podcast.eq(&self.podcast), + episode.eq(&self.episode), + timestamp.eq(&self.timestamp), + guid.eq(&self.guid), + action.eq(&self.action), + started.eq(&self.started), + position.eq(&self.position), + total.eq(&self.total) + )) + .get_result(conn) + } + + pub fn convert_to_episode_dto(&self) -> EpisodeDto { + EpisodeDto { + podcast: self.podcast.clone(), + episode: self.episode.clone(), + timestamp: self.timestamp.clone(), + guid: self.guid.clone(), + action: EpisodeAction::from_string(&self.action), + started: self.started, + position: self.position, + total: self.total, + device: self.clone().device, + } + } + + pub fn convert_to_episode(episode_dto: &EpisodeDto, username: String)->Episode{ + Episode { + id: 0, + username, + device: episode_dto.device.clone(), + podcast: episode_dto.podcast.clone(), + episode: episode_dto.episode.clone(), + timestamp: episode_dto.timestamp.clone(), + guid: episode_dto.guid.clone(), + action: episode_dto.action.clone().to_string(), + started: episode_dto.started, + position: episode_dto.position, + total: episode_dto.total, + } + } + pub async fn get_actions_by_username(username1: String, conn: &mut SqliteConnection)->Vec{ + use crate::schema::episodes::username; + use crate::schema::episodes::dsl::episodes; + + episodes + .filter(username.eq(username1)) + .load::(conn) + .expect("") + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct EpisodeDto { + pub podcast: String, + pub episode: String, + pub timestamp: NaiveDateTime, + pub guid: Option, + pub action: EpisodeAction, + pub started:Option, + pub position:Option, + pub total:Option, + pub device: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +#[derive(PartialEq)] +#[derive(Clone)] +pub enum EpisodeAction { + New, + Download, + Play, + Delete, +} + + +impl EpisodeAction{ + pub fn from_string(s: &str) -> Self { + match s { + "new" => EpisodeAction::New, + "download" => EpisodeAction::Download, + "play" => EpisodeAction::Play, + "delete" => EpisodeAction::Delete, + _ => panic!("Unknown episode action: {}", s), + } + } + + pub fn to_string(&self) -> String { + match self { + EpisodeAction::New => "new".to_string(), + EpisodeAction::Download => "download".to_string(), + EpisodeAction::Play => "play".to_string(), + EpisodeAction::Delete => "delete".to_string(), + } + } +} + +#[serde(rename_all = "lowercase")] +#[derive(Serialize, Deserialize, Debug)] +pub enum EpisodeActionRaw { + New, + Download, + Play, + Delete, +} \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 7ec3261b..13994b6b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -15,3 +15,4 @@ pub mod session; pub mod subscription; pub mod subscription_changes_from_client; pub mod device_subscription; +pub mod episode; diff --git a/src/models/subscription.rs b/src/models/subscription.rs index 7b1bf6b6..98078319 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -81,7 +81,7 @@ impl SubscriptionChangesToClient { web::Json, conn: &mut SqliteConnection)-> Result, Error>{ use crate::schema::subscriptions::dsl as dsl_types; use crate::schema::subscriptions::dsl::subscriptions; - + println!("Update:{:?}", upload_request.0); let res = sql_query("SELECT * FROM subscriptions INNER JOIN podcasts ON subscriptions\ .podcast_id = podcasts.id WHERE subscriptions.device = ? AND username = ?") .bind::(device_id) @@ -90,7 +90,12 @@ impl SubscriptionChangesToClient { // Add subscriptions upload_request.clone().add.iter().for_each(|c| { - let podcast = Podcast::get_by_rss_feed(c, conn).unwrap(); + let podcast = Podcast::get_by_rss_feed(c, conn); + if podcast.is_err() { + return; + } + + let podcast = podcast.unwrap(); let subscription = Subscription::new(username.to_string(), device_id.to_string(), podcast.id); let option_sub = res.iter().find(|&x| x.0.username == subscription.username&& x.0.device == subscription.device && x.0.podcast_id == subscription.podcast_id); diff --git a/src/schema.rs b/src/schema.rs index bf517170..0b343503 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -10,6 +10,22 @@ diesel::table! { } } +diesel::table! { + episodes (id) { + id -> Integer, + username -> Text, + device -> Text, + podcast -> Text, + episode -> Text, + timestamp -> Timestamp, + guid -> Nullable, + action -> Text, + started -> Nullable, + position -> Nullable, + total -> Nullable, + } +} + diesel::table! { favorites (username, podcast_id) { username -> Text, @@ -106,7 +122,15 @@ diesel::table! { } diesel::table! { - subscriptions (username, device, podcast_id) { + subscription_devices (subscription_id, device_id) { + subscription_id -> Integer, + device_id -> Integer, + } +} + +diesel::table! { + subscriptions (id) { + id -> Integer, username -> Text, device -> Text, podcast_id -> Integer, @@ -129,9 +153,12 @@ diesel::table! { diesel::joinable!(favorites -> podcasts (podcast_id)); diesel::joinable!(podcast_episodes -> podcasts (podcast_id)); diesel::joinable!(podcast_history_items -> podcasts (podcast_id)); +diesel::joinable!(subscription_devices -> devices (device_id)); +diesel::joinable!(subscription_devices -> subscriptions (subscription_id)); diesel::allow_tables_to_appear_in_same_query!( devices, + episodes, favorites, invites, notifications, @@ -140,6 +167,7 @@ diesel::allow_tables_to_appear_in_same_query!( podcasts, sessions, settings, + subscription_devices, subscriptions, users, ); diff --git a/src/service/environment_service.rs b/src/service/environment_service.rs index c58e797a..034506f5 100644 --- a/src/service/environment_service.rs +++ b/src/service/environment_service.rs @@ -98,6 +98,7 @@ impl EnvironmentService { "Polling interval for new episodes: {} minutes", self.polling_interval ); + println!("Database url is set to: {}", var("DATABASE_URL").unwrap_or("sqlite://./db/podcast.db".to_string())); println!( "Podindex API key&secret configured: {}", self.podindex_api_key.len() > 0 && self.podindex_api_secret.len() > 0 From b3360b401cc225c9732d5df16bb8f7865ef09725 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Tue, 25 Apr 2023 17:14:13 +0200 Subject: [PATCH 09/26] Added since query. --- .../2023-04-23-115251_gpodder_api/up.sql | 3 ++- src/gpodder/episodes/episodes.rs | 17 ++++++++++---- src/models/episode.rs | 22 ++++++++++++++----- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/migrations/2023-04-23-115251_gpodder_api/up.sql b/migrations/2023-04-23-115251_gpodder_api/up.sql index 04c5c839..ba05ff4d 100644 --- a/migrations/2023-04-23-115251_gpodder_api/up.sql +++ b/migrations/2023-04-23-115251_gpodder_api/up.sql @@ -46,5 +46,6 @@ CREATE TABLE episodes( action VARCHAR(255) NOT NULL, started INTEGER, position INTEGER, - total INTEGER + total INTEGER, + UNIQUE (username, device, podcast, episode, timestamp) ); \ No newline at end of file diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index 77fef4ea..3cfca1dd 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -8,6 +8,7 @@ use crate::DbPool; use crate::models::episode::{Episode, EpisodeAction, EpisodeDto}; use crate::models::models::PodcastWatchedPostModel; use std::borrow::Borrow; +use chrono::NaiveDateTime; use crate::utils::time::get_current_timestamp; #[derive(Serialize, Deserialize)] @@ -16,10 +17,18 @@ pub struct EpisodeActionResponse{ timestamp: i64 } +#[derive(Serialize, Deserialize)] +pub struct EpisodeSinceRequest{ + since: i64 +} + #[get("/episodes/{username}.json")] -pub async fn get_episode_actions(username: web::Path, pool: Data) -> impl Responder { - let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap()).await; - println!("actions: {:?}", actions); +pub async fn get_episode_actions(username: web::Path, pool: Data, since: web::Query) -> + impl Responder { + let since_date = NaiveDateTime::from_timestamp_opt(since.since as i64, 0); + let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap(), since_date) + .await; + HttpResponse::Ok().json(EpisodeActionResponse{ actions, timestamp: get_current_timestamp() @@ -62,6 +71,6 @@ Responder { }); HttpResponse::Ok().json(EpisodeActionResponse{ actions: vec![], - timestamp: 0 + timestamp: get_current_timestamp() }) } \ No newline at end of file diff --git a/src/models/episode.rs b/src/models/episode.rs index bef747bb..13c3cdc2 100644 --- a/src/models/episode.rs +++ b/src/models/episode.rs @@ -82,14 +82,26 @@ impl Episode{ total: episode_dto.total, } } - pub async fn get_actions_by_username(username1: String, conn: &mut SqliteConnection)->Vec{ + pub async fn get_actions_by_username(username1: String, conn: &mut SqliteConnection, since_date: Option) ->Vec{ use crate::schema::episodes::username; use crate::schema::episodes::dsl::episodes; + use crate::schema::episodes::dsl::timestamp; + match since_date { + Some(e)=>{ + episodes + .filter(username.eq(username1)) + .filter(timestamp.gt(e)) + .load::(conn) + .expect("") + }, + None=>{ + episodes + .filter(username.eq(username1)) + .load::(conn) + .expect("") + } + } - episodes - .filter(username.eq(username1)) - .load::(conn) - .expect("") } } From 45453560645b78777ac7377580ccb6915cbfdfa3 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Tue, 25 Apr 2023 19:24:28 +0200 Subject: [PATCH 10/26] Added session cleanup and renewal. --- src/gpodder/auth/auth.rs | 41 ++++++++++----- src/gpodder/device/device_controller.rs | 8 --- src/gpodder/episodes/episodes.rs | 29 +++++++---- src/gpodder/routes.rs | 21 ++------ src/gpodder/subscription/subscriptions.rs | 3 +- src/main.rs | 17 ++++--- src/models/device.rs | 1 - src/models/device_subscription.rs | 51 ------------------- src/models/episode.rs | 19 +++++-- src/models/session.rs | 37 +++++++++++++- src/models/subscription.rs | 2 - .../subscription_changes_from_client.rs | 4 -- src/utils/time.rs | 5 ++ 13 files changed, 117 insertions(+), 121 deletions(-) diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs index 77ae9b3b..a2c7ac61 100644 --- a/src/gpodder/auth/auth.rs +++ b/src/gpodder/auth/auth.rs @@ -1,25 +1,34 @@ use std::collections::HashMap; -use std::sync::Mutex; -use actix_web::dev::ServiceRequest; +use std::sync::{Arc, Mutex, RwLock}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::web::Data; use sha256::digest; -use crate::{DbPool, extract_basic_auth, validator}; +use crate::{DbPool, extract_basic_auth}; use crate::models::user::User; use actix_web::{post}; -use utoipa::openapi::HeaderBuilder; use uuid::Uuid; use crate::mutex::LockResultExt; use crate::service::environment_service::EnvironmentService; -use actix_session::Session; use awc::cookie::{Cookie, SameSite}; +use crate::models::session::Session; #[post("/auth/{username}/login.json")] pub async fn login(username:web::Path, rq: HttpRequest, conn:Data, - env_service: Data>, session:Data>>) + env_service: Data>) ->impl Responder { + match rq.clone().cookie("sessionid") { + Some(cookie) => { + let session_id = cookie.value(); + let opt_session = Session::find_by_session_id(session_id, &mut conn.get().unwrap()); + if opt_session.is_ok(){ + let user_cookie = create_session_cookie(opt_session.unwrap()); + return HttpResponse::Ok().cookie(user_cookie).finish(); + } + } + None=>{} + } + let authorization = rq.headers().get("Authorization").unwrap().to_str().unwrap(); let unwrapped_username = username.into_inner(); let (username_basic, password) = basic_auth_login(authorization.to_string()); @@ -27,19 +36,15 @@ Responder { if username_basic != unwrapped_username { return HttpResponse::Unauthorized().finish(); } - if unwrapped_username == env.username && password == env.password { return HttpResponse::Ok().finish(); } else { match User::find_by_username(&unwrapped_username, &mut conn.get().unwrap()) { Some(user) => { if user.clone().password.unwrap()== digest(password) { - let token = Uuid::new_v4().to_string(); - session.lock().ignore_poison().insert(token.clone(), user.username); - let user_cookie = Cookie::build("sessionid", token) - .http_only(true).secure - (false).same_site - (SameSite::Strict).path("/api").finish(); + let session = Session::new(user.username); + Session::insert_session(&session, &mut conn.get().unwrap()).expect("Error inserting session"); + let user_cookie = create_session_cookie(session); HttpResponse::Ok().cookie(user_cookie).finish() } else { HttpResponse::Unauthorized().finish() @@ -52,6 +57,14 @@ Responder { } } +fn create_session_cookie(session: Session) -> Cookie<'static> { + let user_cookie = Cookie::build("sessionid", session.session_id) + .http_only(true).secure + (false).same_site + (SameSite::Strict).path("/api").finish(); + user_cookie +} + pub fn basic_auth_login(rq: String) -> (String, String) { let (u,p) = extract_basic_auth(rq.as_str()); diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 7188f36c..446442f9 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -1,18 +1,10 @@ -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Mutex; -use actix_session::{Session, SessionExt}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use crate::gpodder::device::dto::device_post::DevicePost; use crate::models::device::Device; use actix_web::{post, get}; use actix_web::web::Data; -use serde::de::Unexpected::Str; -use crate::controllers::user_controller::get_user; use crate::controllers::watch_time_controller::get_username; use crate::DbPool; -use crate::models::user::User; -use crate::mutex::LockResultExt; #[post("/devices/{username}/{deviceid}.json")] pub async fn post_device( diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index 3cfca1dd..0c4b13c0 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -1,4 +1,3 @@ -use std::ops::Deref; use actix_web::{HttpResponse, Responder, web}; use actix_web::{get,post}; @@ -9,7 +8,7 @@ use crate::models::episode::{Episode, EpisodeAction, EpisodeDto}; use crate::models::models::PodcastWatchedPostModel; use std::borrow::Borrow; use chrono::NaiveDateTime; -use crate::utils::time::get_current_timestamp; +use crate::utils::time::{get_current_timestamp, get_current_timestamp_str}; #[derive(Serialize, Deserialize)] pub struct EpisodeActionResponse{ @@ -17,6 +16,13 @@ pub struct EpisodeActionResponse{ timestamp: i64 } + +#[derive(Serialize, Deserialize)] +pub struct EpisodeActionPostResponse{ + update_urls: Vec, + timestamp: i64 +} + #[derive(Serialize, Deserialize)] pub struct EpisodeSinceRequest{ since: i64 @@ -28,7 +34,6 @@ pub async fn get_episode_actions(username: web::Path, pool: Data let since_date = NaiveDateTime::from_timestamp_opt(since.since as i64, 0); let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap(), since_date) .await; - HttpResponse::Ok().json(EpisodeActionResponse{ actions, timestamp: get_current_timestamp() @@ -41,20 +46,24 @@ pub async fn upload_episode_actions(username: web::Path, podcast_episode web::Json>, conn: Data) -> impl Responder { + let mut inserted_episodes:Vec = vec![]; podcast_episode.iter().for_each(|episode| { let episode = Episode::convert_to_episode(episode, username.clone()); - Episode::insert_episode(episode.borrow(), &mut *conn.get().unwrap()).expect("TODO: panic message"); + inserted_episodes.push(Episode::insert_episode(episode.borrow(), &mut *conn.get().unwrap()) + .expect("Unable to insert episode")); if EpisodeAction::from_string(&episode.clone().action) == EpisodeAction::Play{ let mut episode_url = episode.clone().episode; - let mut test = episode.episode.split("?"); - let res = test.next(); + // Sometimes podcast provider like to check which browser access their podcast + let mut first_split = episode.episode.split("?"); + let res = first_split.next(); + if res.is_some(){ episode_url = res.unwrap().parse().unwrap() } + let podcast_episode = DB::query_podcast_episode_by_url(&mut *conn.get().unwrap(), &*episode_url); - println!("Tres {:?}",podcast_episode.clone().unwrap()); if podcast_episode.clone().unwrap().is_none(){ return; } @@ -67,10 +76,10 @@ Responder { .expect("TODO: panic message"); println!("episode: {:?}", episode); } - }); - HttpResponse::Ok().json(EpisodeActionResponse{ - actions: vec![], + // TODO What is rewriting urls https://buildmedia.readthedocs.org/media/pdf/gpoddernet/latest/gpoddernet.pdf + HttpResponse::Ok().json(EpisodeActionPostResponse{ + update_urls: vec![], timestamp: get_current_timestamp() }) } \ No newline at end of file diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index 2cc057c9..c19f6349 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -1,24 +1,12 @@ use actix_session::SessionMiddleware; use actix_session::storage::CookieSessionStore; -use actix_web::{Either, Error, Handler, HttpRequest, HttpResponse, Scope, web}; -use actix_web::body::{BoxBody, EitherBody}; -use actix_web::dev::{Service, ServiceFactory, ServiceRequest, ServiceResponse}; -use actix_web::error::ErrorUnauthorized; -use actix_web::http::header::HeaderMap; -use actix_web_httpauth::middleware::HttpAuthentication; +use actix_web::{Error, Scope, web}; +use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse}; use awc::cookie::Key; -use futures::TryFutureExt; -use futures_util::future::LocalBoxFuture; -use futures_util::FutureExt; -use serde_json::json; -use utoipa::openapi::security::Scopes; -use crate::config::dbconfig::establish_connection; use crate::gpodder::device::device_controller::{get_devices_of_user, post_device}; -use crate::{DbPool, extract_basic_auth, validator}; -use crate::constants::constants::ERROR_LOGIN_MESSAGE; +use crate::{DbPool}; use crate::gpodder::auth::auth::login; use crate::gpodder::episodes::episodes::{get_episode_actions, upload_episode_actions}; -use crate::gpodder::parametrization::get_client_parametrization; use crate::gpodder::subscription::subscriptions::{get_subscriptions, upload_subscription_changes}; pub fn get_gpodder_api(pool: DbPool) ->ScopeScopeactix_web::Scope>{ +pub fn get_authenticated_api(_: DbPool) ->Scope>{ web::scope("") .service(post_device) .service(get_devices_of_user) diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index caed96b3..7429b6eb 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -1,7 +1,6 @@ use actix_web::{HttpResponse, Responder, web}; use actix_web::{get, post}; use actix_web::web::Data; -use chrono::NaiveDateTime; use crate::DbPool; use crate::models::subscription::SubscriptionChangesToClient; use crate::utils::time::get_current_timestamp; @@ -39,7 +38,7 @@ Responder { Ok(res) => { HttpResponse::Ok().json(res) }, - Err(e) => HttpResponse::InternalServerError().finish() + Err(_) => HttpResponse::InternalServerError().finish() } } diff --git a/src/main.rs b/src/main.rs index fba655a4..7192837b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,10 +14,9 @@ use actix_web::dev::{fn_service, ServiceFactory, ServiceRequest, ServiceResponse use actix_web::error::ErrorUnauthorized; use actix_web::middleware::{Condition, Logger}; use actix_web::web::{redirect, Data}; -use actix_web::{web, App, Error, HttpResponse, HttpServer, Responder, Scope, HttpRequest}; -use actix_web_httpauth::extractors::basic::BasicAuth; +use actix_web::{web, App, Error, HttpResponse, HttpServer, Responder, Scope}; use clokwerk::{Scheduler, TimeUnits}; -use std::sync::{Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; use std::{env, thread}; use std::collections::HashMap; @@ -70,8 +69,10 @@ use crate::db::DB; use crate::gpodder::parametrization::get_client_parametrization; use crate::gpodder::routes::get_gpodder_api; use crate::models::oidc_model::{CustomJwk, CustomJwkSet}; +use crate::models::session::Session; use crate::models::user::User; use crate::models::web_socket_message::Lobby; +use crate::mutex::LockResultExt; use crate::service::environment_service::EnvironmentService; use crate::service::file_service::FileService; use crate::service::jwkservice::JWKService; @@ -269,8 +270,6 @@ async fn main() -> std::io::Result<()> { let environment_service = EnvironmentService::new(); let notification_service = NotificationService::new(); let settings_service = SettingsService::new(); - let session_ids:HashMap = HashMap::new(); - let lobby = Lobby::default(); let pool = init_db_pool(&get_database_url()).await.expect("Failed to connect to database"); @@ -314,7 +313,10 @@ async fn main() -> std::io::Result<()> { } }); - scheduler.every(1.day()).run(|| { + scheduler.every(1.day()).run(move || { + // Clears the session ids once per day + Session::cleanup_sessions(&mut establish_connection()).expect("Error clearing old \ + sessions"); let mut db = DB::new().unwrap(); let mut podcast_episode_service = PodcastEpisodeService::new(); let settings = db.get_settings(); @@ -352,7 +354,6 @@ async fn main() -> std::io::Result<()> { .app_data(Data::new(Mutex::new(environment_service.clone()))) .app_data(Data::new(Mutex::new(notification_service.clone()))) .app_data(Data::new(Mutex::new(settings_service.clone()))) - .app_data(Data::new(Mutex::new(session_ids.clone()))) .app_data(Data::new(pool.clone())) .wrap(Condition::new(true,Logger::default())) }) @@ -400,7 +401,7 @@ pub fn get_global_scope(pool1: Pool>) -> Sco fn get_private_api(db: Pool>) -> Scope>>, EitherBody>>>, Error = Error, InitError = ()>> { let oidc_db = db.clone(); let enable_basic_auth = var(BASIC_AUTH).is_ok(); - let auth = HttpAuthentication::basic(move |rq,serv|{ + let auth = HttpAuthentication::basic(move |rq,_|{ validator(rq, db.clone()) }); let enable_oidc_auth = var(OIDC_AUTH).is_ok(); diff --git a/src/models/device.rs b/src/models/device.rs index d168c62c..1a9d8f06 100644 --- a/src/models/device.rs +++ b/src/models/device.rs @@ -1,5 +1,4 @@ use diesel::{Queryable, QueryableByName, RunQueryDsl, Insertable, SqliteConnection}; -use diesel::associations::HasTable; use utoipa::ToSchema; use crate::gpodder::device::dto::device_post::DevicePost; use crate::schema::devices; diff --git a/src/models/device_subscription.rs b/src/models/device_subscription.rs index e35a5567..e69de29b 100644 --- a/src/models/device_subscription.rs +++ b/src/models/device_subscription.rs @@ -1,51 +0,0 @@ -use diesel::{RunQueryDsl}; - -// only for http -pub struct DeviceSubscription { - pub id: String, - pub caption: String, - pub r#type: DeviceType, - pub subscriptions: u32, -} - -// only for http -pub struct DeviceUpdateEvent{ - pub caption: Option, - pub r#type: Option -} - -pub enum DeviceType { - Mobile, - Desktop, - Laptop, - Server, - Other, - Web, - Unknown, -} - -impl DeviceType { - pub fn from_string(s: &str) -> DeviceType { - match s { - "mobile" => DeviceType::Mobile, - "desktop" => DeviceType::Desktop, - "laptop" => DeviceType::Laptop, - "server" => DeviceType::Server, - "other" => DeviceType::Other, - "web" => DeviceType::Web, - _ => DeviceType::Unknown, - } - } - - pub fn to_string(&self) -> String { - match self { - DeviceType::Mobile => "mobile".to_string(), - DeviceType::Desktop => "desktop".to_string(), - DeviceType::Laptop => "laptop".to_string(), - DeviceType::Server => "server".to_string(), - DeviceType::Other => "other".to_string(), - DeviceType::Web => "web".to_string(), - DeviceType::Unknown => "unknown".to_string(), - } - } -} \ No newline at end of file diff --git a/src/models/episode.rs b/src/models/episode.rs index 13c3cdc2..ef5123da 100644 --- a/src/models/episode.rs +++ b/src/models/episode.rs @@ -1,7 +1,6 @@ -use std::ops::Deref; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; -use diesel::{Queryable, QueryableByName, Insertable, SqliteConnection, RunQueryDsl, QueryDsl}; +use diesel::{Queryable, QueryableByName, Insertable, SqliteConnection, RunQueryDsl, QueryDsl, BoolExpressionMethods, OptionalExtension}; use crate::schema::episodes; use utoipa::ToSchema; use diesel::sql_types::{Integer, Text, Nullable, Timestamp}; @@ -37,6 +36,20 @@ pub struct Episode{ impl Episode{ pub fn insert_episode(&self, conn: &mut SqliteConnection) -> Result { use crate::schema::episodes::dsl::*; + + let res = episodes.filter(timestamp.eq(self.clone().timestamp) + .and(device.eq(self.clone().device)) + .and(podcast.eq(self.clone().podcast)) + .and(episode.eq(self.clone().episode)) + .and(timestamp.eq(self.clone().timestamp))) + .first::(conn) + .optional() + .expect(""); + + if res.is_some() { + return Ok(res.unwrap()) + } + diesel::insert_into(episodes) .values(( username.eq(&self.username), @@ -151,8 +164,8 @@ impl EpisodeAction{ } } -#[serde(rename_all = "lowercase")] #[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] pub enum EpisodeActionRaw { New, Download, diff --git a/src/models/session.rs b/src/models/session.rs index acda75d8..3330203e 100644 --- a/src/models/session.rs +++ b/src/models/session.rs @@ -1,11 +1,44 @@ -use chrono::NaiveDateTime; -use diesel::{Insertable, Queryable}; +use chrono::{NaiveDateTime, Utc}; +use diesel::{Insertable, Queryable, RunQueryDsl}; use utoipa::ToSchema; +use uuid::Uuid; use crate::schema::sessions; +use diesel::QueryDsl; +use diesel::ExpressionMethods; #[derive(Queryable, Insertable, Clone, ToSchema, PartialEq)] pub struct Session{ pub username: String, pub session_id: String, pub expires: NaiveDateTime +} + + +impl Session{ + pub fn new(username: String) -> Self{ + Self{ + username, + session_id: Uuid::new_v4().to_string(), + expires: NaiveDateTime::from_timestamp_opt(chrono::Utc::now().timestamp() + 60 * 60 * + 24, 0).unwrap() + } + } + + pub fn insert_session(&self, conn: &mut diesel::SqliteConnection) -> Result{ + diesel::insert_into(sessions::table) + .values(self) + .get_result(conn) + } + + pub fn cleanup_sessions(conn: &mut diesel::SqliteConnection) -> Result{ + diesel::delete(sessions::table. + filter(sessions::expires.lt(Utc::now().naive_utc()))) + .execute(conn) + } + + pub fn find_by_session_id(session_id: &str, conn: &mut diesel::SqliteConnection) -> Result{ + sessions::table + .filter(sessions::session_id.eq(session_id)) + .get_result(conn) + } } \ No newline at end of file diff --git a/src/models/subscription.rs b/src/models/subscription.rs index 98078319..1a1679a8 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -1,6 +1,4 @@ use std::io::Error; -use std::ops::Deref; -use std::time::SystemTime; use actix_web::web; use chrono::{NaiveDateTime, Utc}; use diesel::{BoolExpressionMethods, QueryDsl, RunQueryDsl, sql_query, SqliteConnection}; diff --git a/src/models/subscription_changes_from_client.rs b/src/models/subscription_changes_from_client.rs index b7163869..e69de29b 100644 --- a/src/models/subscription_changes_from_client.rs +++ b/src/models/subscription_changes_from_client.rs @@ -1,4 +0,0 @@ -pub struct SubscriptionChangesFromClient { - pub add: Vec, - pub remove: Vec, -} \ No newline at end of file diff --git a/src/utils/time.rs b/src/utils/time.rs index 1398c1fa..a52612e6 100644 --- a/src/utils/time.rs +++ b/src/utils/time.rs @@ -1,7 +1,12 @@ use std::time::SystemTime; +use chrono::{NaiveDateTime, Utc}; pub fn get_current_timestamp()->i64{ SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .map(|duration| duration.as_secs() as i64).unwrap() +} + +pub fn get_current_timestamp_str()->NaiveDateTime{ + Utc::now().naive_utc() } \ No newline at end of file From 8ec7679432f56438a1cbd5ed2eef18415a988ef2 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Tue, 25 Apr 2023 20:56:48 +0200 Subject: [PATCH 11/26] Added subscriptions and episodes. --- .../2023-04-23-115251_gpodder_api/down.sql | 2 +- .../2023-04-23-115251_gpodder_api/up.sql | 13 +- src/gpodder/auth/auth.rs | 28 +++- src/gpodder/device/device_controller.rs | 28 ++-- src/gpodder/episodes/episodes.rs | 19 ++- src/gpodder/subscription/subscriptions.rs | 21 ++- src/models/device.rs | 18 +++ src/models/subscription.rs | 124 +++++++++--------- src/schema.rs | 12 +- 9 files changed, 162 insertions(+), 103 deletions(-) diff --git a/migrations/2023-04-23-115251_gpodder_api/down.sql b/migrations/2023-04-23-115251_gpodder_api/down.sql index 458919d6..47bce505 100644 --- a/migrations/2023-04-23-115251_gpodder_api/down.sql +++ b/migrations/2023-04-23-115251_gpodder_api/down.sql @@ -2,4 +2,4 @@ DROP TABLE devices; DROP TABLE sessions; DROP TABLE subscriptions; -DROP TABLE subscription_devices; \ No newline at end of file +DROP TABLE episodes; \ No newline at end of file diff --git a/migrations/2023-04-23-115251_gpodder_api/up.sql b/migrations/2023-04-23-115251_gpodder_api/up.sql index ba05ff4d..0c8b8707 100644 --- a/migrations/2023-04-23-115251_gpodder_api/up.sql +++ b/migrations/2023-04-23-115251_gpodder_api/up.sql @@ -20,19 +20,10 @@ CREATE TABLE subscriptions( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username TEXT NOT NULL, device TEXT NOT NULL, - podcast_id INTEGER NOT NULL, + podcast TEXT NOT NULL, created Datetime NOT NULL, deleted Datetime, - UNIQUE (username, device, podcast_id) -); - - -CREATE TABLE subscription_devices( - subscription_id INTEGER NOT NULL, - device_id INTEGER NOT NULL, - foreign key(subscription_id) references subscriptions(id), - foreign key(device_id) references devices(id), - PRIMARY KEY (subscription_id, device_id) + UNIQUE (username, device, podcast) ); CREATE TABLE episodes( diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs index a2c7ac61..e6a88e83 100644 --- a/src/gpodder/auth/auth.rs +++ b/src/gpodder/auth/auth.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::io::Error; use std::sync::{Arc, Mutex, RwLock}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::web::Data; @@ -10,7 +11,9 @@ use uuid::Uuid; use crate::mutex::LockResultExt; use crate::service::environment_service::EnvironmentService; use awc::cookie::{Cookie, SameSite}; +use diesel::SqliteConnection; use crate::models::session::Session; +use crate::schema::sessions::session_id; #[post("/auth/{username}/login.json")] pub async fn login(username:web::Path, rq: HttpRequest, conn:Data, @@ -19,8 +22,8 @@ pub async fn login(username:web::Path, rq: HttpRequest, conn:Data { - let session_id = cookie.value(); - let opt_session = Session::find_by_session_id(session_id, &mut conn.get().unwrap()); + let session = cookie.value(); + let opt_session = Session::find_by_session_id(session, &mut conn.get().unwrap()); if opt_session.is_ok(){ let user_cookie = create_session_cookie(opt_session.unwrap()); return HttpResponse::Ok().cookie(user_cookie).finish(); @@ -71,3 +74,24 @@ pub fn basic_auth_login(rq: String) -> (String, String) { return (u.to_string(),p.to_string()) } +pub async fn auth_checker(conn: &mut SqliteConnection, session: Option, username: String) + ->Result<(), + Error>{ + return match session { + Some(session) => { + let session = Session::find_by_session_id(&session, conn).unwrap(); + if session.username != username { + return Err(Error::new(std::io::ErrorKind::Other, "User and session not matching")) + } + Ok(()) + } + None => { + Err(Error::new(std::io::ErrorKind::Other, "No session")) + } + } +} + +pub fn extract_from_http_request(rq: HttpRequest)->Option{ + rq.cookie("sessionid") + .map(|cookie|cookie.value().to_string()) +} \ No newline at end of file diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 446442f9..8b5f8bbb 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -1,20 +1,25 @@ use actix_web::{HttpRequest, HttpResponse, Responder, web}; use crate::gpodder::device::dto::device_post::DevicePost; -use crate::models::device::Device; +use crate::models::device::{Device, DeviceResponse}; use actix_web::{post, get}; use actix_web::web::Data; use crate::controllers::watch_time_controller::get_username; use crate::DbPool; +use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; #[post("/devices/{username}/{deviceid}.json")] pub async fn post_device( query: web::Path<(String, String)>, device_post: web::Json, + rq: HttpRequest, conn: Data) -> impl Responder { - let username = query.clone().0; let deviceid = query.clone().1; - + let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), + username.clone()).await; + if auth_check_res.is_err(){ + return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + } let device = Device::new(device_post.into_inner(), deviceid, username); let result = device.save(&mut conn.get().unwrap()).unwrap(); @@ -24,17 +29,14 @@ pub async fn post_device( #[get("/devices/{username}.json")] pub async fn get_devices_of_user(query: web::Path, conn: Data, rq: HttpRequest) -> impl Responder { - let username = get_username(rq); - if username.is_err(){ - return username.err().unwrap() + let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), + query.clone()).await; + if auth_check_res.is_err(){ + return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); } + let devices = Device::get_devices_of_user(&mut conn.get().unwrap(), query.clone()).unwrap(); - let username = query.clone(); - - if query.clone() != username { - return HttpResponse::Unauthorized().finish(); - } - let devices = Device::get_devices_of_user(&mut conn.get().unwrap(), username).unwrap(); - HttpResponse::Ok().json(devices) + let dtos = devices.iter().map(|d|d.to_dto()).collect::>(); + HttpResponse::Ok().json(dtos) } \ No newline at end of file diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index 0c4b13c0..91e9154a 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -1,4 +1,4 @@ -use actix_web::{HttpResponse, Responder, web}; +use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::{get,post}; use actix_web::web::Data; @@ -8,6 +8,7 @@ use crate::models::episode::{Episode, EpisodeAction, EpisodeDto}; use crate::models::models::PodcastWatchedPostModel; use std::borrow::Borrow; use chrono::NaiveDateTime; +use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; use crate::utils::time::{get_current_timestamp, get_current_timestamp_str}; #[derive(Serialize, Deserialize)] @@ -29,8 +30,15 @@ pub struct EpisodeSinceRequest{ } #[get("/episodes/{username}.json")] -pub async fn get_episode_actions(username: web::Path, pool: Data, since: web::Query) -> +pub async fn get_episode_actions(username: web::Path, pool: Data, since: +web::Query, rq: HttpRequest) -> impl Responder { + + let auth_check_res= auth_checker(&mut *pool.get().unwrap(), extract_from_http_request(rq), + username.clone()).await; + if auth_check_res.is_err(){ + return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + } let since_date = NaiveDateTime::from_timestamp_opt(since.since as i64, 0); let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap(), since_date) .await; @@ -43,8 +51,13 @@ pub async fn get_episode_actions(username: web::Path, pool: Data #[post("/episodes/{username}.json")] pub async fn upload_episode_actions(username: web::Path, podcast_episode: -web::Json>, conn: Data) -> impl +web::Json>, conn: Data, rq:HttpRequest) -> impl Responder { + let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), + username.clone()).await; + if auth_check_res.is_err(){ + return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + } let mut inserted_episodes:Vec = vec![]; podcast_episode.iter().for_each(|episode| { diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index 7429b6eb..f0e7161a 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -1,7 +1,8 @@ -use actix_web::{HttpResponse, Responder, web}; +use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::{get, post}; use actix_web::web::Data; use crate::DbPool; +use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; use crate::models::subscription::SubscriptionChangesToClient; use crate::utils::time::get_current_timestamp; @@ -24,11 +25,19 @@ pub struct SubscriptionPostResponse { #[get("/subscriptions/{username}/{deviceid}.json")] pub async fn get_subscriptions(paths: web::Path<(String, String)>, - query:web::Query, conn: Data) -> impl + query:web::Query, conn: Data, + rq:HttpRequest +) -> impl Responder { let username = paths.clone().0; let deviceid = paths.clone().1; + let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), + username.clone()).await; + if auth_check_res.is_err(){ + return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + } + let res = SubscriptionChangesToClient::get_device_subscriptions(&deviceid, &username,query .since, &mut *conn.get().unwrap()).await; @@ -44,10 +53,16 @@ Responder { #[post("/subscriptions/{username}/{deviceid}.json")] pub async fn upload_subscription_changes(upload_request: web::Json, - paths: web::Path<(String, String)>, conn: Data)->impl Responder{ + paths: web::Path<(String, String)>, conn: Data, + rq:HttpRequest)->impl Responder{ let username = paths.clone().0; let deviceid = paths.clone().1; + let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), + username.clone()).await; + if auth_check_res.is_err(){ + return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + } SubscriptionChangesToClient::update_subscriptions(&deviceid, &username, upload_request, &mut *conn.get().unwrap()).await.expect diff --git a/src/models/device.rs b/src/models/device.rs index 1a9d8f06..1db56b9e 100644 --- a/src/models/device.rs +++ b/src/models/device.rs @@ -16,6 +16,15 @@ pub struct Device { pub username: String } +#[derive(Serialize, Deserialize, Clone)] +pub struct DeviceResponse{ + id: String, + caption: String, + #[serde(rename = "type")] + type_: String, + subscriptions: u32 +} + impl Device { pub fn new(device_post: DevicePost, device_id: String, username: String) -> Device { @@ -41,4 +50,13 @@ impl Device { devices.filter(username.eq(username_to_insert)) .load::(conn) } + + pub fn to_dto(&self) -> DeviceResponse { + DeviceResponse{ + id: self.deviceid.clone(), + caption: self.name.clone(), + type_: self.kind.clone(), + subscriptions: 0 + } + } } \ No newline at end of file diff --git a/src/models/subscription.rs b/src/models/subscription.rs index 1a1679a8..5242fb4b 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -12,17 +12,19 @@ use diesel::{Insertable, Queryable, QueryableByName, AsChangeset}; use diesel::sql_types::{Integer, Text, Nullable, Timestamp}; use crate::schema::subscriptions; use crate::utils::time::get_current_timestamp; +use diesel::OptionalExtension; -#[derive(Debug, Serialize, Deserialize,QueryableByName, Queryable,AsChangeset,Insertable, Clone, -ToSchema)] -#[changeset_options(treat_none_as_null="true")] +#[derive(Debug, Serialize, Deserialize,QueryableByName, Queryable,AsChangeset,Insertable, Clone, ToSchema)] +#[diesel(treat_none_as_null = true)] pub struct Subscription{ + #[diesel(sql_type = Integer)] + pub id: i32, #[diesel(sql_type = Text)] pub username: String, #[diesel(sql_type = Text)] pub device:String, - #[diesel(sql_type = Integer)] - pub podcast_id: i32, + #[diesel(sql_type = Text)] + pub podcast: String, #[diesel(sql_type = Timestamp)] pub created: NaiveDateTime, #[diesel(sql_type = Nullable)] @@ -30,11 +32,12 @@ pub struct Subscription{ } impl Subscription{ - pub fn new(username: String, device: String, podcast_id: i32) -> Self{ + pub fn new(username: String, device: String, podcast: String) -> Self{ Self{ + id:0, username, device, - podcast_id, + podcast, created: Utc::now().naive_utc(), deleted: None } @@ -54,23 +57,21 @@ impl SubscriptionChangesToClient { pub async fn get_device_subscriptions(device_id: &str, username: &str, since: i32, conn: &mut SqliteConnection) -> Result{ let since = NaiveDateTime::from_timestamp_opt(since as i64, 0).unwrap(); - let res = sql_query("SELECT * FROM subscriptions INNER JOIN podcasts ON subscriptions\ - .podcast_id = podcasts.id WHERE subscriptions.device = ? AND (subscriptions.created > ? OR \ - subscriptions.deleted > ?) AND username = ?") - .bind::(device_id) - .bind::(since) - .bind::(username) - .load::<(Subscription, Podcast)>(conn) - .expect("Error loading subscriptions"); - - let (deleted_subscriptions,created_subscriptions):(Vec<(Subscription, Podcast)>, - Vec<(Subscription, Podcast)> ) = res + let res:Vec = subscriptions::table + .filter(subscriptions::username.eq(username)) + .filter(subscriptions::device.eq(device_id) + .and(subscriptions::created.gt(since))) + .load::(conn) + .expect("Error retrieving changed subscriptions"); + + let (deleted_subscriptions,created_subscriptions):(Vec, + Vec ) = res .into_iter() - .partition(|c| c.0.deleted.is_none()); + .partition(|c| c.deleted.is_none()); Ok(SubscriptionChangesToClient{ - add: created_subscriptions.into_iter().map(|c| c.1.rssfeed).collect(), - remove: deleted_subscriptions.into_iter().map(|c| c.1.rssfeed).collect(), + add: created_subscriptions.into_iter().map(|c| c.podcast).collect(), + remove: deleted_subscriptions.into_iter().map(|c| c.podcast).collect(), timestamp: get_current_timestamp() }) } @@ -79,59 +80,64 @@ impl SubscriptionChangesToClient { web::Json, conn: &mut SqliteConnection)-> Result, Error>{ use crate::schema::subscriptions::dsl as dsl_types; use crate::schema::subscriptions::dsl::subscriptions; - println!("Update:{:?}", upload_request.0); - let res = sql_query("SELECT * FROM subscriptions INNER JOIN podcasts ON subscriptions\ - .podcast_id = podcasts.id WHERE subscriptions.device = ? AND username = ?") - .bind::(device_id) - .bind::(username) - .load::<(Subscription, Podcast)>(conn).unwrap(); // Add subscriptions upload_request.clone().add.iter().for_each(|c| { - let podcast = Podcast::get_by_rss_feed(c, conn); - if podcast.is_err() { - return; - } - let podcast = podcast.unwrap(); - let subscription = Subscription::new(username.to_string(), device_id.to_string(), podcast.id); - - let option_sub = res.iter().find(|&x| x.0.username == subscription.username&& x.0.device == subscription.device && x.0.podcast_id == subscription.podcast_id); - match option_sub { - Some(_) => { - diesel::update(subscriptions.filter(dsl_types::username.eq(&subscription - .username).and(dsl_types::device.eq(&subscription.device)).and - (dsl_types::podcast_id.eq(&subscription.podcast_id)))) - .set(dsl_types::deleted.eq(None::)) - .execute(conn).unwrap(); + let opt_sub = Self::find_by_podcast(username.to_string(), device_id.to_string(), c + .to_string(), conn).expect("Error retrieving \ + subscription"); + match opt_sub { + Some(s)=>{ + diesel::update(subscriptions.filter(dsl_types::id.eq(s.id))) + .set(dsl_types::deleted.eq(None::)).execute(conn).unwrap(); }, - None => { - diesel::insert_into(subscriptions).values(&subscription).execute(conn) - .unwrap(); + None=>{ + let subscription = Subscription::new(username.to_string(), device_id.to_string(), + c.to_string()); + diesel::insert_into(subscriptions).values(( + dsl_types::username.eq(subscription.username), + dsl_types::device.eq(subscription.device), + dsl_types::podcast.eq(subscription.podcast), + dsl_types::created.eq(subscription.created), + dsl_types::deleted.eq(None::) + ) + ) + .execute(conn).unwrap(); } } - }); - upload_request.clone().remove.iter().for_each(|c|{ - let podcast = Podcast::get_by_rss_feed(c, conn).unwrap(); - let subscription = Subscription::new(username.to_string(), device_id.to_string(), podcast.id); - let option_sub = res.iter().find(|&x| x.0.username == subscription.username&& x.0.device == subscription.device && x.0.podcast_id == subscription.podcast_id); - match option_sub { - Some(_) => { - diesel::update(subscriptions.filter(dsl_types::username.eq(&subscription - .username).and(dsl_types::device.eq(&subscription.device)).and - (dsl_types::podcast_id.eq(&subscription.podcast_id)))) + }); + upload_request.clone().remove.iter().for_each(|c|{ + let opt_sub = Self::find_by_podcast(username.to_string(), device_id.to_string(), c + .to_string(), conn).expect("Error retrieving \ + subscription"); + match opt_sub { + Some(s)=>{ + diesel::update(subscriptions. + filter(dsl_types::id.eq(s.id))) .set(dsl_types::deleted.eq(Some(Utc::now().naive_utc()))) .execute(conn).unwrap(); }, - None => { - diesel::insert_into(subscriptions).values(&subscription).execute(conn) - .unwrap(); - } + None=>{} } }); Ok(upload_request.clone().add) } + + pub fn find_by_podcast(username_1: String, deviceid_1: String, podcast_1: String, conn: + &mut SqliteConnection) -> Result, Error>{ + use crate::schema::subscriptions::dsl as dsl_types; + use crate::schema::subscriptions::dsl::*; + + let res = subscriptions.filter(username.eq(username_1).and(device.eq + (deviceid_1)).and(podcast.eq(podcast_1))) + .first::(conn) + .optional() + .expect("Error retrieving subscription"); + + Ok(res) + } } \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs index 0b343503..8f57822d 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -121,19 +121,12 @@ diesel::table! { } } -diesel::table! { - subscription_devices (subscription_id, device_id) { - subscription_id -> Integer, - device_id -> Integer, - } -} - diesel::table! { subscriptions (id) { id -> Integer, username -> Text, device -> Text, - podcast_id -> Integer, + podcast -> Text, created -> Timestamp, deleted -> Nullable, } @@ -153,8 +146,6 @@ diesel::table! { diesel::joinable!(favorites -> podcasts (podcast_id)); diesel::joinable!(podcast_episodes -> podcasts (podcast_id)); diesel::joinable!(podcast_history_items -> podcasts (podcast_id)); -diesel::joinable!(subscription_devices -> devices (device_id)); -diesel::joinable!(subscription_devices -> subscriptions (subscription_id)); diesel::allow_tables_to_appear_in_same_query!( devices, @@ -167,7 +158,6 @@ diesel::allow_tables_to_appear_in_same_query!( podcasts, sessions, settings, - subscription_devices, subscriptions, users, ); From eea84bf74e6aa8de28d05435904a125cf20b63b5 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Tue, 25 Apr 2023 21:12:51 +0200 Subject: [PATCH 12/26] Removed old cookie solution. Added toggle for secure cookie. --- Cargo.lock | 169 ------------------------ Cargo.toml | 1 - README.md | 12 ++ src/gpodder/auth/auth.rs | 17 ++- src/gpodder/device/device_controller.rs | 1 - src/gpodder/episodes/episodes.rs | 2 +- src/gpodder/routes.rs | 20 ++- src/main.rs | 6 +- src/models/subscription.rs | 4 +- src/service/environment_service.rs | 5 +- 10 files changed, 37 insertions(+), 200 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fde4a851..91ae6988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,23 +168,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "actix-session" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da8b818ae1f11049a4d218975345fe8e56ce5a5f92c11f972abcff5ff80e87" -dependencies = [ - "actix-service", - "actix-utils", - "actix-web", - "anyhow", - "async-trait", - "derive_more", - "serde", - "serde_json", - "tracing", -] - [[package]] name = "actix-tls" version = "3.0.3" @@ -318,41 +301,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - [[package]] name = "ahash" version = "0.7.6" @@ -409,12 +357,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anyhow" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" - [[package]] name = "askama_escape" version = "0.10.3" @@ -444,17 +386,6 @@ dependencies = [ "syn 2.0.15", ] -[[package]] -name = "async-trait" -version = "0.1.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] - [[package]] name = "atom_syndication" version = "0.12.1" @@ -515,12 +446,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[package]] -name = "base64" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" - [[package]] name = "base64" version = "0.21.0" @@ -627,16 +552,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clokwerk" version = "0.4.0" @@ -668,14 +583,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ - "aes-gcm", - "base64 0.20.0", - "hkdf", - "hmac", "percent-encoding", - "rand", - "sha2 0.10.6", - "subtle", "time", "version_check", ] @@ -764,19 +672,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", "typenum", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - [[package]] name = "cxx" version = "1.0.94" @@ -952,7 +850,6 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.4", "crypto-common", - "subtle", ] [[package]] @@ -1225,16 +1122,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "ghash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" -dependencies = [ - "opaque-debug", - "polyval", -] - [[package]] name = "h2" version = "0.3.17" @@ -1306,24 +1193,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.6", -] - [[package]] name = "http" version = "0.2.9" @@ -1458,15 +1327,6 @@ dependencies = [ "serde", ] -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - [[package]] name = "instant" version = "0.1.12" @@ -1924,7 +1784,6 @@ version = "0.1.0" dependencies = [ "actix", "actix-files", - "actix-session", "actix-web", "actix-web-actors", "actix-web-httpauth", @@ -1964,18 +1823,6 @@ dependencies = [ "xml-builder", ] -[[package]] -name = "polyval" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2518,12 +2365,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - [[package]] name = "syn" version = "1.0.109" @@ -2806,16 +2647,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "universal-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" -dependencies = [ - "crypto-common", - "subtle", -] - [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 7ce48cd5..416617ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ uuid = {version="1.3.1", features = ["v4", "serde"]} libsqlite3-sys = {version = "0.25.2", features = ["bundled"]} diesel_migrations = "2.0.0" actix-files = "0.6.2" -actix-session = {version="0.7.2",features=["cookie-session"]} actix-web = {version="4.3.0", features=["rustls"]} jsonwebtoken = {version="8.2.0"} log = "0.4.17" diff --git a/README.md b/README.md index 55270c02..3a9e408e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Podfetch is a self-hosted podcast manager. It is a web app that lets you download podcasts and listen to them online. It is written in Rust and uses React for the frontend. +It also contains a GPodder integration so you can continue using your current podcast app. Every time a new commit is pushed to the main branch, a new docker image is built and pushed to docker hub. So it is best to use something like [watchtower](https://github.com/containrrr/watchtower) to automatically update the docker image. @@ -114,6 +115,17 @@ To configure it you need to create an account on that website. After creating an After successful setup you should see on the settings page a green checkmark next to the Podindex config section. +# GPodder + +Podfetch also supports the GPodder api. You can use your current GPodder account to login to Podfetch and continue using your current podcast app. +To do that just go to the settings page and enter your GPodder username and password. + +To enable it you need to set the following environment variables: +| Variable | Description | Default | +|---------------------|---------------------------------------|---------| +| GPODDER_INTEGRATION_ENABLED | Activates the GPodder integration via your server url | false| + + # Roadmap - [x] Add podcasts via Itunes api diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs index e6a88e83..f94d71d2 100644 --- a/src/gpodder/auth/auth.rs +++ b/src/gpodder/auth/auth.rs @@ -1,31 +1,30 @@ -use std::collections::HashMap; use std::io::Error; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Mutex, MutexGuard}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::web::Data; use sha256::digest; use crate::{DbPool, extract_basic_auth}; use crate::models::user::User; use actix_web::{post}; -use uuid::Uuid; use crate::mutex::LockResultExt; use crate::service::environment_service::EnvironmentService; use awc::cookie::{Cookie, SameSite}; use diesel::SqliteConnection; use crate::models::session::Session; -use crate::schema::sessions::session_id; #[post("/auth/{username}/login.json")] pub async fn login(username:web::Path, rq: HttpRequest, conn:Data, env_service: Data>) ->impl Responder { + let env = env_service.lock().ignore_poison(); + match rq.clone().cookie("sessionid") { Some(cookie) => { let session = cookie.value(); let opt_session = Session::find_by_session_id(session, &mut conn.get().unwrap()); if opt_session.is_ok(){ - let user_cookie = create_session_cookie(opt_session.unwrap()); + let user_cookie = create_session_cookie(opt_session.unwrap(), env); return HttpResponse::Ok().cookie(user_cookie).finish(); } } @@ -35,7 +34,6 @@ Responder { let authorization = rq.headers().get("Authorization").unwrap().to_str().unwrap(); let unwrapped_username = username.into_inner(); let (username_basic, password) = basic_auth_login(authorization.to_string()); - let env = env_service.lock().ignore_poison(); if username_basic != unwrapped_username { return HttpResponse::Unauthorized().finish(); } @@ -47,7 +45,7 @@ Responder { if user.clone().password.unwrap()== digest(password) { let session = Session::new(user.username); Session::insert_session(&session, &mut conn.get().unwrap()).expect("Error inserting session"); - let user_cookie = create_session_cookie(session); + let user_cookie = create_session_cookie(session, env); HttpResponse::Ok().cookie(user_cookie).finish() } else { HttpResponse::Unauthorized().finish() @@ -60,10 +58,11 @@ Responder { } } -fn create_session_cookie(session: Session) -> Cookie<'static> { +fn create_session_cookie(session: Session, env: MutexGuard) -> Cookie<'static> { + let secure = env.server_url.starts_with("https"); let user_cookie = Cookie::build("sessionid", session.session_id) .http_only(true).secure - (false).same_site + (secure).same_site (SameSite::Strict).path("/api").finish(); user_cookie } diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 8b5f8bbb..ea603061 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -3,7 +3,6 @@ use crate::gpodder::device::dto::device_post::DevicePost; use crate::models::device::{Device, DeviceResponse}; use actix_web::{post, get}; use actix_web::web::Data; -use crate::controllers::watch_time_controller::get_username; use crate::DbPool; use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index 91e9154a..d77db227 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -9,7 +9,7 @@ use crate::models::models::PodcastWatchedPostModel; use std::borrow::Borrow; use chrono::NaiveDateTime; use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; -use crate::utils::time::{get_current_timestamp, get_current_timestamp_str}; +use crate::utils::time::{get_current_timestamp}; #[derive(Serialize, Deserialize)] pub struct EpisodeActionResponse{ diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index c19f6349..47a8748f 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -1,23 +1,21 @@ -use actix_session::SessionMiddleware; -use actix_session::storage::CookieSessionStore; use actix_web::{Error, Scope, web}; use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse}; -use awc::cookie::Key; use crate::gpodder::device::device_controller::{get_devices_of_user, post_device}; use crate::{DbPool}; use crate::gpodder::auth::auth::login; use crate::gpodder::episodes::episodes::{get_episode_actions, upload_episode_actions}; use crate::gpodder::subscription::subscriptions::{get_subscriptions, upload_subscription_changes}; +use crate::service::environment_service::EnvironmentService; -pub fn get_gpodder_api(pool: DbPool) ->Scope>{ - let secret_key = Key::generate(); - - web::scope("/api/2") - .wrap(SessionMiddleware::new(CookieSessionStore::default(),secret_key)) - .service(login) - .service(get_authenticated_api(pool.clone())) +pub fn get_gpodder_api(pool: DbPool, environment_service: EnvironmentService) ->Scope{ + if environment_service.gpodder_integration_enabled { + web::scope("/api/2") + .service(login) + .service(get_authenticated_api(pool.clone())) + } else { + web::scope("/api/2") + } } diff --git a/src/main.rs b/src/main.rs index 7192837b..b9f70fb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,10 +16,9 @@ use actix_web::middleware::{Condition, Logger}; use actix_web::web::{redirect, Data}; use actix_web::{web, App, Error, HttpResponse, HttpServer, Responder, Scope}; use clokwerk::{Scheduler, TimeUnits}; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Mutex}; use std::time::Duration; use std::{env, thread}; -use std::collections::HashMap; use std::env::var; use std::io::Read; use std::str::FromStr; @@ -72,7 +71,6 @@ use crate::models::oidc_model::{CustomJwk, CustomJwkSet}; use crate::models::session::Session; use crate::models::user::User; use crate::models::web_socket_message::Lobby; -use crate::mutex::LockResultExt; use crate::service::environment_service::EnvironmentService; use crate::service::file_service::FileService; use crate::service::jwkservice::JWKService; @@ -343,7 +341,7 @@ async fn main() -> std::io::Result<()> { App::new() .service(redirect("/", var("SUB_DIRECTORY").unwrap()+"/ui/")) - .service(get_gpodder_api(pool.clone())) + .service(get_gpodder_api(pool.clone(), environment_service.clone())) .service(get_global_scope(pool.clone())) .app_data(Data::new(chat_server.clone())) .app_data(Data::new(Mutex::new(podcast_episode_service.clone()))) diff --git a/src/models/subscription.rs b/src/models/subscription.rs index 5242fb4b..d2d66594 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -1,8 +1,7 @@ use std::io::Error; use actix_web::web; use chrono::{NaiveDateTime, Utc}; -use diesel::{BoolExpressionMethods, QueryDsl, RunQueryDsl, sql_query, SqliteConnection}; -use crate::models::itunes_models::Podcast; +use diesel::{BoolExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection}; use crate::gpodder::subscription::subscriptions::SubscriptionUpdateRequest; use diesel::ExpressionMethods; @@ -129,7 +128,6 @@ impl SubscriptionChangesToClient { pub fn find_by_podcast(username_1: String, deviceid_1: String, podcast_1: String, conn: &mut SqliteConnection) -> Result, Error>{ - use crate::schema::subscriptions::dsl as dsl_types; use crate::schema::subscriptions::dsl::*; let res = subscriptions.filter(username.eq(username_1).and(device.eq diff --git a/src/service/environment_service.rs b/src/service/environment_service.rs index 034506f5..67ee7d7c 100644 --- a/src/service/environment_service.rs +++ b/src/service/environment_service.rs @@ -25,6 +25,7 @@ pub struct EnvironmentService { pub password: String, pub oidc_config: Option, pub oidc_configured: bool, + pub gpodder_integration_enabled: bool } impl EnvironmentService { @@ -68,7 +69,8 @@ impl EnvironmentService { username: var(USERNAME).unwrap_or("".to_string()), password: var(PASSWORD).unwrap_or("".to_string()), oidc_configured, - oidc_config: option_oidc_config + oidc_config: option_oidc_config, + gpodder_integration_enabled: var("GPODDER_INTEGRATION_ENABLED").is_ok() } } @@ -98,6 +100,7 @@ impl EnvironmentService { "Polling interval for new episodes: {} minutes", self.polling_interval ); + println!("GPodder integration enabled: {}", self.gpodder_integration_enabled); println!("Database url is set to: {}", var("DATABASE_URL").unwrap_or("sqlite://./db/podcast.db".to_string())); println!( "Podindex API key&secret configured: {}", From 1a1142b038693b129c2e9b099a3df131bbdfadf5 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Wed, 26 Apr 2023 07:22:15 +0200 Subject: [PATCH 13/26] Added command line runner. --- src/command_line_runner.rs | 67 ++++++++++++++++++++++++++++++++++++++ src/constants/constants.rs | 3 ++ src/main.rs | 10 +++++- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/command_line_runner.rs diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs new file mode 100644 index 00000000..2b2397e6 --- /dev/null +++ b/src/command_line_runner.rs @@ -0,0 +1,67 @@ +use std::env::Args; +use std::fmt::format; +use std::io::stdin; +use diesel::result::Error::RollbackErrorOnCommit; +use crate::constants::constants::Role; +use crate::models::user::User; + +pub fn start_command_line(mut args: Args){ + match args.nth(1).unwrap().as_str() { + "users"=>{ + match args.nth(0).unwrap().as_str() { + "add"=> { + read_user_account(); + } + "remove"=> { + // remove user + } + "list"=> { + // list users + } + _ => { + // error + } + } + } + _ => {} + } +} + + + +pub fn read_user_account()->User{ + let mut username = String::new(); + let mut password = String::new(); + let role = Role::VALUES.map(|v|{ + return v.to_string() + }).join(", "); + retry_read("Enter your username: ", &mut username); + retry_read("Enter your password: ", &mut password); + retry_read(&format!("Select your role {}",&role), &mut password); + + User{ + id: 0, + username, + role: "".to_string(), + password: Some(password), + explicit_consent: false, + created_at: Default::default(), + } +} + +pub fn retry_read(prompt: &str, input: &mut String){ + print!("{}",prompt); + match stdin().read_line(input).unwrap().len()>0{ + Ok(e) => { + if input.trim().len()>0{ + retry_read(prompt, input); + } + } + Err(e) => { + print!("Error reading input: {}", e); + retry_read(prompt, input); + } + } + +} + diff --git a/src/constants/constants.rs b/src/constants/constants.rs index b471d7e8..2a1e3e63 100644 --- a/src/constants/constants.rs +++ b/src/constants/constants.rs @@ -64,6 +64,9 @@ impl FromStr for Role{ } } } +impl Role{ + pub const VALUES: [Self; 3] = [Self::User, Self::Admin, Self::Uploader]; +} // environment keys pub const OIDC_AUTH:&str = "OIDC_AUTH"; diff --git a/src/main.rs b/src/main.rs index b9f70fb2..c8759ee8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,8 +19,9 @@ use clokwerk::{Scheduler, TimeUnits}; use std::sync::{Mutex}; use std::time::Duration; use std::{env, thread}; -use std::env::var; +use std::env::{args, var}; use std::io::Read; +use std::process::exit; use std::str::FromStr; use actix_web_httpauth::extractors::bearer::BearerAuth; use jsonwebtoken::{Algorithm, decode, DecodingKey, Validation}; @@ -58,6 +59,7 @@ use crate::controllers::websocket_controller::{ get_rss_feed, get_rss_feed_for_podcast, start_connection, }; pub use controllers::controller_utils::*; +use crate::command_line_runner::start_command_line; use crate::controllers::user_controller::{create_invite, delete_invite, delete_user, get_invite, get_invite_link, get_invites, get_users, onboard_user, update_role}; mod constants; @@ -87,6 +89,7 @@ pub mod utils; pub mod mutex; mod exception; mod gpodder; +mod command_line_runner; type DbPool = Pool>; @@ -259,6 +262,11 @@ pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); #[actix_web::main] async fn main() -> std::io::Result<()> { + + if args().len()>1 { + start_command_line(args()); + exit(0) + } //services let podcast_episode_service = PodcastEpisodeService::new(); let podcast_service = PodcastService::new(); From ef7e9f56b3ddaa2a1eb99858e7b1f2ebf6725aaa Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:27:11 +0200 Subject: [PATCH 14/26] Remove secure cookie. --- src/gpodder/auth/auth.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs index f94d71d2..38c20edb 100644 --- a/src/gpodder/auth/auth.rs +++ b/src/gpodder/auth/auth.rs @@ -59,10 +59,10 @@ Responder { } fn create_session_cookie(session: Session, env: MutexGuard) -> Cookie<'static> { - let secure = env.server_url.starts_with("https"); let user_cookie = Cookie::build("sessionid", session.session_id) - .http_only(true).secure - (secure).same_site + .http_only(true) + .secure(false) + .same_site (SameSite::Strict).path("/api").finish(); user_cookie } From 59579ceefa88920d4bb6a027906a168b51856177 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:37:31 +0200 Subject: [PATCH 15/26] Fixed build. --- src/command_line_runner.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index 2b2397e6..70105e59 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -51,14 +51,14 @@ pub fn read_user_account()->User{ pub fn retry_read(prompt: &str, input: &mut String){ print!("{}",prompt); - match stdin().read_line(input).unwrap().len()>0{ - Ok(e) => { + stdin().read_line(input).unwrap(); + match input.len()>0{ + true => { if input.trim().len()>0{ retry_read(prompt, input); } } - Err(e) => { - print!("Error reading input: {}", e); + false => { retry_read(prompt, input); } } From 17b5a4336c6b1a463ea368917ebc62f30215a6b5 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Wed, 26 Apr 2023 20:53:52 +0200 Subject: [PATCH 16/26] Added middleware for session validation. --- src/gpodder/device/device_controller.rs | 57 +++++++---- src/gpodder/episodes/episodes.rs | 116 ++++++++++++---------- src/gpodder/mod.rs | 3 +- src/gpodder/routes.rs | 14 ++- src/gpodder/session_middleware.rs | 93 +++++++++++++++++ src/gpodder/subscription/subscriptions.rs | 87 +++++++++------- src/models/session.rs | 2 +- 7 files changed, 255 insertions(+), 117 deletions(-) create mode 100644 src/gpodder/session_middleware.rs diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index ea603061..2707e334 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -1,41 +1,54 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder, web}; use crate::gpodder::device::dto::device_post::DevicePost; use crate::models::device::{Device, DeviceResponse}; use actix_web::{post, get}; use actix_web::web::Data; use crate::DbPool; use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; +use crate::models::session::Session; + #[post("/devices/{username}/{deviceid}.json")] pub async fn post_device( query: web::Path<(String, String)>, device_post: web::Json, - rq: HttpRequest, + opt_flag: Option>, conn: Data) -> impl Responder { - let username = query.clone().0; - let deviceid = query.clone().1; - let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), - username.clone()).await; - if auth_check_res.is_err(){ - return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); - } - let device = Device::new(device_post.into_inner(), deviceid, username); - let result = device.save(&mut conn.get().unwrap()).unwrap(); + match opt_flag{ + Some(flag)=>{ + let username = query.clone().0; + let deviceid = query.clone().1; + if flag.username!= username{ + return HttpResponse::Unauthorized().finish(); + } - HttpResponse::Ok().json(result) -} + let device = Device::new(device_post.into_inner(), deviceid, username); -#[get("/devices/{username}.json")] -pub async fn get_devices_of_user(query: web::Path, conn: Data, rq: HttpRequest) -> impl Responder { - let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), - query.clone()).await; - if auth_check_res.is_err(){ - return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + let result = device.save(&mut conn.get().unwrap()).unwrap(); + + HttpResponse::Ok().json(result) + } + None=>{ + HttpResponse::Unauthorized().finish() + } } +} - let devices = Device::get_devices_of_user(&mut conn.get().unwrap(), query.clone()).unwrap(); +#[get("/devices/{username}.json")] +pub async fn get_devices_of_user(query: web::Path,opt_flag: Option>, conn: Data) -> impl Responder { + match opt_flag { + Some(flag) => { + if flag.username!= query.clone(){ + return HttpResponse::Unauthorized().finish(); + } + let devices = Device::get_devices_of_user(&mut conn.get().unwrap(), query.clone()).unwrap(); - let dtos = devices.iter().map(|d|d.to_dto()).collect::>(); - HttpResponse::Ok().json(dtos) + let dtos = devices.iter().map(|d| d.to_dto()).collect::>(); + HttpResponse::Ok().json(dtos) + } + None => { + HttpResponse::Unauthorized().finish() + } + } } \ No newline at end of file diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index d77db227..dff908a8 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -9,6 +9,7 @@ use crate::models::models::PodcastWatchedPostModel; use std::borrow::Borrow; use chrono::NaiveDateTime; use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; +use crate::models::session::Session; use crate::utils::time::{get_current_timestamp}; #[derive(Serialize, Deserialize)] @@ -30,69 +31,78 @@ pub struct EpisodeSinceRequest{ } #[get("/episodes/{username}.json")] -pub async fn get_episode_actions(username: web::Path, pool: Data, since: -web::Query, rq: HttpRequest) -> - impl Responder { +pub async fn get_episode_actions(username: web::Path, pool: Data, + opt_flag: Option>, + since: web::Query) -> impl Responder { + match opt_flag { + Some(flag) => { + let username = username.clone(); + if flag.username != username.clone() { + return HttpResponse::Unauthorized().finish(); + } - let auth_check_res= auth_checker(&mut *pool.get().unwrap(), extract_from_http_request(rq), - username.clone()).await; - if auth_check_res.is_err(){ - return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + let since_date = NaiveDateTime::from_timestamp_opt(since.since as i64, 0); + let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap(), since_date) + .await; + HttpResponse::Ok().json(EpisodeActionResponse { + actions, + timestamp: get_current_timestamp() + }) + } + None => { + HttpResponse::Unauthorized().finish() + } } - let since_date = NaiveDateTime::from_timestamp_opt(since.since as i64, 0); - let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap(), since_date) - .await; - HttpResponse::Ok().json(EpisodeActionResponse{ - actions, - timestamp: get_current_timestamp() - }) } #[post("/episodes/{username}.json")] -pub async fn upload_episode_actions(username: web::Path, podcast_episode: -web::Json>, conn: Data, rq:HttpRequest) -> impl +pub async fn upload_episode_actions(username: web::Path, podcast_episode: web::Json>,opt_flag: Option>, conn: Data) -> impl Responder { - let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), - username.clone()).await; - if auth_check_res.is_err(){ - return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); - } - - let mut inserted_episodes:Vec = vec![]; - podcast_episode.iter().for_each(|episode| { - let episode = Episode::convert_to_episode(episode, username.clone()); - inserted_episodes.push(Episode::insert_episode(episode.borrow(), &mut *conn.get().unwrap()) - .expect("Unable to insert episode")); + match opt_flag { + Some(flag) => { + if flag.username != username.clone() { + return HttpResponse::Unauthorized().finish(); + } + let mut inserted_episodes: Vec = vec![]; + podcast_episode.iter().for_each(|episode| { + let episode = Episode::convert_to_episode(episode, username.clone()); + inserted_episodes.push(Episode::insert_episode(episode.borrow(), &mut *conn.get().unwrap()) + .expect("Unable to insert episode")); - if EpisodeAction::from_string(&episode.clone().action) == EpisodeAction::Play{ - let mut episode_url = episode.clone().episode; - // Sometimes podcast provider like to check which browser access their podcast - let mut first_split = episode.episode.split("?"); - let res = first_split.next(); + if EpisodeAction::from_string(&episode.clone().action) == EpisodeAction::Play { + let mut episode_url = episode.clone().episode; + // Sometimes podcast provider like to check which browser access their podcast + let mut first_split = episode.episode.split("?"); + let res = first_split.next(); - if res.is_some(){ - episode_url = res.unwrap().parse().unwrap() - } + if res.is_some() { + episode_url = res.unwrap().parse().unwrap() + } - let podcast_episode = DB::query_podcast_episode_by_url(&mut *conn.get().unwrap(), - &*episode_url); - if podcast_episode.clone().unwrap().is_none(){ - return; - } + let podcast_episode = DB::query_podcast_episode_by_url(&mut *conn.get().unwrap(), + &*episode_url); + if podcast_episode.clone().unwrap().is_none() { + return; + } - let model = PodcastWatchedPostModel{ - podcast_episode_id: podcast_episode.clone().unwrap().unwrap().episode_id, - time: episode.position.unwrap() as i32, - }; - DB::log_watchtime(&mut *conn.get().unwrap(), model, "admin".to_string()) - .expect("TODO: panic message"); - println!("episode: {:?}", episode); + let model = PodcastWatchedPostModel { + podcast_episode_id: podcast_episode.clone().unwrap().unwrap().episode_id, + time: episode.position.unwrap() as i32, + }; + DB::log_watchtime(&mut *conn.get().unwrap(), model, "admin".to_string()) + .expect("TODO: panic message"); + println!("episode: {:?}", episode); + } + }); + // TODO What is rewriting urls https://buildmedia.readthedocs.org/media/pdf/gpoddernet/latest/gpoddernet.pdf + HttpResponse::Ok().json(EpisodeActionPostResponse { + update_urls: vec![], + timestamp: get_current_timestamp() + }) } - }); - // TODO What is rewriting urls https://buildmedia.readthedocs.org/media/pdf/gpoddernet/latest/gpoddernet.pdf - HttpResponse::Ok().json(EpisodeActionPostResponse{ - update_urls: vec![], - timestamp: get_current_timestamp() - }) + None => { + HttpResponse::Unauthorized().finish() + } + } } \ No newline at end of file diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index c24bede3..1f5e8557 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -3,4 +3,5 @@ pub mod device; pub mod parametrization; pub mod auth; pub mod subscription; -mod episodes; \ No newline at end of file +mod episodes; +mod session_middleware; \ No newline at end of file diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index 47a8748f..16afc968 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -1,9 +1,14 @@ use actix_web::{Error, Scope, web}; +use actix_web::body::{BoxBody, EitherBody}; use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse}; +use diesel::r2d2::ConnectionManager; +use diesel::SqliteConnection; +use r2d2::Pool; use crate::gpodder::device::device_controller::{get_devices_of_user, post_device}; use crate::{DbPool}; use crate::gpodder::auth::auth::login; use crate::gpodder::episodes::episodes::{get_episode_actions, upload_episode_actions}; +use crate::gpodder::session_middleware::{CookieFilter, CookieFilterMiddleware}; use crate::gpodder::subscription::subscriptions::{get_subscriptions, upload_subscription_changes}; use crate::service::environment_service::EnvironmentService; @@ -12,16 +17,19 @@ pub fn get_gpodder_api(pool: DbPool, environment_service: EnvironmentService) -> if environment_service.gpodder_integration_enabled { web::scope("/api/2") .service(login) - .service(get_authenticated_api(pool.clone())) + .service(get_authenticated_api()) } else { web::scope("/api/2") } } -pub fn get_authenticated_api(_: DbPool) ->Scope>{ +pub fn get_authenticated_api() + ->Scope>, Error = Error, InitError = ()>>{ web::scope("") + .wrap(CookieFilter::new()) .service(post_device) .service(get_devices_of_user) .service(get_subscriptions) diff --git a/src/gpodder/session_middleware.rs b/src/gpodder/session_middleware.rs new file mode 100644 index 00000000..16349a5b --- /dev/null +++ b/src/gpodder/session_middleware.rs @@ -0,0 +1,93 @@ +use std::ops::DerefMut; +use std::rc::Rc; +use actix::ActorFutureExt; +use actix::fut::{err, ok}; +use futures_util::FutureExt; +use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpMessage, HttpResponse}; +use actix_web::body::{EitherBody, MessageBody}; +use actix_web::error::{ErrorForbidden, ErrorUnauthorized}; +use actix_web::web::Data; +use awc::body::BoxBody; +use diesel::{OptionalExtension, QueryDsl, RunQueryDsl, SqliteConnection}; +use futures_util::future::{LocalBoxFuture, Ready}; +use crate::models::session::Session; +use diesel::ExpressionMethods; +use crate::config::dbconfig::establish_connection; +use crate::DbPool; + +pub struct CookieFilter { +} + +impl CookieFilter { + pub fn new() -> Self { + CookieFilter { } + } +} + +pub struct CookieFilterMiddleware{ + service: Rc, +} + +impl Transform for CookieFilter + where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: MessageBody + 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Transform = CookieFilterMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CookieFilterMiddleware { + service: Rc::new(service) + }) + } +} + +impl Service for CookieFilterMiddleware + where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: MessageBody + 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let cookie = req.cookie("sessionid"); + if cookie.is_none(){ + return Box::pin(ok(req.error_response(ErrorUnauthorized("Unauthorized")) + .map_into_right_body + ())); + } + let binding = cookie.unwrap(); + let extracted_cookie = binding.value(); + + use crate::schema::sessions::dsl::*; + let session = sessions.filter(session_id.eq(extracted_cookie)) + .first::(&mut establish_connection()) + .optional() + .expect("Error connecting to database"); + if session.is_none(){ + return Box::pin(ok(req.error_response(ErrorForbidden("Forbidden")) + .map_into_right_body())); + } + + let service = Rc::clone(&self.service); + + req.extensions_mut().insert(session.unwrap()); + async move { + service + .call(req) + .await + .map(|res| res.map_into_left_body()) + } + .boxed_local() + } +} \ No newline at end of file diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index f0e7161a..9e84f11d 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -3,6 +3,7 @@ use actix_web::{get, post}; use actix_web::web::Data; use crate::DbPool; use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; +use crate::models::session::Session; use crate::models::subscription::SubscriptionChangesToClient; use crate::utils::time::get_current_timestamp; @@ -24,52 +25,64 @@ pub struct SubscriptionPostResponse { } #[get("/subscriptions/{username}/{deviceid}.json")] -pub async fn get_subscriptions(paths: web::Path<(String, String)>, - query:web::Query, conn: Data, - rq:HttpRequest -) -> impl +pub async fn get_subscriptions(paths: web::Path<(String, String)>,opt_flag: + Option>, + query:web::Query, conn: Data) -> impl Responder { - let username = paths.clone().0; - let deviceid = paths.clone().1; + match opt_flag { + Some(flag) => { + let username = paths.clone().0; + let deviceid = paths.clone().1; + if flag.username != username.clone() { + return HttpResponse::Unauthorized().finish(); + } - let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), - username.clone()).await; - if auth_check_res.is_err(){ - return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); - } - - let res = SubscriptionChangesToClient::get_device_subscriptions(&deviceid, &username,query - .since, - &mut *conn.get().unwrap()).await; + let res = SubscriptionChangesToClient::get_device_subscriptions(&deviceid, &username, query + .since, + &mut *conn.get().unwrap()).await; - println!("res: {:?}", res); - match res { - Ok(res) => { - HttpResponse::Ok().json(res) - }, - Err(_) => HttpResponse::InternalServerError().finish() + match res { + Ok(res) => { + HttpResponse::Ok().json(res) + }, + Err(_) => HttpResponse::InternalServerError().finish() + } + } + None => { + HttpResponse::Unauthorized().finish() + } } } #[post("/subscriptions/{username}/{deviceid}.json")] pub async fn upload_subscription_changes(upload_request: web::Json, + opt_flag: Option>, paths: web::Path<(String, String)>, conn: Data, - rq:HttpRequest)->impl Responder{ - let username = paths.clone().0; - let deviceid = paths.clone().1; + rq:HttpRequest)->impl Responder { + match opt_flag { + Some(flag) => { + let username = paths.clone().0; + let deviceid = paths.clone().1; + if flag.username != username.clone() { + return HttpResponse::Unauthorized().finish(); + } + let auth_check_res = auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), + username.clone()).await; + if auth_check_res.is_err() { + return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + } + SubscriptionChangesToClient::update_subscriptions(&deviceid, &username, + upload_request, + &mut *conn.get().unwrap()).await.expect + ("TODO: panic message"); - let auth_check_res= auth_checker(&mut *conn.get().unwrap(), extract_from_http_request(rq), - username.clone()).await; - if auth_check_res.is_err(){ - return HttpResponse::Unauthorized().body(auth_check_res.err().unwrap().to_string()); + HttpResponse::Ok().json(SubscriptionPostResponse { + update_urls: vec![], + timestamp: get_current_timestamp() + }) + } + None => { + HttpResponse::Unauthorized().finish() + } } - SubscriptionChangesToClient::update_subscriptions(&deviceid, &username, - upload_request, - &mut *conn.get().unwrap()).await.expect - ("TODO: panic message"); - - HttpResponse::Ok().json(SubscriptionPostResponse{ - update_urls: vec![], - timestamp: get_current_timestamp() - }) } \ No newline at end of file diff --git a/src/models/session.rs b/src/models/session.rs index 3330203e..b5862e18 100644 --- a/src/models/session.rs +++ b/src/models/session.rs @@ -6,7 +6,7 @@ use crate::schema::sessions; use diesel::QueryDsl; use diesel::ExpressionMethods; -#[derive(Queryable, Insertable, Clone, ToSchema, PartialEq)] +#[derive(Queryable, Insertable, Clone, ToSchema, PartialEq, Debug)] pub struct Session{ pub username: String, pub session_id: String, From 7a56ccb947d79587f370440a2ee877c5adb91c08 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Wed, 26 Apr 2023 20:57:43 +0200 Subject: [PATCH 17/26] Reformatted exception. --- src/gpodder/subscription/subscriptions.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index 9e84f11d..83d4ea9a 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -73,8 +73,7 @@ pub async fn upload_subscription_changes(upload_request: web::Json Date: Wed, 26 Apr 2023 21:13:35 +0200 Subject: [PATCH 18/26] Removed unused imports. --- src/command_line_runner.rs | 2 -- src/gpodder/auth/auth.rs | 8 ++++---- src/gpodder/device/device_controller.rs | 3 +-- src/gpodder/episodes/episodes.rs | 4 +--- src/gpodder/routes.rs | 8 ++------ src/gpodder/session_middleware.rs | 11 +++-------- src/gpodder/subscription/subscriptions.rs | 6 +++--- src/main.rs | 6 +++--- src/models/subscription.rs | 15 ++++++++++++--- 9 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index 70105e59..cb7ebf48 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -1,7 +1,5 @@ use std::env::Args; -use std::fmt::format; use std::io::stdin; -use diesel::result::Error::RollbackErrorOnCommit; use crate::constants::constants::Role; use crate::models::user::User; diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs index 38c20edb..4b3834ea 100644 --- a/src/gpodder/auth/auth.rs +++ b/src/gpodder/auth/auth.rs @@ -1,5 +1,5 @@ use std::io::Error; -use std::sync::{Mutex, MutexGuard}; +use std::sync::{Mutex}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::web::Data; use sha256::digest; @@ -24,7 +24,7 @@ Responder { let session = cookie.value(); let opt_session = Session::find_by_session_id(session, &mut conn.get().unwrap()); if opt_session.is_ok(){ - let user_cookie = create_session_cookie(opt_session.unwrap(), env); + let user_cookie = create_session_cookie(opt_session.unwrap()); return HttpResponse::Ok().cookie(user_cookie).finish(); } } @@ -45,7 +45,7 @@ Responder { if user.clone().password.unwrap()== digest(password) { let session = Session::new(user.username); Session::insert_session(&session, &mut conn.get().unwrap()).expect("Error inserting session"); - let user_cookie = create_session_cookie(session, env); + let user_cookie = create_session_cookie(session); HttpResponse::Ok().cookie(user_cookie).finish() } else { HttpResponse::Unauthorized().finish() @@ -58,7 +58,7 @@ Responder { } } -fn create_session_cookie(session: Session, env: MutexGuard) -> Cookie<'static> { +fn create_session_cookie(session: Session) -> Cookie<'static> { let user_cookie = Cookie::build("sessionid", session.session_id) .http_only(true) .secure(false) diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 2707e334..7b424171 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -1,10 +1,9 @@ -use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder, web}; +use actix_web::{HttpResponse, Responder, web}; use crate::gpodder::device::dto::device_post::DevicePost; use crate::models::device::{Device, DeviceResponse}; use actix_web::{post, get}; use actix_web::web::Data; use crate::DbPool; -use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; use crate::models::session::Session; diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index dff908a8..868befc4 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -1,4 +1,4 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use actix_web::{HttpResponse, Responder, web}; use actix_web::{get,post}; use actix_web::web::Data; @@ -8,7 +8,6 @@ use crate::models::episode::{Episode, EpisodeAction, EpisodeDto}; use crate::models::models::PodcastWatchedPostModel; use std::borrow::Borrow; use chrono::NaiveDateTime; -use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; use crate::models::session::Session; use crate::utils::time::{get_current_timestamp}; @@ -95,7 +94,6 @@ Responder { println!("episode: {:?}", episode); } }); - // TODO What is rewriting urls https://buildmedia.readthedocs.org/media/pdf/gpoddernet/latest/gpoddernet.pdf HttpResponse::Ok().json(EpisodeActionPostResponse { update_urls: vec![], timestamp: get_current_timestamp() diff --git a/src/gpodder/routes.rs b/src/gpodder/routes.rs index 16afc968..1fffbeef 100644 --- a/src/gpodder/routes.rs +++ b/src/gpodder/routes.rs @@ -1,18 +1,14 @@ use actix_web::{Error, Scope, web}; use actix_web::body::{BoxBody, EitherBody}; use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse}; -use diesel::r2d2::ConnectionManager; -use diesel::SqliteConnection; -use r2d2::Pool; use crate::gpodder::device::device_controller::{get_devices_of_user, post_device}; -use crate::{DbPool}; use crate::gpodder::auth::auth::login; use crate::gpodder::episodes::episodes::{get_episode_actions, upload_episode_actions}; -use crate::gpodder::session_middleware::{CookieFilter, CookieFilterMiddleware}; +use crate::gpodder::session_middleware::{CookieFilter}; use crate::gpodder::subscription::subscriptions::{get_subscriptions, upload_subscription_changes}; use crate::service::environment_service::EnvironmentService; -pub fn get_gpodder_api(pool: DbPool, environment_service: EnvironmentService) ->Scope{ +pub fn get_gpodder_api(environment_service: EnvironmentService) ->Scope{ if environment_service.gpodder_integration_enabled { web::scope("/api/2") diff --git a/src/gpodder/session_middleware.rs b/src/gpodder/session_middleware.rs index 16349a5b..6d481b09 100644 --- a/src/gpodder/session_middleware.rs +++ b/src/gpodder/session_middleware.rs @@ -1,19 +1,14 @@ -use std::ops::DerefMut; use std::rc::Rc; -use actix::ActorFutureExt; -use actix::fut::{err, ok}; +use actix::fut::{ok}; use futures_util::FutureExt; -use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpMessage, HttpResponse}; +use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpMessage}; use actix_web::body::{EitherBody, MessageBody}; use actix_web::error::{ErrorForbidden, ErrorUnauthorized}; -use actix_web::web::Data; -use awc::body::BoxBody; -use diesel::{OptionalExtension, QueryDsl, RunQueryDsl, SqliteConnection}; +use diesel::{OptionalExtension, QueryDsl, RunQueryDsl}; use futures_util::future::{LocalBoxFuture, Ready}; use crate::models::session::Session; use diesel::ExpressionMethods; use crate::config::dbconfig::establish_connection; -use crate::DbPool; pub struct CookieFilter { } diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index 83d4ea9a..76946f98 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -21,7 +21,7 @@ pub struct SubscriptionUpdateRequest { #[derive(Deserialize, Serialize)] pub struct SubscriptionPostResponse { pub timestamp: i64, - pub update_urls: Vec + pub update_urls: Vec> } #[get("/subscriptions/{username}/{deviceid}.json")] @@ -71,12 +71,12 @@ pub async fn upload_subscription_changes(upload_request: web::Json std::io::Result<()> { App::new() .service(redirect("/", var("SUB_DIRECTORY").unwrap()+"/ui/")) - .service(get_gpodder_api(pool.clone(), environment_service.clone())) + .service(get_gpodder_api(environment_service.clone())) .service(get_global_scope(pool.clone())) .app_data(Data::new(chat_server.clone())) .app_data(Data::new(Mutex::new(podcast_episode_service.clone()))) @@ -531,7 +531,7 @@ pub fn check_server_config(service1: EnvironmentService) { if service1.http_basic { if service1.password.is_empty() || service1.username.is_empty() { log::error!("BASIC_AUTH activated but no username or password set. Please set username and password in the .env file."); - std::process::exit(1); + exit(1); } } @@ -542,7 +542,7 @@ pub fn check_server_config(service1: EnvironmentService) { if var(TELEGRAM_API_ENABLED).is_ok(){ if !var(TELEGRAM_BOT_TOKEN).is_ok() || !var(TELEGRAM_BOT_CHAT_ID).is_ok() { log::error!("TELEGRAM_API_ENABLED activated but no TELEGRAM_API_TOKEN or TELEGRAM_API_CHAT_ID set. Please set TELEGRAM_API_TOKEN and TELEGRAM_API_CHAT_ID in the .env file."); - std::process::exit(1); + exit(1); } } } diff --git a/src/models/subscription.rs b/src/models/subscription.rs index d2d66594..2d0efa2a 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -76,12 +76,17 @@ impl SubscriptionChangesToClient { } pub async fn update_subscriptions(device_id: &str, username: &str, upload_request: - web::Json, conn: &mut SqliteConnection)-> Result, Error>{ + web::Json, conn: &mut SqliteConnection)-> Result>, + Error>{ use crate::schema::subscriptions::dsl as dsl_types; use crate::schema::subscriptions::dsl::subscriptions; - + let mut rewritten_urls:Vec> = vec![vec![]]; // Add subscriptions upload_request.clone().add.iter().for_each(|c| { + if !c.starts_with("http")||!c.starts_with("https"){ + rewritten_urls.push(vec![c.to_string(), "".to_string()]); + return + } let opt_sub = Self::find_by_podcast(username.to_string(), device_id.to_string(), c .to_string(), conn).expect("Error retrieving \ @@ -109,6 +114,10 @@ impl SubscriptionChangesToClient { }); upload_request.clone().remove.iter().for_each(|c|{ + if !c.starts_with("http")||!c.starts_with("https"){ + rewritten_urls.push(vec![c.to_string(), "".to_string()]); + return + } let opt_sub = Self::find_by_podcast(username.to_string(), device_id.to_string(), c .to_string(), conn).expect("Error retrieving \ subscription"); @@ -123,7 +132,7 @@ impl SubscriptionChangesToClient { } }); - Ok(upload_request.clone().add) + Ok(rewritten_urls) } pub fn find_by_podcast(username_1: String, deviceid_1: String, podcast_1: String, conn: From bd400f306765da8f04122d95c7a133954d05f5af Mon Sep 17 00:00:00 2001 From: "samuel1998.schwanzer" Date: Thu, 27 Apr 2023 10:59:34 +0200 Subject: [PATCH 19/26] Added command line runner. --- src/command_line_runner.rs | 49 ++++++++++++++++++++++++++++---------- src/models/user.rs | 2 +- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index cb7ebf48..bab1cdc4 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -1,11 +1,18 @@ +use std::alloc::System; use std::env::Args; -use std::io::stdin; +use std::io::{Read, stdin}; +use std::str::FromStr; +use log::error; use crate::constants::constants::Role; use crate::models::user::User; +use crate::utils::time::get_current_timestamp_str; pub fn start_command_line(mut args: Args){ + println!("Starting from command line"); + println!("{:?}", args); match args.nth(1).unwrap().as_str() { "users"=>{ + println!("User management"); match args.nth(0).unwrap().as_str() { "add"=> { read_user_account(); @@ -17,11 +24,13 @@ pub fn start_command_line(mut args: Args){ // list users } _ => { - // error + error!("Command not found") } } } - _ => {} + _ => { + error!("Command not found") + } } } @@ -30,29 +39,34 @@ pub fn start_command_line(mut args: Args){ pub fn read_user_account()->User{ let mut username = String::new(); let mut password = String::new(); + let mut role_change = Role::User; + let role = Role::VALUES.map(|v|{ return v.to_string() }).join(", "); retry_read("Enter your username: ", &mut username); retry_read("Enter your password: ", &mut password); - retry_read(&format!("Select your role {}",&role), &mut password); + let assigned_role = retry_read_role(&format!("Select your role {}",&role)); - User{ + let user = User{ id: 0, - username, - role: "".to_string(), - password: Some(password), + username: username.trim_end_matches("\n").parse().unwrap(), + role: assigned_role.to_string(), + password: Some(password.trim_end_matches("\n").parse().unwrap()), explicit_consent: false, - created_at: Default::default(), - } + created_at: get_current_timestamp_str(), + }; + println!("{:?}",user); + + user } pub fn retry_read(prompt: &str, input: &mut String){ - print!("{}",prompt); + println!("{}",prompt); stdin().read_line(input).unwrap(); match input.len()>0{ true => { - if input.trim().len()>0{ + if input.trim().len()==0{ retry_read(prompt, input); } } @@ -60,6 +74,17 @@ pub fn retry_read(prompt: &str, input: &mut String){ retry_read(prompt, input); } } +} +pub fn retry_read_role(prompt: &str)->Role{ + let mut input = String::new(); + println!("{}",prompt); + stdin().read_line(&mut input).unwrap(); + let res = Role::from_str(input.as_str().trim_end_matches("\n")); + if res.is_err(){ + println!("Error setting role. Please choose one of the possible roles."); + retry_read_role(prompt); + } + res.unwrap() } diff --git a/src/models/user.rs b/src/models/user.rs index 1ac79bfe..1b23cda2 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -11,7 +11,7 @@ use diesel::ExpressionMethods; use dotenv::var; use crate::constants::constants::{BASIC_AUTH, OIDC_AUTH, Role, USERNAME}; -#[derive(Serialize, Deserialize, Queryable, Insertable, Clone, ToSchema, PartialEq)] +#[derive(Serialize, Deserialize, Queryable, Insertable, Clone, ToSchema, PartialEq, Debug)] #[serde(rename_all = "camelCase")] pub struct User { pub id: i32, From ab83d103e2a2f8d23bc48a88c8a62c423d6ede01 Mon Sep 17 00:00:00 2001 From: "samuel1998.schwanzer" Date: Thu, 27 Apr 2023 14:47:00 +0200 Subject: [PATCH 20/26] Added user management via cli with add, remove,list --- Cargo.lock | 22 +++++++ Cargo.toml | 1 + src/command_line_runner.rs | 129 +++++++++++++++++++++++++++++++++---- src/models/user.rs | 11 +++- 4 files changed, 150 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89163a35..02ac15a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1785,6 +1785,7 @@ dependencies = [ "rand", "regex", "reqwest", + "rpassword", "rss", "serde", "serde_derive", @@ -2020,6 +2021,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "rpassword" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi", +] + [[package]] name = "rss" version = "2.0.3" @@ -2032,6 +2044,16 @@ dependencies = [ "quick-xml", ] +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rust-embed" version = "6.6.1" diff --git a/Cargo.toml b/Cargo.toml index c21e368a..837777d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] awc = {version="3.1.1", features = ["rustls"]} +rpassword = "7.2.0" reqwest = {version="0.11.14", features = ["blocking", "json", "async-compression", "rustls"]} actix = "0.13.0" async-recursion = "1.0.2" diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index bab1cdc4..0fd717c4 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -1,54 +1,124 @@ use std::alloc::System; use std::env::Args; -use std::io::{Read, stdin}; +use std::io::{Error, ErrorKind, Read, stdin, stdout, Write}; +use std::process::exit; use std::str::FromStr; +use diesel::SqliteConnection; use log::error; +use sha256::digest; +use crate::config::dbconfig::establish_connection; use crate::constants::constants::Role; -use crate::models::user::User; +use crate::models::user::{User, UserWithoutPassword}; use crate::utils::time::get_current_timestamp_str; +use rpassword::read_password; + pub fn start_command_line(mut args: Args){ println!("Starting from command line"); - println!("{:?}", args); match args.nth(1).unwrap().as_str() { + "help"|"--help"=>{ + println!(r" The following commands are available: + add => Adds a user + remove => Removes a user + update => Updates a user + list => Lists all users + ") + } "users"=>{ println!("User management"); match args.nth(0).unwrap().as_str() { "add"=> { - read_user_account(); + let mut user = read_user_account(); + + + println!("Should a user with the following settings be applied {:?}",user); + + match ask_for_confirmation(){ + Ok(e)=>{ + user.password = Some(digest(user.password.unwrap())); + match User::insert_user(&mut user, &mut establish_connection()){ + Ok(e)=>{ + println!("User succesfully created") + }, + Err(..)=>{ + + } + } + }, + Err(..)=> { + } + } } "remove"=> { + let mut username = String::new(); // remove user + let available_users = list_users(); + retry_read("Please enter the username of the user you want to delete", + &mut username); + username = trim_string(username); + println!("{}", username); + match available_users.iter().find(|u|u.username==username){ + Some(u)=>{ + User::delete_by_username(trim_string(username), + &mut establish_connection()) + .expect("Error deleting user"); + }, + None=>{ + println!("Username not found") + } + } + } "list"=> { // list users + + list_users(); } _ => { error!("Command not found") } } } + "help"|"--help"=>{ + println!(r" The following commands are available: + users => Handles user management + ") + } _ => { error!("Command not found") } } } +fn list_users() -> Vec { + let users = User::find_all_users(&mut establish_connection()); + + users.iter().for_each(|u| { + println!("|Username|Role|Explicit Consent|Created at|", ); + println!("|{}|{}|{}|{}|", u.username, u.role, u.explicit_consent, u.created_at); + }); + users +} pub fn read_user_account()->User{ let mut username = String::new(); let mut password = String::new(); - let mut role_change = Role::User; let role = Role::VALUES.map(|v|{ return v.to_string() }).join(", "); retry_read("Enter your username: ", &mut username); - retry_read("Enter your password: ", &mut password); + + let user_exists = User::find_by_username(&username, &mut establish_connection()--h).is_some(); + if user_exists{ + println!("User already exists"); + exit(1); + } + password = retry_read_secret("Enter your password: "); let assigned_role = retry_read_role(&format!("Select your role {}",&role)); - let user = User{ + let mut user = User{ id: 0, username: username.trim_end_matches("\n").parse().unwrap(), role: assigned_role.to_string(), @@ -56,7 +126,6 @@ pub fn read_user_account()->User{ explicit_consent: false, created_at: get_current_timestamp_str(), }; - println!("{:?}",user); user } @@ -76,15 +145,51 @@ pub fn retry_read(prompt: &str, input: &mut String){ } } + +pub fn retry_read_secret(prompt: &str)->String{ + println!("{}",prompt); + stdout().flush().unwrap(); + let mut input = read_password().unwrap(); + match input.len()>0{ + true => { + if input.trim().len()==0{ + retry_read(prompt, &mut input); + } + } + false => { + retry_read(prompt, &mut input); + } + } + input +} + pub fn retry_read_role(prompt: &str)->Role{ let mut input = String::new(); println!("{}",prompt); stdin().read_line(&mut input).unwrap(); let res = Role::from_str(input.as_str().trim_end_matches("\n")); - if res.is_err(){ - println!("Error setting role. Please choose one of the possible roles."); - retry_read_role(prompt); + match res{ + Err(..)=> { + println!("Error setting role. Please choose one of the possible roles."); + return retry_read_role(prompt); + } + Ok(..)=>{ + res.unwrap() + } + } +} + +fn ask_for_confirmation()->Result<(),Error>{ + let mut input = String::new(); + println!("Y[es]/N[o]"); + stdin().read_line(&mut input).expect("Error reading from terminal"); + match input.to_lowercase().starts_with("y") { + true=>Ok(()), + false=>Err(Error::new(ErrorKind::WouldBlock, "Interrupted by user.")) } - res.unwrap() } + +fn trim_string(string_to_trim: String)->String{ + string_to_trim.trim_end_matches("\n").parse().unwrap() +} \ No newline at end of file diff --git a/src/models/user.rs b/src/models/user.rs index 1b23cda2..2f8db441 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -28,7 +28,8 @@ pub struct UserWithoutPassword{ pub id: i32, pub username: String, pub role: String, - pub created_at: NaiveDateTime + pub created_at: NaiveDateTime, + pub explicit_consent: bool } @@ -113,6 +114,7 @@ impl User{ pub fn map_to_dto(user: Self) -> UserWithoutPassword{ UserWithoutPassword{ id: user.id, + explicit_consent: user.explicit_consent, username: user.username.clone(), role: user.role.clone(), created_at: user.created_at @@ -180,4 +182,11 @@ impl User{ } None } + + pub fn delete_by_username(username_to_search: String, conn: &mut SqliteConnection)->Result<(), Error>{ + use crate::schema::users::dsl::*; + diesel::delete(users.filter(username.eq(username_to_search))).execute(conn) + .expect("Error deleting user"); + Ok(()) + } } \ No newline at end of file From c0379d4dcb2f51bdf93915055af2bd75ba394ef5 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sat, 29 Apr 2023 12:22:15 +0200 Subject: [PATCH 21/26] Added shared watch log. --- .../2023-04-23-115251_gpodder_api/up.sql | 18 ++++- src/controllers/watch_time_controller.rs | 26 ++++++- src/db.rs | 48 +++++++++++-- src/gpodder/episodes/episodes.rs | 31 +++++--- src/gpodder/subscription/subscriptions.rs | 7 +- src/main.rs | 7 ++ src/models/episode.rs | 71 ++++++++++++++++++- src/models/models.rs | 8 ++- src/schema.rs | 2 +- 9 files changed, 187 insertions(+), 31 deletions(-) diff --git a/migrations/2023-04-23-115251_gpodder_api/up.sql b/migrations/2023-04-23-115251_gpodder_api/up.sql index 0c8b8707..710480e3 100644 --- a/migrations/2023-04-23-115251_gpodder_api/up.sql +++ b/migrations/2023-04-23-115251_gpodder_api/up.sql @@ -8,7 +8,6 @@ CREATE TABLE devices( FOREIGN KEY (username) REFERENCES users(username) ); - CREATE TABLE sessions( username VARCHAR(255) NOT NULL, session_id VARCHAR(255) NOT NULL, @@ -39,4 +38,19 @@ CREATE TABLE episodes( position INTEGER, total INTEGER, UNIQUE (username, device, podcast, episode, timestamp) -); \ No newline at end of file +); + + +CREATE table if not exists podcast_history_items2 ( + id integer primary key not null, + podcast_id integer not null, + episode_id TEXT not null, + watched_time integer not null, + date DATETIME not null, + username text not null, + FOREIGN KEY (podcast_id) REFERENCES podcasts(id)); + +INSERT INTO podcast_history_items2 SELECT * FROM podcast_history_items; + +DROP TABLE podcast_history_items; +ALTER TABLE podcast_history_items2 RENAME TO podcast_history_items; \ No newline at end of file diff --git a/src/controllers/watch_time_controller.rs b/src/controllers/watch_time_controller.rs index c4357e7b..bfa60f93 100644 --- a/src/controllers/watch_time_controller.rs +++ b/src/controllers/watch_time_controller.rs @@ -1,10 +1,11 @@ use crate::db::DB; -use crate::models::models::PodcastWatchedPostModel; +use crate::models::models::{PodcastWatchedEpisodeModelWithPodcastEpisode, PodcastWatchedPostModel}; use actix_web::web::Data; use actix_web::{get, post, web, HttpResponse, Responder, HttpRequest}; use std::sync::{Mutex}; use crate::constants::constants::STANDARD_USER; use crate::DbPool; +use crate::models::episode::Episode; use crate::models::user::User; use crate::mutex::LockResultExt; @@ -51,8 +52,27 @@ Responder { let designated_username = res.unwrap(); let mut db = db.lock().ignore_poison(); - let last_watched = db.get_last_watched_podcasts(&mut conn.get().unwrap(), designated_username).unwrap(); - HttpResponse::Ok().json(last_watched) + let last_watched = db.get_last_watched_podcasts(&mut conn.get().unwrap(), designated_username + .clone()).unwrap(); + let episodes = Episode::get_last_watched_episodes(designated_username, &mut conn.get().unwrap + (), + ); + + let episodes_with_logs = last_watched.iter().map(|e|{ + let episode = episodes.iter().find(|e1| e1.episode_id == e.episode_id); + match episode { + Some(episode) => { + if episode.watched_time>e.watched_time{ + return episode + } + e + }, + None => { + e + } + } + }).collect::>(); + HttpResponse::Ok().json(episodes_with_logs) } #[utoipa::path( diff --git a/src/db.rs b/src/db.rs index d0a0e963..342ef7c4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -8,7 +8,7 @@ use crate::models::models::{ use crate::models::settings::Setting; use crate::service::mapping_service::MappingService; use crate::utils::podcast_builder::PodcastExtra; -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use diesel::dsl::sql; use diesel::prelude::*; use diesel::{insert_into, sql_query, RunQueryDsl, delete}; @@ -16,8 +16,10 @@ use rss::Item; use std::io::Error; use std::sync::MutexGuard; use std::time::SystemTime; -use diesel::sql_types::Text; +use diesel::sql_types::{Text, Timestamp}; +use crate::models::episode::{Episode, EpisodeAction}; use crate::models::favorites::Favorite; +use crate::schema::podcast_episodes::dsl::podcast_episodes; use crate::utils::do_retry::do_retry; pub struct DB { @@ -331,20 +333,38 @@ impl DB { match result { Some(found_podcast) => { let history_item = podcast_history_items - .filter(episode_id.eq(podcast_id_tos_search).and(username.eq(username_to_find))) + .filter(episode_id.eq(podcast_id_tos_search).and(username.eq(username_to_find + .clone()))) .order(date.desc()) .first::(conn) .optional() .expect("Error loading podcast episode by id"); + return match history_item { - Some(found_history_item) => Ok(found_history_item), + Some(found_history_item) => { + let option_episode = Episode::get_watch_log_by_username_and_episode + (username_to_find.clone(), conn, found_podcast.clone().url); + if option_episode.is_some(){ + let episode = option_episode.unwrap(); + if episode.action == EpisodeAction::Play.to_string() && episode + .position.unwrap()>found_history_item.watched_time && episode.timestamp>found_history_item.date{ + + let found_podcast_item = Self::get_podcast(conn, found_history_item + .podcast_id).unwrap(); + return Ok(Episode::convert_to_podcast_history_item(&episode, + found_podcast_item, + found_podcast)); + } + } + Ok(found_history_item) + }, None => Ok(PodcastHistoryItem { id: 0, podcast_id: found_podcast.podcast_id, episode_id: found_podcast.episode_id, watched_time: 0, username: STANDARD_USER.to_string(), - date: "".to_string(), + date: Utc::now().naive_utc() }), }; } @@ -773,4 +793,22 @@ impl DB { res.load::<(PodcastEpisode, Podcast)>(conn).expect("Error loading podcast episode by id") } + + pub fn get_watch_logs_by_username(username_to_search: String, conn: &mut SqliteConnection, + since: NaiveDateTime) + -> + Vec<(PodcastHistoryItem, PodcastEpisode, Podcast)> { + use crate::schema::podcast_history_items::dsl::*; + + + let res = sql_query("SELECT * FROM podcast_history_items,podcast_episodes, podcasts WHERE \ + podcast_history_items.episode_id = podcast_episodes.episode_id AND podcast_history_items\ + .podcast_id= podcasts.id AND username=? AND date >= ?") + .bind::(&username_to_search) + .bind::(&since) + .load::<(PodcastHistoryItem, PodcastEpisode, Podcast)>(conn) + .expect("Error loading watch logs"); + + res + } } diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index 868befc4..1a552ac1 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -5,7 +5,7 @@ use actix_web::web::Data; use crate::db::DB; use crate::DbPool; use crate::models::episode::{Episode, EpisodeAction, EpisodeDto}; -use crate::models::models::PodcastWatchedPostModel; +use crate::models::models::{PodcastWatchedEpisodeModel, PodcastWatchedPostModel}; use std::borrow::Borrow; use chrono::NaiveDateTime; use crate::models::session::Session; @@ -41,8 +41,27 @@ pub async fn get_episode_actions(username: web::Path, pool: Data } let since_date = NaiveDateTime::from_timestamp_opt(since.since as i64, 0); - let actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap(), since_date) + println!("{}",since_date.unwrap()); + let mut actions = Episode::get_actions_by_username(username.clone(), &mut *pool.get().unwrap(), since_date) .await; + let watch_logs = DB::get_watch_logs_by_username(username.clone(), &mut *pool.get() + .unwrap(), since_date.unwrap()).iter().map(|watch_log| { + Episode{ + id: 0, + username: watch_log.clone().0.username, + device: "".to_string(), + podcast: watch_log.clone().2.rssfeed, + episode: watch_log.clone().1.url, + timestamp: watch_log.clone().0.date, + guid: None, + action: EpisodeAction::Play.to_string(), + started: Option::from(watch_log.clone().0.watched_time), + position: Option::from(watch_log.clone().0.watched_time), + total: Option::from(watch_log.clone().1.total_time), + } + }).collect::>(); + + actions.append(&mut watch_logs.clone().to_vec()); HttpResponse::Ok().json(EpisodeActionResponse { actions, timestamp: get_current_timestamp() @@ -84,14 +103,6 @@ Responder { if podcast_episode.clone().unwrap().is_none() { return; } - - let model = PodcastWatchedPostModel { - podcast_episode_id: podcast_episode.clone().unwrap().unwrap().episode_id, - time: episode.position.unwrap() as i32, - }; - DB::log_watchtime(&mut *conn.get().unwrap(), model, "admin".to_string()) - .expect("TODO: panic message"); - println!("episode: {:?}", episode); } }); HttpResponse::Ok().json(EpisodeActionPostResponse { diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index 76946f98..39e6befd 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -66,17 +66,12 @@ pub async fn upload_subscription_changes(upload_request: web::JsonOption{ + use crate::schema::episodes::username; + use crate::schema::episodes::dsl::episodes; + use crate::schema::episodes::dsl::timestamp; + use crate::schema::episodes::dsl::action; + use crate::schema::episodes::dsl::episode; + let res = sql_query( + "SELECT * FROM (SELECT * FROM episodes,podcasts WHERE username=? AND episodes\ + .podcast=podcasts.rssfeed AND episodes.episode = ? ORDER BY timestamp DESC) GROUP BY \ + episode LIMIT 10;") + .bind::(username1.clone()) + .bind::(episode_1) + .load::(conn) + .expect(""); + return if res.len() > 0 { + Some(res[0].clone()) + } else { + None + } + } + + pub fn convert_to_podcast_history_item(&self, podcast_1: Podcast,pod_episode: PodcastEpisode) + -> + PodcastHistoryItem { + PodcastHistoryItem { + id: self.id, + podcast_id: podcast_1.id, + episode_id: pod_episode.episode_id, + watched_time: self.position.unwrap(), + date: self.timestamp, + username: self.username.clone(), + } + } + + pub fn get_last_watched_episodes(username1: String, conn: &mut SqliteConnection) + ->Vec{ + use crate::schema::episodes::username; + use crate::schema::episodes::dsl::episodes; + use crate::schema::episodes::dsl::timestamp; + use crate::schema::episodes::dsl::action; + + let res = sql_query( + r"SELECT * FROM (SELECT * FROM episodes,podcasts, podcast_episodes WHERE + episodes.podcast LIKE podcasts.rssfeed AND episodes.episode=podcast_episodes.url AND + episodes.username=? ORDER BY timestamp DESC) GROUP BY episode LIMIT 10;") + .bind::(username1.clone()) + .load::<(Episode, Podcast,PodcastEpisode)>(conn) + .expect(""); + + res.iter().map(|e|{ + PodcastWatchedEpisodeModelWithPodcastEpisode{ + id: e.clone().0.id, + podcast_id: e.clone().1.id, + episode_id: e.clone().2.episode_id, + url: e.clone().2.url, + name: e.clone().2.name, + image_url: e.clone().2.image_url, + watched_time: e.clone().0.position.unwrap(), + date: e.clone().0.timestamp, + total_time: e.clone().2.total_time, + podcast_episode: e.clone().2, + podcast: e.clone().1, + } + }).collect() } } diff --git a/src/models/models.rs b/src/models/models.rs index c00aadf8..6615c3bb 100644 --- a/src/models/models.rs +++ b/src/models/models.rs @@ -2,6 +2,8 @@ use crate::models::itunes_models::{Podcast, PodcastEpisode}; use diesel::prelude::*; use diesel::sql_types::{Integer, Text}; use utoipa::ToSchema; +use chrono::NaiveDateTime; +use diesel::sql_types::Timestamp; // decode request data #[derive(Deserialize)] @@ -48,8 +50,8 @@ pub struct PodcastHistoryItem { pub episode_id: String, #[diesel(sql_type = Integer)] pub watched_time: i32, - #[diesel(sql_type = Text)] - pub date: String, + #[diesel(sql_type = Timestamp)] + pub date: NaiveDateTime, #[diesel(sql_type = Text)] pub username: String } @@ -78,7 +80,7 @@ pub struct PodcastWatchedEpisodeModelWithPodcastEpisode { pub name: String, pub image_url: String, pub watched_time: i32, - pub date: String, + pub date: NaiveDateTime, pub total_time: i32, pub podcast_episode: PodcastEpisode, pub podcast: Podcast, diff --git a/src/schema.rs b/src/schema.rs index 8f57822d..96e136fa 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -79,7 +79,7 @@ diesel::table! { podcast_id -> Integer, episode_id -> Text, watched_time -> Integer, - date -> Text, + date -> Timestamp, username -> Text, } } From c8790e19a5b8feb16beb73b32e08905e5023b33f Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sat, 29 Apr 2023 12:24:06 +0200 Subject: [PATCH 22/26] Fixed build. --- src/command_line_runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index 0fd717c4..88145356 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -110,7 +110,7 @@ pub fn read_user_account()->User{ }).join(", "); retry_read("Enter your username: ", &mut username); - let user_exists = User::find_by_username(&username, &mut establish_connection()--h).is_some(); + let user_exists = User::find_by_username(&username, &mut establish_connection()).is_some(); if user_exists{ println!("User already exists"); exit(1); From e7d4f62e2fdf800036cf2ef05d13b44c081e301f Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sat, 29 Apr 2023 15:54:53 +0200 Subject: [PATCH 23/26] Added synched listening. --- src/controllers/watch_time_controller.rs | 11 ++++++-- src/db.rs | 15 +++++++--- src/models/episode.rs | 36 +++++++++++++++--------- src/models/itunes_models.rs | 4 +-- src/models/models.rs | 2 +- src/models/subscription.rs | 2 +- 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/controllers/watch_time_controller.rs b/src/controllers/watch_time_controller.rs index bfa60f93..1f9df4cc 100644 --- a/src/controllers/watch_time_controller.rs +++ b/src/controllers/watch_time_controller.rs @@ -54,11 +54,12 @@ Responder { let mut db = db.lock().ignore_poison(); let last_watched = db.get_last_watched_podcasts(&mut conn.get().unwrap(), designated_username .clone()).unwrap(); - let episodes = Episode::get_last_watched_episodes(designated_username, &mut conn.get().unwrap + + let mut episodes = Episode::get_last_watched_episodes(designated_username, &mut conn.get().unwrap (), ); - let episodes_with_logs = last_watched.iter().map(|e|{ + let mut episodes_with_logs = last_watched.iter().map(|e|{ let episode = episodes.iter().find(|e1| e1.episode_id == e.episode_id); match episode { Some(episode) => { @@ -72,6 +73,12 @@ Responder { } } }).collect::>(); + + episodes.iter().for_each(|x|{ + if episodes_with_logs.iter().find(|e| e.episode_id == x.episode_id).is_none(){ + episodes_with_logs.push(x); + } + }); HttpResponse::Ok().json(episodes_with_logs) } diff --git a/src/db.rs b/src/db.rs index 342ef7c4..a30bb607 100644 --- a/src/db.rs +++ b/src/db.rs @@ -379,10 +379,8 @@ impl DB { conn: &mut SqliteConnection, designated_username: String) -> Result, String> { let result = sql_query( - "SELECT * FROM (SELECT * FROM podcast_history_items WHERE username=? ORDER BY \ - datetime\ - (date) \ - DESC) GROUP BY episode_id LIMIT 10;", + "SELECT * FROM (SELECT * FROM podcast_history_items WHERE username=? ORDER BY date \ + DESC) GROUP BY episode_id LIMIT 10;", ) .bind::(designated_username) .load::(&mut self.conn) @@ -811,4 +809,13 @@ impl DB { res } + + pub fn get_podcast_by_rss_feed(rss_feed_1:String, conn: &mut SqliteConnection) -> Podcast { + use crate::schema::podcasts::dsl::*; + + podcasts + .filter(rssfeed.eq(rss_feed_1)) + .first::(conn) + .expect("Error loading podcast by rss feed") + } } diff --git a/src/models/episode.rs b/src/models/episode.rs index 6f675478..cd65f301 100644 --- a/src/models/episode.rs +++ b/src/models/episode.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; +use std::ops::Deref; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use diesel::{Queryable, QueryableByName, Insertable, SqliteConnection, RunQueryDsl, QueryDsl, BoolExpressionMethods, OptionalExtension, TextExpressionMethods, sql_query}; @@ -5,6 +7,7 @@ use crate::schema::episodes; use utoipa::ToSchema; use diesel::sql_types::{Integer, Text, Nullable, Timestamp}; use diesel::ExpressionMethods; +use crate::db::DB; use crate::models::itunes_models::{Podcast, PodcastEpisode}; use crate::models::models::{PodcastHistoryItem, PodcastWatchedEpisodeModelWithPodcastEpisode}; @@ -160,28 +163,35 @@ impl Episode{ use crate::schema::episodes::dsl::episodes; use crate::schema::episodes::dsl::timestamp; use crate::schema::episodes::dsl::action; - + let mut map:HashMap = HashMap::new(); let res = sql_query( - r"SELECT * FROM (SELECT * FROM episodes,podcasts, podcast_episodes WHERE - episodes.podcast LIKE podcasts.rssfeed AND episodes.episode=podcast_episodes.url AND - episodes.username=? ORDER BY timestamp DESC) GROUP BY episode LIMIT 10;") + r"SELECT * FROM (SELECT * FROM episodes e, podcast_episodes pe WHERE + e.username=? AND pe.url=e.episode ORDER BY timestamp DESC) GROUP BY episode LIMIT + 10;") .bind::(username1.clone()) - .load::<(Episode, Podcast,PodcastEpisode)>(conn) + .load::<(Episode,PodcastEpisode)>(conn) .expect(""); res.iter().map(|e|{ + let mut opt_podcast = map.get(&*e.clone().0.podcast); + if opt_podcast.is_none(){ + let podcast = DB::get_podcast_by_rss_feed(e.clone().0.podcast, conn); + map.insert(e.clone().0.podcast.clone(),podcast.clone()); + opt_podcast = Some(&podcast.clone()) + } + let found_podcast = map.get(&e.clone().0.podcast).cloned().unwrap(); PodcastWatchedEpisodeModelWithPodcastEpisode{ id: e.clone().0.id, - podcast_id: e.clone().1.id, - episode_id: e.clone().2.episode_id, - url: e.clone().2.url, - name: e.clone().2.name, - image_url: e.clone().2.image_url, + podcast_id: found_podcast.id, + episode_id: e.clone().1.episode_id, + url: e.clone().1.url, + name: e.clone().1.name, + image_url: e.clone().1.image_url, watched_time: e.clone().0.position.unwrap(), date: e.clone().0.timestamp, - total_time: e.clone().2.total_time, - podcast_episode: e.clone().2, - podcast: e.clone().1, + total_time: e.clone().1.total_time, + podcast_episode: e.clone().1, + podcast: found_podcast.clone(), } }).collect() } diff --git a/src/models/itunes_models.rs b/src/models/itunes_models.rs index 5403b7ed..c9d90b9a 100644 --- a/src/models/itunes_models.rs +++ b/src/models/itunes_models.rs @@ -1,3 +1,4 @@ +use std::ops::Deref; use crate::schema::*; use chrono::NaiveDateTime; use diesel::prelude::{Queryable, Identifiable, Selectable, QueryableByName}; @@ -54,7 +55,7 @@ pub struct ResponseModel { } #[derive(Queryable, Identifiable,QueryableByName, Selectable, Debug, PartialEq, Clone, ToSchema, -Serialize, Deserialize)] +Serialize, Deserialize,)] pub struct Podcast { #[diesel(sql_type = Integer)] pub(crate) id: i32, @@ -87,7 +88,6 @@ pub struct Podcast { pub directory_name:String } - impl Podcast{ pub fn get_by_rss_feed(rssfeed_i: &str, conn: &mut SqliteConnection) -> Result { diff --git a/src/models/models.rs b/src/models/models.rs index 6615c3bb..e9bbbf4b 100644 --- a/src/models/models.rs +++ b/src/models/models.rs @@ -70,7 +70,7 @@ pub struct PodcastWatchedEpisodeModel { pub total_time: i32, } -#[derive(Serialize, Deserialize, ToSchema)] +#[derive(Serialize, Deserialize, ToSchema, Clone)] #[serde(rename_all = "camelCase")] pub struct PodcastWatchedEpisodeModelWithPodcastEpisode { pub id: i32, diff --git a/src/models/subscription.rs b/src/models/subscription.rs index 2d0efa2a..7530b8cd 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -66,7 +66,7 @@ impl SubscriptionChangesToClient { let (deleted_subscriptions,created_subscriptions):(Vec, Vec ) = res .into_iter() - .partition(|c| c.deleted.is_none()); + .partition(|c| c.deleted.is_some()); Ok(SubscriptionChangesToClient{ add: created_subscriptions.into_iter().map(|c| c.podcast).collect(), From bdc2d69a3d6784968b6133608c096904b619917b Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sat, 29 Apr 2023 16:05:15 +0200 Subject: [PATCH 24/26] Removed warnings. --- src/command_line_runner.rs | 33 +++++++++++------------ src/controllers/watch_time_controller.rs | 2 +- src/db.rs | 4 +-- src/gpodder/auth/auth.rs | 24 ----------------- src/gpodder/episodes/episodes.rs | 1 - src/gpodder/subscription/subscriptions.rs | 8 +++--- src/models/episode.rs | 17 +++--------- src/models/itunes_models.rs | 1 - src/service/user_management_service.rs | 4 --- 9 files changed, 25 insertions(+), 69 deletions(-) diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index 88145356..95e0edf8 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -1,9 +1,7 @@ -use std::alloc::System; use std::env::Args; -use std::io::{Error, ErrorKind, Read, stdin, stdout, Write}; +use std::io::{Error, ErrorKind, stdin, stdout, Write}; use std::process::exit; use std::str::FromStr; -use diesel::SqliteConnection; use log::error; use sha256::digest; use crate::config::dbconfig::establish_connection; @@ -16,12 +14,10 @@ use rpassword::read_password; pub fn start_command_line(mut args: Args){ println!("Starting from command line"); match args.nth(1).unwrap().as_str() { + "help"|"--help"=>{ println!(r" The following commands are available: - add => Adds a user - remove => Removes a user - update => Updates a user - list => Lists all users + users => Handles user management ") } "users"=>{ @@ -34,10 +30,10 @@ pub fn start_command_line(mut args: Args){ println!("Should a user with the following settings be applied {:?}",user); match ask_for_confirmation(){ - Ok(e)=>{ + Ok(..)=>{ user.password = Some(digest(user.password.unwrap())); match User::insert_user(&mut user, &mut establish_connection()){ - Ok(e)=>{ + Ok(..)=>{ println!("User succesfully created") }, Err(..)=>{ @@ -58,7 +54,7 @@ pub fn start_command_line(mut args: Args){ username = trim_string(username); println!("{}", username); match available_users.iter().find(|u|u.username==username){ - Some(u)=>{ + Some(..)=>{ User::delete_by_username(trim_string(username), &mut establish_connection()) .expect("Error deleting user"); @@ -74,16 +70,19 @@ pub fn start_command_line(mut args: Args){ list_users(); } + "help"|"--help"=>{ + println!(r" The following commands are available: + add => Adds a user + remove => Removes a user + update => Updates a user + list => Lists all users + ") + } _ => { error!("Command not found") } } } - "help"|"--help"=>{ - println!(r" The following commands are available: - users => Handles user management - ") - } _ => { error!("Command not found") } @@ -103,7 +102,7 @@ fn list_users() -> Vec { pub fn read_user_account()->User{ let mut username = String::new(); - let mut password = String::new(); + let password; let role = Role::VALUES.map(|v|{ return v.to_string() @@ -118,7 +117,7 @@ pub fn read_user_account()->User{ password = retry_read_secret("Enter your password: "); let assigned_role = retry_read_role(&format!("Select your role {}",&role)); - let mut user = User{ + let user = User{ id: 0, username: username.trim_end_matches("\n").parse().unwrap(), role: assigned_role.to_string(), diff --git a/src/controllers/watch_time_controller.rs b/src/controllers/watch_time_controller.rs index 1f9df4cc..c33df7b6 100644 --- a/src/controllers/watch_time_controller.rs +++ b/src/controllers/watch_time_controller.rs @@ -55,7 +55,7 @@ Responder { let last_watched = db.get_last_watched_podcasts(&mut conn.get().unwrap(), designated_username .clone()).unwrap(); - let mut episodes = Episode::get_last_watched_episodes(designated_username, &mut conn.get().unwrap + let episodes = Episode::get_last_watched_episodes(designated_username, &mut conn.get().unwrap (), ); diff --git a/src/db.rs b/src/db.rs index a30bb607..31a098b9 100644 --- a/src/db.rs +++ b/src/db.rs @@ -19,7 +19,6 @@ use std::time::SystemTime; use diesel::sql_types::{Text, Timestamp}; use crate::models::episode::{Episode, EpisodeAction}; use crate::models::favorites::Favorite; -use crate::schema::podcast_episodes::dsl::podcast_episodes; use crate::utils::do_retry::do_retry; pub struct DB { @@ -255,8 +254,8 @@ impl DB { podcast_id_to_be_searched: i32, last_id: Option, ) -> Result, String> { - use crate::schema::podcast_episodes::dsl::podcast_episodes; use crate::schema::podcast_episodes::*; + use crate::schema::podcast_episodes::dsl::podcast_episodes; match last_id { Some(last_id) => { let podcasts_found = podcast_episodes @@ -796,7 +795,6 @@ impl DB { since: NaiveDateTime) -> Vec<(PodcastHistoryItem, PodcastEpisode, Podcast)> { - use crate::schema::podcast_history_items::dsl::*; let res = sql_query("SELECT * FROM podcast_history_items,podcast_episodes, podcasts WHERE \ diff --git a/src/gpodder/auth/auth.rs b/src/gpodder/auth/auth.rs index 4b3834ea..2303e2c7 100644 --- a/src/gpodder/auth/auth.rs +++ b/src/gpodder/auth/auth.rs @@ -1,4 +1,3 @@ -use std::io::Error; use std::sync::{Mutex}; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use actix_web::web::Data; @@ -9,7 +8,6 @@ use actix_web::{post}; use crate::mutex::LockResultExt; use crate::service::environment_service::EnvironmentService; use awc::cookie::{Cookie, SameSite}; -use diesel::SqliteConnection; use crate::models::session::Session; #[post("/auth/{username}/login.json")] @@ -71,26 +69,4 @@ pub fn basic_auth_login(rq: String) -> (String, String) { let (u,p) = extract_basic_auth(rq.as_str()); return (u.to_string(),p.to_string()) -} - -pub async fn auth_checker(conn: &mut SqliteConnection, session: Option, username: String) - ->Result<(), - Error>{ - return match session { - Some(session) => { - let session = Session::find_by_session_id(&session, conn).unwrap(); - if session.username != username { - return Err(Error::new(std::io::ErrorKind::Other, "User and session not matching")) - } - Ok(()) - } - None => { - Err(Error::new(std::io::ErrorKind::Other, "No session")) - } - } -} - -pub fn extract_from_http_request(rq: HttpRequest)->Option{ - rq.cookie("sessionid") - .map(|cookie|cookie.value().to_string()) } \ No newline at end of file diff --git a/src/gpodder/episodes/episodes.rs b/src/gpodder/episodes/episodes.rs index 1a552ac1..ec108e7c 100644 --- a/src/gpodder/episodes/episodes.rs +++ b/src/gpodder/episodes/episodes.rs @@ -5,7 +5,6 @@ use actix_web::web::Data; use crate::db::DB; use crate::DbPool; use crate::models::episode::{Episode, EpisodeAction, EpisodeDto}; -use crate::models::models::{PodcastWatchedEpisodeModel, PodcastWatchedPostModel}; use std::borrow::Borrow; use chrono::NaiveDateTime; use crate::models::session::Session; diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index 39e6befd..4f076561 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -1,8 +1,7 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use actix_web::{HttpResponse, Responder, web}; use actix_web::{get, post}; use actix_web::web::Data; use crate::DbPool; -use crate::gpodder::auth::auth::{auth_checker, extract_from_http_request}; use crate::models::session::Session; use crate::models::subscription::SubscriptionChangesToClient; use crate::utils::time::get_current_timestamp; @@ -57,8 +56,7 @@ Responder { #[post("/subscriptions/{username}/{deviceid}.json")] pub async fn upload_subscription_changes(upload_request: web::Json, opt_flag: Option>, - paths: web::Path<(String, String)>, conn: Data, - rq:HttpRequest)->impl Responder { + paths: web::Path<(String, String)>, conn: Data)->impl Responder { match opt_flag { Some(flag) => { let username = paths.clone().0; @@ -66,7 +64,7 @@ pub async fn upload_subscription_changes(upload_request: web::JsonOption{ - use crate::schema::episodes::username; - use crate::schema::episodes::dsl::episodes; - use crate::schema::episodes::dsl::timestamp; - use crate::schema::episodes::dsl::action; - use crate::schema::episodes::dsl::episode; let res = sql_query( "SELECT * FROM (SELECT * FROM episodes,podcasts WHERE username=? AND episodes\ @@ -159,10 +153,8 @@ impl Episode{ pub fn get_last_watched_episodes(username1: String, conn: &mut SqliteConnection) ->Vec{ - use crate::schema::episodes::username; - use crate::schema::episodes::dsl::episodes; - use crate::schema::episodes::dsl::timestamp; - use crate::schema::episodes::dsl::action; + + let mut map:HashMap = HashMap::new(); let res = sql_query( r"SELECT * FROM (SELECT * FROM episodes e, podcast_episodes pe WHERE @@ -173,11 +165,10 @@ impl Episode{ .expect(""); res.iter().map(|e|{ - let mut opt_podcast = map.get(&*e.clone().0.podcast); + let opt_podcast = map.get(&*e.clone().0.podcast); if opt_podcast.is_none(){ let podcast = DB::get_podcast_by_rss_feed(e.clone().0.podcast, conn); map.insert(e.clone().0.podcast.clone(),podcast.clone()); - opt_podcast = Some(&podcast.clone()) } let found_podcast = map.get(&e.clone().0.podcast).cloned().unwrap(); PodcastWatchedEpisodeModelWithPodcastEpisode{ diff --git a/src/models/itunes_models.rs b/src/models/itunes_models.rs index c9d90b9a..0eb2c527 100644 --- a/src/models/itunes_models.rs +++ b/src/models/itunes_models.rs @@ -1,4 +1,3 @@ -use std::ops::Deref; use crate::schema::*; use chrono::NaiveDateTime; use diesel::prelude::{Queryable, Identifiable, Selectable, QueryableByName}; diff --git a/src/service/user_management_service.rs b/src/service/user_management_service.rs index c944383b..5a4a5dae 100644 --- a/src/service/user_management_service.rs +++ b/src/service/user_management_service.rs @@ -13,10 +13,6 @@ pub struct UserManagementService{ } impl UserManagementService { - pub fn may_add_podcast(user: User)->bool{ - Role::from_str(&user.role).unwrap() == Role::Uploader|| Role::from_str(&user.role).unwrap() == - Role::Admin - } pub fn may_onboard_user(user: User)->bool{ Role::from_str(&user.role).unwrap() == Role::Admin From 44961ff57f0fc3d48f2fe0a3c3ca1ca70d0a51d4 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sat, 29 Apr 2023 17:05:05 +0200 Subject: [PATCH 25/26] Added deleting and updating a user. --- src/command_line_runner.rs | 97 ++++++++++++++++++++++++++++++++++---- src/models/device.rs | 5 ++ src/models/episode.rs | 9 ++++ src/models/favorites.rs | 9 ++++ src/models/models.rs | 10 ++++ src/models/session.rs | 7 +++ src/models/subscription.rs | 7 +++ src/models/user.rs | 13 ++++- 8 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index 95e0edf8..6f7e0dd3 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -3,12 +3,18 @@ use std::io::{Error, ErrorKind, stdin, stdout, Write}; use std::process::exit; use std::str::FromStr; use log::error; -use sha256::digest; +use sha256::{digest}; use crate::config::dbconfig::establish_connection; use crate::constants::constants::Role; use crate::models::user::{User, UserWithoutPassword}; use crate::utils::time::get_current_timestamp_str; use rpassword::read_password; +use crate::models::device::Device; +use crate::models::episode::Episode; +use crate::models::favorites::Favorite; +use crate::models::models::PodcastHistoryItem; +use crate::models::session::Session; +use crate::models::subscription::Subscription; pub fn start_command_line(mut args: Args){ @@ -55,9 +61,45 @@ pub fn start_command_line(mut args: Args){ println!("{}", username); match available_users.iter().find(|u|u.username==username){ Some(..)=>{ - User::delete_by_username(trim_string(username), + PodcastHistoryItem::delete_by_username(trim_string(username.clone()), + &mut establish_connection()) + .expect("Error deleting entries for podcast history item"); + Device::delete_by_username(username.clone(), &mut + establish_connection()) + .expect("Error deleting devices"); + Episode::delete_by_username_and_episode(trim_string(username.clone()), + &mut establish_connection()) + .expect("Error deleting episodes"); + Favorite::delete_by_username(trim_string(username.clone()), + &mut establish_connection()) + .expect("Error deleting favorites"); + Session::delete_by_username(&trim_string(username.clone()), + &mut establish_connection()) + .expect("Error deleting sessions"); + Subscription::delete_by_username(&trim_string(username.clone()), + &mut establish_connection()).expect("TODO: panic message"); + User::delete_by_username(trim_string(username.clone()), &mut establish_connection()) .expect("Error deleting user"); + println!("User deleted") + }, + None=>{ + println!("Username not found") + } + } + } + "update"=>{ + //update a user + list_users(); + let mut username = String::new(); + + retry_read("Please enter the username of the user you want to delete", + &mut username); + username = trim_string(username); + println!(">{}<", username); + match User::find_by_username(username.as_str(), &mut establish_connection()){ + Some(user)=>{ + do_user_update(user) }, None=>{ println!("Username not found") @@ -119,9 +161,9 @@ pub fn read_user_account()->User{ let user = User{ id: 0, - username: username.trim_end_matches("\n").parse().unwrap(), + username: trim_string(username.clone()), role: assigned_role.to_string(), - password: Some(password.trim_end_matches("\n").parse().unwrap()), + password: Some(trim_string(password)), explicit_consent: false, created_at: get_current_timestamp_str(), }; @@ -148,15 +190,15 @@ pub fn retry_read(prompt: &str, input: &mut String){ pub fn retry_read_secret(prompt: &str)->String{ println!("{}",prompt); stdout().flush().unwrap(); - let mut input = read_password().unwrap(); + let input = read_password().unwrap(); match input.len()>0{ true => { if input.trim().len()==0{ - retry_read(prompt, &mut input); + retry_read_secret(prompt); } } false => { - retry_read(prompt, &mut input); + retry_read_secret(prompt); } } input @@ -166,7 +208,7 @@ pub fn retry_read_role(prompt: &str)->Role{ let mut input = String::new(); println!("{}",prompt); stdin().read_line(&mut input).unwrap(); - let res = Role::from_str(input.as_str().trim_end_matches("\n")); + let res = Role::from_str(&trim_string(input)); match res{ Err(..)=> { println!("Error setting role. Please choose one of the possible roles."); @@ -190,5 +232,42 @@ fn ask_for_confirmation()->Result<(),Error>{ fn trim_string(string_to_trim: String)->String{ - string_to_trim.trim_end_matches("\n").parse().unwrap() + string_to_trim.trim_end_matches("\n").trim().parse().unwrap() +} + + +fn do_user_update(mut user:User){ + let mut input = String::new(); + println!("The following settings of a user should be updated: {:?}",user); + println!("Enter which field of a user should be updated [role, password, \ + explicit_consent]"); + stdin().read_line(&mut input) + .expect("Error reading from terminal"); + input = trim_string(input); + match input.as_str() { + "role" =>{ + user.role = Role::to_string(&retry_read_role("Enter the new role [user,admin]")); + User::update_user(user, &mut establish_connection()) + .expect("Error updating role"); + println!("Role updated"); + }, + "password"=>{ + let mut password = retry_read_secret("Enter the new username"); + password = digest(password); + user.password = Some(password); + User::update_user(user, &mut establish_connection()) + .expect("Error updating username"); + println!("Password updated"); + }, + "explicit_consent"=>{ + user.explicit_consent = !user.explicit_consent; + User::update_user(user, &mut establish_connection()) + .expect("Error updating explicit_consent"); + println!("Explicit consent updated"); + } + _=>{ + println!("Field not found"); + } + } + } \ No newline at end of file diff --git a/src/models/device.rs b/src/models/device.rs index 1db56b9e..a702a40d 100644 --- a/src/models/device.rs +++ b/src/models/device.rs @@ -59,4 +59,9 @@ impl Device { subscriptions: 0 } } + pub fn delete_by_username(username1: String, conn: &mut SqliteConnection) -> Result { + use crate::schema::devices::dsl::*; + diesel::delete(devices.filter(username.eq(username1))).execute(conn) + } } \ No newline at end of file diff --git a/src/models/episode.rs b/src/models/episode.rs index 2edb6c88..1801d858 100644 --- a/src/models/episode.rs +++ b/src/models/episode.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::io::Error; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use diesel::{Queryable, QueryableByName, Insertable, SqliteConnection, RunQueryDsl, QueryDsl, BoolExpressionMethods, OptionalExtension, sql_query}; @@ -186,6 +187,14 @@ impl Episode{ } }).collect() } + + pub fn delete_by_username_and_episode(username1: String, conn: &mut SqliteConnection) ->Result<(),Error>{ + use crate::schema::episodes::username; + use crate::schema::episodes::dsl::episodes; + diesel::delete(episodes.filter(username.eq(username1))) + .execute(conn).expect(""); + Ok(()) + } } #[derive(Debug, Deserialize, Serialize)] diff --git a/src/models/favorites.rs b/src/models/favorites.rs index c4780aad..5fdc3fc9 100644 --- a/src/models/favorites.rs +++ b/src/models/favorites.rs @@ -11,4 +11,13 @@ pub struct Favorite{ pub username: String, pub podcast_id: i32, pub favored: bool +} + +impl Favorite{ + pub fn delete_by_username(username1: String, conn: &mut SqliteConnection) -> Result<(), + diesel::result::Error>{ + use crate::schema::favorites::dsl::*; + diesel::delete(favorites.filter(username.eq(username1))).execute(conn)?; + Ok(()) + } } \ No newline at end of file diff --git a/src/models/models.rs b/src/models/models.rs index e9bbbf4b..66f14336 100644 --- a/src/models/models.rs +++ b/src/models/models.rs @@ -56,6 +56,16 @@ pub struct PodcastHistoryItem { pub username: String } +impl PodcastHistoryItem{ + pub fn delete_by_username(username1: String, conn: &mut SqliteConnection) -> Result<(), + diesel::result::Error>{ + use crate::schema::podcast_history_items::dsl::*; + diesel::delete(podcast_history_items.filter(username.eq(username1))) + .execute(conn)?; + Ok(()) + } +} + #[derive(Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PodcastWatchedEpisodeModel { diff --git a/src/models/session.rs b/src/models/session.rs index b5862e18..06d8abae 100644 --- a/src/models/session.rs +++ b/src/models/session.rs @@ -41,4 +41,11 @@ impl Session{ .filter(sessions::session_id.eq(session_id)) .get_result(conn) } + + pub fn delete_by_username(username1: &str, conn: &mut diesel::SqliteConnection) -> + Result{ + diesel::delete(sessions::table + .filter(sessions::username.eq(username1))) + .execute(conn) + } } \ No newline at end of file diff --git a/src/models/subscription.rs b/src/models/subscription.rs index 7530b8cd..5327573e 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -41,6 +41,13 @@ impl Subscription{ deleted: None } } + pub fn delete_by_username(username1: &str, conn: &mut SqliteConnection) -> + Result<(), Error>{ + use crate::schema::subscriptions::dsl::*; + diesel::delete(subscriptions.filter(username.eq(username1))) + .execute(conn).expect("Error deleting subscriptions of user"); + Ok(()) + } } diff --git a/src/models/user.rs b/src/models/user.rs index 2f8db441..1c73f8ea 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -2,7 +2,7 @@ use std::io::Error; use actix_web::HttpResponse; use chrono::NaiveDateTime; use diesel::prelude::{Insertable, Queryable}; -use diesel::{OptionalExtension, RunQueryDsl, SqliteConnection}; +use diesel::{OptionalExtension, RunQueryDsl, SqliteConnection, AsChangeset}; use diesel::associations::HasTable; use utoipa::ToSchema; use crate::schema::users; @@ -11,7 +11,8 @@ use diesel::ExpressionMethods; use dotenv::var; use crate::constants::constants::{BASIC_AUTH, OIDC_AUTH, Role, USERNAME}; -#[derive(Serialize, Deserialize, Queryable, Insertable, Clone, ToSchema, PartialEq, Debug)] +#[derive(Serialize, Deserialize, Queryable, Insertable, Clone, ToSchema, PartialEq, Debug, +AsChangeset)] #[serde(rename_all = "camelCase")] pub struct User { pub id: i32, @@ -189,4 +190,12 @@ impl User{ .expect("Error deleting user"); Ok(()) } + + pub fn update_user(user: User, conn: &mut SqliteConnection)->Result<(), Error>{ + use crate::schema::users::dsl::*; + diesel::update(users.filter(id.eq(user.clone().id))) + .set(user).execute(conn) + .expect("Error updating user"); + Ok(()) + } } \ No newline at end of file From 28b3d05590e95803c878b064544509569538472b Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Sat, 29 Apr 2023 17:08:43 +0200 Subject: [PATCH 26/26] Added cli documentation. --- README.md | 4 ++++ docs/CLI.md | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 docs/CLI.md diff --git a/README.md b/README.md index 3a9e408e..ec211f75 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,10 @@ Several Auth methods are described here: [AUTH.md](docs/AUTH.md) Hosting options are described here: [HOSTING.md](docs/HOSTING.md) +# CLI usage + +The cli usage is described here: [CLI.md](docs/CLI.md) + # Environment Variables | Variable | Description | Default | diff --git a/docs/CLI.md b/docs/CLI.md new file mode 100644 index 00000000..c4f6c918 --- /dev/null +++ b/docs/CLI.md @@ -0,0 +1,23 @@ +# CLI usage + +The CLI can be used to update, remove, list registered users in PodFetch. You can get help anytime by typing --help/help + +# Usage + +# Get general help + +```bash +podfetch --help +``` + +# Get help for a specific command + +```bash +podfetch --help +``` + +e.g. + +```bash +podfetch users --help +``` \ No newline at end of file