Skip to content

Commit 803e216

Browse files
committed
What did I do
1 parent e383910 commit 803e216

File tree

14 files changed

+910
-123
lines changed

14 files changed

+910
-123
lines changed

Cargo.lock

Lines changed: 416 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@ actix-web = { version = "4.9.0", features = ["cookies"] }
99
anyhow = "1.0.88"
1010
base64 = "0.22.1"
1111
chrono = { version = "0.4.38", features = ["serde"] }
12+
clap = { version = "4.5.20", features = ["derive"] }
1213
dotenv = "0.15.0"
1314
env_logger = "0.11.5"
1415
futures-util = "0.3.30"
1516
include_dir = "0.7.4"
1617
log = "0.4.22"
1718
mime_guess = "2.0.5"
1819
oauth2 = "4.4.2"
20+
redis = { version = "0.26.1", features = ["aio", "tokio-comp"] }
21+
redis-work-queue = "0.3.0"
1922
reqwest = { version = "0.12.7", features = ["json"] }
2023
serde = { version = "1.0.210", features = ["derive"] }
2124
serde_json = "1.0.128"
2225
sqlx = { version = "0.8.2", features = ["chrono", "postgres", "runtime-tokio"] }
26+
tokio = { version = "1.40.0", features = ["full"] }
2327
utoipa = { version = "5.0.0-beta.0", features = ["actix_extras", "chrono"] }
2428
utoipa-swagger-ui = { version = "7.1.1-beta.0", features = ["actix-web"] }

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ WORKDIR /app
5151

5252
COPY --from=builder /app/target/release/rideboard-v2 .
5353

54-
CMD ["./rideboard-v2"]
54+
CMD ["./rideboard-v2", "server"]

src/api/v1/auth/csh.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::api::v1::auth::models::UserRealm;
22
use crate::api::v1::auth::models::{CSHUserInfo, UserInfo};
3-
use crate::AppState;
3+
use crate::app::AppState;
44
use actix_session::Session;
55
use actix_web::http::header;
66
use actix_web::{get, Scope};

src/api/v1/auth/google.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::api::v1::auth::common;
22
use crate::api::v1::auth::models::UserRealm;
33
use crate::api::v1::auth::models::{GoogleUserInfo, UserInfo};
4-
use crate::AppState;
4+
use crate::app::AppState;
55
use actix_session::Session;
66
use actix_web::http::header;
77
use actix_web::{get, web, Scope};

