diff --git a/backend/src/auth.rs b/backend/src/auth.rs new file mode 100644 index 0000000..06de426 --- /dev/null +++ b/backend/src/auth.rs @@ -0,0 +1,47 @@ +use rocket::fairing::{Fairing, Info, Kind}; +use rocket::http::{Header, Status}; +use rocket::request::{FromRequest, Outcome}; +use rocket::{Request, Response}; +use std::env; + +pub struct Authenticated; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Authenticated { + type Error = (); + + async fn from_request(req: &'r Request<'_>) -> Outcome { + let token = req.headers().get_one("Authorization"); + if let Some(token) = token { + if token == env::var("API_KEY").unwrap().as_str() { + Outcome::Success(Authenticated) + } else { + Outcome::Error((Status::Unauthorized, ())) + } + } else { + Outcome::Error((Status::Unauthorized, ())) + } + } +} + +pub struct CORS; + +#[rocket::async_trait] +impl Fairing for CORS { + fn info(&self) -> Info { + Info { + name: "Add CORS headers to responses", + kind: Kind::Response, + } + } + + async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) { + response.set_header(Header::new("Access-Control-Allow-Origin", "*")); + response.set_header(Header::new( + "Access-Control-Allow-Methods", + "GET, POST, PUT, PATCH, DELETE, OPTIONS", + )); + response.set_header(Header::new("Access-Control-Allow-Headers", "*")); + response.set_header(Header::new("Access-Control-Allow-Credentials", "true")); + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index a1a3669..ddb3dc7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,5 +1,65 @@ +mod auth; +mod models; mod schema; -fn main() { - println!("Hello, world!"); +#[macro_use] +extern crate rocket; +extern crate fern; +extern crate log; + +use auth::CORS; +use chrono::Local; +use diesel::{Connection, PgConnection}; +use models::AppState; +use rocket::State; +use std::env; +use std::str::FromStr; + +fn setup_logging(log_file: Option) -> Result<(), fern::InitError> { + let level_raw = env::var("LOG_LEVEL").unwrap_or("INFO".to_string()); + let level = log::LevelFilter::from_str(&level_raw).expect("LOG_LEVEL invalid"); + + let log = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} {} [{}] {}", + Local::now().format("%Y-%m-%d %H:%M:%S"), + record.level(), + record.target(), + message + )) + }) + .level(level) + .chain(std::io::stdout()); + + if let Some(log_file) = log_file { + log.chain(fern::log_file(log_file)?).apply()?; + } else { + log.apply()?; + } + + Ok(()) +} + +#[get("/")] +fn index(state: &State) -> String { + "Hello world!".to_string() +} + +#[launch] +fn rocket() -> _ { + dotenv::dotenv().ok(); + + let log_file = env::var("LOG_FILE").ok(); + setup_logging(log_file).expect("Failed to setup logging"); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let db = PgConnection::establish(&database_url) + .expect(&format!("Error connecting to {}", database_url)); + + let app_data = AppState::new(db); + rocket::build() + .manage(app_data) + .attach(CORS {}) + .mount("/", routes![index]) } diff --git a/backend/src/models/action.rs b/backend/src/models/action.rs new file mode 100644 index 0000000..51a7904 --- /dev/null +++ b/backend/src/models/action.rs @@ -0,0 +1,14 @@ +use chrono::NaiveDateTime; +use diesel::{Queryable, Selectable}; +use uuid::Uuid; + +#[derive(Queryable, Selectable)] +#[diesel(table_name = crate::schema::actions)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Action { + pub id: Uuid, + pub name: String, + pub source: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs new file mode 100644 index 0000000..51bb773 --- /dev/null +++ b/backend/src/models/mod.rs @@ -0,0 +1,16 @@ +use diesel::PgConnection; +use std::sync::{Arc, Mutex}; + +mod action; + +pub struct AppState { + pub db: Arc>, +} + +impl AppState { + pub fn new(db: PgConnection) -> Self { + Self { + db: Arc::new(Mutex::new(db)), + } + } +}