├── .gitignore ├── .ignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── dev.py ├── public ├── authorized.html ├── css │ └── views │ │ ├── index.css │ │ └── ranking.css └── rust_teloxide_example.png ├── rustfmt.toml └── src ├── api ├── authorized.rs ├── car.rs ├── game.rs ├── handlers.rs ├── hello.rs ├── hi.rs ├── index.rs ├── mod.rs ├── private │ ├── mod.rs │ ├── password.rs │ └── profile.rs ├── ranking.rs ├── routes.rs └── user.rs ├── bin ├── car.rs ├── car_with_user.rs ├── cash.rs ├── draft.rs ├── game.rs ├── game_play.rs ├── game_ranking.rs ├── game_with_user.rs └── user.rs ├── db ├── mod.rs └── sqlite.rs ├── handlers ├── authorized_handler.rs ├── car_handler.rs ├── error_handler.rs ├── game_handler.rs ├── hello_handler.rs ├── hi_handler.rs ├── index_handler.rs ├── mod.rs ├── private │ ├── mod.rs │ ├── password_handler.rs │ └── profile_handler.rs ├── ranking_handler.rs └── user_handler.rs ├── lib.rs ├── main.rs ├── models ├── README.md ├── car.rs ├── car_with_user.rs ├── cash.rs ├── game.rs ├── game_with_user.rs ├── mod.rs ├── private │ ├── game.rs │ ├── mod.rs │ ├── password.rs │ └── profile.rs ├── ranking.rs └── user │ ├── mod.rs │ ├── new_user.rs │ ├── requests.rs │ ├── responses.rs │ ├── user.rs │ └── user_monolithic.rs ├── read.rs ├── routes ├── authorized_route.rs ├── car_route.rs ├── game_route.rs ├── hello_route.rs ├── hi_route.rs ├── index_route.rs ├── mod.rs ├── private │ ├── mod.rs │ ├── password_route.rs │ └── profile_route.rs ├── ranking_route.rs └── user_route.rs ├── security ├── argon.rs └── mod.rs ├── server.rs ├── session.rs ├── template_setup ├── mod.rs └── tera.rs ├── tests ├── cors_test.rs ├── hello_test.rs └── mod.rs ├── utils ├── game.rs ├── mod.rs └── random.rs └── views ├── hi.tera ├── index.tera └── ranking.tera /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | cookie.txt 3 | main 4 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | # This is for $cargo watch. 2 | # The rules are equal to .gitignore. 3 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 4 | 5 | cookie.txt 6 | *.db 7 | .ignore 8 | 9 | *.css 10 | *.html 11 | *.js 12 | 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gamble" 3 | version = "0.0.0" 4 | authors = ["https://www.steadylearner.com"] 5 | edition = "2018" 6 | autobins = false 7 | build = "build.rs" 8 | 9 | # `cargo run --bin name` will point to the path we define here(Should use autobins = false) 10 | [[bin]] 11 | name = "main" # Use web or whatever later. 12 | path = "src/main.rs" 13 | 14 | [[bin]] 15 | name = "user" 16 | path = "src/bin/user.rs" 17 | [[bin]] 18 | name = "cash" 19 | path = "src/bin/cash.rs" 20 | 21 | [[bin]] 22 | name = "car" 23 | path = "src/bin/car.rs" 24 | [[bin]] 25 | name = "car_with_user" 26 | path = "src/bin/car_with_user.rs" 27 | 28 | [[bin]] 29 | name = "game" 30 | path = "src/bin/game.rs" 31 | [[bin]] 32 | name = "game_play" 33 | path = "src/bin/game_play.rs" 34 | [[bin]] 35 | name = "game_with_user" 36 | path = "src/bin/game_with_user.rs" 37 | [[bin]] 38 | name = "game_ranking" 39 | path = "src/bin/game_ranking.rs" 40 | 41 | # Test whatever simple Rust code here. 42 | [[bin]] 43 | name = "draft" 44 | path = "src/bin/draft.rs" 45 | 46 | [lib] 47 | name = "gamble" 48 | path = "src/lib.rs" 49 | 50 | [dependencies] 51 | chrono = { version = "0.4.11", features = ["serde"] } 52 | 53 | # CLI and stdout 54 | console = "0.10.0" 55 | prettytable-rs = "0.8.0" 56 | 57 | # SQLite and to reuse connection etc. 58 | # I used $cargo tree -d to find the duplicate dependency problem. 59 | lazy_static = "1.4.0" 60 | rusqlite = { version = "0.22.0", features = ["chrono"] } 61 | r2d2 = "0.8.8" 62 | r2d2_sqlite = "0.15.0" 63 | 64 | #security 65 | rand = "0.7.3" 66 | rust-argon2 = "0.8.2" 67 | 68 | # Web app with Warp 69 | # https://www.steadylearner.com/blog/read/How-to-use-Rust-Warp 70 | tokio = { version = "0.2", features = ["macros"] } 71 | warp = "0.2.2" 72 | 73 | # Serde 74 | serde = { version = "1.0.101", features = ["derive"] } 75 | serde_json = "1.0.41" 76 | serde_derive = "1.0.101" 77 | 78 | # Tempalte Engine 79 | tera = "1.2.0" 80 | 81 | # Pretty_env_logger uses env_logger and env_logger uses log. 82 | # So, you just need to know how to use pretty_env_logger mostly. 83 | # Log, debug etc. 84 | log = "0.4.8" 85 | pretty_env_logger = "0.4.0" 86 | bincode = "1.2.1" 87 | futures = "0.3.4" 88 | 89 | # Error handling, https://crates.io/crates/thiserror 90 | # You can also use anyhow https://github.com/dtolnay/anyhow 91 | # Or make custom_error handler with Warp API. 92 | # thiserror = "1.0.15" 93 | # anyhow = "1.0.28" 94 | 95 | 96 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2020] [Steadylearner(www.steadylearner.com)] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Warp backend server 2 | 3 | This is a Rust Warp backend server prototype for the demo video below. I made it for a freelance client as a POF a few months before. I had a freedom to use the language for a backend server. So, I used Rust to prove myself that I can do it with Rust. If I have to do it again, I would use Python. 4 | 5 | [![React Rust demo](https://img.youtube.com/vi/I1iNhOuXESQ/0.jpg)](https://www.youtube.com/watch?v=I1iNhOuXESQ) 6 | 7 | I share it because I have to send some private works for Rust working opportunity. If I have to, I prefer it to be an open source. 8 | 9 | If writing Rust were easy and take less time, I would rewrite. You can imporve it yourselves with TODO list below. 10 | 11 | The payload will be **session.rs** file. 12 | 13 | ## How to test it 14 | 15 | You can use **$python3 dev.py** or **$cargo run --bin main** or **$RUST_LOG=debug cargo run --bin main** to test a web server. 16 | 17 | If you want to start simple, start with **hello** and **hi** apis. 18 | 19 | You can also test other CLI commands with cargo run --bin name. Refer to **Cargo.toml** for that. 20 | 21 | ## End points 22 | 23 | I let CURL commands for each files in routes/ folder to help you test the end points. But, you can start with these first. 24 | 25 | * Register a user 26 | 27 | ```console 28 | $curl -X POST localhost:8000/api/user/v1 -H "Content-Type: application/json" -d '{ "email": "random@email.com", "password": "password" }' 29 | ``` 30 | 31 | * List users 32 | 33 | ```console 34 | $curl localhost:8000/api/user/v1 35 | ``` 36 | 37 | * Login 38 | 39 | ```console 40 | curl -X POST localhost:8000/api/user/v1/login -c cookie.txt -H "Content-Type: application/json" -d '{ "email": "random@email.com", "password": "password" }' 41 | ``` 42 | 43 | * Update cash 44 | 45 | ```console 46 | $curl -X PATCH localhost:8000/api/user/v1/cash -b cookie.txt -L -H "Content-Type: application/json" -d '{ "amount": 100000 }' 47 | ``` 48 | 49 | * Buy a car 50 | 51 | ```console 52 | $curl -X POST localhost:8000/api/user/v1/car -b cookie.txt -L -H "Content-Type: application/json" -d '{ "price": 10000, "color": "red" }' 53 | ``` 54 | 55 | * List cars 56 | 57 | ```console 58 | $curl -X GET localhost:8000/api/user/v1/car -b cookie.txt -L 59 | ``` 60 | 61 | * Gamble with cash 62 | 63 | ```console 64 | $curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L -H "Content-Type: application/json" -d '{ "stake_amount": 10000, "car_id": null, "number_of_participants": 2 }' 65 | ``` 66 | 67 | * Gamble with a car 68 | 69 | ```console 70 | $curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L -H "Content-Type: application/json" -d '{ "stake_amount": 10000, "car_id": null, "number_of_participants": 2 }' 71 | ``` 72 | 73 | * Ranking 74 | 75 | ```console 76 | $curl localhost:8000/api/ranking/v1/game 77 | ``` 78 | 79 | * Delete a user 80 | 81 | ```console 82 | $curl -X GET localhost:8000/api/user/v1/logout -b cookie.txt -L 83 | ``` 84 | 85 | ## TODO 86 | 87 | This was just a prototype to clone a function of a gambling website. It is far from perfect. I will include some lists that you can improve. 88 | 89 | If you want working Rust code to reuse, refer to the [Rust Full Stack repository](https://github.com/steadylearner/Rust-Full-Stack). 90 | 91 | * Proper error handling with [thiserror](https://github.com/dtolnay/thiserror) and [anyhow](https://github.com/dtolnay/thiserror). 92 | 93 | * [Domain driven project design](https://github.com/golang-standards/project-layout) instead of [the current group by function(models/, handlers/, routes/ etc) and remove utils/ and other unecessary ones](https://www.youtube.com/watch?v=oL6JBUk6tj0). It was difficult to structure the Warp app this way. 94 | 95 | * Extract common parts to functions. 96 | 97 | * Find how to reuse SQLite connection or substitute it with [Postgresql and reuse connection with lazy_static](https://github.com/steadylearner/Rust-Full-Stack/tree/master/warp/database/2.%20with_db_pool). 98 | 99 | * Currently, error responses from Warp relevant code are not that relevant. It will be only worth doing that if you develop it with frontend part also. 100 | 101 | * User session needs the timeout relevant code. You can find better solutions or use [prebuilt ones such as Redis etc](https://github.com/steadylearner/Rust-Full-Stack/tree/master/microservices_with_docker). 102 | 103 | * [Include tests](https://github.com/steadylearner/Rust-Full-Stack/tree/master/microservices_with_docker/warp_client/src/tests/user) for every possible routes instead of CURL commands. 104 | 105 | * Remove every unwrap(); parts. 106 | 107 | * Use trustable 3rd API for random number generation and other manual implementations etc. 108 | 109 | * [Use documenation features of Rust better.](https://github.com/steadylearner/born) 110 | 111 | I did code with it a few months ago so they are only what I can think currently instead of investing so much time to read all code again. 112 | 113 | There are not many Rust web server examples. I wouldn't write this way again if the development were for myself. But, hope you can save the compile time with it at least. 114 | 115 | Frontend part is up to you. [You can implement it on your own](https://github.com/steadylearner/Rust-Full-Stack/tree/master/parcel-react) referring to the example above. 116 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Inside build.rs. Should I use the function in db/sqlite.rs here?"); 3 | } 4 | 5 | // https://github.com/steadylearner/Rust-Full-Stack/tree/master/auth/javascript/express/db/sql 6 | 7 | // id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), 8 | // password VARCHAR NOT NULL CHECK (char_length(password) >= 5) 9 | // email VARCHAR(255) UNIQUE NOT NULL 10 | // CONSTRAINT valid_email CHECK (email ~* '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'), 11 | // password VARCHAR NOT NULL CHECK (char_length(password) >= 5), 12 | -------------------------------------------------------------------------------- /dev.py: -------------------------------------------------------------------------------- 1 | # https://www.steadylearner.com/blog/read/How-to-automatically-commit-files-to-GitHub-with-Python 2 | 3 | import subprocess as cmd 4 | 5 | # trace, debug, info, warn, error 6 | # (https://github.com/seanmonstar/pretty-env-logger) 7 | 8 | # You should also refer to these. 9 | # (Use them while you are developing details.) 10 | # https://doc.rust-lang.org/std/macro.eprintln.html 11 | # https://doc.rust-lang.org/std/macro.dbg.html 12 | 13 | response = input("Cargo [c]heck, [r]un, [b]inary file made before or [l]og?\n") 14 | 15 | if response.startswith("c"): 16 | cp = cmd.run(f"cargo watch -x 'check --bin main'", check=True, shell=True) 17 | elif response.startswith("r"): 18 | cp = cmd.run(f"cargo watch -x 'run --bin main'", check=True, shell=True) 19 | elif response.startswith("b"): 20 | cp = cmd.run(f"cp target/debug/main main && ./main", check=True, shell=True) 21 | else: 22 | log_type = input("What do you want to log(debug)?\n") 23 | default = "debug" 24 | 25 | if response.startswith("i"): 26 | log_type = "info" 27 | elif response.startswith("e"): 28 | log_type = "error" 29 | else: 30 | log_type = default 31 | 32 | cp = cmd.run(f"RUST_LOG={log_type} cargo run --bin main", check=True, shell=True) 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/authorized.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You are authorized 7 | 8 | 9 | 10 |

You are authorized

