From 34a2445c67d468e7894dc7ecbe754ab781438e40 Mon Sep 17 00:00:00 2001 From: Sophie Date: Tue, 23 Apr 2024 15:12:40 -0700 Subject: [PATCH 1/5] Add sql database and tables for user and session --- .env | 6 + Cargo.lock | 173 +++++++++++++++++- Cargo.toml | 8 + .../toolbar/components/UserButton.tsx | 8 +- .../features/toolbar/hooks/useGithubAuth.ts | 7 +- config.toml | 7 + diesel.toml | 9 + migrations/.keep | 0 .../down.sql | 6 + .../up.sql | 36 ++++ .../2024-04-19-174554_create_users/down.sql | 1 + .../2024-04-19-174554_create_users/up.sql | 10 + .../down.sql | 1 + .../2024-04-19-174903_create_sessions/up.sql | 6 + scripts/start_local_db.sh | 21 +++ src/api.rs | 23 ++- src/db/error.rs | 13 ++ src/db/mod.rs | 55 ++++++ src/db/user_session.rs | 67 +++++++ src/github.rs | 47 +++-- src/lib.rs | 6 + src/main.rs | 61 +++--- src/models.rs | 45 +++++ src/schema.rs | 27 +++ tests/db_integration.rs | 77 ++++++++ 25 files changed, 659 insertions(+), 61 deletions(-) create mode 100644 .env create mode 100644 config.toml create mode 100644 diesel.toml create mode 100644 migrations/.keep create mode 100644 migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 migrations/2024-04-19-174554_create_users/down.sql create mode 100644 migrations/2024-04-19-174554_create_users/up.sql create mode 100644 migrations/2024-04-19-174903_create_sessions/down.sql create mode 100644 migrations/2024-04-19-174903_create_sessions/up.sql create mode 100755 scripts/start_local_db.sh create mode 100644 src/db/error.rs create mode 100644 src/db/mod.rs create mode 100644 src/db/user_session.rs create mode 100644 src/lib.rs create mode 100644 src/models.rs create mode 100644 src/schema.rs create mode 100644 tests/db_integration.rs diff --git a/.env b/.env new file mode 100644 index 0000000..15b7d07 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +POSTGRES_USER="postgres" +POSTGRES_PASSWORD="localpw" +POSTGRES_URI="localhost" +POSTGRES_PORT="5432" +POSTGRES_DB_NAME="forc_pub" +DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_URI}/${POSTGRES_DB_NAME}" diff --git a/Cargo.lock b/Cargo.lock index 14524b5..19dea3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,12 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.3.0" @@ -309,6 +315,53 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "diesel" +version = "2.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2" +dependencies = [ + "bitflags 2.5.0", + "byteorder", + "diesel_derives", + "itoa", + "pq-sys", + "r2d2", + "uuid", +] + +[[package]] +name = "diesel_derives" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.57", +] + [[package]] name = "digest" version = "0.10.7" @@ -320,6 +373,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.8.0" @@ -359,7 +418,7 @@ dependencies = [ "atomic", "pear", "serde", - "toml", + "toml 0.5.10", "uncased", "version_check", ] @@ -374,6 +433,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" name = "forc_pub" version = "0.1.0" dependencies = [ + "diesel", + "diesel_migrations", + "dotenvy", "hex", "nanoid", "regex", @@ -383,6 +445,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "uuid", ] [[package]] @@ -898,6 +961,27 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "mime" version = "0.3.16" @@ -1170,6 +1254,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pq-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" +dependencies = [ + "vcpkg", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -1201,6 +1294,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -1485,6 +1589,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1561,6 +1674,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1892,6 +2014,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2068,6 +2224,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" + [[package]] name = "valuable" version = "0.1.0" @@ -2442,6 +2604,15 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 4b777e6..72cd0b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "forc_pub" version = "0.1.0" edition = "2021" +[lib] +name = "forc_pub" +path = "src/lib.rs" + [dependencies] nanoid = "0.4.0" hex = "0.4.3" @@ -13,3 +17,7 @@ rocket = { version = "0.5.0-rc.2", features = ["tls", "json"] } serde = { version = "1.0", features = ["derive"] } reqwest = { version = "0.12.2", features = ["json"] } thiserror = "1.0.58" +diesel = { version = "2.1.6", features = ["postgres", "uuid", "r2d2"] } +dotenvy = "0.15" +uuid = "1.8.0" +diesel_migrations = "2.1.0" diff --git a/app/src/features/toolbar/components/UserButton.tsx b/app/src/features/toolbar/components/UserButton.tsx index bfd9ce9..0bb7a4d 100644 --- a/app/src/features/toolbar/components/UserButton.tsx +++ b/app/src/features/toolbar/components/UserButton.tsx @@ -52,12 +52,12 @@ function UserButton() { /dev/null; then + echo "Docker is not installed. Please install Docker to run this script." + exit 1 +fi + +# Check if the PostgreSQL container is already running +if docker ps --format '{{.Names}}' | grep -q '^postgres$'; then + echo "PostgreSQL container is already running." + exit 0 +fi + +# Source environment variables +source .env + +# Start PostgreSQL container +docker run --name $POSTGRES_USER -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD -e POSTGRES_DB=$POSTGRES_DB_NAME -d -p $POSTGRES_PORT:$POSTGRES_PORT postgres + +echo "PostgreSQL container started successfully." \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index bdf896b..3448dba 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,29 @@ use rocket::serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] +use crate::models; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] pub struct User { - pub name: String, + pub full_name: String, pub email: Option, pub avatar_url: Option, + pub github_url: String, + pub github_login: String, + pub is_admin: bool, +} + +impl From for User { + fn from(user: models::User) -> Self { + User { + full_name: user.full_name, + email: user.email, + avatar_url: user.avatar_url, + github_url: user.github_url, + github_login: user.github_login, + is_admin: user.is_admin, + } + } } /// The login request. diff --git a/src/db/error.rs b/src/db/error.rs new file mode 100644 index 0000000..097ecbb --- /dev/null +++ b/src/db/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("Invalid UUID: {0}")] + InvalidUuid(String), + #[error("Entry for ID not found: {0}")] + NotFound(String), + #[error("Failed to save user: {0}")] + InsertUserFailed(String), + #[error("Failed to save session for user; {0}")] + InsertSessionFailed(String), +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..327cf5d --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,55 @@ +mod error; +mod user_session; + +use self::error::DatabaseError; +use crate::{api, models, schema}; +use diesel::pg::PgConnection; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use dotenvy::dotenv; +use std::env; +use uuid::Uuid; + +pub type DbPool = Pool>; +pub type DbConnection = PooledConnection>; + +/// The representation of a SQL database connection pool and its operations. +pub struct Database { + pub pool: DbPool, +} + +impl Default for Database { + fn default() -> Self { + Database::new() + } +} + +impl Database { + pub fn new() -> Self { + // Create a connection pool + dotenv().ok(); + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let pool = Pool::builder() + .build(ConnectionManager::::new(database_url)) + .expect("db connection pool"); + + // Run migrations + const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + let mut connection = pool.get().expect("db connection"); + let migrations = connection + .run_pending_migrations(MIGRATIONS) + .expect("diesel migrations"); + println!("Ran {} migrations", migrations.len()); + + Database { pool } + } + + /// Get a connection from the pool. + pub fn connection(&self) -> DbConnection { + self.pool.get().expect("db connection") + } +} + +pub(crate) fn string_to_uuid(s: String) -> Result { + Uuid::parse_str(s.as_str()).map_err(|_| DatabaseError::InvalidUuid(s)) +} diff --git a/src/db/user_session.rs b/src/db/user_session.rs new file mode 100644 index 0000000..30d826d --- /dev/null +++ b/src/db/user_session.rs @@ -0,0 +1,67 @@ +use super::error::DatabaseError; +use super::{api, models, schema}; +use super::{string_to_uuid, Database}; +use diesel::prelude::*; +use diesel::upsert::excluded; +use std::time::{Duration, SystemTime}; + +impl Database { + /// Insert a user session into the database and return the session ID. + /// If the user doesn't exist, insert the user as well. + /// If the user does exist, update the user's full name and avatar URL if they have changed. + pub fn insert_user_session( + &self, + user: &api::User, + expires_in: u32, + ) -> Result { + let connection = &mut self.connection(); + + // Insert or update a user + let new_user = models::NewUser { + full_name: user.full_name.clone(), + github_login: user.github_login.clone(), + github_url: user.github_url.clone(), + avatar_url: user.avatar_url.clone(), + email: user.email.clone(), + is_admin: user.is_admin, + }; + + let saved_user = diesel::insert_into(schema::users::table) + .values(&new_user) + .returning(models::User::as_returning()) + .on_conflict(schema::users::github_login) + .do_update() + .set(( + schema::users::full_name.eq(excluded(schema::users::full_name)), + schema::users::avatar_url.eq(excluded(schema::users::avatar_url)), + )) + .get_result(connection) + .map_err(|_| DatabaseError::InsertUserFailed(user.github_login.clone()))?; + + let new_session = models::NewSession { + user_id: saved_user.id, + expires_at: SystemTime::now() + Duration::from_secs(u64::from(expires_in)), + }; + + // Insert new session + let saved_session = diesel::insert_into(schema::sessions::table) + .values(&new_session) + .returning(models::Session::as_returning()) + .get_result(connection) + .map_err(|_| DatabaseError::InsertSessionFailed(user.github_login.clone()))?; + + Ok(saved_session.id.to_string()) + } + + /// Fetch a user from the database for a given session ID. + pub fn get_user_for_session(&self, session_id: String) -> Result { + let session_uuid = string_to_uuid(session_id.clone())?; + let connection = &mut self.connection(); + schema::sessions::table + .inner_join(schema::users::table) + .filter(schema::sessions::id.eq(session_uuid)) + .select(models::User::as_returning()) + .first::(connection) + .map_err(|_| DatabaseError::NotFound(session_id)) + } +} diff --git a/src/github.rs b/src/github.rs index 717647b..8da941e 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1,14 +1,23 @@ extern crate reqwest; -use std::env; - use crate::api::User; use serde::Deserialize; +use std::env; use thiserror::Error; #[derive(Deserialize, Debug)] struct GithubOauthResponse { access_token: String, + expires_in: u32, +} + +#[derive(Deserialize, Debug)] +struct GithubUserResponse { + pub name: String, + pub email: Option, + pub avatar_url: Option, + pub html_url: String, + pub login: String, } #[derive(Error, Debug)] @@ -23,12 +32,13 @@ pub enum GithubError { const GITHUB_CLIENT_ID: &str = "Iv1.ebdf596c6c548759"; -pub async fn handle_login(code: String) -> Result { - let access_token = exchange_code(code).await?; - fetch_user(access_token).await +pub async fn handle_login(code: String) -> Result<(User, u32), GithubError> { + let (access_token, expires_in) = exchange_code(code).await?; + let user = fetch_user(access_token).await?; + Ok((user, expires_in)) } -async fn exchange_code(code: String) -> Result { +async fn exchange_code(code: String) -> Result<(String, u32), GithubError> { let client = reqwest::Client::new(); // todo: reuse client? let client_secret = env::var("GITHUB_CLIENT_SECRET").expect("GITHUB_CLIENT_SECRET not set"); @@ -51,7 +61,7 @@ async fn exchange_code(code: String) -> Result { .await .map_err(|_| GithubError::Auth(status.to_string()))?; - Ok(body.access_token) + Ok((body.access_token, body.expires_in)) } async fn fetch_user(token: String) -> Result { @@ -68,10 +78,21 @@ async fn fetch_user(token: String) -> Result { let status = res.status(); - let body = res.json::().await.map_err(|_| GithubError::Api { - name: "user".to_string(), - status: status.to_string(), - })?; - - Ok(body) + let body = res + .json::() + .await + .map_err(|_| GithubError::Api { + name: "user".to_string(), + status: status.to_string(), + })?; + + let user = User { + full_name: body.name, + email: body.email, + avatar_url: body.avatar_url, + github_url: body.html_url, + github_login: body.login, + is_admin: false, // TODO: Check if user is admin + }; + Ok(user) } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..537aaa1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod api; +pub mod cors; +pub mod db; +pub mod github; +pub mod models; +pub mod schema; diff --git a/src/main.rs b/src/main.rs index 94dc680..f61b992 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,51 +3,35 @@ #[macro_use] extern crate rocket; -mod api; -mod cors; -mod github; - -use crate::github::handle_login; -use api::{LoginRequest, LoginResponse, PublishRequest, PublishResponse, SessionResponse, User}; -use cors::Cors; +use forc_pub::api::{ + LoginRequest, LoginResponse, PublishRequest, PublishResponse, SessionResponse, User, +}; +use forc_pub::cors::Cors; +use forc_pub::db::Database; +use forc_pub::github::handle_login; use rocket::{serde::json::Json, State}; -use std::{collections::HashMap, sync::Mutex}; #[derive(Default)] struct ServerState { - // TODO: Set up SQL database and use sessions table. - sessions: Mutex>, -} - -impl ServerState { - pub fn new() -> Self { - ServerState { - sessions: Mutex::new(HashMap::new()), - } - } - - pub fn insert(&self, user: &User) -> String { - let session_id = nanoid::nanoid!(); - self.sessions - .lock() - .expect("lock sessions") - .insert(session_id.clone(), user.clone()); - session_id - } + pub db: Database, } /// The endpoint to authenticate with GitHub. #[post("/login", data = "")] async fn login(state: &State, request: Json) -> Json { match handle_login(request.code.clone()).await { - Ok(user) => { - let session_id = state.insert(&user); - Json(LoginResponse { + Ok((user, expires_in)) => match state.db.insert_user_session(&user, expires_in) { + Ok(session_id) => Json(LoginResponse { user: Some(user), session_id: Some(session_id), error: None, - }) - } + }), + Err(e) => Json(LoginResponse { + user: None, + session_id: None, + error: Some(e.to_string()), + }), + }, Err(e) => Json(LoginResponse { user: None, session_id: None, @@ -59,15 +43,14 @@ async fn login(state: &State, request: Json) -> Json< /// The endpoint to authenticate with GitHub. #[get("/session?")] async fn session(state: &State, id: String) -> Json { - let sessions = state.sessions.lock().expect("lock sessions"); - match sessions.get(&id) { - Some(user) => Json(SessionResponse { - user: Some(user.clone()), + match state.db.get_user_for_session(id) { + Ok(user) => Json(SessionResponse { + user: Some(User::from(user)), error: None, }), - None => Json(SessionResponse { + Err(error) => Json(SessionResponse { user: None, - error: Some("Invalid session".to_string()), + error: Some(error.to_string()), }), } } @@ -101,7 +84,7 @@ fn health() -> String { #[launch] fn rocket() -> _ { rocket::build() - .manage(ServerState::new()) + .manage(ServerState::default()) .attach(Cors) .mount("/", routes![login, session, publish, all_options, health]) .register("/", catchers![not_found]) diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..93c2096 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,45 @@ +use diesel::prelude::*; +use std::time::SystemTime; +use uuid::Uuid; + +#[derive(Queryable, Selectable, Debug)] +#[diesel(table_name = crate::schema::users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + pub id: Uuid, + pub full_name: String, + pub github_login: String, + pub github_url: String, + pub avatar_url: Option, + pub email: Option, + pub is_admin: bool, + pub created_at: SystemTime, +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::users)] +pub struct NewUser { + pub full_name: String, + pub github_login: String, + pub github_url: String, + pub avatar_url: Option, + pub email: Option, + pub is_admin: bool, +} + +#[derive(Queryable, Selectable)] +#[diesel(table_name = crate::schema::sessions)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Session { + pub id: Uuid, + pub user_id: Uuid, + pub expires_at: SystemTime, + pub created_at: SystemTime, +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::sessions)] +pub struct NewSession { + pub user_id: Uuid, + pub expires_at: SystemTime, +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..9f19130 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,27 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + sessions (id) { + id -> Uuid, + user_id -> Uuid, + expires_at -> Timestamp, + created_at -> Timestamp, + } +} + +diesel::table! { + users (id) { + id -> Uuid, + full_name -> Varchar, + github_login -> Varchar, + github_url -> Varchar, + avatar_url -> Nullable, + email -> Nullable, + is_admin -> Bool, + created_at -> Timestamp, + } +} + +diesel::joinable!(sessions -> users (user_id)); + +diesel::allow_tables_to_appear_in_same_query!(sessions, users,); diff --git a/tests/db_integration.rs b/tests/db_integration.rs new file mode 100644 index 0000000..99685b6 --- /dev/null +++ b/tests/db_integration.rs @@ -0,0 +1,77 @@ +use diesel::RunQueryDsl as _; +use forc_pub::api; +use forc_pub::db::Database; +use uuid::Uuid; + +/// Note: Integration tests for the database module assume that the database is running and that the DATABASE_URL environment variable is set. +/// This should be done by running `./scripts/start_local_db.sh` before running the tests. + +const TEST_LOGIN_1: &str = "AliceBobbins"; +const TEST_FULL_NAME_1: &str = "Alice Bobbins"; +const TEST_EMAIL_1: &str = "alice@bob.com"; +const TEST_URL_1: &str = "url1.url"; +const TEST_URL_2: &str = "url2.url"; +const TEST_LOGIN_2: &str = "foobar"; + +fn clear_tables(db: &Database) { + let connection = &mut db.connection(); + diesel::delete(forc_pub::schema::sessions::table) + .execute(connection) + .expect("clear sessions table"); + diesel::delete(forc_pub::schema::users::table) + .execute(connection) + .expect("clear users table"); +} + +fn mock_user_1() -> api::User { + api::User { + github_login: TEST_LOGIN_1.to_string(), + full_name: TEST_FULL_NAME_1.to_string(), + email: Some(TEST_EMAIL_1.to_string()), + avatar_url: Some(TEST_URL_1.to_string()), + github_url: TEST_URL_2.to_string(), + is_admin: true, + } +} + +fn mock_user_2() -> api::User { + api::User { + github_login: TEST_LOGIN_2.to_string(), + ..Default::default() + } +} + +#[test] +fn test_multiple_user_sessions() { + let db = Database::default(); + + let user1 = mock_user_1(); + let user2 = mock_user_2(); + + let session1 = db.insert_user_session(&user1, 1000).expect("result is ok"); + Uuid::parse_str(session1.as_str()).expect("result is a valid UUID"); + + // Insert an existing user + let session2 = db.insert_user_session(&user1, 1000).expect("result is ok"); + Uuid::parse_str(session2.as_str()).expect("result is a valid UUID"); + + // Insert another user + let session3 = db.insert_user_session(&user2, 1000).expect("result is ok"); + Uuid::parse_str(session3.as_str()).expect("result is a valid UUID"); + + let result = db.get_user_for_session(session1).expect("result is ok"); + assert_eq!(result.github_login, TEST_LOGIN_1); + assert_eq!(result.full_name, TEST_FULL_NAME_1); + assert_eq!(result.email.expect("is some"), TEST_EMAIL_1); + assert_eq!(result.avatar_url.expect("is some"), TEST_URL_1); + assert_eq!(result.github_url, TEST_URL_2); + assert_eq!(result.is_admin, true); + + let result = db.get_user_for_session(session2).expect("result is ok"); + assert_eq!(result.github_login, TEST_LOGIN_1); + + let result = db.get_user_for_session(session3).expect("result is ok"); + assert_eq!(result.github_login, TEST_LOGIN_2); + + clear_tables(&db); +} From 88cdb5226c221dc8c41766acec7d638947980090 Mon Sep 17 00:00:00 2001 From: Sophie Date: Tue, 23 Apr 2024 16:41:37 -0700 Subject: [PATCH 2/5] run postgres docker image in ci --- .github/workflows/backend-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 5d94f0a..b5cace2 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -74,6 +74,9 @@ jobs: with: profile: minimal toolchain: ${{ env.RUST_VERSION }} + - uses: docker-practice/actions-setup-docker@master + timeout-minutes: 12 + - run: ./scripts/start_local_db.sh - uses: Swatinem/rust-cache@v1 - name: Run tests uses: actions-rs/cargo@v1 From 4943e9aae83770a176eed86022a494e3d4836eb6 Mon Sep 17 00:00:00 2001 From: Sophie Date: Tue, 23 Apr 2024 16:42:36 -0700 Subject: [PATCH 3/5] clippy --- tests/db_integration.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/db_integration.rs b/tests/db_integration.rs index 99685b6..ed8fdde 100644 --- a/tests/db_integration.rs +++ b/tests/db_integration.rs @@ -65,8 +65,8 @@ fn test_multiple_user_sessions() { assert_eq!(result.email.expect("is some"), TEST_EMAIL_1); assert_eq!(result.avatar_url.expect("is some"), TEST_URL_1); assert_eq!(result.github_url, TEST_URL_2); - assert_eq!(result.is_admin, true); - + assert!(result.is_admin); + let result = db.get_user_for_session(session2).expect("result is ok"); assert_eq!(result.github_login, TEST_LOGIN_1); From 88affc5643fe2d9ef89a6fe9b6f125073b9ff964 Mon Sep 17 00:00:00 2001 From: Sophie Date: Tue, 23 Apr 2024 16:46:20 -0700 Subject: [PATCH 4/5] cargo fmt --- tests/db_integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/db_integration.rs b/tests/db_integration.rs index ed8fdde..61af983 100644 --- a/tests/db_integration.rs +++ b/tests/db_integration.rs @@ -66,7 +66,7 @@ fn test_multiple_user_sessions() { assert_eq!(result.avatar_url.expect("is some"), TEST_URL_1); assert_eq!(result.github_url, TEST_URL_2); assert!(result.is_admin); - + let result = db.get_user_for_session(session2).expect("result is ok"); assert_eq!(result.github_login, TEST_LOGIN_1); From 059c519f97689ca9021d07173e2f17ee87e4b998 Mon Sep 17 00:00:00 2001 From: Sophie Date: Tue, 23 Apr 2024 17:07:49 -0700 Subject: [PATCH 5/5] Move server URI to constant --- app/src/constants.ts | 1 + app/src/features/toolbar/hooks/useGithubAuth.ts | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 app/src/constants.ts diff --git a/app/src/constants.ts b/app/src/constants.ts new file mode 100644 index 0000000..9fc5846 --- /dev/null +++ b/app/src/constants.ts @@ -0,0 +1 @@ +export const SERVER_URI = process.env.REACT_APP_SERVER_URI ?? "http://localhost:8080" \ No newline at end of file diff --git a/app/src/features/toolbar/hooks/useGithubAuth.ts b/app/src/features/toolbar/hooks/useGithubAuth.ts index f07b189..50a7550 100644 --- a/app/src/features/toolbar/hooks/useGithubAuth.ts +++ b/app/src/features/toolbar/hooks/useGithubAuth.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useLocalSession } from '../../../utils/localStorage'; +import { SERVER_URI } from '../../../constants'; interface AuthenticatedUser { fullName: string; @@ -57,7 +58,7 @@ export function useGithubAuth(): [AuthenticatedUser | null, () => void] { const params = { code: githubCode, }; - const request = new Request('http://localhost:8080/login', { + const request = new Request(`${SERVER_URI}/login`, { method: 'POST', body: JSON.stringify(params), }); @@ -96,12 +97,9 @@ export function useGithubAuth(): [AuthenticatedUser | null, () => void] { return; } - const request = new Request( - `http://localhost:8080/session?id=${sessionId}`, - { - method: 'GET', - } - ); + const request = new Request(`${SERVER_URI}/session?id=${sessionId}`, { + method: 'GET', + }); fetch(request) .then((response) => { if (response.status < 400) {