Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
DATABASE_URL=postgresql://user:[email protected]:8001/godsaeng
DATABASE_URL=postgresql://user:[email protected]:8001/godsaeng
REDIS_URL=redis://127.0.0.1:6379
SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions scripts/init_db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down
21 changes: 21 additions & 0 deletions scripts/init_redis.sh
Original file line number Diff line number Diff line change
@@ -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!"
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ 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)
.await
.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(())
}
104 changes: 90 additions & 14 deletions src/routes/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -19,20 +20,43 @@ pub struct CreateEventBody {
pub struct PatchEventBody {
pub id: i32,
pub new_note: String,
pub new_event_date: String,
}

#[derive(Deserialize)]
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<i32> = 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<AppState>, body: Json<CreateEventBody>) -> impl Responder {
pub async fn create_event(
state: Data<AppState>,
body: Json<CreateEventBody>,
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(),
Expand All @@ -43,32 +67,84 @@ pub async fn create_event(state: Data<AppState>, body: Json<CreateEventBody>) ->
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<AppState>, body: Json<PatchEventBody>) -> 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<AppState>,
body: Json<PatchEventBody>,
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<AppState>, body: Json<DeleteEventBody>) -> impl Responder {
pub async fn delete_event(
state: Data<AppState>,
body: Json<DeleteEventBody>,
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)
Expand Down
99 changes: 79 additions & 20 deletions src/routes/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppState>, body: Json<CreateUserBody>) -> impl Responder {
// check name duplication
match check_duplication(&state, body.name.to_string()).await {
Expand Down Expand Up @@ -67,25 +69,24 @@ async fn check_duplication(state: &Data<AppState>, name: String) -> bool {
}
}

async fn user_auth(state: &Data<AppState>, 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<i32> = 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<AppState>, body: Json<PatchUserBody>) -> 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<AppState>,
body: Json<PatchUserBody>,
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 => {}
Expand All @@ -107,18 +108,76 @@ pub async fn patch_user(state: Data<AppState>, body: Json<PatchUserBody>) -> imp
}

#[delete("/user")]
pub async fn delete_user(state: Data<AppState>, body: Json<DeleteUserBody>) -> impl Responder {
let user_id: i32 =
pub async fn delete_user(
state: Data<AppState>,
body: Json<DeleteUserBody>,
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
{
Ok(_) => HttpResponse::Ok().json("Successfully deleted"),
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<AppState>, 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<AppState>,
body: Json<LoginUserBody>,
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(),
})
}
Loading