diff --git a/.gitignore b/.gitignore
index 088ba6ba..22d35163 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,8 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
+
+
+# Added by cargo
+
+/target
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..13566b81
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/cli.iml b/.idea/cli.iml
new file mode 100644
index 00000000..d900b784
--- /dev/null
+++ b/.idea/cli.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..cff13543
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 00000000..105ce2da
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..dd602185
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..6fcc0dcf
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb
new file mode 100644
index 00000000..e69de29b
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 00000000..f11b327d
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "cli"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "screenly"
+path = "src/main.rs"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.65"
+clap = { version = "4.0.17", features = ["derive"] }
+envtestkit = "1.1.2"
+log = { version = "0.4.17", features = ["release_max_level_info", "max_level_trace"] }
+reqwest = { version = "0.11.12", features = ["json", "blocking"] }
+simple_logger = "3.0.0"
+tempdir = "0.3.7"
+thiserror = "1.0.37"
+
+[dev-dependencies]
+httpmock = "0.6"
diff --git a/src/authentication.rs b/src/authentication.rs
new file mode 100644
index 00000000..59e6c45e
--- /dev/null
+++ b/src/authentication.rs
@@ -0,0 +1,149 @@
+use reqwest::header;
+use std::{env, fs};
+use thiserror::Error;
+
+const API_BASE_URL: &str = "https://api.screenlyapp.com/api";
+
+pub struct Config {
+ url: String,
+}
+
+#[derive(Error, Debug)]
+pub enum AuthenticationError {
+ #[error("wrong credentials error")]
+ WrongCredentialsError,
+ #[error("request error")]
+ RequestError(#[from] reqwest::Error),
+ #[error("i/o error")]
+ IoError(#[from] std::io::Error),
+ #[error("env error")]
+ EnvError(#[from] env::VarError),
+ #[error("unknown error")]
+ Unknown,
+}
+
+pub struct Authentication {
+ config: Config,
+}
+
+impl Config {
+ pub fn default() -> Self {
+ Self {
+ url: API_BASE_URL.to_string(),
+ }
+ }
+
+ #[cfg(test)]
+ pub fn new(url: String) -> Self {
+ Self {
+ url: url.to_string(),
+ }
+ }
+}
+
+impl Authentication {
+ pub fn new() -> Self {
+ Self {
+ config: Config::default(),
+ }
+ }
+
+ #[cfg(test)]
+ pub fn new_with_config(config: Config) -> Self {
+ Self { config }
+ }
+
+ pub fn verify_and_store_token(&self, token: &str) -> anyhow::Result<(), AuthenticationError> {
+ self.verify_token(token)?;
+
+ match std::env::var("HOME") {
+ Ok(home) => {
+ fs::write(home + "/.screenly", &token)?;
+ Ok(())
+ }
+ Err(e) => Err(AuthenticationError::EnvError(e)),
+ }
+ }
+
+ fn verify_token(&self, token: &str) -> anyhow::Result<(), AuthenticationError> {
+ // Using uuid of non existing playlist. If we get 404 it means we authenticated successfully.
+ let url = self.config.url.clone() + "/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/";
+ let secret = "Token ".to_owned() + token;
+ let client = reqwest::blocking::Client::builder().build()?;
+
+ let res = client
+ .get(url)
+ .header(header::AUTHORIZATION, &secret)
+ .send()?;
+
+ return match res.status().as_u16() {
+ 401 => Err(AuthenticationError::WrongCredentialsError),
+ 404 => Ok(()),
+ _ => Err(AuthenticationError::Unknown),
+ };
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ffi::OsString;
+ use crate::authentication::Config;
+ use crate::Authentication;
+ use httpmock::{Method::GET, MockServer};
+ use simple_logger::SimpleLogger;
+
+ use std::fs;
+ use tempdir::TempDir;
+
+ use envtestkit::lock::lock_test;
+ use envtestkit::set_env;
+
+ #[test]
+ fn test_verify_and_store_token_when_token_is_valid() {
+ SimpleLogger::new()
+ .with_level(log::LevelFilter::Debug)
+ .init()
+ .unwrap();
+ let tmp_dir = TempDir::new("test").unwrap();
+ let _lock = lock_test();
+ let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap());
+ let mock_server = MockServer::start();
+ mock_server.mock(|when, then| {
+ when.method(GET)
+ .path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/");
+ then.status(404);
+ });
+
+ let config = Config::new(mock_server.base_url());
+ let authentication = Authentication::new_with_config(config);
+ assert!(authentication
+ .verify_and_store_token("correct_token")
+ .is_ok());
+ let path = tmp_dir.path().join(".screenly");
+ assert!(path.exists());
+ let contents = fs::read_to_string(path).unwrap();
+ assert!(contents.eq("correct_token"));
+ tmp_dir.close().unwrap();
+ }
+
+ #[test]
+ fn test_verify_and_store_token_when_token_is_invalid() {
+ let tmp_dir = TempDir::new("invalid").unwrap();
+ let _lock = lock_test();
+ let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap());
+ let mock_server = MockServer::start();
+ mock_server.mock(|when, then| {
+ when.method(GET)
+ .path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/");
+ then.status(401);
+ });
+
+ let config = Config::new(mock_server.base_url());
+ let authentication = Authentication::new_with_config(config);
+ assert!(authentication
+ .verify_and_store_token("wrong_token")
+ .is_err());
+ let path = tmp_dir.path().join(".screenly");
+ assert!(!path.exists());
+ }
+}
diff --git a/src/commands.rs b/src/commands.rs
new file mode 100644
index 00000000..384daf04
--- /dev/null
+++ b/src/commands.rs
@@ -0,0 +1,15 @@
+use crate::Authentication;
+
+pub struct ScreenCommand {
+ authentication: Authentication,
+}
+
+impl ScreenCommand {
+ fn new(authentication: Authentication) -> Self {
+ Self { authentication }
+ }
+
+ fn list() {}
+
+ fn get(_id: &str) {}
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 00000000..9b84d2a0
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,68 @@
+mod authentication;
+mod commands;
+
+use crate::authentication::{Authentication, AuthenticationError};
+use clap::{command, Parser, Subcommand};
+use log::warn;
+use simple_logger::SimpleLogger;
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+#[command(propagate_version = true)]
+struct Cli {
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+ /// Logins with the token and stores it for further use if it's valid
+ Login { token: String },
+ /// Screen related commands
+ #[command(subcommand)]
+ Screen(ScreenCommands),
+}
+
+#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord)]
+enum ScreenCommands {
+ /// Lists your screens
+ List,
+ /// Gets a single screen by id
+ Get { id: String },
+}
+
+fn main() {
+ SimpleLogger::new()
+ .with_level(log::LevelFilter::Info)
+ .init()
+ .unwrap();
+ let cli = Cli::parse();
+ let authentication = Authentication::new();
+ match &cli.command {
+ Commands::Login { token } => match authentication.verify_and_store_token(token) {
+ Ok(()) => {
+ println!("Login credentials have been saved.");
+ std::process::exit(0);
+ }
+
+ Err(e) => match e {
+ AuthenticationError::WrongCredentialsError => {
+ println!("Token verification failed.");
+ std::process::exit(1);
+ }
+ _ => {
+ println!("Unknown error");
+ std::process::exit(2);
+ }
+ },
+ },
+ Commands::Screen(command) => match command {
+ ScreenCommands::List => {
+ warn!("List: to be implemented");
+ }
+ ScreenCommands::Get { id: _ } => {
+ warn!("Get: to be implemented");
+ }
+ },
+ }
+}