diff --git a/.env.template b/.env.template index ad77d7e..df3e56d 100644 --- a/.env.template +++ b/.env.template @@ -1 +1,3 @@ -DATABASE_URL=postgresql://user:password@127.0.0.1:8001/godsaeng \ No newline at end of file +DATABASE_URL=postgresql://user:password@127.0.0.1:8001/godsaeng +REDIS_URL=redis://127.0.0.1:6379 +SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b31653b..b0a8a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ path = "src/main.rs" name = "godsaeng-backend" [dependencies] +actix-session = { version = "0.7.2", features = ["redis-rs-tls-session"] } actix-web = "4.2.1" chrono = { version = "0.4.23", default-features = false } dotenv = "0.15.0" diff --git a/scripts/init_db.sh b/scripts/init_db.sh index 086b0c8..b3a278c 100644 --- a/scripts/init_db.sh +++ b/scripts/init_db.sh @@ -17,13 +17,13 @@ if ! [ -x "$(command -v sqlx)" ]; then fi # Check if a custom user has been set, otherwise default to 'postgres' -DB_USER="${POSTGRES_USER:=postgres}" +DB_USER="${POSTGRES_USER:=user}" # Check if a custom password has been set, otherwise default to 'password' DB_PASSWORD="${POSTGRES_PASSWORD:=password}" # Check if a custom database name has been set, otherwise default to 'newsletter' -DB_NAME="${POSTGRES_DB:=newsletter}" +DB_NAME="${POSTGRES_DB:=godsaeng}" # Check if a custom port has been set, otherwise default to '5432' -DB_PORT="${POSTGRES_PORT:=5432}" +DB_PORT="${POSTGRES_PORT:=8001}" # Check if a custom host has been set, otherwise default to 'localhost' DB_HOST="${POSTGRES_HOST:=localhost}" diff --git a/scripts/init_redis.sh b/scripts/init_redis.sh new file mode 100644 index 0000000..84d6e30 --- /dev/null +++ b/scripts/init_redis.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# copied from https://github.com/LukeMathWalker/zero-to-production/blob/main/scripts/init_redis.sh +set -x +set -eo pipefail + +# if a redis container is running, print instructions to kill it and exit +RUNNING_CONTAINER=$(docker ps --filter 'name=redis' --format '{{.ID}}') +if [[ -n $RUNNING_CONTAINER ]]; then + echo >&2 "there is a redis container already running, kill it with" + echo >&2 " docker kill ${RUNNING_CONTAINER}" + exit 1 +fi + +# Launch Redis using Docker +docker run \ + -p "6379:6379" \ + -d \ + --name "redis_$(date '+%s')" \ + redis + +>&2 echo "Redis is ready to go!" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 390b58c..28e64c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,17 @@ use godsaeng_backend::runner::run; use sqlx::postgres::PgPoolOptions; use std::net::TcpListener; +use actix_session::storage::RedisSessionStore; + #[tokio::main] async fn main() -> std::io::Result<()> { dotenv().ok(); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let store = { + let redis_url = std::env::var("REDIS_URL").expect("REDI must be set"); + RedisSessionStore::new(redis_url).await.unwrap() + }; + let secret_key = std::env::var("SECRET_KEY").expect("SECRET_KEY must be set"); let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) @@ -14,6 +21,6 @@ async fn main() -> std::io::Result<()> { .expect("Error building a connection pool"); let listener = TcpListener::bind("127.0.0.1:8000")?; - run(listener, pool)?.await?; + run(listener, pool, store, secret_key)?.await?; Ok(()) } diff --git a/src/routes/events.rs b/src/routes/events.rs index 999aa29..78a846b 100644 --- a/src/routes/events.rs +++ b/src/routes/events.rs @@ -5,12 +5,13 @@ use actix_web::{ HttpResponse, Responder, }; use chrono::NaiveDate; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use sqlx::{self, FromRow}; +use actix_session::Session; + #[derive(Deserialize)] pub struct CreateEventBody { - pub user_id: i32, pub note: String, pub event_date: String, } @@ -19,6 +20,7 @@ pub struct CreateEventBody { pub struct PatchEventBody { pub id: i32, pub new_note: String, + pub new_event_date: String, } #[derive(Deserialize)] @@ -26,13 +28,35 @@ pub struct DeleteEventBody { pub id: i32, } -#[derive(FromRow)] +#[derive(Serialize, FromRow)] struct IdRow { pub id: i32, } +#[derive(Serialize, FromRow)] +struct UserIdRow { + pub user_id: i32, +} +// -> rust macro +pub fn is_valid_user(session: &Session) -> i32 { + let user_id: Option = match session.get("user_id") { + Ok(x) => x, + Err(_) => Some(-1), + }; + user_id.unwrap_or(-1) +} + #[post("/event")] -pub async fn create_event(state: Data, body: Json) -> impl Responder { +pub async fn create_event( + state: Data, + body: Json, + session: Session, +) -> impl Responder { + let user_id = is_valid_user(&session); + if user_id == -1 { + return HttpResponse::Unauthorized().json("Login first"); + } + let ymd: Vec<&str> = body.event_date.split('-').collect(); let date = NaiveDate::from_ymd_opt( ymd[0].parse().unwrap(), @@ -43,32 +67,84 @@ pub async fn create_event(state: Data, body: Json) -> match sqlx::query_as::<_, IdRow>( "INSERT INTO event (user_id, note, event_date) VALUES ($1, $2, $3) RETURNING id", ) - .bind(body.user_id) + .bind(user_id) .bind(body.note.to_string()) .bind(date) .fetch_one(&state.db) .await { - Ok(id_row) => HttpResponse::Ok().json(id_row.id), + Ok(id_row) => HttpResponse::Ok().json(id_row), Err(_) => HttpResponse::InternalServerError().json("Failed to create event"), } } #[patch("/event")] -pub async fn patch_event(state: Data, body: Json) -> impl Responder { - match sqlx::query_as::<_, IdRow>("UPDATE event SET note = $1 WHERE id = $2 RETURNING id") - .bind(body.new_note.to_string()) - .bind(body.id) - .fetch_one(&state.db) - .await +pub async fn patch_event( + state: Data, + body: Json, + session: Session, +) -> impl Responder { + let ymd: Vec<&str> = body.new_event_date.split('-').collect(); + let new_date = NaiveDate::from_ymd_opt( + ymd[0].parse().unwrap(), + ymd[1].parse().unwrap(), + ymd[2].parse().unwrap(), + ); + let user_id = is_valid_user(&session); + if user_id == -1 { + return HttpResponse::Unauthorized().json("Login first"); + } + let user_id_check: i32 = + match sqlx::query_as::<_, UserIdRow>("SELECT user_id FROM event WHERE id = $1") + .bind(body.id) + .fetch_one(&state.db) + .await + { + Ok(id_row) => id_row.user_id, + Err(_) => return HttpResponse::BadRequest().json("Failed to find the event"), + }; + + if user_id != user_id_check { + return HttpResponse::Unauthorized().json("Unauthorized event"); + } + + match sqlx::query_as::<_, IdRow>( + "UPDATE event SET note = $1, event_date = $2 WHERE id = $3 RETURNING id", + ) + .bind(body.new_note.to_string()) + .bind(new_date) + .bind(body.id) + .fetch_one(&state.db) + .await { - Ok(id_row) => HttpResponse::Ok().json(id_row.id), + Ok(id_row) => HttpResponse::Ok().json(id_row), Err(_) => HttpResponse::InternalServerError().json("Failed to patch event"), } } #[delete("/event")] -pub async fn delete_event(state: Data, body: Json) -> impl Responder { +pub async fn delete_event( + state: Data, + body: Json, + session: Session, +) -> impl Responder { + let user_id = is_valid_user(&session); + if user_id == -1 { + return HttpResponse::Unauthorized().json("Login first"); + } + let user_id_check = + match sqlx::query_as::<_, UserIdRow>("SELECT user_id from event WHERE id = $1") + .bind(body.id) + .fetch_one(&state.db) + .await + { + Ok(id_row) => id_row.user_id, + Err(_) => return HttpResponse::BadRequest().json("Failed to find the event"), + }; + if user_id != user_id_check { + return HttpResponse::Unauthorized().json("Unauthorized event"); + } + match sqlx::query("DELETE FROM event WHERE id = $1") .bind(body.id) .execute(&state.db) diff --git a/src/routes/users.rs b/src/routes/users.rs index 70a9a81..413381b 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -34,7 +34,9 @@ struct IdRow { pub id: i32, } -#[post("/create-user")] +use actix_session::Session; + +#[post("/user")] pub async fn create_user(state: Data, body: Json) -> impl Responder { // check name duplication match check_duplication(&state, body.name.to_string()).await { @@ -67,25 +69,24 @@ async fn check_duplication(state: &Data, name: String) -> bool { } } -async fn user_auth(state: &Data, name: String, password: String) -> i32 { - match sqlx::query_as::<_, IdRow>("SELECT id FROM userInfo WHERE name = $1 AND password = $2") - .bind(name) - .bind(password) - .fetch_one(&state.db) - .await - { - Ok(id_row) => id_row.id, - Err(_) => -1, - } +pub fn is_valid_user(session: &Session) -> i32 { + let user_id: Option = match session.get("user_id") { + Ok(x) => x, + Err(_) => Some(-1), + }; + user_id.unwrap_or(-1) } #[patch("/user")] -pub async fn patch_user(state: Data, body: Json) -> impl Responder { - let user_id: i32 = - match user_auth(&state, body.name.to_string(), body.password.to_string()).await { - -1 => return HttpResponse::Unauthorized().json("Authentication failed"), - x => x, - }; +pub async fn patch_user( + state: Data, + body: Json, + session: Session, +) -> impl Responder { + let user_id = is_valid_user(&session); + if user_id == -1 { + return HttpResponse::Unauthorized().json("Login first"); + } match check_duplication(&state, body.new_name.to_string()).await { true => {} @@ -107,14 +108,27 @@ pub async fn patch_user(state: Data, body: Json) -> imp } #[delete("/user")] -pub async fn delete_user(state: Data, body: Json) -> impl Responder { - let user_id: i32 = +pub async fn delete_user( + state: Data, + body: Json, + session: Session, +) -> impl Responder { + let user_id = is_valid_user(&session); + if user_id == -1 { + return HttpResponse::Unauthorized().json("Login first"); + } + // check one more time + let user_id_check: i32 = match user_auth(&state, body.name.to_string(), body.password.to_string()).await { -1 => return HttpResponse::Unauthorized().json("Authentication failed"), x => x, }; + if user_id != user_id_check { + return HttpResponse::Unauthorized().json("Input your ID and Password"); + } + match sqlx::query("DELETE FROM userInfo WHERE id = $1") - .bind(user_id) + .bind(user_id_check) .execute(&state.db) .await { @@ -122,3 +136,48 @@ pub async fn delete_user(state: Data, body: Json) -> i Err(_) => HttpResponse::InternalServerError().json("Failed to delete user"), } } + +#[derive(Deserialize)] +pub struct LoginUserBody { + pub name: String, + pub password: String, +} + +async fn user_auth(state: &Data, name: String, password: String) -> i32 { + match sqlx::query_as::<_, IdRow>("SELECT id FROM userInfo WHERE name = $1 AND password = $2") + .bind(name) + .bind(password) + .fetch_one(&state.db) + .await + { + Ok(id_row) => id_row.id, + Err(_) => -1, + } +} + +#[post("/login")] +pub async fn login( + state: Data, + body: Json, + session: Session, +) -> impl Responder { + let user_id: i32 = + match user_auth(&state, body.name.to_string(), body.password.to_string()).await { + -1 => return HttpResponse::Unauthorized().json("Authentication failed"), + x => x, + }; + + session + .insert("user_id", user_id) + .expect("Error to insert session"); + session.renew(); + + // ## + // have to add session counter + // ## + + HttpResponse::Ok().json(User { + id: user_id, + name: body.name.to_string(), + }) +} diff --git a/src/runner.rs b/src/runner.rs index a8e3e56..7978dce 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,17 +1,34 @@ use crate::routes::events::{create_event, delete_event, patch_event}; -use crate::routes::users::{create_user, delete_user, patch_user}; +use crate::routes::users::{create_user, delete_user, login, patch_user}; use crate::AppState; use actix_web::{dev::Server, web::Data, App, HttpServer}; use sqlx::PgPool; use std::net::TcpListener; -pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { +use actix_web::cookie::Key; + +use actix_session::storage::RedisSessionStore; +use actix_session::SessionMiddleware; + +pub fn run( + listener: TcpListener, + db_pool: PgPool, + store: RedisSessionStore, + secret_key: String, +) -> Result { + let key = Key::from(secret_key.as_bytes()); // at least 64 byte let server = HttpServer::new(move || { App::new() + .wrap( + SessionMiddleware::builder(store.clone(), key.clone()) + .cookie_name("session".to_string()) + .build(), + ) .app_data(Data::new(AppState { db: db_pool.clone(), })) + .service(login) .service(create_user) .service(patch_user) .service(delete_user)