diff --git a/Cargo.lock b/Cargo.lock index 51114f1..d01f843 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "cookie" version = "0.17.0" @@ -349,6 +355,41 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.38", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.38", +] + [[package]] name = "data-encoding" version = "2.4.0" @@ -375,6 +416,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -402,6 +456,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -443,6 +506,8 @@ dependencies = [ "bcrypt", "futures-util", "poem", + "poem-openapi", + "serde", "sqlx", "tokio", "tracing-subscriber", @@ -804,6 +869,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -978,6 +1049,25 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "tokio", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -1183,21 +1273,26 @@ dependencies = [ "http", "hyper", "mime", + "multer", "parking_lot", "percent-encoding", "pin-project-lite", "poem-derive", "priority-queue", + "quick-xml", "rand", "regex", "rfc7239", "serde", "serde_json", "serde_urlencoded", + "serde_yaml", "smallvec", + "tempfile", "thiserror", "time", "tokio", + "tokio-stream", "tokio-tungstenite", "tokio-util", "tracing", @@ -1215,6 +1310,49 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "poem-openapi" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62659dcc7ca09a525881300646f3b28e319889072e83cd16a2865ba024a185e" +dependencies = [ + "base64", + "bytes", + "derive_more", + "futures-util", + "indexmap 2.0.2", + "mime", + "num-traits", + "poem", + "poem-openapi-derive", + "quick-xml", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "thiserror", + "tokio", +] + +[[package]] +name = "poem-openapi-derive" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90bf699e87e95b8303f9b59684cf3b9a8fff840872b13cbe0680aa4330d5226" +dependencies = [ + "darling", + "http", + "indexmap 2.0.2", + "mime", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 2.0.38", + "thiserror", +] + [[package]] name = "polyval" version = "0.6.1" @@ -1268,6 +1406,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.33" @@ -1397,6 +1545,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.20" @@ -1423,19 +1580,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.189" +name = "semver" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -1465,6 +1628,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1782,6 +1958,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -2130,6 +2312,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + [[package]] name = "url" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 74e0f4d..4b242c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,9 @@ tracing-subscriber = { version ="0.3.9", features = ["env-filter"] } sqlx = { version = "0.7.2", features = ["runtime-tokio", "sqlite"] } bcrypt = "0.15.0" anyhow = "1.0.75" +poem-openapi = { version = "3.0.5", features = ["websocket"] } +serde = "1.0.190" + +[profile.dev.package.sqlx-macros] +opt-level = 3 + diff --git a/client/index.html b/client/index.html index ac30053..8e76679 100644 --- a/client/index.html +++ b/client/index.html @@ -1,17 +1,16 @@ - - - - - Vite App - - -
-

Hello, Bootstrap and Vite!

- -
-
- - + + + + + + Vite App + + + +
+ + + diff --git a/client/src/App.vue b/client/src/App.vue index 633a5df..0c7c639 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,47 +1,35 @@ + - - diff --git a/client/src/components/Clicker.vue b/client/src/components/Clicker.vue new file mode 100644 index 0000000..7c474d1 --- /dev/null +++ b/client/src/components/Clicker.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/client/src/components/HelloWorld.vue b/client/src/components/HelloWorld.vue deleted file mode 100644 index 5fb372c..0000000 --- a/client/src/components/HelloWorld.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/client/src/components/Login.vue b/client/src/components/Login.vue new file mode 100644 index 0000000..3ce2f40 --- /dev/null +++ b/client/src/components/Login.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/client/src/components/Signup.vue b/client/src/components/Signup.vue new file mode 100644 index 0000000..64ef0b6 --- /dev/null +++ b/client/src/components/Signup.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/client/src/components/TheWelcome.vue b/client/src/components/TheWelcome.vue deleted file mode 100644 index dab9536..0000000 --- a/client/src/components/TheWelcome.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - diff --git a/client/src/components/WelcomeItem.vue b/client/src/components/WelcomeItem.vue deleted file mode 100644 index 6d7086a..0000000 --- a/client/src/components/WelcomeItem.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/src/main.rs b/src/main.rs index e6c07c5..7aab609 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,25 @@ -use anyhow::Result; -use bcrypt::{hash, verify, DEFAULT_COST}; +//use anyhow::Result; +//use bcrypt::{hash, verify, DEFAULT_COST}; use futures_util::{SinkExt, StreamExt}; use poem::{ - get, handler, + //get, + handler, //Result, listener::TcpListener, session::{CookieConfig, MemoryStorage, ServerSession, Session}, web::{ websocket::{Message, WebSocket}, Data, Html, Path, }, - EndpointExt, IntoResponse, Route, Server, + EndpointExt, + IntoResponse, + Route, + Server, }; -use sqlx::SqlitePool; +use poem_openapi::{ + payload::Json, types::Password, ApiResponse, Object, OpenApi, OpenApiService, Tags, +}; +use serde::{Deserialize, Serialize}; +use sqlx::{Pool, Sqlite}; #[handler] fn index() -> Html<&'static str> { @@ -94,8 +102,168 @@ fn ws( }) } +#[derive(Debug, Object, Clone, Eq, PartialEq, Serialize, Deserialize)] +struct User { + #[oai(validator(max_length = 64))] + username: String, +} +#[derive(ApiResponse)] +enum UserResponse { + #[oai(status = 200)] + Ok(Json), + #[oai(status = 403)] + AuthError, +} +#[derive(Debug, Object, Clone, Eq, PartialEq)] +struct NewUser { + /// Username + #[oai(validator(max_length = 64))] + username: String, + /// Password + #[oai(validator(max_length = 50))] + password: Password, + /// Invite / Referral Code + #[oai(validator(max_length = 8))] + referral: Option, +} +#[derive(ApiResponse)] +enum NewUserResponse { + #[oai(status = 200)] + Ok(Json), + #[oai(status = 403)] + UsernameTaken, + #[oai(status = 403)] + InvalidReferral, + #[oai(status = 403)] + SignupClosed, + #[oai(status = 403)] + InvalidPassword, + #[oai(status = 500)] + InternalServerError, +} +#[derive(Debug, Object, Clone, Eq, PartialEq)] +struct UserLogin { + /// Username + #[oai(validator(max_length = 64))] + username: String, + /// Password + #[oai(validator(max_length = 50))] + password: Password, +} +#[derive(ApiResponse)] +enum NumResponse { + #[oai(status = 200)] + Ok(Json), + #[oai(status = 403)] + AuthError, +} +#[derive(Tags)] +enum ApiTags { + /// Operations about user + User, +} +async fn valid_code(code: &str) -> bool { + "changeme" == code +} +struct Api { + db: Pool, + signup_open: bool, //TODO Should be in the db so it can change without restart +} +#[OpenApi] +impl Api { + #[oai(path = "/user", method = "post", tag = "ApiTags::User")] + async fn create_user(&self, user_form: Json, session: &Session) -> NewUserResponse { + let has_referral = match &user_form.referral { + Some(code) if valid_code(code).await => true, + Some(_) => return NewUserResponse::InvalidReferral, + None => false, + }; + if !self.signup_open && !has_referral { + return NewUserResponse::SignupClosed; + } + + //TODO propper handling of query errors + let username = user_form.username.clone(); + let x = sqlx::query!("SELECT * FROM users WHERE username = ?", username) + .fetch_optional(&self.db) + .await; + if let Ok(Some(_current_user)) = x { + return NewUserResponse::UsernameTaken; + } + + let hash = match bcrypt::hash(user_form.password.as_str(), bcrypt::DEFAULT_COST) { + Ok(hash) => hash, + _ => return NewUserResponse::InvalidPassword, + }; + + let res = sqlx::query!( + "INSERT INTO users(username, password, admin) VALUES(?,?,?)", + username, + hash, + false + ) + .execute(&self.db) + .await; + + match res { + Err(_e) => NewUserResponse::InternalServerError, + Ok(_) => { + let user = User { username }; + session.set("user", user.username.clone()); + NewUserResponse::Ok(Json(user)) + } + } + } + #[oai(path = "/user", method = "get", tag = "ApiTags::User")] + async fn get_user(&self, session: &Session) -> UserResponse { + if let Some(username) = session.get::("user") { + UserResponse::Ok(Json(User { username })) + } else { + UserResponse::AuthError + } + } + #[oai(path = "/auth", method = "post", tag = "ApiTags::User")] + async fn auth_user(&self, user: Json, session: &Session) -> UserResponse { + let password = user.password.as_str(); + let username = user.username.to_string(); + + let result = sqlx::query!("SELECT * FROM users WHERE username = ?", username) + .fetch_one(&self.db) + .await; + match result { + Ok(user) if bcrypt::verify(password, &user.password).unwrap_or(false) => { + let current_user = User { + username: username.clone(), + }; + session.set("user", username); + UserResponse::Ok(Json(current_user)) + } + _ => UserResponse::AuthError, + } + } + + #[oai(path = "/num", method = "get", tag = "ApiTags::User")] + //async fn get_num(&self, session: &Session) -> Json { + async fn get_num(&self, session: &Session) -> NumResponse { + //if session.get("user") + if let None = session.get::("user") { + return NumResponse::AuthError; + } + match session.get("num") { + Some(i) => { + session.set("num", i + 1); + NumResponse::Ok(Json(i)) + } + None => { + session.set("num", 1_u32); + NumResponse::Ok(Json(1)) + } + } + } +} + #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> anyhow::Result<()> { if std::env::var_os("RUST_LOG").is_none() { std::env::set_var("RUST_LOG", "poem=debug"); } @@ -103,16 +271,26 @@ async fn main() -> Result<()> { let session = ServerSession::new(CookieConfig::default(), MemoryStorage::new()); - let dbpool = SqlitePool::connect("sqlite:chat.db").await?; + //let dbpool = SqlitePool::connect("sqlite:chat.db").await?; + let dbpool = Pool::::connect("sqlite:chat.db").await?; + let api = OpenApiService::new( + Api { + db: dbpool, + signup_open: false, + }, + "Chat", + "0.0", + ); let app = Route::new() - .at("/api/", get(index)) - .at( - "/api/ws/:name", - get(ws.data(tokio::sync::broadcast::channel::(32).0)), - ) - .with(session) - .data(dbpool); + //.at("/api/", get(index)) + //.at( + // "/api/ws/:name", + // get(ws.data(tokio::sync::broadcast::channel::(32).0)), + //) + //.data(dbpool) + .nest("/api", api) + .with(session); Server::new(TcpListener::bind("127.0.0.1:3000")) .run(app)