11 | 12 | 13 | -------------------------------------------------------------------------------- /public/css/views/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, *::before, *::after { 6 | box-sizing: inherit; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | :root { 12 | --light-green: #00ff00; 13 | --dark-green: #003b00; 14 | --dark-grey: #777; 15 | --light-grey: #dadce0; 16 | } 17 | 18 | body { 19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | color: #333; 25 | } 26 | 27 | a:hover { 28 | text-decoration: underline; 29 | } 30 | 31 | a.button { 32 | border: 2px solid #004400; 33 | color: var(--dark-green); 34 | border-radius: 4px; 35 | padding: 6px 24px; 36 | font-size: 14px; 37 | font-weight: 400; 38 | } 39 | 40 | a.button:hover { 41 | text-decoration: none; 42 | background-color: var(--dark-green); 43 | color: var(--light-green); 44 | } 45 | 46 | header { 47 | width: 100%; 48 | height: 50px; 49 | position: fixed; 50 | top: 0; 51 | left: 0; 52 | right: 0; 53 | display: flex; 54 | justify-content: space-between; 55 | background-color: var(--light-green); 56 | padding: 5px 10px; 57 | align-items: center; 58 | } 59 | 60 | .logo { 61 | color: #002200; 62 | } 63 | 64 | form { 65 | height: calc(100% - 10px); 66 | } 67 | 68 | .search-input { 69 | width: 500px; 70 | height: 100%; 71 | border-radius: 4px; 72 | border-color: transparent; 73 | background-color: var(--dark-green); 74 | color: var(--light-green); 75 | font-size: 16px; 76 | line-height: 1.4; 77 | padding-left: 5px; 78 | } 79 | 80 | .container { 81 | width: 100%; 82 | max-width: 720px; 83 | margin: 0 auto; 84 | padding: 80px 20px 40px; 85 | } 86 | 87 | .result-count { 88 | color: var(--dark-grey); 89 | text-align: center; 90 | margin-bottom: 15px; 91 | } 92 | 93 | .search-results { 94 | list-style: none; 95 | } 96 | 97 | .news-article { 98 | display: flex; 99 | align-items: flex-start; 100 | margin-bottom: 30px; 101 | border: 1px solid var(--light-grey); 102 | padding: 15px; 103 | border-radius: 4px; 104 | justify-content: space-between; 105 | } 106 | 107 | .article-image { 108 | width: 200px; 109 | flex-grow: 0; 110 | flex-shrink: 0; 111 | margin-left: 20px; 112 | } 113 | 114 | .title { 115 | margin-bottom: 15px; 116 | } 117 | 118 | .description { 119 | color: var(--dark-grey); 120 | margin-bottom: 15px; 121 | } 122 | 123 | .metadata { 124 | display: flex; 125 | color: var(--dark-green); 126 | font-size: 14px; 127 | } 128 | 129 | .published-date::before { 130 | content: '\0000a0\002022\0000a0'; 131 | margin: 0 3px; 132 | } 133 | 134 | .pagination { 135 | margin-top: 20px; 136 | } 137 | 138 | .previous-page { 139 | margin-right: 20px; 140 | } 141 | 142 | @media screen and (max-width: 550px) { 143 | header { 144 | flex-direction: column; 145 | height: auto; 146 | padding-bottom: 10px; 147 | } 148 | 149 | .logo { 150 | display: inline-block; 151 | margin-bottom: 10px; 152 | } 153 | 154 | form, .search-input { 155 | width: 100%; 156 | } 157 | 158 | .github-button { 159 | display: none; 160 | } 161 | 162 | .title { 163 | font-size: 18px; 164 | } 165 | 166 | .description { 167 | font-size: 14px; 168 | } 169 | 170 | .article-image { 171 | display: none; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /public/css/views/ranking.css: -------------------------------------------------------------------------------- 1 | table { 2 | font-family: arial, sans-serif; 3 | border-collapse: collapse; 4 | width: 100%; 5 | } 6 | 7 | td, th { 8 | border: 1px solid #121212; 9 | text-align: left; 10 | padding: 8px; 11 | } 12 | 13 | tr:nth-child(even) { 14 | background-color: #ff7676; 15 | } 16 | -------------------------------------------------------------------------------- /public/rust_teloxide_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steadylearner/Rust-Warp-Example/5f7fb1bb04dd9cbf9ec72f761ccbfa50235d6c19/public/rust_teloxide_example.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # https://rust-lang.github.io/rustfmt/ 2 | 3 | ignore = [ 4 | "src/server.rs", 5 | "src/api/", 6 | ] 7 | -------------------------------------------------------------------------------- /src/api/authorized.rs: -------------------------------------------------------------------------------- 1 | // #[macro_export] 2 | // macro_rules! authorized { 3 | // () => { 4 | // authorized_route::authorized() 5 | // .and(user_session_filter()) 6 | // .and_then(authorized_handler::authorized) 7 | // } 8 | // }a -------------------------------------------------------------------------------- /src/api/car.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! buy_a_car { 3 | () => { 4 | car_route::buy() 5 | .and(user_session_filter()) 6 | .and_then(car_handler::buy) 7 | }; 8 | } 9 | 10 | #[macro_export] 11 | macro_rules! list_cars { 12 | () => { 13 | car_route::list() 14 | .and(user_session_filter()) 15 | .and_then(car_handler::list) 16 | }; 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! refund_a_car { 21 | () => { 22 | car_route::refund() 23 | .and(user_session_filter()) 24 | .and_then(car_handler::refund) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/api/game.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! new_game { 3 | () => { 4 | game_route::new() 5 | .and(user_session_filter()) 6 | .and_then(game_handler::new) 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/handlers.rs: -------------------------------------------------------------------------------- 1 | // These are to reuse at handlers/ 2 | use serde::{Serialize, Deserialize}; 3 | 4 | // Use other macro name later. 5 | #[macro_export] 6 | macro_rules! temporary_redirect_to_home { 7 | () => { 8 | Response::builder() 9 | .status(StatusCode::OK) 10 | .header( 11 | header::SET_COOKIE, 12 | "EXAUTH=; Max-Age=0; SameSite=Strict; HttpOpnly", 13 | ) 14 | .body(b"".to_vec()); 15 | }; 16 | } 17 | 18 | // Should find and read the documentaion here. 19 | // https://docs.rs/http/0.2.1/http/response/struct.Builder.html 20 | // https://docs.rs/warp/0.1.12/warp/reply/index.html 21 | // https://github.com/steadylearner/Rust-Full-Stack/blob/master/auth/javascript/express/__tests__/routes/login.js 22 | // $curl -X POST localhost:8000/api/user/v1/login -c cookie-file.txt -H "Content-Type: application/json" -d '{ "email": "whatever@email.com", "password": "randompassword" }' 23 | // $cat cookie-file.txt to see identity_id are the same. 24 | 25 | // Should return statusCode, statusText etc? 26 | #[macro_export] 27 | macro_rules! set_identity_id { 28 | // https://doc.rust-lang.org/1.7.0/book/macros.html#hygiene 29 | ($id:expr) => { 30 | Response::builder() 31 | .status(StatusCode::FOUND) 32 | // I don't need this because React client will handle this. 33 | // .header(header::LOCATION, "/") // Should be at /profile, email or (user)name to show the profile. 34 | .header( 35 | header::SET_COOKIE, 36 | format!("EXAUTH={}; SameSite=Strict; HttpOpnly", $id), 37 | ) 38 | .body(LoginSuccessResponse { 39 | identity_id: $id, 40 | }); 41 | // .body(b"".to_vec()); 42 | }; 43 | } 44 | 45 | // React frontend will handle this also. 46 | #[macro_export] 47 | macro_rules! redirect_to_login { 48 | () => { 49 | redirect(Uri::from_static("/login")) 50 | }; 51 | } 52 | 53 | // #[macro_export] 54 | // macro_rules! redirect_to_profile { 55 | // () => { 56 | // redirect(Uri::from_static("/profile")) 57 | // }; 58 | // } 59 | -------------------------------------------------------------------------------- /src/api/hello.rs: -------------------------------------------------------------------------------- 1 | // #[macro_export] 2 | // macro_rules! hello { 3 | // () => { 4 | // hello_route::hello() 5 | // .and_then(hello_handler::hello) 6 | // } 7 | // } -------------------------------------------------------------------------------- /src/api/hi.rs: -------------------------------------------------------------------------------- 1 | // #[macro_export] 2 | // macro_rules! hi { 3 | // () => { 4 | // hi_route::hi() 5 | // .and_then(hi_handler::hi) 6 | // } 7 | // } -------------------------------------------------------------------------------- /src/api/index.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! index { 3 | () => { 4 | index_route::get().and_then(index_handler::get) 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod routes; 2 | pub mod handlers; 3 | 4 | // pub mod hello; 5 | // pub mod hi; 6 | // pub mod authorized; 7 | 8 | pub mod index; 9 | pub mod user; 10 | 11 | pub mod private; 12 | 13 | pub mod car; 14 | pub mod game; 15 | pub mod ranking; 16 | -------------------------------------------------------------------------------- /src/api/private/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod profile; 2 | pub mod password; -------------------------------------------------------------------------------- /src/api/private/password.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! update_password { 3 | () => { 4 | password_route::update_password() 5 | .and(user_session_filter()) 6 | .and_then(password_handler::update_password) 7 | }; 8 | } -------------------------------------------------------------------------------- /src/api/private/profile.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! get_profile { 3 | () => { 4 | profile_route::get() 5 | .and(user_session_filter()) 6 | .and_then(profile_handler::get) 7 | }; 8 | } -------------------------------------------------------------------------------- /src/api/ranking.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! game_ranking_list { 3 | () => { 4 | ranking_route::game_ranking_list() 5 | .and_then(ranking_handler::game_ranking_list) 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/api/routes.rs: -------------------------------------------------------------------------------- 1 | // These are to reuse at routes/ 2 | 3 | #[macro_export] 4 | macro_rules! json_body { 5 | () => { 6 | // let json_body = content_length_limit(1024 * 16).and(json()) 7 | content_length_limit(1024 * 16).and(json()) 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/api/user.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! register_user { 3 | () => { 4 | user_route::register().and_then(user_handler::register) 5 | }; 6 | } 7 | 8 | #[macro_export] 9 | macro_rules! list_users { 10 | () => { 11 | user_route::list().and_then(user_handler::list) 12 | }; 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! do_login { 17 | () => { 18 | user_route::do_login().and_then(user_handler::do_login) 19 | }; 20 | } 21 | 22 | // #[macro_export] 23 | // macro_rules! update_password { 24 | // () => { 25 | // user_route::update_password() 26 | // .and(user_session_filter()) 27 | // .and_then(user_handler::update_password) 28 | // }; 29 | // } 30 | 31 | #[macro_export] 32 | macro_rules! update_cash { 33 | () => { 34 | user_route::update_cash() 35 | .and(user_session_filter()) 36 | .and_then(user_handler::update_cash) 37 | }; 38 | } 39 | 40 | #[macro_export] 41 | macro_rules! delete_user { 42 | () => { 43 | user_route::delete_user() 44 | .and(user_session_filter()) 45 | .and_then(user_handler::delete_user) 46 | }; 47 | } 48 | 49 | #[macro_export] 50 | macro_rules! logout { 51 | () => { 52 | user_route::logout() 53 | .and(user_session_filter()) 54 | .and_then(user_handler::logout) 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/bin/car.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{ 3 | from_stdin, 4 | models::{ 5 | car::{Car, CarList, NewCar}, 6 | user::user::User, 7 | }, 8 | }; 9 | 10 | extern crate rusqlite; 11 | use rusqlite::{Connection, Result}; 12 | 13 | // People should sell it to another user or return it to the site and get money also. 14 | // For this prototype, just suppose that there are sufficient number of cars.(Virtual Cars for the game.) 15 | // Verify first that user have enough cash. 16 | 17 | fn main() -> Result<()> { 18 | let mut conn = Connection::open("gamble.db")?; 19 | println!("Use [b]uy, [r]efund or [l]ist to manage cars."); 20 | 21 | let event = from_stdin(); 22 | match event.as_ref() { 23 | "b" => { 24 | // Use id or identity_id etc later from a web app if you want. 25 | println!("What is the email of a car buyer?"); 26 | let email = from_stdin(); 27 | 28 | let user = User::get(&conn, &email)?; 29 | let user = user.get(0); 30 | match user { 31 | Some(user) => { 32 | let User { cash, id, .. } = user; 33 | println!("What is the price of the car?"); 34 | let price = from_stdin().parse::().unwrap(); 35 | // Build a web app or 36 | // a SQL level limit to be a cash can't be less than 0. 37 | if cash >= &price { 38 | println!("What is the color of the car?"); 39 | let color = from_stdin(); 40 | 41 | let new_car = NewCar { 42 | price, 43 | color, 44 | user_id: id.to_owned(), 45 | }; 46 | new_car.create(&mut conn)?; 47 | println!("The user buyed a car with ${}.", &price); 48 | } else { 49 | println!("The user doesn't have enough money to buy a car."); 50 | } 51 | } 52 | None => { 53 | println!("The email is not registered."); 54 | } 55 | } 56 | } 57 | "r" => { 58 | println!("What is the email of a user who wants to refund a car?"); 59 | let email = from_stdin(); 60 | 61 | let user = User::get(&conn, &email)?; 62 | let user = user.get(0); 63 | match user { 64 | Some(user) => { 65 | let User { id, .. } = user; 66 | println!("What is the id of the car?"); 67 | let car_id = from_stdin().parse::().unwrap(); 68 | 69 | // Make it work only when user_if is equal to id. 70 | let car = Car::get(&conn, &car_id)?; 71 | let car = car.get(0); 72 | match car { 73 | Some(car) => { 74 | let Car { user_id, price, .. } = car; 75 | if user_id != id { 76 | println!("The user is not the author of the car.") 77 | } else { 78 | car.refund(&mut conn)?; 79 | println!("The user get ${} instead of the car.", &price); 80 | } 81 | } 82 | None => { 83 | println!("The car is not registered."); 84 | } 85 | } 86 | } 87 | None => { 88 | println!("The email is not registered."); 89 | } 90 | } 91 | } 92 | "l" => { 93 | let cars = CarList::list(&conn)?; 94 | println!("{:#?}", cars); 95 | } 96 | _ => { 97 | println!("Use [b, r, l] to mangage cars."); 98 | } 99 | } 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /src/bin/car_with_user.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{ 3 | db::sqlite::SQLITEPOOL, 4 | from_stdin, 5 | models::car_with_user::{CarWithUser, CarWithUserList}, 6 | }; 7 | 8 | extern crate rusqlite; 9 | use rusqlite::Result; 10 | 11 | fn main() -> Result<()> { 12 | let conn = SQLITEPOOL.get().unwrap(); 13 | println!("Use [r]ead, [l]ist to show the data of a car and its owner."); 14 | 15 | let event = from_stdin(); 16 | match event.as_ref() { 17 | "r" => { 18 | println!("What is the id of a car?"); 19 | let car_id = from_stdin().parse::().unwrap(); 20 | 21 | let car_with_user = CarWithUser::get(&conn, car_id)?; 22 | let car_with_user = car_with_user.get(0); 23 | match car_with_user { 24 | Some(car_with_user) => { 25 | println!("{:#?}", car_with_user); 26 | } 27 | None => { 28 | println!("There is no car with the id."); 29 | } 30 | } 31 | } 32 | "l" => { 33 | let cars_with_users = CarWithUserList::list(&conn)?; 34 | println!("{:#?}", cars_with_users); 35 | } 36 | _ => { 37 | println!("Use [r, l] to show the data of a car and its author."); 38 | } 39 | } 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/bin/cash.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{ 3 | db::sqlite::SQLITEPOOL, 4 | from_stdin, 5 | models::{cash, user::user::User}, 6 | }; 7 | 8 | extern crate rusqlite; 9 | use rusqlite::Result; 10 | 11 | // It should be imporved with error checking. 12 | fn main() -> Result<()> { 13 | let conn = SQLITEPOOL.get().unwrap(); 14 | 15 | println!("What is the id of a user?"); 16 | let email = from_stdin(); 17 | let user = User::get(&conn, &email)?; 18 | let user = user.get(0); 19 | match user { 20 | Some(user) => { 21 | let User { email, .. } = user; 22 | println!("Will you give the user cash or vice versa?(Use - when you want to deduct.)"); 23 | let amount = from_stdin(); // Is this necessary? 24 | let amount = amount.parse::().unwrap(); 25 | 26 | cash::update(&conn, &amount, &email)?; 27 | } 28 | None => { 29 | println!("The email is not registered."); 30 | } 31 | } 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /src/bin/draft.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Test simple Rust code here."); 3 | } 4 | -------------------------------------------------------------------------------- /src/bin/game.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{ 3 | db::sqlite::SQLITEPOOL, 4 | from_stdin, 5 | models::game::{ 6 | Game, 7 | GameList, // To show records and use rankings later? 8 | }, 9 | }; 10 | 11 | extern crate rusqlite; 12 | use rusqlite::Result; 13 | 14 | // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html 15 | extern crate rand; 16 | 17 | fn main() -> Result<()> { 18 | let conn = SQLITEPOOL.get().unwrap(); 19 | 20 | println!("Use [r, l] to see game records."); 21 | let event = from_stdin(); 22 | match event.as_ref() { 23 | "r" => { 24 | println!("What is the id of a game?"); 25 | let id = from_stdin(); 26 | 27 | let game_list = Game::get(&conn, id)?; 28 | println!("{:#?}", game_list); 29 | } 30 | "l" => { 31 | let game_list = GameList::list(&conn)?; 32 | println!("{:#?}", game_list); 33 | } 34 | _ => { 35 | println!("Use [r, l] to see game records."); 36 | } 37 | } 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /src/bin/game_play.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{ 3 | db::sqlite::SQLITEPOOL, 4 | from_stdin, 5 | models::{car::Car, cash, game::NewGame, user::user::User}, 6 | }; 7 | 8 | extern crate rusqlite; 9 | use rusqlite::Result; 10 | 11 | // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html 12 | extern crate rand; 13 | use rand::Rng; 14 | 15 | fn main() -> Result<()> { 16 | let conn = SQLITEPOOL.get().unwrap(); 17 | 18 | // This is currently solo playing game simialr to the https://www.mafiaway.nl 19 | println!("What is your email?"); 20 | let email = from_stdin(); 21 | let user = User::get(&conn, &email)?; 22 | let user = user.get(0); 23 | 24 | // Should have some minimum value for a production project. 25 | // let minimum_bet = 1000f64; 26 | 27 | match user { 28 | Some(user) => { 29 | let User { 30 | cash, email, id, .. 31 | } = user; 32 | 33 | println!("How many participants?"); 34 | let number_of_participants = from_stdin().parse::().unwrap(); 35 | 36 | let odd = 1f64 / number_of_participants; 37 | let odd_to_percent = format!("{}%", odd * 100f64); 38 | println!("Your odd is {}.", odd_to_percent); 39 | 40 | if number_of_participants >= 2f64 { 41 | println!("Will you bet with [m]oney or a [c]ar?"); 42 | let pay_option = from_stdin(); // m for cash, c for car.] 43 | if pay_option == "c" { 44 | println!("What is the id of a car?"); 45 | let car_id = from_stdin().parse::().unwrap(); 46 | 47 | let car = Car::get(&conn, &car_id)?; 48 | let car = car.get(0); 49 | match car { 50 | Some(car) => { 51 | let Car { 52 | user_id, 53 | price: stake_amount, 54 | .. 55 | } = car; 56 | if user_id != id { 57 | println!("The user is not the owner of the car."); 58 | } else { 59 | println!( 60 | "The price of the car({}) will be used for the game.", 61 | &stake_amount 62 | ); 63 | 64 | let mut rng = rand::thread_rng(); 65 | let from_zero_to_one = rng.gen::(); // [0, 1) 66 | 67 | let condition: bool = from_zero_to_one >= odd; 68 | 69 | let win = if condition { 70 | println!("You lost the gamble."); 71 | false 72 | } else { 73 | println!("You won the gamble."); 74 | true 75 | }; 76 | 77 | let profit = if !win { 78 | let loss = stake_amount * -1.0f64; 79 | loss 80 | } else { 81 | let rest = number_of_participants - 1f64; 82 | let earning = stake_amount * rest; 83 | earning 84 | }; 85 | 86 | cash::update(&conn, &profit, &email)?; 87 | 88 | let new_game = NewGame { 89 | stake_amount: stake_amount.to_owned(), 90 | number_of_participants: number_of_participants as i64, 91 | win, 92 | user_id: id.to_owned(), 93 | }; 94 | new_game.create(&conn)?; 95 | 96 | println!("Save the new game result.",); 97 | } 98 | } 99 | None => { 100 | println!("There is no car with the id"); 101 | } 102 | } 103 | } else { 104 | println!("How much is your stake amount?"); 105 | let stake_amount = from_stdin().parse::().unwrap(); // Could be bet 106 | 107 | if stake_amount <= 0f64 { 108 | println!("You should bet more than 0 cash."); 109 | } else { 110 | if cash < &stake_amount { 111 | println!( 112 | "You need enough cash to play this game.($cargo run --bin cash)" 113 | ) 114 | } else { 115 | // Extract it to function. 116 | let mut rng = rand::thread_rng(); 117 | let from_zero_to_one = rng.gen::(); // [0, 1) 118 | 119 | // Include = because ) from_zero_to_one 120 | let condition: bool = from_zero_to_one >= odd; 121 | 122 | let win = if condition { 123 | println!("You lost the gamble."); 124 | false 125 | } else { 126 | println!("You won the gamble."); 127 | true 128 | }; 129 | 130 | let profit = if !win { 131 | let loss = stake_amount * -1.0f64; 132 | loss 133 | } else { 134 | let rest = number_of_participants - 1f64; 135 | let earning = stake_amount * rest; 136 | earning 137 | }; 138 | 139 | cash::update(&conn, &profit, &email)?; 140 | 141 | let new_game = NewGame { 142 | stake_amount, 143 | number_of_participants: number_of_participants as i64, 144 | win, 145 | user_id: id.to_owned(), 146 | }; 147 | new_game.create(&conn)?; 148 | 149 | println!("Save the new game record.",); 150 | } 151 | } 152 | }; 153 | } else { 154 | println!("You need at least two players to play this."); 155 | } 156 | } 157 | None => { 158 | println!("The email is not registered."); 159 | } 160 | } 161 | 162 | Ok(()) 163 | } 164 | -------------------------------------------------------------------------------- /src/bin/game_ranking.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{ 3 | db::sqlite::SQLITEPOOL, 4 | models::ranking::{GameRanking, GameRankingList}, 5 | }; 6 | 7 | extern crate rusqlite; 8 | use rusqlite::Result; 9 | 10 | #[macro_use] 11 | extern crate prettytable; 12 | use prettytable::Table; 13 | 14 | fn main() -> Result<()> { 15 | let conn = SQLITEPOOL.get().unwrap(); 16 | 17 | let mut table = Table::new(); 18 | // let game_ranking_table_headers = format!(" Rank | Email | Total Prize"); 19 | table.add_row(row![FY => "Rank", "Email", "Total Prize"]); 20 | 21 | let game_ranking_list = GameRankingList::rank(&conn)?; 22 | for (index, game_ranking) in game_ranking_list.into_iter().enumerate() { 23 | let rank = index + 1; 24 | let GameRanking { email, total_prize } = game_ranking; 25 | // let game_ranking_table_row = format!("{}, | {}, | {}", &rank, &email, &total_prize); 26 | // table.add_row(row![Fy->game_ranking_table_row]); 27 | // table.add_row(row![&rank, &email, &total_prize]); 28 | table.add_row(row![Fw => &rank, &email, &total_prize]); 29 | } 30 | 31 | table.printstd(); 32 | 33 | Ok(()) 34 | } 35 | 36 | // println!("{}. {}({})", rank, email, total_prize); 37 | 38 | // | Rank | email | Total Prize | 39 | // | 1. | steady@learner.com | 100000 | 40 | // | 2. | example@email.com | 10000 | 41 | -------------------------------------------------------------------------------- /src/bin/game_with_user.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{db::sqlite::SQLITEPOOL, models::game_with_user::GameWithUserList}; 3 | 4 | extern crate rusqlite; 5 | use rusqlite::Result; 6 | 7 | fn main() -> Result<()> { 8 | let conn = SQLITEPOOL.get().unwrap(); 9 | 10 | let games_with_users = GameWithUserList::list(&conn)?; 11 | println!("{:#?}", games_with_users); 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /src/bin/user.rs: -------------------------------------------------------------------------------- 1 | extern crate gamble; 2 | use gamble::{ 3 | db::sqlite::SQLITEPOOL, 4 | from_stdin, 5 | models::user::{ 6 | new_user::{NewUser}, 7 | user::{User, UserList} 8 | }, 9 | }; 10 | 11 | extern crate rusqlite; 12 | use rusqlite::Result; 13 | 14 | fn main() -> Result<()> { 15 | println!("Use [c, r, (u), d, l, p] to manage users."); 16 | // let conn = Connection::open("gamble.db")?; 17 | let conn = SQLITEPOOL.get().unwrap(); 18 | 19 | let event = from_stdin(); 20 | match event.as_ref() { 21 | "c" => { 22 | println!("What is the email for the account?"); 23 | let email = from_stdin(); 24 | println!("What is the password for it?"); 25 | let password = from_stdin(); 26 | 27 | let new_user = NewUser { 28 | email, 29 | password, 30 | ..Default::default() 31 | }; 32 | 33 | new_user.create(&conn)?; 34 | } 35 | "r" => { 36 | println!("Which email you want to read?"); 37 | let email = from_stdin(); 38 | let user = User::get(&conn, &email)?; 39 | println!("{:#?}", user); 40 | } 41 | // "u" => { 42 | // println!("Which email you want to update its password?"); 43 | // let email = from_stdin(); 44 | // println!("What is the new password?"); 45 | // let password = from_stdin(); 46 | // println!("The password of {} will be updated.", email); 47 | // User::update(&conn, email, password)?; 48 | // } 49 | "d" => { 50 | println!("Which email you want to delete?"); 51 | let email = from_stdin(); 52 | println!("{} will be deleted.", &email); 53 | User::delete(&conn, &email)?; 54 | } 55 | "l" => { 56 | let users = UserList::list(&conn)?; 57 | println!("{:#?}", users); 58 | } 59 | "p" => { 60 | println!("Which email you want to update its password?"); 61 | let email = from_stdin(); 62 | println!("What is the new password?"); 63 | let password = from_stdin(); 64 | println!("The password of {} will be updated.", &email); 65 | User::update_password(&conn, &email, &password)?; 66 | } 67 | _ => { 68 | println!("Use [c, r, u, d, l] to manage users."); 69 | } 70 | } 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sqlite; 2 | -------------------------------------------------------------------------------- /src/db/sqlite.rs: -------------------------------------------------------------------------------- 1 | // Should it be here and used at main.rs or used at build.rs? 2 | 3 | // https://docs.rs/r2d2_sqlite/0.3.0/r2d2_sqlite/ 4 | // https://www.reddit.com/r/rust/comments/6z7gs2/using_rusqlite_from_multiple_threads/ 5 | 6 | use r2d2; 7 | use r2d2_sqlite::SqliteConnectionManager; 8 | 9 | use rusqlite::Result; 10 | use rusqlite::NO_PARAMS; 11 | 12 | // https://github.com/steadylearner/Rust-Full-Stack/tree/master/auth/javascript/express/db/sql 13 | 14 | // https://docs.rs/r2d2-sqlite3/0.1.1/r2d2_sqlite3/ 15 | pub type SqlitePool = r2d2::Pool; 16 | 17 | lazy_static! { 18 | pub static ref SQLITEPOOL: SqlitePool = { 19 | let sqlite_database = "gamble.db"; 20 | let manager = SqliteConnectionManager::file(&sqlite_database); 21 | let pool = r2d2::Pool::builder().build(manager).unwrap(); 22 | 23 | pool 24 | }; 25 | } 26 | 27 | // https://doc.rust-lang.org/stable/rust-by-example/custom_types/constants.html 28 | const USER: &str = "CREATE TABLE IF NOT EXISTS users ( 29 | id INTEGER PRIMARY KEY AUTOINCREMENT, 30 | email TEXT NOT NULL UNIQUE, 31 | password TEXT NOT NULL, 32 | cash FLOAT CHECK (cash >= 0.0), 33 | created_at DATE DEFAULT (datetime('now','localtime')), 34 | updated_at DATE DEFAULT (datetime('now','localtime')), 35 | identity_id TEXT NOT NULL UNIQUE 36 | )"; 37 | 38 | const CAR: &str = "CREATE TABLE IF NOT EXISTS cars ( 39 | id INTEGER PRIMARY KEY AUTOINCREMENT, 40 | price FLOAT CHECK (price >= 0.0), 41 | color TEXT NOT NULL, 42 | user_id INTEGER NOT NULL, 43 | FOREIGN KEY (user_id) REFERENCES users (id) 44 | )"; 45 | 46 | const GAME: &str = "CREATE TABLE IF NOT EXISTS games ( 47 | id INTEGER PRIMARY KEY AUTOINCREMENT, 48 | stake_amount FLOAT CHECK (stake_amount > 0.0), 49 | number_of_participants INTEGER NOT NULL CHECK (number_of_participants >= 2), 50 | win INTEGER, 51 | created_at DATE DEFAULT (datetime('now','localtime')), 52 | user_id INTEGER NOT NULL, 53 | FOREIGN KEY (user_id) REFERENCES users (id) 54 | )"; 55 | 56 | pub fn setup() -> Result<()> { 57 | match SQLITEPOOL.get() { 58 | Ok(conn) => { 59 | // Give limit to cash to be >= 0.0 60 | // https://www.sqlitetutorial.net/sqlite-check-constraint/ 61 | conn.execute(USER, NO_PARAMS)?; 62 | 63 | // I can also use products instead of cars with more datas. 64 | // https://www.sqlitetutorial.net/sqlite-foreign-key/ 65 | conn.execute(CAR, NO_PARAMS)?; 66 | 67 | // SQLite don't have boolean type. 68 | conn.execute(GAME, NO_PARAMS)?; 69 | }, 70 | Err(e) => { 71 | eprintln!("{:#?}", e); 72 | } 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | // Can I resue conn? Test it later or there are restrctions from SQLite. 79 | 80 | // for i in 0..10i32 { 81 | // let pool = pool.clone(); 82 | // thread::spawn(move || { 83 | // let conn = pool.get().unwrap(); 84 | // let mut stmt = conn.prepare("INSERT INTO foo (bar) VALUES (?)").unwrap(); 85 | // stmt.bind(1, 42).unwrap(); 86 | // }); 87 | // } -------------------------------------------------------------------------------- /src/handlers/authorized_handler.rs: -------------------------------------------------------------------------------- 1 | // use warp::{ 2 | // Reply, 3 | // // reject::{custom, Reject}, 4 | // Rejection, 5 | // http::{Uri}, 6 | // }; 7 | 8 | // use crate::{ 9 | // session::UserSession, 10 | // }; 11 | 12 | // pub async fn authorized(user_session: Option) -> Result { 13 | // if let Some(_user_session) = user_session { 14 | // Ok(warp::redirect(Uri::from_static("/authorized.html"))) 15 | // } else { 16 | // // Should be not allowed here 17 | // Err(warp::reject::not_found()) 18 | // } 19 | // } 20 | -------------------------------------------------------------------------------- /src/handlers/car_handler.rs: -------------------------------------------------------------------------------- 1 | use warp::{ 2 | Reply, reply, 3 | reject::{ 4 | custom, 5 | // Reject 6 | }, 7 | // redirect, 8 | Rejection, 9 | // http::{Uri}, 10 | }; 11 | // use warp::http::{header, Response, StatusCode}; 12 | 13 | use crate::{ 14 | models::{ 15 | // user::{ 16 | // NewUser, 17 | // NewUserRequest, 18 | // LoginRequest, 19 | // UpdateUserRequest, 20 | // UpdateCashRequest, 21 | // User, 22 | // UserList, 23 | // }, 24 | car::{ 25 | NewCar, 26 | NewCarRequest, 27 | Car, 28 | CarRefundRequest, 29 | // CarPublic, 30 | CarPublicList, 31 | }, 32 | // cash, 33 | }, 34 | db::sqlite::SQLITEPOOL, 35 | session::UserSession, 36 | // redirect_to_login, 37 | }; 38 | 39 | use super::{ 40 | UNAUTHORIZED, 41 | INTERNAL_SERVER_ERROR, 42 | NOT_ACCEPTABLE, 43 | }; 44 | 45 | use log::{debug}; 46 | 47 | pub async fn list( 48 | user_session: Option, 49 | ) -> Result { 50 | let response = match SQLITEPOOL.get() { 51 | Ok(conn) => { 52 | if let Some(user_session) = user_session { 53 | let UserSession { user_id, .. } = user_session; 54 | 55 | match CarPublicList::list(&conn, &user_id) { 56 | Ok(cars) => { 57 | Ok(reply::json(&cars)) 58 | }, 59 | Err(e) => { 60 | error!("{:#?}", e); 61 | Err(custom(INTERNAL_SERVER_ERROR)) 62 | } 63 | } 64 | } else { 65 | debug!("Fail to buy a car without authorization. Should redirect a user to /login."); 66 | // currently shows expected opaque type, found a different opaque type error 67 | // Ok(redirect_to_login!()) // Should rebuild it with Warp API? 68 | Err(custom(UNAUTHORIZED)) 69 | } 70 | }, 71 | Err(e) => { 72 | error!("{:#?}", e); 73 | Err(custom(INTERNAL_SERVER_ERROR)) 74 | } 75 | }; 76 | response 77 | } 78 | 79 | pub async fn buy( 80 | new_car_request: NewCarRequest, 81 | user_session: Option, 82 | ) -> Result { 83 | let response = match SQLITEPOOL.get() { 84 | Ok(mut conn) => { 85 | if let Some(user_session) = user_session { 86 | let UserSession { cash, user_id, .. } = user_session; 87 | let NewCarRequest { price, color } = new_car_request; 88 | 89 | if &cash >= &price { 90 | let new_car = NewCar { 91 | price, 92 | color, 93 | user_id, 94 | }; 95 | if let Err(e) = new_car.create(&mut conn) { 96 | error!("{:#?}", e); 97 | Err(custom(INTERNAL_SERVER_ERROR)) 98 | } else { 99 | // Should handle it correctly. 100 | debug!("The user bought a car.\n"); 101 | Ok(reply::html("Redirect the user where he can see a new car.\n")) 102 | } 103 | } else { 104 | // Should handle it correctly. 105 | debug!("The user need more money to buy a car.\n"); 106 | Ok(reply::html("Redirect the user to deposit more money.\n")) 107 | } 108 | } else { 109 | debug!("Fail to buy a car without authorization. Should redirect a user to /login."); 110 | // currently shows expected opaque type, found a different opaque type error 111 | // Ok(redirect_to_login!()) // Should rebuild it with Warp API? 112 | Err(custom(UNAUTHORIZED)) 113 | } 114 | }, 115 | Err(e) => { 116 | error!("{:#?}", e); 117 | Err(custom(INTERNAL_SERVER_ERROR)) 118 | } 119 | }; 120 | response 121 | } 122 | 123 | pub async fn refund( 124 | car_refund_request: CarRefundRequest, 125 | user_session: Option, 126 | ) -> Result { 127 | let response = match SQLITEPOOL.get() { 128 | Ok(mut conn) => { 129 | if let Some(user_session) = user_session { 130 | let UserSession { user_id, .. } = user_session; 131 | let CarRefundRequest { car_id, } = car_refund_request; 132 | 133 | let car = Car::get(&conn, &car_id).unwrap(); 134 | let car = car.get(0); 135 | 136 | if let Some(car) = car { 137 | let Car { user_id: author_id, .. } = car; 138 | if &user_id != author_id { 139 | Err(custom(UNAUTHORIZED)) 140 | } else { 141 | if let Err(e) = car.refund(&mut conn) { 142 | eprintln!("{:#?}", e); 143 | Err(custom(INTERNAL_SERVER_ERROR)) 144 | } else { 145 | // Should handle it correctly. 146 | debug!("The user refunded the car."); 147 | Ok(reply::html("Redirect the user to see the money from the acar.\n")) 148 | } 149 | // Ok(reply::html("The user refunded the car.".into())) // It has a type relevant problem currently. 150 | } 151 | } else { 152 | // Should handle it correctly. 153 | debug!("The car is not registered."); 154 | Err(custom(NOT_ACCEPTABLE)) 155 | } 156 | } else { 157 | debug!("Fail to refund a car without authorization. Should redirect a user to /login."); 158 | // currently shows expected opaque type, found a different opaque type error 159 | // Ok(redirect_to_login!()) 160 | Err(custom(UNAUTHORIZED)) 161 | } 162 | }, 163 | Err(e) => { 164 | error!("{:#?}", e); 165 | Err(custom(INTERNAL_SERVER_ERROR)) 166 | } 167 | }; 168 | response 169 | } 170 | -------------------------------------------------------------------------------- /src/handlers/error_handler.rs: -------------------------------------------------------------------------------- 1 | // I should mix 1.(For a single page app?) and 2. 2 | 3 | // Refer to these. 4 | // https://github.com/seanmonstar/warp/blob/master/examples/rejections.rs 5 | // Err(reject::custom(DivideByZero)) 6 | 7 | use std::convert::Infallible; 8 | 9 | use warp::{ 10 | http::StatusCode, 11 | Rejection, 12 | Reply, 13 | reject, 14 | }; 15 | 16 | use serde_derive::Serialize; 17 | 18 | use super::{ 19 | UNAUTHORIZED, 20 | INTERNAL_SERVER_ERROR, 21 | NOT_ACCEPTABLE, 22 | BAD_REQUEST 23 | }; 24 | 25 | /// An API error serializable to JSON. 26 | // 1. JSON with error message and code example. 27 | 28 | #[derive(Serialize)] 29 | struct ErrorMessage { 30 | code: u16, 31 | message: String, 32 | } 33 | 34 | // https://github.com/seanmonstar/warp/blob/master/examples/rejections.rs 35 | // https://docs.rs/warp/0.2.2/warp/reject/index.html 36 | pub async fn handle_rejection(err: Rejection) -> Result { 37 | let (code, message) = if err.is_not_found() { 38 | ( 39 | StatusCode::NOT_FOUND, 40 | "NOT_FOUND" 41 | ) // return template here? 42 | } else if let Some(_) = err.find::() { 43 | ( 44 | StatusCode::METHOD_NOT_ALLOWED, 45 | "METHOD_NOT_ALLOWED" 46 | ) 47 | } else if let Some(_) = err.find::() { 48 | ( 49 | StatusCode::UNAUTHORIZED, 50 | "UNAUTHORIZED" 51 | ) 52 | } else if let Some(_) = err.find::() { 53 | ( 54 | StatusCode::NOT_ACCEPTABLE, 55 | "NOT_ACCEPTABLE" 56 | ) 57 | } else if let Some(_) = err.find::() { 58 | ( 59 | StatusCode::BAD_REQUEST, 60 | "BAD_REQUEST" 61 | ) 62 | } else if let Some(_) = err.find::() { 63 | // Is this necesary here? 64 | ( 65 | StatusCode::INTERNAL_SERVER_ERROR, 66 | "INTERNAL_SERVER_ERROR" 67 | ) 68 | } else { 69 | eprintln!("unhandled rejection: {:?}", err); 70 | ( 71 | StatusCode::INTERNAL_SERVER_ERROR, 72 | "UNHANDLED_REJECTION" 73 | ) 74 | }; 75 | 76 | let json = warp::reply::json(&ErrorMessage { 77 | code: code.as_u16(), 78 | message: message.into(), 79 | }); 80 | 81 | Ok(warp::reply::with_status(json, code)) 82 | } 83 | 84 | // 2. With templates. 85 | 86 | // Create custom error pages. 87 | // fn customize_error(err: Rejection) -> Result { 88 | // match err.status() { 89 | // StatusCode::NOT_FOUND => { 90 | // eprintln!("Got a 404: {:?}", err); 91 | // // We have a custom 404 page! 92 | // Response::builder().status(StatusCode::NOT_FOUND).html(|o| { 93 | // templates::error( 94 | // o, 95 | // StatusCode::NOT_FOUND, 96 | // "The resource you requested could not be located.", 97 | // ) 98 | // }) 99 | // } 100 | // code => { 101 | // eprintln!("Got a {}: {:?}", code.as_u16(), err); 102 | // Response::builder() 103 | // .status(code) 104 | // .html(|o| templates::error(o, code, "Something went wrong.")) 105 | // } 106 | // } 107 | // } 108 | 109 | // let code; 110 | // let message; 111 | 112 | // if err.is_not_found() { 113 | // code = StatusCode::NOT_FOUND; 114 | // message = "NOT_FOUND"; // Put template here? 115 | // } else if let Some(_) = err.find::() { 116 | // // We can handle a specific error, here METHOD_NOT_ALLOWED, 117 | // // and render it however we want 118 | // code = StatusCode::METHOD_NOT_ALLOWED; 119 | // message = "METHOD_NOT_ALLOWED"; // How to make this error?s 120 | // } else if let Some(_) = err.find::() { 121 | // code = StatusCode::UNAUTHORIZED; 122 | // message = "UNAUTHORIZED"; 123 | // } else if let Some(_) = err.find::() { 124 | // code = StatusCode::INTERNAL_SERVER_ERROR; 125 | // message = "INTERNAL_SERVER_ERROR"; 126 | // } else if let Some(_) = err.find::() { 127 | // code = StatusCode::NOT_ACCEPTABLE; 128 | // message = "NOT_ACCEPTABLE"; 129 | // } else if let Some(_) = err.find::() { 130 | // code = StatusCode::BAD_REQUEST; 131 | // message = "BAD_REQUEST"; 132 | // } else { 133 | // // We should have expected this... Just log and say its a 500 134 | // eprintln!("unhandled rejection: {:?}", err); 135 | // code = StatusCode::INTERNAL_SERVER_ERROR; 136 | // message = "UNHANDLED_REJECTION"; 137 | // } -------------------------------------------------------------------------------- /src/handlers/game_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | models::{ 3 | car::Car, 4 | cash, 5 | game::{ 6 | NewGame, 7 | NewGameRequest, 8 | find_game_result_and_profit, 9 | }, 10 | private::{ 11 | game::NewGameReply, 12 | } 13 | }, 14 | session::UserSession, 15 | }; 16 | 17 | use warp::{ 18 | reply, 19 | Rejection, 20 | Reply, 21 | reject::{ 22 | // https://docs.rs/warp/0.1.6/warp/reject/index.html 23 | custom, 24 | // not_found 25 | }, 26 | // redirect, 27 | // http::{Uri}, 28 | }; 29 | 30 | use crate::{ 31 | db::sqlite::SQLITEPOOL, 32 | // redirect_to_login 33 | }; 34 | 35 | use log::{debug, error, warn}; 36 | 37 | use super::{ 38 | UNAUTHORIZED, 39 | INTERNAL_SERVER_ERROR, 40 | BAD_REQUEST, 41 | // NOT_ACCEPTABLE, 42 | }; 43 | 44 | // Could make it compile following the request of a freelance client. 45 | // But, you should handle errors better. Extract them with fuctnions etc. 46 | pub async fn new( 47 | new_game_request: NewGameRequest, 48 | user_session: Option, 49 | ) -> Result { 50 | if !new_game_request.is_logically_valid() { 51 | debug!("Fail to play the game because it is logically invalid."); 52 | Err(custom(BAD_REQUEST)) 53 | } else { 54 | if let Some(user_session) = user_session { 55 | match SQLITEPOOL.get() { 56 | Ok(conn) => { 57 | let UserSession { user_id, email, .. } = user_session; 58 | 59 | let NewGameRequest { 60 | stake_amount, 61 | car_id, 62 | number_of_participants, 63 | } = new_game_request; 64 | 65 | if let Some(stake_amount) = stake_amount { 66 | debug!("Finally play the game with the cash."); 67 | 68 | let (win, profit) = find_game_result_and_profit(number_of_participants, &stake_amount); 69 | let new_game_reply = NewGameReply { 70 | win, 71 | profit, 72 | }; 73 | 74 | let new_game = NewGame { 75 | stake_amount, 76 | number_of_participants: number_of_participants as i64, 77 | win, 78 | user_id: user_id.to_owned(), 79 | }; 80 | 81 | if let Err(e) = new_game.create(&conn) { 82 | error!("{:#?}", e); 83 | Err(custom(INTERNAL_SERVER_ERROR)) 84 | } else { 85 | if let Err(e) = cash::update(&conn, &profit, &email) { 86 | error!("{:#?}", e); 87 | // Should I destroy or revert game creation here? 88 | // Handle it correctly later. 89 | Err(custom(INTERNAL_SERVER_ERROR)) 90 | } else { 91 | debug!("Save the new car game record played with cash."); 92 | // Ok(reply()) 93 | Ok(reply::json(&new_game_reply)) 94 | } 95 | } 96 | } else { 97 | if let Some(car_id) = car_id { 98 | match Car::get(&conn, &car_id) { 99 | Ok(car) => { 100 | let car = car.get(0); 101 | if let Some(car) = car { 102 | let Car { 103 | user_id: author_id, 104 | price: stake_amount, 105 | .. 106 | } = car; 107 | 108 | if &user_id != author_id { 109 | debug!("The user is not the owner of the car."); 110 | Err(custom(UNAUTHORIZED)) 111 | } else { 112 | debug!("Finally play the game with the car."); 113 | 114 | let (win, profit) = find_game_result_and_profit(number_of_participants, &stake_amount); 115 | let new_game_reply = NewGameReply { 116 | win, 117 | profit, 118 | }; 119 | 120 | cash::update(&conn, &profit, &email).unwrap(); 121 | 122 | let new_game = NewGame { 123 | stake_amount: stake_amount.to_owned(), 124 | number_of_participants: number_of_participants as i64, 125 | win, 126 | user_id: user_id.to_owned(), 127 | }; 128 | 129 | if let Err(e) = new_game.create(&conn) { 130 | error!("{:#?}", e); 131 | Err(custom(INTERNAL_SERVER_ERROR)) 132 | } else { 133 | if let Err(e) = cash::update(&conn, &profit, &email) { 134 | error!("{:#?}", e); 135 | // Should I destroy or revert game creation here? 136 | // Handle it correctly later. 137 | Err(custom(INTERNAL_SERVER_ERROR)) 138 | } else { 139 | debug!("Save the new car game record played with a car."); 140 | if !win { 141 | if let Err(e) = Car::delete(&conn, &car_id) { 142 | error!("{:#?}", e); 143 | // Should I destroy or revert game creation and cash here? 144 | // Handle it correctly later. 145 | Err(custom(INTERNAL_SERVER_ERROR)) 146 | } else { 147 | debug!("The user couldn't win the game. So, the car was detroyed."); 148 | Ok(reply::json(&new_game_reply)) 149 | } 150 | } else { 151 | debug!("The user win the game. He will get the prize and car won't be deleted."); 152 | Ok(reply::json(&new_game_reply)) 153 | } 154 | } 155 | } 156 | } 157 | } else { 158 | // Should handle it correctly 159 | warn!("The car is not registered. But, the user could send the request?"); 160 | // Ok(reply::html("Should send the user to buy a car?")) 161 | Err(custom(BAD_REQUEST)) 162 | } 163 | }, 164 | Err(e) => { 165 | error!("{:#?}", e); 166 | Err(custom(INTERNAL_SERVER_ERROR)) 167 | } 168 | } 169 | } else { 170 | debug!("The user should have offered either cash or a car to play the game."); 171 | Err(custom(BAD_REQUEST)) 172 | } 173 | } 174 | }, 175 | Err(e) => { 176 | error!("{:#?}", e); 177 | Err(custom(INTERNAL_SERVER_ERROR)) 178 | } 179 | } 180 | } else { 181 | debug!("Fail to play the game without authorization. Should redirect a user to /login."); 182 | // Ok(redirect_to_login!()) // Should handle type not match error. 183 | Err(custom(UNAUTHORIZED)) 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/handlers/hello_handler.rs: -------------------------------------------------------------------------------- 1 | // use warp::{Reply, Rejection}; 2 | 3 | // // type Reply = impl warp::Reply; `impl Trait` in type aliases is unstable 4 | // // type Rejection = warp::Rejection; 5 | 6 | // // pub async fn hello(name: String) -> Result { 7 | // pub async fn hello(name: String) -> Result { 8 | // let reply = format!("Hello, {}!\n", name); 9 | // print!("{}", &reply); 10 | // Ok(warp::reply::html(reply)) 11 | // } -------------------------------------------------------------------------------- /src/handlers/hi_handler.rs: -------------------------------------------------------------------------------- 1 | // use warp::{reply, Reply, Rejection}; 2 | // use tera::{Context}; 3 | 4 | // use crate::{ 5 | // template_setup::tera::{render}, 6 | // }; 7 | 8 | // pub async fn hi(name: String) -> Result, Rejection> { 9 | // let mut ctx = Context::new(); 10 | // ctx.insert("name", &name); 11 | // let payload = render("hi.tera", &ctx)?; 12 | // Ok(Box::new(reply::html(payload))) 13 | // } -------------------------------------------------------------------------------- /src/handlers/index_handler.rs: -------------------------------------------------------------------------------- 1 | use tera::Context; 2 | use warp::{reply, Rejection, Reply}; 3 | 4 | use crate::template_setup::tera::render; 5 | 6 | pub async fn get() -> Result { 7 | let mut ctx = Context::new(); 8 | 9 | let name = "Steadylearner"; 10 | ctx.insert("name", &name); 11 | 12 | let payload = render("index.tera", &ctx)?; 13 | Ok(reply::html(payload)) 14 | } 15 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error_handler; 2 | 3 | // pub mod hello_handler; 4 | // pub mod hi_handler; 5 | // pub mod authorized_handler; 6 | 7 | pub mod index_handler; // Should be a single page app. 8 | pub mod user_handler; 9 | pub mod car_handler; 10 | pub mod game_handler; 11 | pub mod ranking_handler; 12 | 13 | pub mod private; 14 | 15 | // Will be used at error_hanlder.rs and others synchronously. 16 | use warp::{ 17 | reject::{ 18 | Reject 19 | }, 20 | }; 21 | 22 | #[allow(non_camel_case_types)] 23 | #[derive(Debug)] 24 | pub struct UNAUTHORIZED; 25 | impl Reject for UNAUTHORIZED {} 26 | 27 | #[allow(non_camel_case_types)] 28 | #[derive(Debug)] 29 | pub struct INTERNAL_SERVER_ERROR; 30 | impl Reject for INTERNAL_SERVER_ERROR {} 31 | 32 | #[allow(non_camel_case_types)] 33 | #[derive(Debug)] 34 | pub struct NOT_ACCEPTABLE; 35 | impl Reject for NOT_ACCEPTABLE {} 36 | 37 | #[allow(non_camel_case_types)] 38 | #[derive(Debug)] 39 | pub struct BAD_REQUEST; 40 | impl Reject for BAD_REQUEST {} -------------------------------------------------------------------------------- /src/handlers/private/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod profile_handler; 2 | pub mod password_handler; -------------------------------------------------------------------------------- /src/handlers/private/password_handler.rs: -------------------------------------------------------------------------------- 1 | use warp::{ 2 | reply, 3 | reject::{ 4 | custom, 5 | }, 6 | Rejection, 7 | Reply, 8 | }; 9 | 10 | use log::{debug, error}; 11 | 12 | use crate::{ 13 | db::sqlite::SQLITEPOOL, 14 | models::{ 15 | user::{ 16 | user::{User}, 17 | }, 18 | private::{ 19 | password::UpdatePasswordRequest, 20 | }, 21 | }, 22 | security::argon::verify, 23 | session::UserSession, 24 | }; 25 | 26 | use super::super::{ 27 | UNAUTHORIZED, 28 | INTERNAL_SERVER_ERROR, 29 | NOT_ACCEPTABLE, 30 | }; 31 | 32 | pub async fn update_password( 33 | update_password_request: UpdatePasswordRequest, 34 | user_session: Option, 35 | ) -> Result { 36 | let response = match SQLITEPOOL.get() { 37 | Ok(conn) => { 38 | if let Some(user_session) = user_session { 39 | let UserSession { email, password, .. } = user_session; 40 | 41 | let UpdatePasswordRequest { 42 | old_password, 43 | new_password, 44 | } = update_password_request; 45 | 46 | // Should use argon here. 47 | let correct_password = verify(&password, &old_password.as_bytes()); 48 | if correct_password == false { 49 | error!("The password({}) given by the user is not correct.", &old_password); 50 | Err(custom(NOT_ACCEPTABLE)) 51 | } else { 52 | if let Err(e) = User::update_password(&conn, &email, &new_password) { 53 | error!("{:#?}", e); 54 | Err(custom(INTERNAL_SERVER_ERROR)) 55 | } else { 56 | debug!("Could update the password."); 57 | Ok(reply()) 58 | } 59 | } 60 | } else { 61 | debug!("Fail to update the password without authorization. Should redirect a user to /login."); 62 | Err(custom(UNAUTHORIZED)) 63 | } 64 | }, 65 | Err(e) => { 66 | error!("{:#?}", e); 67 | Err(custom(INTERNAL_SERVER_ERROR)) 68 | } 69 | }; 70 | response 71 | } 72 | -------------------------------------------------------------------------------- /src/handlers/private/profile_handler.rs: -------------------------------------------------------------------------------- 1 | use warp::{ 2 | reply, 3 | reject::{ 4 | // https://docs.rs/warp/0.1.6/warp/reject/index.html 5 | custom, 6 | not_found 7 | }, 8 | Rejection, 9 | Reply, 10 | }; 11 | 12 | use log::{debug, error}; 13 | 14 | use crate::{ 15 | db::sqlite::SQLITEPOOL, 16 | models::{ 17 | user::{ 18 | user::{User}, 19 | }, 20 | private::{ 21 | profile::Profile, 22 | } 23 | }, 24 | session::UserSession, 25 | }; 26 | 27 | use super::super::{ 28 | UNAUTHORIZED, 29 | INTERNAL_SERVER_ERROR, 30 | }; 31 | 32 | pub async fn get(user_session: Option,) -> Result { 33 | let response = match SQLITEPOOL.get() { 34 | Ok(conn) => { 35 | if let Some(user_session) = user_session { 36 | let UserSession { email, .. } = user_session; 37 | 38 | let response = match User::get(&conn, &email) { 39 | Ok(user) => { 40 | let user = user.get(0); 41 | let response = if let Some(user) = user { 42 | let User { email, cash, .. } = user; 43 | let profile = Profile { 44 | email: email.to_string(), 45 | cash: *cash, 46 | }; 47 | // Content-Type: application/json 48 | // Should make it return with this header. 49 | Ok(reply::json(&profile)) 50 | } else { 51 | Err(not_found()) 52 | }; 53 | response 54 | }, 55 | Err(e) => { 56 | error!("{:#?}", e); 57 | Err(custom(INTERNAL_SERVER_ERROR)) 58 | } 59 | }; 60 | 61 | response 62 | } else { 63 | debug!("Failed without autorization. Should redirect a user to /login at React Frontend."); 64 | Err(custom(UNAUTHORIZED)) 65 | } 66 | }, 67 | Err(e) => { 68 | error!("{:#?}", e); 69 | Err(custom(INTERNAL_SERVER_ERROR)) 70 | } 71 | }; 72 | response 73 | } 74 | 75 | // I get this error from the client. 76 | // XML Parsing Error: syntax error 77 | // Location: http://localhost:1234/api/user/v1/login 78 | // Line Number 1, Column 1: 79 | 80 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type 81 | // Fix it with header Content-Type: application/json 82 | -------------------------------------------------------------------------------- /src/handlers/ranking_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | models::{ 3 | ranking::GameRankingList, 4 | }, 5 | }; 6 | 7 | use warp::{ 8 | reply, 9 | Rejection, 10 | Reply, 11 | reject::{ 12 | custom, 13 | } 14 | }; 15 | 16 | use crate::{db::sqlite::SQLITEPOOL}; 17 | 18 | use log::{ 19 | // debug, 20 | error 21 | }; 22 | 23 | use super::{ 24 | // UNAUTHORIZED, 25 | INTERNAL_SERVER_ERROR, 26 | // NOT_ACCEPTABLE, 27 | }; 28 | 29 | pub async fn game_ranking_list() -> Result { 30 | let response = match SQLITEPOOL.get() { 31 | Ok(conn) => { 32 | let game_ranking_list = GameRankingList::rank(&conn); 33 | 34 | match game_ranking_list { 35 | Ok(data) => { 36 | Ok(reply::json(&data)) 37 | } 38 | Err(e) => { 39 | error!("{:#?}", e); 40 | Err(custom(INTERNAL_SERVER_ERROR)) 41 | } 42 | } 43 | }, 44 | Err(e) => { 45 | error!("{:#?}", e); 46 | Err(custom(INTERNAL_SERVER_ERROR)) 47 | } 48 | }; 49 | 50 | response 51 | } 52 | 53 | // pub async fn game_ranking_list() -> Result { 54 | // let response = match SQLITEPOOL.get() { 55 | // Ok(conn) => { 56 | // let mut ctx = Context::new(); 57 | 58 | // let game_ranking_list = GameRankingList::rank(&conn); 59 | 60 | // match game_ranking_list { 61 | // Ok(data) => { 62 | // // ctx.insert("game_ranking_list", &items); 63 | // // let payload = render("ranking.tera", &ctx)?; 64 | // Ok(reply::json(&data)) 65 | // } 66 | // Err(e) => { 67 | // error!("{:#?}", e); 68 | // Err(custom(INTERNAL_SERVER_ERROR)) 69 | // } 70 | // } 71 | // }, 72 | // Err(e) => { 73 | // error!("{:#?}", e); 74 | // Err(custom(INTERNAL_SERVER_ERROR)) 75 | // } 76 | // }; 77 | 78 | // response 79 | // } 80 | -------------------------------------------------------------------------------- /src/handlers/user_handler.rs: -------------------------------------------------------------------------------- 1 | use warp::http::{header, Response, StatusCode}; 2 | use warp::{ 3 | reply, 4 | reject::{ 5 | // https://docs.rs/warp/0.1.6/warp/reject/index.html 6 | custom, 7 | not_found, 8 | }, 9 | redirect, 10 | Rejection, 11 | Reply, 12 | http::{Uri}, 13 | }; 14 | 15 | use log::{debug, error, warn}; 16 | 17 | use bincode; 18 | 19 | use crate::{ 20 | db::sqlite::SQLITEPOOL, 21 | models::{ 22 | cash, 23 | user::{ 24 | new_user::{NewUser}, 25 | requests::{NewUserRequest, LoginRequest, UpdateCashRequest, UpdateUserRequest}, 26 | // responses::{LoginSuccessResponse}, 27 | user::{User, UserList}, 28 | }, 29 | private::{ 30 | profile::Profile, 31 | }, 32 | }, 33 | security::argon::verify, 34 | session::UserSession, 35 | utils::random::alphanumeric_key, 36 | temporary_redirect_to_home, 37 | redirect_to_login, 38 | }; 39 | 40 | use super::{ 41 | UNAUTHORIZED, 42 | INTERNAL_SERVER_ERROR, 43 | NOT_ACCEPTABLE, 44 | }; 45 | 46 | pub async fn register(new_user_request: NewUserRequest) -> Result { 47 | let response = match SQLITEPOOL.get() { 48 | Ok(conn) => { 49 | let new_user = NewUser { 50 | email: new_user_request.email, 51 | password: new_user_request.password, 52 | ..Default::default() 53 | }; 54 | println!("{:#?}", &new_user); 55 | // Shoud verify email(unique, length, regex etc) and password(security) here. 56 | 57 | if let Err(e) = new_user.create(&conn) { 58 | error!("{:#?}", e); 59 | Err(custom(NOT_ACCEPTABLE)) 60 | } else { 61 | // debug!("Register success and redirect a user to /login with the frontend(React)."); 62 | // Ok(redirect_to_login!()) 63 | let response = Response::builder() 64 | .body(b"".to_vec()); 65 | 66 | Ok(response) 67 | } 68 | }, 69 | Err(e) => { 70 | error!("{:#?}", e); 71 | Err(custom(INTERNAL_SERVER_ERROR)) 72 | } 73 | }; 74 | response 75 | } 76 | 77 | pub async fn list() -> Result { 78 | let response = match SQLITEPOOL.get() { 79 | Ok(conn) => { 80 | match UserList::list_public(&conn) { 81 | Ok(public_user_list) => { 82 | Ok(reply::json(&public_user_list)) 83 | }, 84 | Err(e) => { 85 | error!("{:#?}", e); 86 | Err(custom(INTERNAL_SERVER_ERROR)) 87 | } 88 | } 89 | }, 90 | Err(e) => { 91 | error!("{:#?}", e); 92 | Err(custom(INTERNAL_SERVER_ERROR)) 93 | } 94 | }; 95 | response 96 | } 97 | 98 | // Refer to https://github.com/kaj/warp-diesel-ructe-sample/tree/master/src to save session_id to user and database 99 | // Use do here temporaily because I will make login page? 100 | // Should I make custom session struct? 101 | pub async fn do_login(login_request: LoginRequest) -> Result { 102 | let response = match SQLITEPOOL.get() { 103 | Ok(conn) => { 104 | let LoginRequest { email, password } = login_request; 105 | let password_from_database = User::is_registered(&conn, &email); 106 | 107 | if let Some(hash) = password_from_database { 108 | if verify(&hash, password.as_bytes()) { 109 | let identity_id = alphanumeric_key(48); 110 | debug!("New identity_id for {} is {}.", &email, &identity_id); 111 | 112 | // Should set identified_at field with NaiveDateTime to compare when user did login later. 113 | User::set_identity_id(&conn, &email, &identity_id).unwrap(); // Remove this unwrap later with correct error handler 114 | 115 | let cookie = format!("EXAUTH={}; SameSite=Strict; HttpOpnly", &identity_id); 116 | 117 | // This helps the user to navigate after login, but it is not identity_id 118 | // Save it to the user database also? Use it to see how users behave etc. 119 | let session_id = alphanumeric_key(48); 120 | let body = session_id.into_bytes(); 121 | 122 | // let body = LoginSuccessResponse { 123 | // session_id, 124 | // }; 125 | // let encoded: Vec = bincode::serialize(&body).unwrap(); 126 | 127 | let response = Response::builder() 128 | .status(StatusCode::OK) 129 | .header( 130 | header::SET_COOKIE, 131 | cookie, 132 | ) 133 | .body(body.to_vec()); 134 | 135 | // .body(encoded); 136 | 137 | Ok(response) 138 | } else { 139 | debug!("The password is not correct."); 140 | Err(custom(UNAUTHORIZED)) 141 | } 142 | } else { 143 | // Should handle this correctly. 144 | warn!("There is no hashed password for the user {}. Something severe problem happend. Where to send this user to where?", &email); 145 | // Password is none. Where to send the user? 146 | Err(warp::reject::not_found()) 147 | } 148 | }, 149 | Err(e) => { 150 | error!("{:#?}", e); 151 | Err(custom(INTERNAL_SERVER_ERROR)) 152 | } 153 | }; 154 | response 155 | } 156 | 157 | pub async fn update_cash( 158 | update_cash_request: UpdateCashRequest, 159 | user_session: Option, 160 | ) -> Result { 161 | let response = match SQLITEPOOL.get() { 162 | Ok(conn) => { 163 | if let Some(user_session) = user_session { 164 | let UpdateCashRequest { amount } = update_cash_request; 165 | debug!("The cash will be updated with {}.", &amount); 166 | let UserSession { email, .. } = user_session; 167 | 168 | if let Err(e) = cash::update(&conn, &amount, &email) { 169 | error!("{:#?}", e); 170 | Err(custom(INTERNAL_SERVER_ERROR)) // Custom error here? 171 | } else { 172 | debug!("The cash of the user is updated."); 173 | // Ok(redirect(Uri::from_static("/profile"))) // Redirect is handled at the React frontend. 174 | Ok(reply()) 175 | } 176 | } else { 177 | debug!("Fail to update the cash without authorization. Should redirect a user to /login."); 178 | // Ok(redirect_to_login!()) 179 | Ok(reply()) // Use this to make it compile. Should handle it correctly later. 180 | } 181 | }, 182 | Err(e) => { 183 | error!("{:#?}", e); 184 | Err(custom(INTERNAL_SERVER_ERROR)) 185 | } 186 | }; 187 | response 188 | } 189 | 190 | pub async fn delete_user(user_session: Option) -> Result { 191 | let response = match SQLITEPOOL.get() { 192 | Ok(conn) => { 193 | if let Some(user_session) = user_session { 194 | let UserSession { email, .. } = user_session; 195 | 196 | if let Err(e) = User::delete(&conn, &email) { 197 | error!("{:#?}", e); 198 | Err(custom(INTERNAL_SERVER_ERROR)) // Custom error here? 199 | } else { 200 | debug!("The user is deleted and redirect to / without any session data."); 201 | Ok(temporary_redirect_to_home!()) 202 | } 203 | } else { 204 | debug!("Fail to delete the user without authorization. Should redirect a user to /."); 205 | Ok(temporary_redirect_to_home!()) 206 | } 207 | }, 208 | Err(e) => { 209 | error!("{:#?}", e); 210 | Err(custom(INTERNAL_SERVER_ERROR)) 211 | } 212 | }; 213 | response 214 | } 215 | 216 | pub async fn logout(user_session: Option) -> Result { 217 | 218 | let response = match SQLITEPOOL.get() { 219 | Ok(conn) => { 220 | if let Some(user_session) = user_session { 221 | if let Err(e) = User::remove_identity_id(&conn, &user_session.identity_id) { 222 | error!("{:#?}", e); 223 | Err(custom(INTERNAL_SERVER_ERROR)) // Custom error here? 224 | } else { 225 | debug!("Logout success and redirect a user to / without any session data ."); 226 | Ok(temporary_redirect_to_home!()) 227 | } 228 | } else { 229 | debug!("Fail to logout without autorization. Should redirect a user to /."); 230 | Err(custom(UNAUTHORIZED)) 231 | } 232 | }, 233 | Err(e) => { 234 | error!("{:#?}", e); 235 | Err(custom(INTERNAL_SERVER_ERROR)) 236 | } 237 | }; 238 | response 239 | } 240 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // This is to use executable files in bin/ 2 | // Should refactor the app to use this more? 3 | 4 | extern crate chrono; 5 | extern crate rusqlite; 6 | 7 | #[macro_use] 8 | extern crate lazy_static; 9 | 10 | use std::io::stdin; 11 | 12 | pub mod models; 13 | pub mod security; 14 | 15 | pub mod db; 16 | pub mod utils; 17 | 18 | pub fn from_stdin() -> String { 19 | let mut input = String::new(); 20 | stdin().read_line(&mut input).unwrap(); 21 | let input = input[..(input.len() - 1)].to_string(); 22 | 23 | input 24 | } 25 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use warp::{self, Filter}; 3 | 4 | #[macro_use] 5 | extern crate lazy_static; 6 | 7 | use pretty_env_logger; 8 | #[macro_use] 9 | extern crate log; 10 | 11 | mod api; 12 | mod handlers; 13 | mod routes; 14 | 15 | mod models; 16 | mod security; 17 | mod template_setup; 18 | mod utils; 19 | 20 | mod db; 21 | mod session; 22 | 23 | mod server; 24 | 25 | use self::{ 26 | db::sqlite::setup, 27 | server::{end, routes_info}, 28 | handlers::error_handler::handle_rejection, 29 | }; 30 | 31 | #[tokio::main] 32 | async fn main() -> Result<(), Box> { 33 | pretty_env_logger::init(); 34 | 35 | if let Err(e) = setup() { 36 | eprintln!("{:#?}", e); 37 | ::std::process::exit(1); 38 | } 39 | 40 | routes_info(); 41 | 42 | Ok(warp::serve(end().recover(handle_rejection)) 43 | .run(([0, 0, 0, 0], 8000)) 44 | .await) 45 | } 46 | -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | # Refer to user/ 2 | 3 | When car.rs, game.rs becomes complicated. Make it similar to user/ -------------------------------------------------------------------------------- /src/models/car.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::{params, Connection, Result, NO_PARAMS}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct NewCar { 6 | pub price: f64, 7 | pub color: String, 8 | pub user_id: i64, 9 | } 10 | 11 | impl Default for NewCar { 12 | fn default() -> Self { 13 | NewCar { 14 | price: 10000 as f64, 15 | color: "black".into(), 16 | user_id: 1, // SQLIte id starts with 1. 17 | } 18 | } 19 | } 20 | 21 | // 1. Make a car with the user id as foreign key of it. 22 | // 2. Minus cash to the price of the car. 23 | impl NewCar { 24 | pub fn create(&self, conn: &mut Connection) -> Result<()> { 25 | let tx = conn.transaction()?; 26 | 27 | tx.execute( 28 | "insert into cars (price, color, user_id) values (?1, ?2, ?3)", 29 | &[ 30 | &self.price.to_string(), 31 | &self.color, 32 | &self.user_id.to_string(), 33 | ], 34 | )?; 35 | tx.execute( 36 | "UPDATE users SET cash = cash - (?1) WHERE id = (?2);", 37 | &[&self.price.to_string(), &self.user_id.to_string()], 38 | )?; 39 | 40 | tx.commit() 41 | } 42 | } 43 | 44 | #[derive(Debug, Serialize, Deserialize)] 45 | pub struct NewCarRequest { 46 | pub price: f64, 47 | pub color: String, 48 | } 49 | 50 | #[derive(Debug)] 51 | pub struct Car { 52 | pub id: i64, 53 | pub price: f64, 54 | pub color: String, 55 | pub user_id: i64, 56 | // Include it later if you want. 57 | // pub created_at: NaiveDateTime, 58 | // pub updated_at: NaiveDateTime, 59 | } 60 | 61 | #[derive(Debug, Serialize, Deserialize)] 62 | pub struct CarPublic { 63 | pub price: f64, 64 | pub color: String, 65 | } 66 | 67 | impl Car { 68 | pub fn get(conn: &Connection, id: &i64) -> Result> { 69 | let mut stmt = conn.prepare("SELECT * FROM cars WHERE id = (?1);")?; 70 | 71 | let result = stmt.query_map(params![&id.to_owned()], |row| { 72 | Ok(Car { 73 | id: row.get(0)?, 74 | price: row.get(1)?, 75 | color: row.get(2)?, 76 | user_id: row.get(3)?, 77 | }) 78 | })?; 79 | 80 | let mut car = Vec::new(); 81 | for c in result { 82 | car.push(c?); 83 | } 84 | 85 | Ok(car) 86 | } 87 | 88 | pub fn refund(&self, conn: &mut Connection) -> Result<()> { 89 | let tx = conn.transaction()?; 90 | 91 | tx.execute( 92 | "UPDATE users SET cash = cash + (?1) WHERE id = (?2);", 93 | &[&self.price.to_string(), &self.user_id.to_string()], 94 | )?; 95 | tx.execute("DELETE FROM cars WHERE id = (?1)", &[&self.id])?; 96 | 97 | tx.commit() 98 | } 99 | 100 | pub fn delete(conn: &Connection, id: &i64) -> Result<()> { 101 | conn.execute("DELETE FROM cars WHERE id = (?1);", &[&id.to_owned()])?; 102 | Ok(()) 103 | } 104 | } 105 | 106 | #[derive(Debug, Serialize, Deserialize)] 107 | pub struct CarRefundRequest { 108 | pub car_id: i64, 109 | } 110 | 111 | #[derive(Debug)] 112 | pub struct CarList(pub Vec); 113 | 114 | impl CarList { 115 | pub fn list(conn: &Connection) -> Result> { 116 | let mut stmt = conn.prepare("SELECT * FROM cars;")?; 117 | 118 | let result = stmt.query_map(NO_PARAMS, |row| { 119 | Ok(Car { 120 | id: row.get(0)?, 121 | price: row.get(1)?, 122 | color: row.get(2)?, 123 | user_id: row.get(3)?, 124 | }) 125 | })?; 126 | 127 | let mut cars = Vec::new(); 128 | for u in result { 129 | cars.push(u?); 130 | } 131 | // println!("{:#?}", cars); 132 | 133 | Ok(cars) 134 | } 135 | } 136 | 137 | #[derive(Debug, Serialize, Deserialize)] 138 | pub struct CarPublicList(pub Vec); 139 | 140 | impl CarPublicList { 141 | pub fn list(conn: &Connection, user_id: &i64) -> Result> { 142 | let mut stmt = conn.prepare("SELECT price, color FROM cars where user_id = (?1)")?; 143 | 144 | let result = stmt.query_map(params![&user_id], |row| { 145 | Ok(CarPublic { 146 | price: row.get(0)?, 147 | color: row.get(1)?, 148 | }) 149 | })?; 150 | 151 | let mut cars = Vec::new(); 152 | for u in result { 153 | cars.push(u?); 154 | } 155 | // println!("{:#?}", cars); 156 | 157 | Ok(cars) 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /src/models/car_with_user.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::{params, Connection, Result, NO_PARAMS}; 2 | 3 | #[derive(Debug)] 4 | pub struct CarWithUser { 5 | pub email: String, 6 | pub user_id: i64, 7 | pub car_id: i64, 8 | pub price: f64, 9 | pub color: String, 10 | } 11 | 12 | impl CarWithUser { 13 | pub fn get(conn: &Connection, id: i64) -> Result> { 14 | let mut stmt = conn.prepare( 15 | " 16 | SELECT users.email, users.id, cars.id, price, color 17 | FROM cars 18 | INNER JOIN users ON users.id = cars.user_id 19 | WHERE cars.id = (?1); 20 | ", 21 | )?; 22 | 23 | let result = stmt.query_map(params![&id], |row| { 24 | Ok(CarWithUser { 25 | email: row.get(0)?, 26 | user_id: row.get(1)?, 27 | car_id: row.get(2)?, 28 | price: row.get(3)?, 29 | color: row.get(4)?, 30 | }) 31 | })?; 32 | 33 | let mut car_with_user = Vec::new(); 34 | for c in result { 35 | car_with_user.push(c?); 36 | } 37 | 38 | Ok(car_with_user) 39 | } 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct CarWithUserList(pub Vec); 44 | 45 | impl CarWithUserList { 46 | pub fn list(conn: &Connection) -> Result> { 47 | let mut stmt = conn.prepare( 48 | " 49 | SELECT users.email, users.id, cars.id, price, color 50 | FROM cars 51 | INNER JOIN users ON users.id = cars.user_id; 52 | ", 53 | )?; 54 | 55 | let result = stmt.query_map(NO_PARAMS, |row| { 56 | Ok(CarWithUser { 57 | email: row.get(0)?, 58 | user_id: row.get(1)?, 59 | car_id: row.get(2)?, 60 | price: row.get(3)?, 61 | color: row.get(4)?, 62 | }) 63 | })?; 64 | 65 | let mut cars_with_users = Vec::new(); 66 | for c in result { 67 | cars_with_users.push(c?); 68 | } 69 | 70 | Ok(cars_with_users) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/models/cash.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::{Connection, Result}; 2 | 3 | // Should be transaction later. 4 | pub fn update(conn: &Connection, amount: &f64, email: &str) -> Result<()> { 5 | let amount = amount.to_string(); 6 | 7 | conn.execute( 8 | "UPDATE users SET cash = cash + (?1) WHERE email = (?2);", 9 | &[amount, email.to_owned()], 10 | )?; 11 | 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /src/models/game.rs: -------------------------------------------------------------------------------- 1 | // https://www.macmillandictionary.com/us/thesaurus-category/american/money-involved-in-gambling 2 | // https://www.begambleaware.org/understanding-gambling/gambling-words-and-phrases-explained/ 3 | 4 | use rusqlite::{params, Connection, Result, NO_PARAMS}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use chrono::naive::NaiveDateTime; 8 | 9 | use crate::{ 10 | utils::game::get_fair_odd, 11 | }; 12 | 13 | use rand::Rng; 14 | 15 | use log::{debug}; 16 | 17 | #[derive(Debug, Serialize, Deserialize)] 18 | #[serde(rename_all = "camelCase")] 19 | pub struct NewGameRequest { 20 | pub stake_amount: Option, 21 | pub car_id: Option, 22 | pub number_of_participants: i64, 23 | } 24 | 25 | impl NewGameRequest { 26 | pub fn is_logically_valid(&self) -> bool { 27 | let bet_with_cash = self.stake_amount.is_some(); 28 | let bet_with_car = self.car_id.is_some(); 29 | 30 | let validity = if bet_with_cash && bet_with_car { 31 | false 32 | } else if &self.number_of_participants < &2i64 { 33 | false 34 | } else if &self.stake_amount.unwrap() <= &0f64 { 35 | // Logiaclly correct, but there should be better way. 36 | // Use this because of the type problem. 37 | false 38 | } else { 39 | true 40 | }; 41 | validity 42 | } 43 | } 44 | 45 | pub fn find_game_result_and_profit(number_of_participants: i64, stake_amount: &f64) -> (bool, f64) { 46 | let odd = get_fair_odd(number_of_participants); 47 | 48 | let mut rng = rand::thread_rng(); 49 | let from_zero_to_one = rng.gen::(); // [0, 1) 50 | let condition: bool = from_zero_to_one >= odd; // Include = because ) from_zero_to_one 51 | 52 | let win = if condition { 53 | debug!("You lost the gamble."); 54 | false 55 | } else { 56 | debug!("You won the gamble."); 57 | true 58 | }; 59 | 60 | let profit = if !win { 61 | let loss = stake_amount * -1.0f64; 62 | loss 63 | } else { 64 | let rest = number_of_participants - 1; 65 | let earning = stake_amount * (rest as f64); 66 | earning 67 | }; 68 | (win, profit) 69 | } 70 | 71 | #[derive(Debug, Serialize, Deserialize)] 72 | pub struct NewGame { 73 | pub stake_amount: f64, 74 | pub number_of_participants: i64, 75 | pub user_id: i64, 76 | pub win: bool, // 0(false, lost) or 1(true, won) temporarily because there is no boolean in SQLite. 77 | } 78 | 79 | impl NewGame { 80 | pub fn create(&self, conn: &Connection) -> Result<()> { 81 | // 0(false, lost) or 1(true, won) temporarily because there is no boolean in SQLite. 82 | let win = if self.win { 1 } else { 0 }; 83 | 84 | conn.execute( 85 | "INSERT INTO 86 | games (stake_amount, number_of_participants, win, user_id) 87 | values (?1, ?2, ?3, ?4) 88 | ", 89 | &[ 90 | &self.stake_amount.to_string(), 91 | &self.number_of_participants.to_string(), 92 | &win.to_string(), 93 | &self.user_id.to_string(), 94 | ], 95 | )?; 96 | Ok(()) 97 | } 98 | } 99 | 100 | #[derive(Debug)] 101 | pub struct Game { 102 | pub id: i64, 103 | pub stake_amount: f64, 104 | pub number_of_participants: i64, 105 | // 0(false, lost) or 1(true, won) because there is no boolean in SQLite. 106 | pub win: i8, 107 | pub created_at: NaiveDateTime, 108 | pub user_id: i64, 109 | } 110 | 111 | impl Game { 112 | pub fn get(conn: &Connection, id: String) -> Result> { 113 | let mut stmt = conn.prepare("SELECT * FROM games WHERE id = (?1);")?; 114 | 115 | let result = stmt.query_map(params![&id], |row| { 116 | Ok(Game { 117 | id: row.get(0)?, 118 | stake_amount: row.get(1)?, 119 | number_of_participants: row.get(2)?, 120 | win: row.get(3)?, 121 | created_at: row.get(4)?, 122 | user_id: row.get(5)?, 123 | }) 124 | })?; 125 | 126 | let mut game = Vec::new(); 127 | for u in result { 128 | game.push(u?); 129 | } 130 | 131 | Ok(game) 132 | } 133 | } 134 | 135 | #[derive(Debug)] 136 | pub struct GameList(pub Vec); 137 | 138 | impl GameList { 139 | pub fn list(conn: &Connection) -> Result> { 140 | let mut stmt = conn.prepare("SELECT * FROM Games;")?; 141 | 142 | let result = stmt.query_map(NO_PARAMS, |row| { 143 | Ok(Game { 144 | id: row.get(0)?, 145 | stake_amount: row.get(1)?, 146 | number_of_participants: row.get(2)?, 147 | win: row.get(3)?, 148 | created_at: row.get(4)?, 149 | user_id: row.get(5)?, 150 | }) 151 | })?; 152 | 153 | let mut games = Vec::new(); 154 | for u in result { 155 | games.push(u?); 156 | } 157 | 158 | Ok(games) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/models/game_with_user.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::{ 2 | // params, 3 | Connection, 4 | Result, 5 | NO_PARAMS, 6 | }; 7 | 8 | use chrono::naive::NaiveDateTime; 9 | 10 | // Profit Can be positive or negative. Because it is gamble. When you lose it will be negative. When you win it will be positive. 11 | // Temporary and should imporve it. 12 | #[derive(Debug)] 13 | pub struct GameWithUser { 14 | pub email: String, 15 | pub win: i8, 16 | pub stake_amount: f64, 17 | pub number_of_participants: i64, 18 | pub created_at: NaiveDateTime, 19 | // pub profit: f64, 20 | } 21 | 22 | // impl GameWithUser { 23 | // pub fn profit(&self) { 24 | 25 | // } 26 | // } 27 | 28 | // Make it to function 29 | // let profit = if win { 30 | // stake_amount * (number_of_participants - 1) 31 | // } else { 32 | // stake_amount * -1f64 33 | // }; 34 | 35 | #[derive(Debug)] 36 | pub struct GameWithUserList(pub Vec); 37 | 38 | impl GameWithUserList { 39 | pub fn list(conn: &Connection) -> Result> { 40 | let mut stmt = conn.prepare( 41 | " 42 | SELECT users.email, win, stake_amount, number_of_participants, created_at, 43 | FROM games 44 | INNER JOIN users ON users.id = games.user_id 45 | ORDER BY users.email; 46 | ", 47 | )?; 48 | 49 | let results = stmt.query_map(NO_PARAMS, |row| { 50 | let win: i8 = row.get(1)?; 51 | let stake_amount: f64 = row.get(2)?; 52 | // let number_of_participants: i64 = row.get(3)?; 53 | 54 | // let profit = if win == 0 { 55 | // let loss = stake_amount * -1.0f64; 56 | // loss 57 | // } else { 58 | // let rest = number_of_participants -1; 59 | // let earning = stake_amount * rest as f64; 60 | // earning 61 | // }; 62 | 63 | Ok(GameWithUser { 64 | email: row.get(0)?, 65 | win, 66 | stake_amount, 67 | number_of_participants: row.get(3)?, 68 | created_at: row.get(4)?, 69 | // profit, 70 | }) 71 | })?; 72 | 73 | let mut game_results = Vec::new(); 74 | for c in results { 75 | game_results.push(c?); 76 | } 77 | 78 | Ok(game_results) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod car; 2 | pub mod car_with_user; 3 | pub mod cash; 4 | pub mod game; 5 | pub mod ranking; 6 | pub mod game_with_user; 7 | pub mod user; 8 | 9 | pub mod private; 10 | -------------------------------------------------------------------------------- /src/models/private/game.rs: -------------------------------------------------------------------------------- 1 | // ../game.rs should be here. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct NewGameReply { 8 | pub win: bool, 9 | pub profit: f64, 10 | } -------------------------------------------------------------------------------- /src/models/private/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod profile; 2 | pub mod password; 3 | pub mod game; -------------------------------------------------------------------------------- /src/models/private/password.rs: -------------------------------------------------------------------------------- 1 | // https://serde.rs/container-attrs.html 2 | use serde::{Deserialize, Serialize}; 3 | 4 | // Serialize to send the data to a client(used at the client side) 5 | // Deserialize to use the data from a client 6 | #[derive(Debug, Serialize, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct UpdatePasswordRequest { 9 | pub old_password: String, 10 | pub new_password: String, 11 | } -------------------------------------------------------------------------------- /src/models/private/profile.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | // Should be ProfileReply. 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct Profile { 6 | pub email: String, 7 | pub cash: f64, 8 | } -------------------------------------------------------------------------------- /src/models/ranking.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::{params, Connection, Result, NO_PARAMS}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct GameRanking { 7 | pub email: String, 8 | pub total_prize: f64, 9 | } 10 | 11 | #[derive(Debug, Serialize, Deserialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct GameRankingList(pub Vec); 14 | 15 | impl GameRankingList { 16 | pub fn rank(conn: &Connection) -> Result> { 17 | // Find the emails for game participants. 18 | let mut email_stmt = conn.prepare( 19 | " 20 | SELECT DISTINCT users.email 21 | FROM games 22 | INNER JOIN users ON users.id = games.user_id; 23 | ", 24 | )?; 25 | 26 | let email_results = email_stmt.query_map(NO_PARAMS, |row| { 27 | let email: String = row.get(0)?; 28 | Ok(email) 29 | })?; 30 | 31 | let mut emails = Vec::new(); 32 | for e in email_results { 33 | emails.push(e?); 34 | } 35 | // println!("{:#?}", emails); 36 | 37 | let mut group_of_profits = Vec::new(); 38 | for email in emails.iter() { 39 | // println!("{}", email); 40 | 41 | let mut profits_stmt = conn.prepare( 42 | " 43 | SELECT win, stake_amount, number_of_participants 44 | FROM games 45 | INNER JOIN users ON users.id = games.user_id WHERE users.email = (?1); 46 | ", 47 | )?; 48 | 49 | let mut profits: Vec = Vec::new(); 50 | 51 | // Should organize this. 52 | let profit_results = profits_stmt.query_map(params![&email], |row| { 53 | let win: i8 = row.get(0)?; 54 | let stake_amount: f64 = row.get(1)?; 55 | let number_of_participants: i64 = row.get(2)?; 56 | 57 | // Make this to a function. 58 | let profit = if win == 0 { 59 | let loss = stake_amount * -1.0f64; 60 | loss 61 | } else { 62 | let rest = number_of_participants - 1; 63 | let earning = stake_amount * rest as f64; 64 | earning 65 | }; 66 | 67 | // println!("{}", profit); 68 | profits.push(profit); 69 | 70 | Ok(profit) 71 | })?; 72 | for profit_result in profit_results { 73 | profit_result?; 74 | // println!("{:#?}", profit_result?); 75 | } 76 | 77 | group_of_profits.push(profits); 78 | } 79 | // println!("{:#?}", group_of_profits); 80 | 81 | let total_prizes: Vec = group_of_profits 82 | .into_iter() 83 | .map(|group_of_profit| { 84 | let total_prize: f64 = group_of_profit.iter().sum(); 85 | total_prize 86 | }) 87 | .collect(); 88 | // println!("{:#?}", total_prizes); 89 | 90 | // zip emails and total_prizes and turn them to the GameRanking. 91 | 92 | let emails_with_total_prizes = emails.iter().zip(total_prizes.iter()); 93 | // println!("{:#?}", emails_with_total_prizes); 94 | 95 | let mut game_ranking_list: Vec = Vec::new(); 96 | for (email, total_prize) in emails_with_total_prizes { 97 | let game_ranking = GameRanking { 98 | email: email.to_owned(), 99 | total_prize: total_prize.to_owned(), 100 | }; 101 | game_ranking_list.push(game_ranking); 102 | } 103 | 104 | // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/sorting.html 105 | // https://users.rust-lang.org/t/how-to-sort-a-vec-of-floats/2838 106 | // b,a => DESC and a,b vice versa. 107 | game_ranking_list.sort_by(|b, a| a.total_prize.partial_cmp(&b.total_prize).unwrap()); 108 | 109 | Ok(game_ranking_list) 110 | } 111 | } 112 | 113 | // let game_ranking_example = GameRanking { 114 | // email: "steady@learner.com".into(), 115 | // total_prize: 100000f64, 116 | // }; 117 | 118 | // let game_ranking_list = vec!(game_ranking_example); 119 | 120 | // Ok(game_ranking_list) 121 | -------------------------------------------------------------------------------- /src/models/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod new_user; 2 | pub mod user; 3 | pub mod requests; 4 | pub mod responses; -------------------------------------------------------------------------------- /src/models/user/new_user.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::{Connection, Result}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use log::debug; 5 | 6 | use crate::{security::argon::hash, utils::random::alphanumeric_key}; 7 | 8 | // These are for CLI to prototype features. 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct NewUser { 11 | pub email: String, // Use type here? 12 | pub password: String, 13 | pub cash: f64, 14 | pub identity_id: String, 15 | } 16 | 17 | impl Default for NewUser { 18 | fn default() -> Self { 19 | NewUser { 20 | email: "default@email.com".into(), 21 | password: "password".into(), 22 | cash: 0.0, 23 | identity_id: alphanumeric_key(48), 24 | } 25 | } 26 | } 27 | 28 | // Use conn for param to every functions. 29 | // Remove let conn = Connection::open("gamble.db")?; with connection pool and lazy static later. 30 | impl NewUser { 31 | pub fn create(&self, conn: &Connection) -> Result<()> { 32 | let hashed_password = hash(&self.password.as_bytes()); 33 | 34 | conn.execute( 35 | "INSERT INTO users (email, password, cash, identity_id) values (?1, ?2, ?3, ?4)", 36 | &[ 37 | &self.email, 38 | &hashed_password, 39 | &self.cash.to_string(), 40 | &self.identity_id, 41 | ], 42 | )?; 43 | debug!("Save {} to gamble.db.", &self.email); 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/models/user/requests.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | // These are for a web server. 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct NewUserRequest { 6 | pub email: String, 7 | pub password: String, 8 | } 9 | 10 | // Could be update password request or just use NewUserRequest instead. 11 | #[derive(Debug, Serialize, Deserialize)] 12 | pub struct UpdateUserRequest { 13 | pub email: String, 14 | pub password: String, 15 | } 16 | 17 | #[derive(Debug, Serialize, Deserialize)] 18 | pub struct UpdateCashRequest { 19 | pub amount: f64, 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | pub struct LoginRequest { 24 | pub email: String, 25 | pub password: String, 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/models/user/responses.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct LoginSuccessResponse { 5 | pub session_id: String, 6 | } 7 | -------------------------------------------------------------------------------- /src/models/user/user.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::{params, Connection, Result, NO_PARAMS}; 2 | use chrono::naive::NaiveDateTime; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use log::debug; 6 | 7 | use crate::{security::argon::hash}; 8 | 9 | // Should separate cash with created_at and update_at? 10 | // Should separate identity_id with identified_at and session_id? 11 | #[derive(Debug, Serialize, Deserialize)] 12 | pub struct User { 13 | pub id: i64, 14 | pub email: String, 15 | pub password: String, 16 | pub cash: f64, 17 | // 18 | pub created_at: NaiveDateTime, 19 | pub updated_at: NaiveDateTime, 20 | // Should include identified_at: NaiveDateTime 21 | // to work with identity_id and separate them to another model? 22 | pub identity_id: String, 23 | } 24 | 25 | impl User { 26 | pub fn get(conn: &Connection, email: &str) -> Result> { 27 | let mut stmt = conn.prepare("SELECT * FROM users WHERE email = (?1);")?; 28 | 29 | let result = stmt.query_map(params![&email.to_owned()], |row| { 30 | Ok(User { 31 | id: row.get(0)?, 32 | email: row.get(1)?, 33 | password: row.get(2)?, 34 | cash: row.get(3)?, 35 | created_at: row.get(4)?, 36 | updated_at: row.get(5)?, 37 | identity_id: row.get(6)?, 38 | }) 39 | })?; 40 | 41 | let mut user = Vec::new(); 42 | for u in result { 43 | user.push(u?); 44 | } 45 | // debug!("{:#?}", user); 46 | 47 | Ok(user) 48 | } 49 | 50 | pub fn get_by_identity_id(conn: &Connection, identity_id: &str) -> Result> { 51 | let mut stmt = conn.prepare("SELECT * FROM users WHERE identity_id = (?1);")?; 52 | 53 | let result = stmt.query_map(params![&identity_id.to_owned()], |row| { 54 | Ok(User { 55 | id: row.get(0)?, 56 | email: row.get(1)?, 57 | password: row.get(2)?, 58 | cash: row.get(3)?, 59 | created_at: row.get(4)?, 60 | updated_at: row.get(5)?, 61 | identity_id: row.get(6)?, 62 | }) 63 | })?; 64 | 65 | let mut user = Vec::new(); 66 | for u in result { 67 | user.push(u?); 68 | } 69 | debug!("{:#?}", user); 70 | 71 | Ok(user) 72 | } 73 | 74 | pub fn delete(conn: &Connection, email: &str) -> Result<()> { 75 | conn.execute( 76 | "DELETE FROM users WHERE email = (?1);", 77 | &[&email.to_owned()], 78 | )?; 79 | 80 | Ok(()) 81 | } 82 | 83 | pub fn update_password(conn: &Connection, email: &str, new_password: &str) -> Result<()> { 84 | let hashed_new_password = hash(&new_password.as_bytes()); 85 | 86 | conn.execute( 87 | "UPDATE users 88 | SET password = (?1), updated_at = datetime('now','localtime') 89 | WHERE email = (?2);", 90 | &[&hashed_new_password, &email.to_owned()], 91 | )?; 92 | 93 | Ok(()) 94 | } 95 | 96 | // Should use Result later. 97 | pub fn is_registered(conn: &Connection, email: &str) -> Option { 98 | // Should set updated_at here or default behavior for that at SQL in build.rs. 99 | // Search more. 100 | // https://stackoverflow.com/questions/14461851/how-to-have-an-automatic-timestamp-in-sqlite 101 | let user = User::get(&conn, email.into()).unwrap(); // Remove this unwrap later with correct error handler 102 | let user = user.get(0); 103 | 104 | match user { 105 | Some(user) => { 106 | let User { 107 | password: hashed, .. 108 | } = user; 109 | Some(hashed.to_owned()) 110 | } 111 | None => None, 112 | } 113 | } 114 | 115 | pub fn set_identity_id(conn: &Connection, email: &str, identity_id: &str) -> Result<()> { 116 | conn.execute( 117 | "UPDATE users 118 | SET identity_id = (?1) 119 | WHERE email = (?2);", 120 | &[&identity_id.to_owned(), &email.to_owned()], 121 | )?; 122 | 123 | Ok(()) 124 | } 125 | 126 | pub fn remove_identity_id(conn: &Connection, previous_identity_id: &str) -> Result<()> { 127 | conn.execute( 128 | "UPDATE users 129 | SET identity_id = '' 130 | WHERE identity_id = (?1);", 131 | &[&previous_identity_id.to_owned()], 132 | )?; 133 | 134 | Ok(()) 135 | } 136 | } 137 | 138 | #[derive(Debug, Serialize, Deserialize)] 139 | pub struct UserPublic { 140 | pub email: String, 141 | pub created_at: NaiveDateTime, 142 | pub updated_at: NaiveDateTime, 143 | } 144 | 145 | #[derive(Debug, Serialize, Deserialize)] 146 | pub struct UserList(pub Vec); 147 | 148 | impl UserList { 149 | pub fn list(conn: &Connection) -> Result> { 150 | let mut stmt = conn.prepare("SELECT * FROM users;")?; 151 | 152 | let result = stmt.query_map(NO_PARAMS, |row| { 153 | Ok(User { 154 | id: row.get(0)?, 155 | email: row.get(1)?, 156 | password: row.get(2)?, 157 | cash: row.get(3)?, 158 | created_at: row.get(4)?, 159 | updated_at: row.get(5)?, 160 | identity_id: row.get(6)?, 161 | }) 162 | })?; 163 | 164 | let mut users = Vec::new(); 165 | for u in result { 166 | users.push(u?); 167 | } 168 | 169 | Ok(users) 170 | } 171 | 172 | pub fn list_public(conn: &Connection) -> Result> { 173 | let mut stmt = conn.prepare("SELECT * FROM users;")?; 174 | 175 | let result = stmt.query_map(NO_PARAMS, |row| { 176 | Ok(UserPublic { 177 | email: row.get(1)?, 178 | created_at: row.get(4)?, 179 | updated_at: row.get(5)?, 180 | }) 181 | })?; 182 | 183 | let mut users_public = Vec::new(); 184 | for u in result { 185 | users_public.push(u?); 186 | } 187 | 188 | Ok(users_public) 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /src/models/user/user_monolithic.rs: -------------------------------------------------------------------------------- 1 | // use rusqlite::{params, Connection, Result, NO_PARAMS}; 2 | 3 | // use chrono::naive::NaiveDateTime; 4 | 5 | // use crate::{security::argon::hash, utils::random::alphanumeric_key}; 6 | 7 | // use serde::{Deserialize, Serialize}; 8 | 9 | // use log::debug; 10 | 11 | // // These are for CLI to prototype features. 12 | // #[derive(Debug, Serialize, Deserialize)] 13 | // pub struct NewUser { 14 | // pub email: String, // Use type here? 15 | // pub password: String, 16 | // pub cash: f64, 17 | // pub identity_id: String, 18 | // } 19 | 20 | // impl Default for NewUser { 21 | // fn default() -> Self { 22 | // NewUser { 23 | // email: "default@email.com".into(), 24 | // password: "password".into(), 25 | // cash: 0.0, 26 | // identity_id: alphanumeric_key(48), 27 | // } 28 | // } 29 | // } 30 | 31 | // // Use conn for param to every functions. 32 | // // Remove let conn = Connection::open("gamble.db")?; with connection pool and lazy static later. 33 | // impl NewUser { 34 | // pub fn create(&self, conn: &Connection) -> Result<()> { 35 | // let hashed_password = hash(&self.password.as_bytes()); 36 | 37 | // conn.execute( 38 | // "INSERT INTO users (email, password, cash, identity_id) values (?1, ?2, ?3, ?4)", 39 | // &[ 40 | // &self.email, 41 | // &hashed_password, 42 | // &self.cash.to_string(), 43 | // &self.identity_id, 44 | // ], 45 | // )?; 46 | // println!("Save {} to gamble.db.", &self.email); 47 | // Ok(()) 48 | // } 49 | // } 50 | 51 | // // These are for a web server. 52 | // #[derive(Debug, Serialize, Deserialize)] 53 | // pub struct NewUserRequest { 54 | // pub email: String, 55 | // pub password: String, 56 | // } 57 | 58 | // // Could be UpdateUserPassordRequest later. 59 | // #[derive(Debug, Serialize, Deserialize)] 60 | // pub struct UpdateUserRequest { 61 | // pub email: String, 62 | // pub password: String, 63 | // } 64 | 65 | // // Should include it to UpdateUserRequest? 66 | // #[derive(Debug, Serialize, Deserialize)] 67 | // pub struct UpdateCashRequest { 68 | // pub amount: f64, 69 | // } 70 | 71 | // #[derive(Debug, Serialize, Deserialize)] 72 | // pub struct LoginRequest { 73 | // pub email: String, 74 | // pub password: String, 75 | // } 76 | 77 | // // Should be synchronize with SQLite commands at build.rs 78 | // #[derive(Debug, Serialize, Deserialize)] 79 | // pub struct User { 80 | // pub id: i64, 81 | // pub email: String, 82 | // pub password: String, 83 | // pub cash: f64, 84 | // // 85 | // pub created_at: NaiveDateTime, 86 | // pub updated_at: NaiveDateTime, 87 | // pub identity_id: String, 88 | // } 89 | 90 | // // Use id instead of web app later if necessary. 91 | // // String to &str and use .to_owned() later.s 92 | // impl User { 93 | // pub fn get(conn: &Connection, email: String) -> Result> { 94 | // let mut stmt = conn.prepare("SELECT * FROM users WHERE email = (?1);")?; 95 | 96 | // let result = stmt.query_map(params![&email], |row| { 97 | // Ok(User { 98 | // id: row.get(0)?, 99 | // email: row.get(1)?, 100 | // password: row.get(2)?, 101 | // cash: row.get(3)?, 102 | // created_at: row.get(4)?, 103 | // updated_at: row.get(5)?, 104 | // identity_id: row.get(6)?, 105 | // }) 106 | // })?; 107 | 108 | // let mut user = Vec::new(); 109 | // for u in result { 110 | // user.push(u?); 111 | // } 112 | // debug!("{:#?}", user); 113 | 114 | // Ok(user) 115 | // } 116 | 117 | // pub fn get_by_identity_id(conn: &Connection, identity_id: &str) -> Result> { 118 | // let mut stmt = conn.prepare("SELECT * FROM users WHERE identity_id = (?1);")?; 119 | 120 | // let result = stmt.query_map(params![&identity_id.to_owned()], |row| { 121 | // Ok(User { 122 | // id: row.get(0)?, 123 | // email: row.get(1)?, 124 | // password: row.get(2)?, 125 | // cash: row.get(3)?, 126 | // created_at: row.get(4)?, 127 | // updated_at: row.get(5)?, 128 | // identity_id: row.get(6)?, 129 | // }) 130 | // })?; 131 | 132 | // let mut user = Vec::new(); 133 | // for u in result { 134 | // user.push(u?); 135 | // } 136 | // debug!("{:#?}", user); 137 | 138 | // Ok(user) 139 | // } 140 | 141 | // pub fn delete(conn: &Connection, email: &str) -> Result<()> { 142 | // conn.execute( 143 | // "DELETE FROM users WHERE email = (?1);", 144 | // &[&email.to_owned()], 145 | // )?; 146 | 147 | // Ok(()) 148 | // } 149 | 150 | // pub fn update_password(conn: &Connection, email: &str, new_password: &str) -> Result<()> { 151 | // let hashed_new_password = hash(&new_password.as_bytes()); 152 | 153 | // conn.execute( 154 | // "UPDATE users 155 | // SET password = (?1), updated_at = datetime('now','localtime') 156 | // WHERE email = (?2);", 157 | // &[&hashed_new_password, &email.to_owned()], 158 | // )?; 159 | 160 | // Ok(()) 161 | // } 162 | 163 | // // Should use Result later. 164 | // pub fn is_registered(conn: &Connection, email: &str) -> Option { 165 | // // Should set updated_at here or default behavior for that at SQL in build.rs. 166 | // // Search more. 167 | // // https://stackoverflow.com/questions/14461851/how-to-have-an-automatic-timestamp-in-sqlite 168 | // let user = User::get(&conn, email.into()).unwrap(); // Remove this unwrap later with correct error handler 169 | // let user = user.get(0); 170 | 171 | // match user { 172 | // Some(user) => { 173 | // let User { 174 | // password: hashed, .. 175 | // } = user; 176 | // Some(hashed.to_owned()) 177 | // } 178 | // None => None, 179 | // } 180 | // } 181 | 182 | // pub fn set_identity_id(conn: &Connection, email: &str, identity_id: &str) -> Result<()> { 183 | // conn.execute( 184 | // "UPDATE users 185 | // SET identity_id = (?1) 186 | // WHERE email = (?2);", 187 | // &[&identity_id.to_owned(), &email.to_owned()], 188 | // )?; 189 | 190 | // Ok(()) 191 | // } 192 | 193 | // pub fn remove_identity_id(conn: &Connection, previous_identity_id: &str) -> Result<()> { 194 | // conn.execute( 195 | // "UPDATE users 196 | // SET identity_id = '' 197 | // WHERE identity_id = (?1);", 198 | // &[&previous_identity_id.to_owned()], 199 | // )?; 200 | 201 | // Ok(()) 202 | // } 203 | // } 204 | 205 | // #[derive(Debug, Serialize, Deserialize)] 206 | // pub struct UserPublic { 207 | // pub email: String, 208 | // pub created_at: NaiveDateTime, 209 | // pub updated_at: NaiveDateTime, 210 | // } 211 | 212 | // #[derive(Debug, Serialize, Deserialize)] 213 | // pub struct UserList(pub Vec); 214 | 215 | // impl UserList { 216 | // pub fn list(conn: &Connection) -> Result> { 217 | // let mut stmt = conn.prepare("SELECT * FROM users;")?; 218 | 219 | // let result = stmt.query_map(NO_PARAMS, |row| { 220 | // Ok(User { 221 | // id: row.get(0)?, 222 | // email: row.get(1)?, 223 | // password: row.get(2)?, 224 | // cash: row.get(3)?, 225 | // created_at: row.get(4)?, 226 | // updated_at: row.get(5)?, 227 | // identity_id: row.get(6)?, 228 | // }) 229 | // })?; 230 | 231 | // let mut users = Vec::new(); 232 | // for u in result { 233 | // users.push(u?); 234 | // } 235 | 236 | // Ok(users) 237 | // } 238 | 239 | // pub fn list_public(conn: &Connection) -> Result> { 240 | // let mut stmt = conn.prepare("SELECT * FROM users;")?; 241 | 242 | // let result = stmt.query_map(NO_PARAMS, |row| { 243 | // Ok(UserPublic { 244 | // email: row.get(1)?, 245 | // created_at: row.get(4)?, 246 | // updated_at: row.get(5)?, 247 | // }) 248 | // })?; 249 | 250 | // let mut users_public = Vec::new(); 251 | // for u in result { 252 | // users_public.push(u?); 253 | // } 254 | 255 | // Ok(users_public) 256 | // } 257 | // } 258 | -------------------------------------------------------------------------------- /src/read.rs: -------------------------------------------------------------------------------- 1 | // [Warp - React] 2 | 3 | // Server 4 | 5 | // 1. Move update_cash, delete_user, logout to private/ 6 | // and remove from user_router and user_handler. 7 | 8 | // 2. Travis CI or whatever to make it do continuous integration and testing. 9 | // (No need to manually use cargo c all the time.) 10 | 11 | // Client 12 | 13 | // 1. Organize menubar CSS and write more html components in separate HTML only file 14 | // 2. Write sign up form to home and when signup button is clicked? 15 | // 3. Login to Logout button when login. 16 | 17 | // Todo 18 | 19 | // Extract common parts to functions. 20 | // Find how to reuse SQLite connection. Should I remove build.rs and move it to function?) 21 | // Make a profit function and method. 22 | 23 | // Multipalyer Game 24 | 25 | // Compare 26 | 27 | // https://github.com/steadylearner/warp-diesel-ructe-sample 28 | // https://github.com/steadylearner/rust-warp-realworld-backend 29 | -------------------------------------------------------------------------------- /src/routes/authorized_route.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/steadylearner/Rust-Full-Stack/blob/master/microservices_with_docker/warp_client/src/routes/user_route.rs 2 | // https://docs.rs/warp/0.1.22/warp/filters/path/ 3 | 4 | // use warp::{ 5 | // filters::BoxedFilter, 6 | // path, 7 | // Filter, 8 | // }; 9 | 10 | // use super::{ 11 | // user_api_v1_path_prefix, 12 | // }; 13 | 14 | // pub fn authorized() -> BoxedFilter<()> { 15 | // warp::get() 16 | // .and(user_api_v1_path_prefix()) 17 | // .and(path("authorized")) 18 | // .and(path::end()) 19 | // .boxed() 20 | // } 21 | 22 | // 1. Login with CURL. 23 | // $curl -X POST localhost:8000/api/user/v1/login -c cookie.txt -H "Content-Type: application/json" -d '{ "email": "random@email.com", "password": "password" }' 24 | 25 | // 2. Test it work. 26 | // $curl -X GET localhost:8000/api/user/vi/authorized -b cookie.txt -L 27 | 28 | // Should return this. 29 | 30 | // 31 | // 32 | 33 | // 34 | // 35 | // You are authorized 36 | // 37 | 38 | // 39 | //

You are authorized

40 | // 41 | 42 | // -------------------------------------------------------------------------------- /src/routes/car_route.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/steadylearner/Rust-Full-Stack/blob/master/microservices_with_docker/warp_client/src/routes/user_route.rs 2 | // https://docs.rs/warp/0.1.22/warp/filters/path/ 3 | 4 | use crate::{ 5 | json_body, 6 | models::car::{CarRefundRequest, NewCarRequest}, 7 | }; 8 | 9 | use warp::{ 10 | body::{content_length_limit, json}, 11 | filters::BoxedFilter, 12 | path, Filter, 13 | }; 14 | 15 | use super::{ 16 | user_api_v1_path_prefix, 17 | // car_api_v1_path_prefix, 18 | }; 19 | 20 | pub fn list() -> BoxedFilter<()> { 21 | warp::get() 22 | .and(user_api_v1_path_prefix()) // Should be car_api_v1_path_prefix later? 23 | .and(path("car")) 24 | .and(path::end()) 25 | .boxed() 26 | } 27 | 28 | // $curl -X GET localhost:8000/api/user/v1/car -b cookie.txt -L 29 | 30 | pub fn buy() -> BoxedFilter<(NewCarRequest,)> { 31 | warp::post() 32 | .and(user_api_v1_path_prefix()) // Should be car_api_v1_path_prefix later? 33 | .and(path("car")) 34 | .and(path::end()) 35 | .and(json_body!()) 36 | .boxed() 37 | } 38 | 39 | // $curl -X POST localhost:8000/api/user/v1/car -b cookie.txt -L 40 | // -H "Content-Type: application/json" 41 | // -d '{ "price": 10000, "color": "red" }' 42 | // ($curl -X POST localhost:8000/api/user/v1/car -b cookie.txt -L -H "Content-Type: application/json" -d '{ "price": 10000, "color": "red" }') 43 | 44 | // Should be improved. 45 | pub fn refund() -> BoxedFilter<(CarRefundRequest,)> { 46 | warp::delete() 47 | .and(user_api_v1_path_prefix()) // Should be car_api_v1_path_prefix later? 48 | .and(path("car")) 49 | .and(path::end()) 50 | .and(json_body!()) 51 | .boxed() 52 | } 53 | 54 | // $curl -X DELETE localhost:8000/api/user/v1/car -b cookie.txt -L 55 | // -H "Content-Type: application/json" 56 | // -d '{ "car_id": 1 }' 57 | // ($curl -X DELETE localhost:8000/api/user/v1/car -b cookie.txt -L -H "Content-Type: application/json" -d '{ "car_id": 1 }') 58 | 59 | // These fails because of it is not started with /api/user/v1 and header set there? 60 | // Should find the better way and search about it? 61 | 62 | // pub fn buy() -> BoxedFilter<(NewCarRequest,)> { 63 | 64 | // warp::post() 65 | // .and(car_api_v1_path_prefix()) 66 | // .and(warp::path::end()) 67 | // .and(json_body) 68 | // .boxed() 69 | // } 70 | 71 | // $curl -X POST localhost:8000/api/car/v1 -b cookie.txt -L 72 | // -H "Content-Type: application/json" 73 | // -d '{ "price": 10000, "color": "red" }' 74 | // ($curl -X POST localhost:8000/api/car/v1 -b cookie.txt -L -H "Content-Type: application/json" -d '{ "price": 10000, "color": "red" }') 75 | 76 | // Should be simialr to user if you want to use the real cars. -------------------------------------------------------------------------------- /src/routes/game_route.rs: -------------------------------------------------------------------------------- 1 | use warp::{ 2 | body::{content_length_limit, json}, 3 | filters::BoxedFilter, 4 | path, Filter, 5 | }; 6 | 7 | use crate::{json_body, models::game::NewGameRequest}; 8 | 9 | use super::{ 10 | user_api_v1_path_prefix, 11 | // game_api_v1_path_prefix, 12 | }; 13 | 14 | pub fn new() -> BoxedFilter<(NewGameRequest,)> { 15 | warp::post() 16 | .and(user_api_v1_path_prefix()) // Should be game_api_v1_path_prefix later? 17 | .and(path("game")) 18 | .and(path::end()) 19 | .and(json_body!()) 20 | .boxed() 21 | } 22 | 23 | // Ok 24 | 25 | // 1. Cash(stake_amount) only 26 | 27 | // $curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L 28 | // -H "Content-Type: application/json" 29 | // -d '{ "stake_amount": 10000, "car_id": null, "number_of_participants": 2 }' 30 | // ($curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L -H "Content-Type: application/json" -d '{ "stake_amount": 10000, "car_id": null, "number_of_participants": 2 }') 31 | 32 | // 2. Car only 33 | 34 | // $curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L 35 | // -H "Content-Type: application/json" 36 | // -d '{ "stake_amount": null, "car_id": 1, "number_of_participants": 2 }' 37 | // ($curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L -H "Content-Type: application/json" -d '{ "stake_amount": 10000, "car_id": null, "number_of_participants": 2 }') 38 | 39 | // Err 40 | 41 | // 1. without Cash and Car(both null) 42 | 43 | // $curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L 44 | // -H "Content-Type: application/json" 45 | // -d '{ "stake_amount": null, "car_id": null, "number_of_participants": 2 }' 46 | // ($curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L -H "Content-Type: application/json" -d '{ "stake_amount": 10000, "car_id": null, "number_of_participants": 2 }') 47 | 48 | // 2. Both Cash and Car(both with value) 49 | 50 | // $curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L 51 | // -H "Content-Type: application/json" 52 | // -d '{ "stake_amount": null, "car_id": null, "number_of_participants": 2 }' 53 | // ($curl -X POST localhost:8000/api/user/v1/game -b cookie.txt -L -H "Content-Type: application/json" -d '{ "stake_amount": 10000, "car_id": null, "number_of_participants": 2 }') 54 | 55 | // Should write more tests to handle errors here. 56 | 57 | // 3. Numer of participants less than 2. -------------------------------------------------------------------------------- /src/routes/hello_route.rs: -------------------------------------------------------------------------------- 1 | // use warp::{ 2 | // filters::BoxedFilter, 3 | // path, 4 | // Filter, 5 | // }; 6 | 7 | // pub fn hello() -> BoxedFilter<(String, )> { 8 | // warp::get() 9 | // .and(path("hello")) 10 | // .and(path::param::()) 11 | // .boxed() 12 | // } 13 | -------------------------------------------------------------------------------- /src/routes/hi_route.rs: -------------------------------------------------------------------------------- 1 | // use warp::{ 2 | // filters::BoxedFilter, 3 | // path, 4 | // Filter, 5 | // }; 6 | 7 | // pub fn hi() -> BoxedFilter<(String, )> { 8 | // warp::get() 9 | // .and(path("hi")) 10 | // .and(path::param::()) 11 | // .boxed() 12 | // } 13 | -------------------------------------------------------------------------------- /src/routes/index_route.rs: -------------------------------------------------------------------------------- 1 | use warp::{filters::BoxedFilter, path, Filter}; 2 | 3 | pub fn get() -> BoxedFilter<()> { 4 | warp::get().and(path::end()).boxed() 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | // pub mod hello_route; 2 | // pub mod hi_route; 3 | // pub mod authorized_route; 4 | 5 | use warp::{ 6 | filters::BoxedFilter, 7 | path, Filter 8 | }; 9 | 10 | pub mod index_route; 11 | pub mod user_route; 12 | 13 | pub mod private; 14 | 15 | pub mod car_route; 16 | pub mod game_route; 17 | pub mod ranking_route; 18 | 19 | // Compare it with this that chain /String param. 20 | // https://github.com/steadylearner/Rust-Full-Stack/blob/master/microservices_with_docker/warp_client/src/routes/user_route.rs 21 | 22 | // Should I make a function or macro later only to substitute user, car, game etc part? 23 | // https://docs.rs/warp/0.2.2/warp/macro.path.htmal#path-prefixes 24 | pub fn user_api_v1_path_prefix() -> BoxedFilter<()> { 25 | path!("api" / "user" / "v1" / ..).boxed() 26 | } 27 | 28 | pub fn ranking_api_v1_path_prefix() -> BoxedFilter<()> { 29 | path!("api" / "ranking" / "v1" / ..).boxed() 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/private/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod profile_route; 2 | pub mod password_route; -------------------------------------------------------------------------------- /src/routes/private/password_route.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | json_body, 3 | models::private::{ 4 | password::UpdatePasswordRequest, 5 | } 6 | }; 7 | 8 | use warp::{ 9 | body::{content_length_limit, json}, 10 | filters::BoxedFilter, 11 | path, Filter, 12 | }; 13 | 14 | use super::super::user_api_v1_path_prefix; 15 | 16 | pub fn update_password() -> BoxedFilter<(UpdatePasswordRequest,)> { 17 | warp::patch() 18 | .and(user_api_v1_path_prefix()) 19 | .and(path("password")) 20 | .and(path::end()) 21 | .and(json_body!()) 22 | .boxed() 23 | } 24 | 25 | // $curl -X POST localhost:8000/api/user/v1/login -c cookie.txt -H "Content-Type: application/json" -d '{ "email": "random@email.com", "password": "password" }' 26 | 27 | // $curl -X PATCH localhost:8000/api/user/v1/password -b cookie.txt -L 28 | // -H "Content-Type: application/json" 29 | // -d '{ "oldPassword": "random@email.com", "newPassword": "newpassword" }' 30 | // ($curl -X PATCH localhost:8000/api/user/v1/password -b cookie.txt -L -H "Content-Type: application/json" -d '{ "oldPassword": "oldpassword", "newPassword": "newpassword" }') 31 | 32 | -------------------------------------------------------------------------------- /src/routes/private/profile_route.rs: -------------------------------------------------------------------------------- 1 | use warp::{ 2 | filters::BoxedFilter, 3 | path, Filter, 4 | }; 5 | 6 | use super::super::user_api_v1_path_prefix; 7 | 8 | pub fn get() -> BoxedFilter<()> { 9 | warp::get() 10 | .and(user_api_v1_path_prefix()) 11 | .and(path("profile")) 12 | .and(path::end()) 13 | .boxed() 14 | } 15 | 16 | // $curl -X POST localhost:8000/api/user/v1/login -c cookie.txt -H "Content-Type: application/json" -d '{ "email": "random@email.com", "password": "password" }' 17 | // $curl -X GET localhost:8000/api/user/v1/profile -b cookie.txt -L 18 | -------------------------------------------------------------------------------- /src/routes/ranking_route.rs: -------------------------------------------------------------------------------- 1 | use warp::{ 2 | filters::BoxedFilter, 3 | path, Filter, 4 | }; 5 | 6 | use super::{ 7 | ranking_api_v1_path_prefix, 8 | }; 9 | 10 | pub fn game_ranking_list() -> BoxedFilter<()> { 11 | warp::get() 12 | .and(ranking_api_v1_path_prefix()) 13 | .and(path("game")) 14 | .and(path::end()) 15 | .boxed() 16 | } 17 | 18 | // $curl localhost:8000/api/ranking/v1/game -------------------------------------------------------------------------------- /src/routes/user_route.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | json_body, 3 | models::user::{ 4 | requests::{NewUserRequest, LoginRequest, UpdateCashRequest, UpdateUserRequest} 5 | }, 6 | }; 7 | 8 | use warp::{ 9 | body::{content_length_limit, json}, 10 | filters::BoxedFilter, 11 | path, Filter, 12 | }; 13 | 14 | use super::user_api_v1_path_prefix; 15 | 16 | // https://github.com/axios/axios/issues/569 17 | pub fn register() -> BoxedFilter<(NewUserRequest,)> { 18 | 19 | // warp::any() 20 | warp::post() 21 | .and(user_api_v1_path_prefix()) 22 | .and(warp::path::end()) 23 | .and(json_body!()) 24 | .boxed() 25 | } 26 | 27 | // 1. Without CORS (Need to test with a separate frontend.) - POST (warp::post()) 28 | 29 | // $curl -X POST localhost:8000/api/user/v1 -H "Content-Type: application/json" 30 | // -d '{ "email": "random@email.com", "password": "password" }' 31 | // ($curl -X POST localhost:8000/api/user/v1 -H "Content-Type: application/json" -d '{ "email": "random@email.com", "password": "password" }') 32 | 33 | // 2. CORS with React - any here and OPTIONS in CURL or axios request. 34 | // (https://github.com/seanmonstar/warp/blob/master/tests/cors.rs) 35 | 36 | // $curl -i -X OPTIONS localhost:8000/api/user/v1 -H "origin: *" -H "access-control-request-method: POST" -H "Content-Type: application/json" -d '{ "email": "steady@learner.com", "password": "password" }' 37 | 38 | // Retruns this simialr to tests/cors_test.rs (Is this preflight?) 39 | 40 | // HTTP/1.1 200 OK 41 | // access-control-allow-headers: 42 | // access-control-allow-methods: POST 43 | // access-control-allow-origin: * 44 | // content-length: 0 45 | 46 | pub fn list() -> BoxedFilter<()> { 47 | warp::get() 48 | .and(user_api_v1_path_prefix()) 49 | .and(warp::path::end()) 50 | .boxed() 51 | } 52 | 53 | // $curl localhost:8000/api/user/v1 54 | 55 | pub fn do_login() -> BoxedFilter<(LoginRequest,)> { 56 | warp::post() 57 | .and(user_api_v1_path_prefix()) 58 | .and(path("login")) 59 | .and(path::end()) 60 | .and(json_body!()) 61 | .boxed() 62 | } 63 | 64 | // $curl -X POST localhost:8000/api/user/v1 -c cookie-file.txt -H "Content-Type: application/json" 65 | // -d '{ "email": "random@email.com", "password": "password" }' 66 | // ($curl -X POST localhost:8000/api/user/v1/login -c cookie.txt -H "Content-Type: application/json" -d '{ "email": "random@email.com", "password": "password" }') 67 | 68 | pub fn update_cash() -> BoxedFilter<(UpdateCashRequest,)> { 69 | warp::patch() 70 | .and(user_api_v1_path_prefix()) 71 | .and(path("cash")) 72 | .and(path::end()) 73 | .and(json_body!()) 74 | .boxed() 75 | } 76 | 77 | // $curl -X PATCH localhost:8000/api/user/v1/cash -b cookie.txt -L 78 | // -H "Content-Type: application/json" 79 | // -d '{ "amount": 1000000 }' 80 | // ($curl -X PATCH localhost:8000/api/user/v1/cash -b cookie.txt -L -H "Content-Type: application/json" -d '{ "amount": 100000 }') 81 | 82 | pub fn delete_user() -> BoxedFilter<()> { 83 | warp::delete() 84 | .and(user_api_v1_path_prefix()) 85 | .and(path::end()) 86 | .boxed() 87 | } 88 | 89 | // $curl -X DELETE localhost:8000/api/user/v1 -b cookie.txt -L 90 | 91 | pub fn logout() -> BoxedFilter<()> { 92 | // Does path_prefix is necesary here? 93 | warp::get() 94 | .and(user_api_v1_path_prefix()) 95 | .and(path("logout")) 96 | .and(path::end()) 97 | .boxed() 98 | } 99 | 100 | // $curl -X GET localhost:8000/api/user/v1/logout -b cookie.txt -L 101 | -------------------------------------------------------------------------------- /src/security/argon.rs: -------------------------------------------------------------------------------- 1 | use argon2::{hash_encoded, verify_encoded, Config}; 2 | use rand::Rng; 3 | 4 | // hash is from the database and credential is password from the user input. 5 | pub fn hash(credential: &[u8]) -> String { 6 | let salt = rand::thread_rng().gen::<[u8; 32]>(); 7 | let config = Config::default(); 8 | hash_encoded(credential, &salt, &config).unwrap() 9 | } 10 | 11 | pub fn verify(hash: &str, credential: &[u8]) -> bool { 12 | verify_encoded(hash, credential).unwrap_or(false) 13 | } 14 | -------------------------------------------------------------------------------- /src/security/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod argon; 2 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use warp::{self, fs, path, Filter}; 2 | 3 | use crate::{ 4 | session::user_session_filter, 5 | handlers::{ 6 | index_handler, 7 | user_handler, 8 | private::{ 9 | profile_handler, 10 | password_handler, 11 | }, 12 | car_handler, 13 | game_handler, 14 | ranking_handler, 15 | // The commented handlers are to test minimal examples. 16 | // hello_handler, 17 | // hi_handler, 18 | // authorized_handler, 19 | }, 20 | // Below are macros from api/ 21 | routes::{ 22 | index_route, 23 | user_route, 24 | private::{ 25 | profile_route, 26 | password_route, 27 | }, 28 | car_route, 29 | game_route, 30 | ranking_route, 31 | // The commented routes are to test minimal examples. 32 | // hello_route, 33 | // hi_route, 34 | // authorized_route, 35 | }, 36 | // From api/ 37 | index, 38 | register_user, 39 | do_login, 40 | // authorized, 41 | update_cash, 42 | get_profile, 43 | update_password, 44 | delete_user, 45 | logout, 46 | buy_a_car, 47 | list_cars, 48 | refund_a_car, 49 | new_game, 50 | game_ranking_list, 51 | // 52 | list_users, 53 | }; 54 | 55 | // Refer to them to make CORS work. 56 | // https://github.com/seanmonstar/warp/blob/master/tests/cors.rs 57 | // https://www.steadylearner.com/blog/read/How-to-use-CORS-and-OPTIONS-HTTP-request-with-Rust-Rocket 58 | 59 | pub fn end() -> impl Filter + Clone { 60 | // https://docs.rs/warp/0.2.2/warp/filters/fs/fn.dir.html 61 | // Use $RUST_LOG=warp::filters::fs=info cargo run --release 62 | // if you see the problem with this. 63 | // (https://github.com/steadylearner/Rust-Full-Stack/blob/master/React_Rust/server/warp/src/main.rs) 64 | let public_files = path("public") 65 | .and(fs::dir("./public/")) 66 | .with(warp::log("warp::filters::fs")); 67 | 68 | // https://docs.rs/warp/0.1.13/warp/filters/cors/fn.cors.html 69 | // https://github.com/seanmonstar/warp/blob/master/tests/cors.rs 70 | // Follow the example from others. 71 | // https://github.com/seanmonstar/warp/issues/361 72 | 73 | // https://docs.rs/warp/0.1.12/warp/trait.Filter.html#method.recover 74 | // Separate it with by GET / POST / PATCH / DELETE etc. 75 | // Then, separte it with auto required and others if necessary. 76 | index!() 77 | // .or(hello!()) 78 | // .or(hi!()) 79 | // It doens't work well with Parcel. 80 | // .or(register_user!().with(warp::cors().allow_any_origin().allow_method(Method::POST))) 81 | .or(register_user!()) 82 | .or(do_login!()) 83 | // .or(authorized!()) 84 | .or(get_profile!()) 85 | .or(update_password!()) 86 | .or(update_cash!()) 87 | .or(buy_a_car!()) 88 | .or(list_cars!()) 89 | .or(refund_a_car!()) 90 | .or(new_game!()) 91 | .or(delete_user!()) 92 | .or(logout!()) 93 | .or(list_users!()) 94 | .or(game_ranking_list!()) 95 | .or(public_files) 96 | } 97 | 98 | pub fn routes_info() { 99 | let target: String = "0.0.0.0:8000".parse().unwrap(); 100 | 101 | if !log_enabled!(log::Level::Info) { 102 | use console::Style; 103 | let blue = Style::new().blue(); 104 | println!("\nRust Warp Server ready at {}\n", blue.apply_to(&target)); 105 | } 106 | 107 | // info!("$curl 0.0.0.0:8000/hello/www.steadylearner.com to test the minimal end point."); 108 | // info!("$curl 0.0.0.0:8000/hi/www.steadylearner.com to test the Tera template views/"); 109 | // info!("$curl 0.0.0.0:8000/public/rust_teloxide_example.png to test the ./public/ files."); 110 | 111 | // Test Ok(Success) parts with (CURL, frontend, tests/). 112 | info!("/ is to see the index(home) page."); 113 | info!("/api/user/v1 is to test user CRUD relevant routes."); 114 | // 1. Register (true, true, false) 115 | // 2. List (true, false, false) 116 | // 3. Delete user (true, false, false) 117 | info!("/api/user/v1/login is to test login relevant routes."); 118 | // POST - Login (true, true, false) 119 | // The path below are only allowed for who already did login. 120 | 121 | // info!("/api/user/v1/authorized is to test the auth required page after a user do login."); 122 | info!("/api/user/v1/cash is to manage the cash of the user."); 123 | // PATHC- Update (true, true, false) 124 | info!("/api/user/v1/profile is to manage the profile of the user."); 125 | // 1. Get (true, false, false) 126 | info!("/api/user/v1/car is to manage the cars of the user."); 127 | // 1. Buy (true, false, false) 128 | // 2. List (false, false, false) 129 | // 3. Refund (true, false, false) 130 | info!("/api/user/v1/game is relevant to the car game currently."); 131 | // Should be /api/game/v1/car later? 132 | // 1. Play with Cash(true, false, false) 133 | // 2. Play with Car(true, false, false) 134 | // Then, make this work. 135 | info!("/api/user/v1/password is to update the password."); 136 | // PATCH - Update Password (curl, false, false) 137 | info!("/api/user/v1/logout is to test the logout."); 138 | // Get - Logout (true, true, false) 139 | 140 | info!("/api/ranking/v1/game is to see the car game ranking currently."); 141 | // 1. List of the car game ranking (true, false, false) 142 | } 143 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | use crate::{db::sqlite::SQLITEPOOL, models::user::user::User}; 2 | use warp::filters::{cookie, BoxedFilter}; 3 | use warp::{self, Filter}; 4 | 5 | // It is difficult with Warp sometimes. Then, use Hyper directly. 6 | // use hyper::HeaderMap; 7 | 8 | // pub fn get_header(name: &str) -> Option { 9 | // let mut map = HeaderMap::new(); 10 | // let value = map.get(name); 11 | // value 12 | // } 13 | 14 | #[derive(Debug)] 15 | pub struct UserSession { 16 | pub identity_id: String, 17 | pub email: String, 18 | pub password: String, 19 | pub cash: f64, 20 | pub user_id: i64, 21 | } 22 | 23 | pub fn create_user_session(identity_id: &str) -> Option { 24 | let conn = SQLITEPOOL.get().unwrap(); 25 | 26 | let user = User::get_by_identity_id(&conn, &identity_id); 27 | let user_session = match user { 28 | Ok(user) => { 29 | let user = user.get(0); 30 | // debug!("create_user_session match user {:#?}", &user); 31 | match user { 32 | Some(user) => { 33 | let User { 34 | email, 35 | password, 36 | cash, 37 | id: user_id, 38 | .. 39 | } = user; 40 | let user_session = UserSession { 41 | identity_id: identity_id.into(), 42 | email: email.to_owned(), 43 | password: password.to_owned(), 44 | cash: cash.to_owned(), 45 | user_id: user_id.to_owned(), 46 | }; 47 | Some(user_session) 48 | } 49 | None => None, 50 | } 51 | } 52 | Err(e) => { 53 | error!("{:#?}", e); 54 | None 55 | } 56 | }; 57 | // debug!("create_user_session user_session {:#?}", &user_session); 58 | user_session 59 | } 60 | 61 | // https://docs.rs/warp/0.2.2/warp/filters/any/fn.any.html 62 | pub fn user_session_filter() -> BoxedFilter<(Option,)> { 63 | // Handling session is just to read the private data from the browser. 64 | // Then, use it inside the server. 65 | 66 | cookie::optional("EXAUTH") // It returns filter 67 | .map(move |key: Option| { 68 | let key = key.as_ref().map(|s| &**s); 69 | // Current problem is here. Should find the reason. 70 | // println!("{:#?}", &key); Why this is none? 71 | // When I test it with /api/user/v1 it works. 72 | // But, it fails when I test it with /api/car/v1 73 | 74 | let user_session = if let Some(identity_id) = key { 75 | create_user_session(identity_id) 76 | } else { 77 | debug!("{}", "Fail to find identity_key from EXAUTH."); 78 | None 79 | }; 80 | // debug!("user_session filter user_session {:#?}", &user_session); 81 | 82 | user_session 83 | }) 84 | .boxed() 85 | } 86 | -------------------------------------------------------------------------------- /src/template_setup/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tera; 2 | -------------------------------------------------------------------------------- /src/template_setup/tera.rs: -------------------------------------------------------------------------------- 1 | // https://tera.netlify.com/docs 2 | // You can also refer to this. 3 | // https://github.com/seanmonstar/warp/blob/master/examples/handlebars_template.rs 4 | 5 | use tera::{Context, Tera}; 6 | use warp::{ 7 | self, 8 | reject::{custom, Reject}, 9 | Rejection, 10 | }; 11 | 12 | lazy_static! { 13 | pub static ref TERA: Tera = { 14 | let target = "src/views/**/*"; 15 | let mut tera = match Tera::new(&target) { 16 | Ok(t) => t, 17 | Err(e) => { 18 | eprintln!("Parsing error(s): {}", e); 19 | ::std::process::exit(1); 20 | } 21 | }; 22 | // https://www.google.com/search?client=firefox-b-d&q=what+is+HTML+escaping 23 | tera.autoescape_on(vec!["html", ".sql"]); 24 | // https://docs.rs/tera/0.5.0/tera/struct.Tera.html#method.register_filter 25 | // tera.register_filter("do_nothing", do_nothing_filter); // What this do? 26 | tera 27 | }; 28 | } 29 | 30 | // You should write more code here following the documenation of Warp. 31 | #[derive(Debug)] 32 | struct TemplateError; 33 | impl Reject for TemplateError {} 34 | 35 | pub fn render(name: &str, ctx: &Context) -> Result { 36 | TERA.render(name, &ctx).or(Err(custom(TemplateError))) 37 | } 38 | -------------------------------------------------------------------------------- /src/tests/cors_test.rs: -------------------------------------------------------------------------------- 1 | use warp::http::method::Method; 2 | use warp::Filter; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let a = warp::path!("api" / "user" / "v1") 7 | .and(warp::post()) 8 | .map(|| "") 9 | .with(warp::cors().allow_any_origin().allow_method(Method::POST)); 10 | let filter = a; 11 | let response = warp::test::request() 12 | .path("/api/user/v1") 13 | .method("OPTIONS") 14 | .header("origin", "*") 15 | .header("access-control-request-method", "POST") 16 | .reply(&filter) 17 | .await; 18 | 19 | println!("{:#?}", response); 20 | } 21 | 22 | Response { 23 | status: 200, 24 | version: HTTP/1.1, 25 | headers: { 26 | "access-control-allow-headers": "", 27 | "access-control-allow-methods": "POST", 28 | "access-control-allow-origin": "*", 29 | }, 30 | body: b"", 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/tests/hello_test.rs: -------------------------------------------------------------------------------- 1 | use warp::Filter; 2 | 3 | use crate::{ 4 | handlers::hello_handler, 5 | routes::hello_route, 6 | hello, 7 | }; 8 | 9 | // $cargo test -- --nocapture if you want to use println! etc. 10 | 11 | // or test just one function each time. 12 | // For example, $cargo test hello and it passes. 13 | 14 | #[cfg(test)] 15 | mod tests { 16 | // Note this useful idiom: importing names from outer (for mod tests) scope. 17 | use super::*; 18 | 19 | // Refer to curl commands in main.rs 20 | #[tokio::test] 21 | async fn hello() { 22 | // let hello = hello_route::hello() 23 | // .and_then(hello_handler::hello); 24 | 25 | let res = warp::test::request() 26 | .method("GET") 27 | .path("/hello/www.steadylearner.com") // 1. [Client] - Define request(path with datas) until this 28 | .reply(&hello!()) // 2. [Server] - How will you respond to it? With what? 29 | .await; 30 | 31 | assert_eq!(res.status(), 200, "Should return 200 OK."); 32 | println!("{:#?}", res.body()); 33 | } 34 | } -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hello_test; 2 | pub mod register_test; -------------------------------------------------------------------------------- /src/utils/game.rs: -------------------------------------------------------------------------------- 1 | pub fn get_fair_odd(number_of_participants: i64) -> f64 { 2 | let odd = 1f64 / number_of_participants as f64; 3 | odd 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod random; 2 | pub mod game; -------------------------------------------------------------------------------- /src/utils/random.rs: -------------------------------------------------------------------------------- 1 | use rand::distributions::Alphanumeric; 2 | use rand::thread_rng; 3 | use rand::Rng; 4 | 5 | // Can be used to make identity_id also. 6 | pub fn alphanumeric_key(len: usize) -> String { 7 | thread_rng().sample_iter(&Alphanumeric).take(len).collect() 8 | } 9 | -------------------------------------------------------------------------------- /src/views/hi.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | warp-example-app 7 | 8 | 9 | 10 |

Hi, {{name}}!

11 | 12 | 13 | -------------------------------------------------------------------------------- /src/views/index.tera: -------------------------------------------------------------------------------- 1 | {# It will be mostly used the render error pages. #} 2 | 3 | 4 | 5 | 6 | {% block head %} 7 | 8 | 9 | 10 | 11 | {% block title %} Game Demo by © {{name}} {% endblock title %} 12 | {% endblock head %} 13 | {% block css %} 14 | 15 | {% endblock css %} 16 | 17 | 18 |
19 | 37 |
38 | {% block content %} 39 |
40 | 41 |
42 | {% endblock content %} 43 |
44 | {# #} 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /src/views/ranking.tera: -------------------------------------------------------------------------------- 1 | {% extends "index.tera" %} 2 | 3 | 4 | {% block head %} 5 | {% block title %}Game Ranking{% endblock title %} 6 | {% endblock head %} 7 | {% block css %} 8 | {{ super() }} 9 | 10 | {% endblock css %} 11 | 12 | {# Move it to public/css/views/.css later #} 13 | {# #} 30 | 31 | {% block content %} 32 |

Game Ranking

33 | 34 | 35 | 36 | 37 | 38 | 39 | {# https://tera.netlify.com/docs/#for #} 40 | {% for ranking in game_ranking_list %} 41 | 42 | 43 | 44 | 45 | 46 | {% else %} 47 | No game records yet. 48 | {% endfor %} 49 |
RankEmailTotal Prize
{{loop.index}}{{ranking.email}}{{ranking.total_prize}}
50 | {% endblock content %} 51 | 52 | 53 | 54 | --------------------------------------------------------------------------------