├── .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 | [](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 |