src/api/v1/event/car/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::api::v1::auth::models::UserInfo;
2-
use crate::AppState;
2+
use crate::app::AppState;
33
use crate::{api::v1::auth::models::UserData, auth::SessionAuth};
44
use actix_session::Session;
55
use actix_web::{
@@ -315,9 +315,13 @@ async fn update_car(
315315
match updated {
316316
Ok(Some(_)) => {}
317317
Ok(None) => {
318-
return HttpResponse::NotFound().body("Car not found or you are not the driver.")
318+
tx.rollback().await.unwrap();
319+
return HttpResponse::NotFound().body("Car not found or you are not the driver.");
320+
}
321+
Err(_) => {
322+
tx.rollback().await.unwrap();
323+
return HttpResponse::InternalServerError().body("Failed to update car");
319324
}
320-
Err(_) => return HttpResponse::InternalServerError().body("Failed to update car"),
321325
}
322326

323327
// Used for sending pings

src/api/v1/event/car/rider/mod.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
use crate::api::v1::event::UserInfo;
1+
use std::borrow::BorrowMut;
2+
3+
use crate::app::{AppState, SimpleRiderChange};
24
use crate::auth::SessionAuth;
3-
use crate::AppState;
5+
use crate::{api::v1::event::UserInfo, app::RedisJob};
46
use actix_session::Session;
57
use actix_web::{
68
delete, post,
79
web::{self},
810
HttpResponse, Responder, Scope,
911
};
1012
use log::error;
13+
use redis_work_queue::{Item, WorkQueue};
1114
use sqlx::query;
1215
use utoipa::OpenApi;
1316

@@ -67,7 +70,21 @@ async fn create_rider(
6770
.await;
6871

6972
match result {
70-
Ok(_) => HttpResponse::Ok().body("Joined Car"),
73+
Ok(_) => {
74+
let work_queue = WorkQueue::new(data.work_queue_key.clone());
75+
let item = Item::from_json_data(&RedisJob::Join(SimpleRiderChange {
76+
event_id,
77+
car_id,
78+
rider_id: user_id,
79+
}))
80+
.unwrap();
81+
let mut redis = data.redis.lock().unwrap().clone();
82+
work_queue
83+
.add_item(&mut redis, &item)
84+
.await
85+
.expect("failed to add item to work queue");
86+
HttpResponse::Ok().body("Joined Car")
87+
}
7188
Err(e) => {
7289
error!("Failed to Add Rider: {}", e);
7390
HttpResponse::InternalServerError().body("Failed to create car")
@@ -90,18 +107,33 @@ async fn delete_rider(
90107
session: Session,
91108
path: web::Path<(i32, i32)>,
92109
) -> impl Responder {
93-
let (_event_id, car_id) = path.into_inner();
110+
let (event_id, car_id) = path.into_inner();
111+
let user_id = session.get::<UserInfo>("userinfo").unwrap().unwrap().id;
94112

95113
let deleted = sqlx::query!(
96114
"DELETE FROM rider WHERE car_id = $1 AND rider = $2",
97115
car_id,
98-
session.get::<UserInfo>("userinfo").unwrap().unwrap().id
116+
user_id
99117
)
100118
.execute(&data.db)
101119
.await;
102120

103121
match deleted {
104-
Ok(_) => HttpResponse::Ok().body("Rider deleted"),
122+
Ok(_) => {
123+
let work_queue = WorkQueue::new(data.work_queue_key.clone());
124+
let item = Item::from_json_data(&RedisJob::Leave(SimpleRiderChange {
125+
event_id,
126+
car_id,
127+
rider_id: user_id,
128+
}))
129+
.unwrap();
130+
let mut redis = data.redis.lock().unwrap().clone();
131+
work_queue
132+
.add_item(&mut redis, &item)
133+
.await
134+
.expect("failed to add item to work queue");
135+
HttpResponse::Ok().body("Rider deleted")
136+
}
105137
Err(_) => HttpResponse::InternalServerError().body("Failed to delete rider"),
106138
}
107139
}

src/api/v1/event/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize};
1111
use serde_json::json;
1212
use sqlx::query_as;
1313

14+
use crate::app::AppState;
1415
use crate::auth::SessionAuth;
15-
use crate::AppState;
1616

1717
use utoipa::{OpenApi, ToSchema};
1818

src/api/v1/user.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use actix_web::{get, web, HttpResponse, Responder, Scope};
22
use serde::Deserialize;
33
use sqlx::query_as;
44

5+
use crate::app::AppState;
56
use crate::auth::SessionAuth;
6-
use crate::AppState;
77

88
use utoipa::OpenApi;
99

src/app.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use std::sync::{Arc, Mutex};
2+
3+
use oauth2::basic::BasicClient;
4+
use redis::aio::MultiplexedConnection;
5+
use redis_work_queue::KeyPrefix;
6+
use serde::{Deserialize, Serialize};
7+
use sqlx::PgPool;
8+
use utoipa::ToSchema;
9+
10+
#[derive(Clone)]
11+
pub struct AppState {
12+
pub db: PgPool,
13+
pub redis: Arc<Mutex<MultiplexedConnection>>,
14+
pub work_queue_key: KeyPrefix,
15+
pub google_oauth: BasicClient,
16+
pub google_userinfo_url: String,
17+
pub csh_oauth: BasicClient,
18+
pub csh_userinfo_url: String,
19+
}
20+
21+
#[derive(Serialize, Deserialize, sqlx::Type, ToSchema, Clone)]
22+
#[serde(rename_all = "camelCase")]
23+
pub struct UserData {
24+
pub id: String,
25+
pub realm: String,
26+
pub name: String,
27+
pub email: String,
28+
}
29+
30+
#[derive(Serialize, Deserialize)]
31+
pub struct SimpleRiderChange {
32+
pub event_id: i32,
33+
pub car_id: i32,
34+
pub rider_id: String,
35+
}
36+
37+
#[derive(Serialize, Deserialize)]
38+
pub struct MultipleRiderChange {
39+
pub event_id: i32,
40+
pub car_id: i32,
41+
pub old_riders: Vec<String>,
42+
pub new_riders: Vec<String>,
43+
}
44+
45+
#[derive(Serialize, Deserialize)]
46+
#[serde(tag = "type")]
47+
pub enum RedisJob {
48+
Join(SimpleRiderChange),
49+
Leave(SimpleRiderChange),
50+
RiderUpdate(MultipleRiderChange),
51+
}

src/main.rs

Lines changed: 26 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,34 @@
1-
use actix_session::storage::CookieSessionStore;
2-
use actix_session::SessionMiddleware;
3-
use actix_web::cookie::Key;
4-
use actix_web::{middleware::Logger, web, App, HttpResponse, HttpServer, Responder};
5-
use anyhow::anyhow;
6-
use base64::prelude::*;
7-
use include_dir::{include_dir, Dir};
8-
use log::info;
9-
use oauth2::basic::BasicClient;
10-
use sqlx::{postgres::PgPoolOptions, PgPool};
11-
use std::env;
12-
13-
mod api;
1+
pub mod api;
2+
pub mod app;
143
mod auth;
15-
//mod pings; // Undo this when developing it
16-
17-
#[derive(Clone)]
18-
struct AppState {
19-
db: PgPool,
20-
google_oauth: BasicClient,
21-
google_userinfo_url: String,
22-
csh_oauth: BasicClient,
23-
csh_userinfo_url: String,
24-
}
25-
26-
// Embed the 'static' directory into the binary
27-
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/frontend/dist");
28-
29-
async fn serve_file(path: web::Path<String>) -> impl Responder {
30-
let file_path = path.into_inner();
31-
if let Some(file) = STATIC_DIR.get_file(&file_path) {
32-
let content = file.contents();
33-
let mime = mime_guess::from_path(&file_path).first_or_octet_stream();
34-
HttpResponse::Ok().content_type(mime.as_ref()).body(content)
35-
} else {
36-
HttpResponse::NotFound().body("File not found")
37-
}
4+
pub mod pings;
5+
mod server;
6+
mod worker;
7+
8+
use clap::{Parser, Subcommand};
9+
10+
#[derive(Parser)]
11+
#[command(name = "App")]
12+
#[command(about = "An application with async server and worker subcommands", long_about = None)]
13+
struct Cli {
14+
#[command(subcommand)]
15+
command: Commands,
3816
}
3917

40-
async fn serve_index() -> impl Responder {
41-
if let Some(file) = STATIC_DIR.get_file("index.html") {
42-
let content = file.contents();
43-
let mime = mime_guess::from_path("index.html").first_or_octet_stream();
44-
HttpResponse::Ok().content_type(mime.as_ref()).body(content)
45-
} else {
46-
HttpResponse::NotFound().body("File not found")
47-
}
18+
#[derive(Subcommand)]
19+
enum Commands {
20+
/// Start the async server
21+
Server,
22+
/// Start the async worker
23+
Worker,
4824
}
4925

50-
#[actix_web::main]
26+
#[tokio::main]
5127
async fn main() -> std::io::Result<()> {
52-
env_logger::init();
53-
dotenv::dotenv().ok();
54-
55-
let host = env::var("HOST").unwrap_or("127.0.0.1".to_string());
56-
let host_inner = host.clone();
57-
let port: i32 = match &env::var("PORT").map(|port| port.parse()) {
58-
Ok(Ok(p)) => *p,
59-
Ok(Err(_)) => 8080,
60-
Err(_) => 8080,
61-
};
62-
63-
let db_pool = PgPoolOptions::new()
64-
.max_connections(5)
65-
.connect(&env::var("DATABASE_URL").expect("DATABASE_URL must be set"))
66-
.await
67-
.expect("Failed to create pool");
28+
let cli = Cli::parse();
6829

69-
let session_key = env::var("SESSION_KEY")
70-
.map_err(|e| anyhow!("Failed to get Env Var: {}", e))
71-
.and_then(|key64| {
72-
BASE64_STANDARD
73-
.decode(key64)
74-
.map_err(|e| anyhow!("Failed to decode session key: {}", e))
75-
})
76-
.map(|key| Key::from(&key))
77-
.unwrap_or(Key::generate());
78-
79-
info!("Starting server at http://{host}:{port}");
80-
HttpServer::new(move || {
81-
let (google_client, csh_client) = auth::get_clients(&host_inner, port);
82-
83-
App::new()
84-
.app_data(web::Data::new(AppState {
85-
db: db_pool.clone(),
86-
google_oauth: google_client,
87-
google_userinfo_url: "https://openidconnect.googleapis.com/v1/userinfo".to_string(),
88-
csh_oauth: csh_client,
89-
csh_userinfo_url: env::var("CSH_USERINFO_URL")
90-
.expect("Missing Userinfo URL for CSH Auth"),
91-
}))
92-
.wrap(
93-
SessionMiddleware::builder(CookieSessionStore::default(), session_key.clone())
94-
.cookie_secure(env::var("DEVELOPMENT").is_err())
95-
.build(),
96-
)
97-
.wrap(Logger::default())
98-
.service(api::scope())
99-
.route("/", web::get().to(serve_index))
100-
.route("/history", web::get().to(serve_index))
101-
.route("/login", web::get().to(serve_index))
102-
.route("/{filename:.*}", web::get().to(serve_file))
103-
})
104-
.bind(format!("{host}:{port}"))?
105-
.run()
106-
.await
30+
match &cli.command {
31+
Commands::Server => server::main().await,
32+
Commands::Worker => worker::main().await,
33+
}
10734
}

0 commit comments

Comments
 (0)