diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 5dfb2c8..e7ad7ea 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -13,6 +13,8 @@ tags: description: related to authentication - name: user description: related to user data + - name: group + description: related to group - name: schedule description: related to scheduling @@ -66,6 +68,15 @@ paths: 401: description: Not authorized + /group: + post: + summary: make group + tags: + - group + responses: + 200: + description: Success + /schedule/{groupId}: get: summary: schedule view diff --git a/server/app/Cargo.toml b/server/app/Cargo.toml index 618ac88..c40dfdc 100644 --- a/server/app/Cargo.toml +++ b/server/app/Cargo.toml @@ -15,3 +15,12 @@ once_cell = "1.19.0" uuid = { version = "1.8.0", features = ["v7"] } tower-http = {version = "0.5.2", features = ["fs"]} bcrypt = "0.15.1" +async-session = "3.0.0" +anyhow = "1.0.86" +serde = "1.0.204" + +[dependencies.async-sqlx-session] +git = "https://github.com/maxcountryman/async-sqlx-session.git" +default-features = false +branch = "sqlx-0.7" +features = ["mysql"] diff --git a/server/app/src/db.rs b/server/app/src/db.rs deleted file mode 100644 index 127735f..0000000 --- a/server/app/src/db.rs +++ /dev/null @@ -1,138 +0,0 @@ -use once_cell::sync::Lazy; - -use sqlx::{ - mysql::{MySqlConnectOptions, MySqlPoolOptions, MySqlQueryResult}, - query, query_as, FromRow, MySql, Pool, -}; -use std::env; -use std::string::String; - -use openapi::models::GroupItem; - -pub struct DataBaseConfig { - db_hostname: String, - db_database: String, - db_username: String, - db_password: String, - db_port: u16, -} - -#[derive(FromRow)] -pub struct Password(String); - -impl Password { - fn to_string(&self) -> &str { - &self.0 - } -} - -#[derive(FromRow)] -struct DbGroupItem { - group_uuid: sqlx::types::Uuid, - group_name: String, -} - -impl DbGroupItem { - fn to_group_item(&self) -> GroupItem { - GroupItem::new(self.group_uuid, self.group_name.clone()) - } -} - -impl DataBaseConfig { - fn options(&self) -> MySqlConnectOptions { - MySqlConnectOptions::new() - .host(&self.db_hostname) - .port(self.db_port) - .username(&self.db_username) - .password(&self.db_password) - .database(&self.db_database) - } -} - -static CONFIG: Lazy = Lazy::new(|| DataBaseConfig { - db_hostname: env::var("DB_HOSTNAME").unwrap(), - db_port: env::var("DB_PORT").unwrap().parse().unwrap(), - db_username: env::var("DB_USERNAME").unwrap(), - db_password: env::var("DB_PASSWORD").unwrap(), - db_database: env::var("DB_DATABASE").unwrap(), -}); - -#[tokio::main(flavor = "current_thread")] -pub async fn setup_db() -> Result, String> { - // コネクションプールの設定 - let pool = MySqlPoolOptions::new() - .max_connections(10) - .connect_with(CONFIG.options()) - .await; - - match pool { - Ok(p) => return Ok(p), - Err(e) => return Err(e.to_string()), - }; -} - -pub async fn add_user( - pool: Pool, - user_name: String, - password: String, -) -> Result { - // Insert the task, then obtain the ID of this row - let password = bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(); - println!("binary: {}, length: {}", password, password.len()); - - // let password = format!("{:x}", hashed_password); - let result = query("INSERT INTO `users` ( `userName`, `password` ) VALUES ( ?, ? )") - .bind(user_name) - .bind(password) - .execute(&pool) - .await; - - match result { - Ok(r) => Ok(r), - Err(e) => Err(e.to_string()), - } -} - -pub async fn check_user( - pool: Pool, - user_name: String, - password: String, -) -> Result { - let correct_password = - query_as::<_, Password>("SELECT `password` FROM `users` WHERE `userName`=?") - .bind(user_name) - .fetch_one(&pool) - .await; - - match correct_password { - Ok(correct) => match bcrypt::verify(password, correct.to_string()) { - Ok(r) => Ok(r), - Err(e) => Err(e.to_string()), - }, - Err(e) => Err(e.to_string()), - } -} - -pub async fn get_groups_by_user( - pool: Pool, - user_name: String, -) -> Result, String> { - // TODO: テーブル結合を使いながらUUIDを返せるようなsqlを書く - let groups = query_as::<_, DbGroupItem>( - "SELECT `groupUuid`, `groupName` FROM `groups` JOIN `userGroup`", - ) - .bind(user_name) - .fetch_all(&pool) - .await; - - match groups { - Ok(groups) => { - let mut result_groups: Vec = Vec::::new(); - for group in groups { - result_groups.push(group.to_group_item().clone()); - } - Ok(result_groups) - } - Err(aaa) => Err(aaa.to_string()), - } -} diff --git a/server/app/src/handler.rs b/server/app/src/handler.rs index 70dd8dc..afcb44c 100644 --- a/server/app/src/handler.rs +++ b/server/app/src/handler.rs @@ -1,29 +1,20 @@ use axum_extra::extract::CookieJar; use core::{future::Future, marker, pin}; -use sqlx::{mysql::MySqlQueryResult, MySql, Pool}; +use sqlx::mysql::MySqlQueryResult; -use crate::db::{self}; +use crate::repository::Repository; use axum::async_trait; use openapi::{ models::{ GroupItem, PostLogin, ScheduleGroupIdGetPathParams, ScheduleGroupIdPostPathParams, ScheduleGroupIdPutPathParams, ScheduleItem, }, - Api, LoginPostResponse, MeGetResponse, ScheduleGroupIdGetResponse, ScheduleGroupIdPostResponse, - ScheduleGroupIdPutResponse, SignUpPostResponse, + Api, GroupPostResponse, LoginPostResponse, MeGetResponse, ScheduleGroupIdGetResponse, + ScheduleGroupIdPostResponse, ScheduleGroupIdPutResponse, SignUpPostResponse, }; -#[derive(Clone)] -pub struct Count(pub Pool); - -impl AsRef for Count { - fn as_ref(&self) -> &Count { - self - } -} - #[async_trait] -impl Api for Count { +impl Api for Repository { fn sign_up_post<'life0, 'async_trait>( &'life0 self, _method: axum::http::Method, @@ -38,13 +29,12 @@ impl Api for Count { Self: 'async_trait, { let db_result: Result = tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { - db::add_user(self.0.clone(), body.user_name, body.password).await - }) + tokio::runtime::Handle::current() + .block_on(async move { self.add_user(body.user_name, body.password).await }) }); let result = match db_result { - Ok(_) => Ok(SignUpPostResponse::Status200_Success), + core::result::Result::Ok(_) => Ok(SignUpPostResponse::Status200_Success), Err(a) => { println!("{}", a); Ok(SignUpPostResponse::Status400_BadRequest) @@ -70,9 +60,8 @@ impl Api for Count { { let copied_password: String = body.password.clone(); let db_result: Result = tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { - db::check_user(self.0.clone(), body.user_name, body.password).await - }) + tokio::runtime::Handle::current() + .block_on(async move { self.check_user(body.user_name, body.password).await }) }); let result = match db_result { @@ -106,7 +95,7 @@ impl Api for Count { let db_result: Result, String> = tokio::task::block_in_place(move || { tokio::runtime::Handle::current() - .block_on(async move { db::get_groups_by_user(self.0.clone(), user_name).await }) + .block_on(async move { self.get_groups_by_user(user_name).await }) }); let result = match db_result { @@ -117,6 +106,15 @@ impl Api for Count { Box::pin(async { result }) } + async fn group_post( + &self, + _method: axum::http::Method, + _host: axum::extract::Host, + _cookies: CookieJar, + ) -> Result { + Ok(GroupPostResponse::Status200_Success) + } + fn schedule_group_id_get<'life0, 'async_trait>( &'life0 self, _method: axum::http::Method, diff --git a/server/app/src/main.rs b/server/app/src/main.rs index 21bdfe7..e27752a 100644 --- a/server/app/src/main.rs +++ b/server/app/src/main.rs @@ -1,35 +1,24 @@ -pub mod db; -pub mod handler; +mod handler; +mod repository; + +use anyhow::Ok; extern crate openapi; -// use sqlx::migrate::Migrator; -// use std::{path::Path, thread}; -use std::thread; use tower_http::services::{ServeDir, ServeFile}; -use db::setup_db; - -use crate::handler::Count; +use repository::Repository; #[tokio::main] -async fn main() { - let db_pool = thread::spawn(|| match setup_db() { - Ok(a) => { - println!("Correctly connected to DB!"); - a - } - Err(a) => panic!("{a}"), - }) - .join() - .expect("Thread panicked"); - - // let _m = Migrator::new(Path::new("./migrations")).await.unwrap(); - - let state = Count(db_pool); +async fn main() -> anyhow::Result<()> { + // DB 初期化 + let state = Repository::setup_db().await?; + // state.migrate().await?; + + // 静的ファイル配信設定 let static_dir = ServeFile::new("/dist/index.html"); let serve_dir_from_assets = ServeDir::new("/dist/assets/"); - // 最初に初期化をする + // API 初期化 let app = openapi::server::new(state) .nest_service("/assets", serve_dir_from_assets) .fallback_service(static_dir); @@ -38,5 +27,6 @@ async fn main() { let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); // start listening - axum::serve(listener, app).await.unwrap(); + axum::serve(listener, app).await?; + Ok(()) } diff --git a/server/app/src/repository.rs b/server/app/src/repository.rs new file mode 100644 index 0000000..ed0760f --- /dev/null +++ b/server/app/src/repository.rs @@ -0,0 +1,105 @@ +use anyhow::Ok; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions, MySqlQueryResult}; +use sqlx::{FromRow, MySqlPool}; + +use std::env; +use std::string::String; + +// const MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../../docs"); + +use async_sqlx_session::MySqlSessionStore; +use openapi::models::GroupItem; + +mod user_groups; +mod user_passwords; +mod user_sessions; + +#[derive(Clone)] +pub struct Repository { + pool: MySqlPool, + session_store: MySqlSessionStore, + bcrypt_cost: u32, +} + +impl Repository { + pub async fn setup_db() -> anyhow::Result { + // コネクションプールの設定 + let pool = MySqlPoolOptions::new() + .max_connections(10) + .connect_with(CONFIG.options()) + .await?; + let session_store = + MySqlSessionStore::from_client(pool.clone()).with_table_name("user_sessions"); + Ok(Self { + pool, + session_store, + bcrypt_cost: bcrypt::DEFAULT_COST, + }) + } + + // pub async fn migrate(&self) -> anyhow::Result<()> { + // MIGRATOR.run(&self.pool).await?; + // self.session_store.migrate().await?; + // Ok(()) + // } +} + +impl AsRef for Repository { + fn as_ref(&self) -> &Repository { + self + } +} + +pub struct DataBaseConfig { + db_hostname: String, + db_database: String, + db_username: String, + db_password: String, + db_port: u16, +} + +#[derive(FromRow)] +pub struct Password(String); + +impl Password { + fn to_string(&self) -> &str { + &self.0 + } +} + +#[derive(FromRow)] +struct DbGroupItem { + group_uuid: sqlx::types::Uuid, + group_name: String, +} + +impl DbGroupItem { + fn to_group_item(&self) -> GroupItem { + GroupItem::new(self.group_uuid, self.group_name.clone()) + } +} + +impl DataBaseConfig { + fn options(&self) -> MySqlConnectOptions { + MySqlConnectOptions::new() + .host(&self.db_hostname) + .port(self.db_port) + .username(&self.db_username) + .password(&self.db_password) + .database(&self.db_database) + } +} + +static CONFIG: Lazy = Lazy::new(|| DataBaseConfig { + db_hostname: env::var("DB_HOSTNAME").unwrap(), + db_port: env::var("DB_PORT").unwrap().parse().unwrap(), + db_username: env::var("DB_USERNAME").unwrap(), + db_password: env::var("DB_PASSWORD").unwrap(), + db_database: env::var("DB_DATABASE").unwrap(), +}); + +#[derive(FromRow, Deserialize, Serialize)] +pub struct UserName(String); diff --git a/server/app/src/repository/user_groups.rs b/server/app/src/repository/user_groups.rs new file mode 100644 index 0000000..4523135 --- /dev/null +++ b/server/app/src/repository/user_groups.rs @@ -0,0 +1,33 @@ +use super::Repository; +use crate::repository::{DbGroupItem, GroupItem}; +use sqlx::query_as; + +impl Repository { + pub async fn get_groups_by_user(&self, user_name: String) -> Result, String> { + // TODO: テーブル結合を使いながらUUIDを返せるようなsqlを書く + let groups = query_as::<_, DbGroupItem>( + " + SELECT `groupUuid`, `groupName` + FROM + `users` + INNER JOIN `userGroup` ON users.userId = userGroup.userId + INNER JOIN `groups` ON userGroup.groupId = groups.groupId + WHERE users.userName = ? + ", + ) + .bind(user_name) + .fetch_all(&self.pool) + .await; + + match groups { + Ok(groups) => { + let mut result_groups: Vec = Vec::::new(); + for group in groups { + result_groups.push(group.to_group_item().clone()); + } + Ok(result_groups) + } + Err(aaa) => Err(aaa.to_string()), + } + } +} diff --git a/server/app/src/repository/user_passwords.rs b/server/app/src/repository/user_passwords.rs new file mode 100644 index 0000000..385a4a1 --- /dev/null +++ b/server/app/src/repository/user_passwords.rs @@ -0,0 +1,42 @@ +use super::Repository; +use crate::repository::{MySqlQueryResult, Password}; +use sqlx::{query, query_as}; + +impl Repository { + pub async fn add_user( + &self, + user_name: String, + password: String, + ) -> Result { + // Insert the task, then obtain the ID of this row + let password = bcrypt::hash(password, self.bcrypt_cost).unwrap(); + println!("binary: {}, length: {}", password, password.len()); + + let result = query("INSERT INTO `users` ( `userName`, `password` ) VALUES ( ?, ? )") + .bind(user_name) + .bind(password) + .execute(&self.pool) + .await; + + match result { + Ok(r) => Ok(r), + Err(e) => Err(e.to_string()), + } + } + + pub async fn check_user(&self, user_name: String, password: String) -> Result { + let correct_password = + query_as::<_, Password>("SELECT `password` FROM `users` WHERE `userName`=?") + .bind(user_name) + .fetch_one(&self.pool) + .await; + + match correct_password { + Ok(correct) => match bcrypt::verify(password, correct.to_string()) { + Ok(r) => Ok(r), + Err(e) => Err(e.to_string()), + }, + Err(e) => Err(e.to_string()), + } + } +} diff --git a/server/app/src/repository/user_sessions.rs b/server/app/src/repository/user_sessions.rs new file mode 100644 index 0000000..00f700e --- /dev/null +++ b/server/app/src/repository/user_sessions.rs @@ -0,0 +1,35 @@ +use anyhow::Context; +use async_session::{Session, SessionStore}; + +use super::UserName; +use crate::Repository; + +#[allow(unused)] +impl Repository { + pub async fn create_session_for_user(&self, user: UserName) -> anyhow::Result { + let mut session = Session::new(); + session + .insert("user", user) + .with_context(|| "failed to insert user into session")?; + let res = self + .session_store + .store_session(session) + .await + .with_context(|| "failed to save session to database")? + .with_context(|| "unexpected error while converting session to cookie")?; + Ok(res) + } + + pub async fn load_session_from_cookie(&self, cookie: &str) -> anyhow::Result> { + let session = self.session_store.load_session(cookie.to_string()).await?; + Ok(session.and_then(|s| s.get("user"))) + } + + pub async fn destroy_session_for_cookie(&self, cookie: &str) -> anyhow::Result> { + let Some(session) = self.session_store.load_session(cookie.to_string()).await? else { + return Ok(None); + }; + self.session_store.destroy_session(session).await?; + Ok(Some(())) + } +}