├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── backend ├── .env.example ├── Cargo.toml ├── README.md └── src │ ├── dbs │ ├── mod.rs │ └── mongo.rs │ ├── gql │ ├── mod.rs │ ├── mutations.rs │ └── queries.rs │ ├── main.rs │ ├── projects │ ├── mod.rs │ ├── models.rs │ └── services.rs │ ├── users │ ├── cred.rs │ ├── mod.rs │ ├── models.rs │ └── services.rs │ └── util │ ├── common.rs │ ├── constant.rs │ └── mod.rs ├── data ├── budshome.sql ├── graphiql.jpg ├── handlebars.jpg └── yew.jpg ├── frontend-handlebars ├── .env.example ├── Cargo.toml ├── README.md ├── graphql │ ├── all_projects.graphql │ ├── all_users.graphql │ └── schema.graphql ├── scripts │ ├── length.rhai │ ├── rhai-repl.rs │ └── rhai-run.rs ├── src │ ├── main.rs │ ├── routes │ │ ├── mod.rs │ │ ├── projects.rs │ │ └── users.rs │ └── util │ │ ├── common.rs │ │ ├── constant.rs │ │ └── mod.rs ├── static │ ├── css │ │ └── style.css │ ├── favicon.png │ ├── imgs │ │ └── budshome.png │ └── js │ │ └── .gitkeep └── templates │ ├── index.html │ ├── projects │ └── index.html │ └── users │ └── index.html ├── frontend-yew ├── Cargo.toml ├── README.md ├── assets │ ├── css │ │ ├── style.css │ │ ├── style.sass │ │ └── style.scss │ ├── favicon.png │ ├── imgs │ │ └── budshome.png │ └── js │ │ └── .gitkeep ├── cfg.toml ├── graphql │ ├── all_projects.graphql │ ├── all_users.graphql │ └── schema.graphql ├── index.html ├── src │ ├── main.rs │ ├── pages │ │ ├── home.rs │ │ ├── mod.rs │ │ ├── projects.rs │ │ └── users.rs │ └── util │ │ ├── common.rs │ │ ├── constant.rs │ │ └── mod.rs └── trunk.toml └── rustfmt.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{graphql,sql,toml}] 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.rs] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.swp 4 | 5 | # Cargo.lock 6 | target 7 | *.rs.bk 8 | 9 | .env 10 | examples 11 | dist 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emmet.includeLanguages": { 3 | "rust": "html", 4 | }, 5 | "files.associations": { 6 | "*.html": "handlebars" 7 | }, 8 | "html.format.endWithNewline": true, 9 | "html.format.indentHandlebars": true, 10 | "html.format.indentInnerHtml": true 11 | } 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["./backend", "./frontend-handlebars", "./frontend-yew"] 3 | 4 | resolver = "2" 5 | 6 | [profile.dev] 7 | split-debuginfo = "unpacked" 8 | 9 | [profile.release] 10 | # panic = "abort" 11 | codegen-units = 1 12 | opt-level = "z" 13 | lto = true 14 | -------------------------------------------------------------------------------- /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] [云上于天 zzy linshi@budshome.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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 云上于天 zzy linshi@budshome.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tide-async-graphql-mongodb 2 | 3 | Clean boilerplate for graphql services, wasm/yew frontend, handlebars frontend. 4 | 5 | Using tide, rhai, async-graphql, surf, graphql-client, yew, handlebars, jsonwebtoken, and mongodb. 6 | 7 | See also: 8 | - https://github.com/zzy/surfer - Simple WIP blog & upcoming upgrades. 9 | - https://github.com/piexue/piexue.com - Multi-language CMS based on the Rust web stacks. 10 | 11 | ## Features 12 | 13 | - Demo site: 14 | - [niqin.com - NiQin Books Platform | 泥芹书馆](https://niqin.com) 15 | - [piexue.com - Project Matchmaking | 项目对接](https://piexue.com) 16 | 17 | - Graphql Services 18 | - [x] User register 19 | - [x] Salt and hash a password with PBKDF2 - 使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算 20 | - [x] Sign in 21 | - [x] JSON web token authentication - JWT 鉴权整合 22 | - [x] Change password 23 | - [x] Profile Update 24 | - [x] User: query & mutation 25 | - [x] Project: query & mutation 26 | - Web Application 27 | - [x] Client request, bring & parse GraphQL data 28 | - [x] Render data to template engine 29 | - [x] Define custom helper with Rhai scripting language 30 | 31 | ## Stacks 32 | 33 | - [Rust](https://www.rust-lang.org) - [中文资料集萃](https://niqin.com) 34 | - [Tide](https://crates.io/crates/tide) - [中文文档](https://tide-book.niqin.com) 35 | - [rhai](https://crates.io/crates/rhai) - Embedded Scripting for Rust 36 | - [async-graphql](https://crates.io/crates/async-graphql) - [中文文档](https://async-graphql.niqin.com) 37 | - [mongodb & mongo-rust-driver](https://crates.io/crates/mongodb) 38 | - [Surf](https://crates.io/crates/surf) 39 | - [yew](https://crates.io/crates/yew) 40 | - [graphql_client](https://crates.io/crates/graphql_client) 41 | - [handlebars-rust](https://crates.io/crates/handlebars) 42 | - [jsonwebtoken](https://crates.io/crates/jsonwebtoken) 43 | - [cookie-rs](https://crates.io/crates/cookie) 44 | 45 | ## MongoDB data 46 | 47 | MongoDB data(include structure & documents) file is `/data/budshome.sql`. 48 | 49 | If you need mongodb cloud count, email to me or wechat(微信): yupen-com, please. 50 | 51 | ## How to run? 52 | 53 | Please read: 54 | 55 | - [**Backend: graphql servies server**](./backend/README.md) 56 | - [**Frontend-yew: web application server**](./frontend-yew/README.md) 57 | - [**Frontend-handlebars: web application server**](./frontend-handlebars/README.md) 58 | 59 | ## Contributing 60 | 61 | You are welcome in contributing to this project. 62 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | ADDRESS=127.0.0.1 2 | PORT=8000 3 | 4 | GRAPHQL_PATH=graphql 5 | GRAPHIQL_PATH=graphiql 6 | 7 | MONGODB_URI=mongodb://surfer:surfer@127.0.0.1:27017 8 | MONGODB_BUDSHOME=budshome-test 9 | 10 | SITE_KEY=0F4EHz+1/hqVvZjuB8EcooQs1K6QKBvLUxqTHt4tpxE= 11 | CLAIM_EXP=10000000000 12 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "0.1.0" 4 | authors = ["zzy <9809920@qq.com>"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | futures = "0.3.17" 9 | async-std = { version = "1.10.0", features = ["attributes"] } 10 | tide = "0.16.0" 11 | 12 | dotenv = "0.15.0" 13 | lazy_static = "1.4.0" 14 | regex = "1.5.4" 15 | 16 | async-graphql = { version = "2.10.2", features = ["bson", "chrono"] } 17 | mongodb = { version = "2.0.0", default-features = false, features = [ 18 | "async-std-runtime", 19 | ] } 20 | 21 | serde = { version = "1.0.130", features = ["derive"] } 22 | jsonwebtoken = "7.2.0" 23 | ring = "0.16.20" 24 | base64 = "0.13.0" 25 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Graphql Services Server 2 | 3 | ## MongoDB data 4 | 5 | MongoDB data(include structure & documents) file is `/data/budshome.sql`. 6 | 7 | If you need mongodb cloud count, email to me or wechat(微信): yupen-com, please. 8 | 9 | ## Build & run 10 | 11 | ``` Bash 12 | git clone https://github.com/zzy/tide-async-graphql-mongodb.git 13 | cd tide-async-graphql-mongodb 14 | cargo build 15 | 16 | cd backend 17 | ``` 18 | 19 | Rename file `.env.example` to `.env`, or put the environment variables into a `.env` file: 20 | 21 | ``` 22 | ADDRESS=127.0.0.1 23 | PORT=8000 24 | 25 | GRAPHQL_PATH=graphql 26 | GRAPHIQL_PATH=graphiql 27 | 28 | MONGODB_URI=mongodb://mongo:mongo@127.0.0.1:27017 29 | MONGODB_BUDSHOME=budshome 30 | 31 | SITE_KEY=0F4EHz+1/hqVvZjuB8EcooQs1K6QKBvLUxqTHt4tpxE= 32 | CLAIM_EXP=10000000000 33 | ``` 34 | 35 | Then, build & run: 36 | 37 | ``` Bash 38 | cargo run 39 | ``` 40 | 41 | GraphiQL: connect to http://127.0.0.1:8000/graphiql with browser. 42 | 43 | ![Graphql Image](../data/graphiql.jpg) 44 | 45 | ## Queries 46 | 47 | - getUserByEmail(...): User! 48 | - getUserByUsername(...): User! 49 | - userSignIn(...): SignInfo! 50 | - allUsers(...): [User!]! 51 | - allProjects: [Project!]! 52 | - allProjectsByUser(...): [Project!]! 53 | 54 | ## MUTATIONS 55 | 56 | - userRegister(...): User! 57 | - userChangePassword(...): User! 58 | - userUpdateProfile(...): User! 59 | - addProject(...): Project! 60 | 61 | ## Sample Usage 62 | 63 | Sample mutation for user register: 64 | ``` 65 | mutation { 66 | userRegister( 67 | newUser: { 68 | email: "example@budshome.com", 69 | username: "我是谁", 70 | password: "wo#$shi^$shui" 71 | } 72 | ) { 73 | id 74 | email 75 | username 76 | } 77 | } 78 | ``` 79 | 80 | Sample query for user sign in: 81 | ``` 82 | { 83 | userSignIn( 84 | userAccount: { 85 | email: "example@budshome.com" 86 | username: "" 87 | password: "wo#$shi^$shui" 88 | } 89 | ) { 90 | email 91 | username 92 | token 93 | } 94 | } 95 | ``` 96 | 97 | When submit method `userSignIn`, a token would be generated, use this token for query all users and every user's projects: 98 | ``` 99 | { 100 | allUsers( 101 | token: "fyJ0eXAiOiJKV1Q..." 102 | ) { 103 | id 104 | email 105 | username 106 | 107 | projects { 108 | id 109 | userId 110 | subject 111 | website 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | Sample query and mutation for projects was similar to users. 118 | 119 | ## Contributing 120 | 121 | You are welcome in contributing to this project. 122 | -------------------------------------------------------------------------------- /backend/src/dbs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mongo; 2 | // pub mod postgres; 3 | // pub mod mysql; 4 | -------------------------------------------------------------------------------- /backend/src/dbs/mongo.rs: -------------------------------------------------------------------------------- 1 | use crate::util::constant::CFG; 2 | 3 | use mongodb::{Client, options::ClientOptions, Database}; 4 | 5 | pub struct DataSource { 6 | client: Client, 7 | pub db_budshome: Database, 8 | } 9 | 10 | #[allow(dead_code)] 11 | impl DataSource { 12 | pub async fn client(&self) -> Client { 13 | self.client.clone() 14 | } 15 | 16 | pub async fn init() -> DataSource { 17 | // Parse a connection string into an options struct. 18 | // environment variables defined in .env file 19 | let mut client_options = 20 | ClientOptions::parse(CFG.get("MONGODB_URI").unwrap()) 21 | .await 22 | .expect("Failed to parse options!"); 23 | // Manually set an option. 24 | client_options.app_name = 25 | Some("tide-async-graphql-mongodb".to_string()); 26 | 27 | // Get a handle to the deployment. 28 | let client = Client::with_options(client_options) 29 | .expect("Failed to initialize database!"); 30 | 31 | // Get a handle to a database. 32 | let db_budshome = client.database(CFG.get("MONGODB_BUDSHOME").unwrap()); 33 | 34 | // return mongodb datasource. 35 | DataSource { client: client, db_budshome: db_budshome } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/gql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod queries; 2 | pub mod mutations; 3 | 4 | use tide::{http::mime, Request, Response, StatusCode, Body}; 5 | 6 | use async_graphql::{ 7 | Schema, EmptySubscription, 8 | http::{GraphQLPlaygroundConfig, playground_source, receive_json}, 9 | }; 10 | 11 | use crate::State; 12 | 13 | use crate::util::constant::CFG; 14 | use crate::dbs::mongo; 15 | 16 | use crate::gql::{queries::QueryRoot, mutations::MutationRoot}; 17 | 18 | pub async fn build_schema() -> Schema 19 | { 20 | // get mongodb datasource. It can be added to: 21 | // 1. As global data for async-graphql. 22 | // 2. As application scope state of Tide 23 | // 3. Use lazy-static.rs. 24 | let mongo_ds = mongo::DataSource::init().await; 25 | 26 | // The root object for the query and Mutatio, and use EmptySubscription. 27 | // Add global mongodb datasource in the schema object. 28 | // let mut schema = Schema::new(QueryRoot, MutationRoot, EmptySubscription) 29 | Schema::build(QueryRoot, MutationRoot, EmptySubscription) 30 | .data(mongo_ds) 31 | .finish() 32 | } 33 | 34 | pub async fn graphql(req: Request) -> tide::Result { 35 | let schema = req.state().schema.clone(); 36 | let gql_resp = schema.execute(receive_json(req).await?).await; 37 | 38 | let mut resp = Response::new(StatusCode::Ok); 39 | resp.set_body(Body::from_json(&gql_resp)?); 40 | 41 | Ok(resp.into()) 42 | } 43 | 44 | pub async fn graphiql(_: Request) -> tide::Result { 45 | let mut resp = Response::new(StatusCode::Ok); 46 | resp.set_body(playground_source(GraphQLPlaygroundConfig::new( 47 | CFG.get("GRAPHQL_PATH").unwrap(), 48 | ))); 49 | resp.set_content_type(mime::HTML); 50 | 51 | Ok(resp.into()) 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/gql/mutations.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::Context; 2 | 3 | use crate::dbs::mongo::DataSource; 4 | use crate::util::constant::GqlResult; 5 | use crate::users::{ 6 | self, 7 | models::{User, NewUser}, 8 | }; 9 | use crate::projects::{ 10 | self, 11 | models::{Project, NewProject}, 12 | }; 13 | 14 | pub struct MutationRoot; 15 | 16 | #[async_graphql::Object] 17 | impl MutationRoot { 18 | // Add new user 19 | async fn user_register( 20 | &self, 21 | ctx: &Context<'_>, 22 | new_user: NewUser, 23 | ) -> GqlResult { 24 | let db = ctx.data_unchecked::().db_budshome.clone(); 25 | users::services::user_register(db, new_user).await 26 | } 27 | 28 | // Change user password 29 | async fn user_change_password( 30 | &self, 31 | ctx: &Context<'_>, 32 | cur_password: String, 33 | new_password: String, 34 | token: String, 35 | ) -> GqlResult { 36 | let db = ctx.data_unchecked::().db_budshome.clone(); 37 | users::services::user_change_password( 38 | db, 39 | &cur_password, 40 | &new_password, 41 | &token, 42 | ) 43 | .await 44 | } 45 | 46 | // update user profile 47 | async fn user_update_profile( 48 | &self, 49 | ctx: &Context<'_>, 50 | new_user: NewUser, 51 | token: String, 52 | ) -> GqlResult { 53 | let db = ctx.data_unchecked::().db_budshome.clone(); 54 | users::services::user_update_profile(db, new_user, &token).await 55 | } 56 | 57 | // Add new project 58 | async fn add_project( 59 | &self, 60 | ctx: &Context<'_>, 61 | new_project: NewProject, 62 | ) -> GqlResult { 63 | let db = ctx.data_unchecked::().db_budshome.clone(); 64 | projects::services::add_project(db, new_project).await 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/gql/queries.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::Context; 2 | use mongodb::bson::oid::ObjectId; 3 | 4 | use crate::dbs::mongo::DataSource; 5 | use crate::util::constant::GqlResult; 6 | use crate::users::{ 7 | self, 8 | models::{User, NewUser, SignInfo}, 9 | }; 10 | use crate::projects::{self, models::Project}; 11 | 12 | pub struct QueryRoot; 13 | 14 | #[async_graphql::Object] 15 | impl QueryRoot { 16 | // get user info by email 17 | async fn get_user_by_email( 18 | &self, 19 | ctx: &Context<'_>, 20 | email: String, 21 | ) -> GqlResult { 22 | let db = ctx.data_unchecked::().db_budshome.clone(); 23 | users::services::get_user_by_email(db, &email).await 24 | } 25 | 26 | // get user info by username 27 | async fn get_user_by_username( 28 | &self, 29 | ctx: &Context<'_>, 30 | username: String, 31 | ) -> GqlResult { 32 | let db = ctx.data_unchecked::().db_budshome.clone(); 33 | users::services::get_user_by_username(db, &username).await 34 | } 35 | 36 | async fn user_sign_in( 37 | &self, 38 | ctx: &Context<'_>, 39 | unknown_user: NewUser, 40 | ) -> GqlResult { 41 | let db = ctx.data_unchecked::().db_budshome.clone(); 42 | users::services::user_sign_in(db, unknown_user).await 43 | } 44 | 45 | // Get all Users, 46 | async fn all_users( 47 | &self, 48 | ctx: &Context<'_>, 49 | token: String, 50 | ) -> GqlResult> { 51 | let db = ctx.data_unchecked::().db_budshome.clone(); 52 | users::services::all_users(db, &token).await 53 | } 54 | 55 | // Get all Projects 56 | async fn all_projects(&self, ctx: &Context<'_>) -> GqlResult> { 57 | let db = ctx.data_unchecked::().db_budshome.clone(); 58 | projects::services::all_projects(db).await 59 | } 60 | 61 | // Get all Projects of one User 62 | async fn all_projects_by_user( 63 | &self, 64 | ctx: &Context<'_>, 65 | user_id: ObjectId, 66 | ) -> GqlResult> { 67 | let db = ctx.data_unchecked::().db_budshome.clone(); 68 | projects::services::all_projects_by_user(db, user_id).await 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /backend/src/main.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | mod dbs; 3 | mod gql; 4 | 5 | mod users; 6 | mod projects; 7 | 8 | use tide::http::headers::HeaderValue; 9 | use tide::security::{CorsMiddleware, Origin}; 10 | 11 | use crate::util::constant::CFG; 12 | use crate::gql::{build_schema, graphql, graphiql}; 13 | 14 | #[async_std::main] 15 | async fn main() -> Result<(), std::io::Error> { 16 | // tide logger 17 | tide::log::start(); 18 | 19 | // Initialize the application with state. 20 | let schema = build_schema().await; 21 | let app_state = State { schema: schema }; 22 | let mut app = tide::with_state(app_state); 23 | 24 | //environment variables defined in .env file 25 | app.at(CFG.get("GRAPHQL_PATH").unwrap()).post(graphql); 26 | app.at(CFG.get("GRAPHIQL_PATH").unwrap()).get(graphiql); 27 | 28 | // suggest to specify urls 29 | let cors = CorsMiddleware::new() 30 | .allow_methods("GET, POST, OPTIONS".parse::().unwrap()) 31 | .allow_origin(Origin::from("*")) 32 | .allow_credentials(false); 33 | app.with(cors); 34 | 35 | app.listen(format!( 36 | "{}:{}", 37 | CFG.get("ADDRESS").unwrap(), 38 | CFG.get("PORT").unwrap() 39 | )) 40 | .await?; 41 | 42 | Ok(()) 43 | } 44 | 45 | // Tide application scope state. 46 | #[derive(Clone)] 47 | pub struct State { 48 | pub schema: async_graphql::Schema< 49 | gql::queries::QueryRoot, 50 | gql::mutations::MutationRoot, 51 | async_graphql::EmptySubscription, 52 | >, 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/projects/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod services; 3 | -------------------------------------------------------------------------------- /backend/src/projects/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use mongodb::bson::oid::ObjectId; 3 | 4 | #[derive(Serialize, Deserialize, Clone)] 5 | pub struct Project { 6 | pub _id: ObjectId, 7 | pub user_id: ObjectId, 8 | pub subject: String, 9 | pub website: String, 10 | } 11 | 12 | #[async_graphql::Object] 13 | impl Project { 14 | pub async fn id(&self) -> ObjectId { 15 | self._id.clone() 16 | } 17 | 18 | pub async fn user_id(&self) -> ObjectId { 19 | self.user_id.clone() 20 | } 21 | 22 | pub async fn subject(&self) -> &str { 23 | self.subject.as_str() 24 | } 25 | 26 | pub async fn website(&self) -> &str { 27 | self.website.as_str() 28 | } 29 | } 30 | 31 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 32 | pub struct NewProject { 33 | pub user_id: ObjectId, 34 | pub subject: String, 35 | pub website: String, 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/projects/services.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::StreamExt; 2 | use mongodb::{ 3 | Database, 4 | bson::{oid::ObjectId, Document, doc, to_document, from_document}, 5 | }; 6 | 7 | use crate::util::constant::GqlResult; 8 | use crate::projects::models::{Project, NewProject}; 9 | 10 | // Create new project 11 | pub async fn add_project( 12 | db: Database, 13 | new_project: NewProject, 14 | ) -> GqlResult { 15 | let coll = db.collection::("projects"); 16 | 17 | let exist_document = coll 18 | .find_one( 19 | doc! {"user_id": &new_project.user_id, "subject": &new_project.subject}, 20 | None, 21 | ) 22 | .await?; 23 | if let Some(_document) = exist_document { 24 | println!("MongoDB document is exist!"); 25 | } else { 26 | let new_project_document = to_document(&new_project)?; 27 | 28 | // Insert into a MongoDB collection 29 | coll.insert_one(new_project_document, None) 30 | .await 31 | .expect("Failed to insert into a MongoDB collection!"); 32 | } 33 | 34 | let project_document = coll 35 | .find_one( 36 | doc! {"user_id": &new_project.user_id, "subject": &new_project.subject}, 37 | None, 38 | ) 39 | .await 40 | .expect("Document not found") 41 | .unwrap(); 42 | 43 | let project: Project = from_document(project_document)?; 44 | Ok(project) 45 | } 46 | 47 | // Find all projects 48 | pub async fn all_projects(db: Database) -> GqlResult> { 49 | let coll = db.collection::("projects"); 50 | 51 | let mut projects: Vec = vec![]; 52 | 53 | // Query all documents in the collection. 54 | let mut cursor = coll.find(None, None).await.unwrap(); 55 | 56 | // Iterate over the results of the cursor. 57 | while let Some(result) = cursor.next().await { 58 | match result { 59 | Ok(document) => { 60 | let project = from_document(document)?; 61 | projects.push(project); 62 | } 63 | Err(error) => { 64 | println!("Error to find doc: {}", error); 65 | } 66 | } 67 | } 68 | 69 | Ok(projects) 70 | } 71 | 72 | // Find all projects by user 73 | pub async fn all_projects_by_user( 74 | db: Database, 75 | user_id: ObjectId, 76 | ) -> GqlResult> { 77 | let coll = db.collection::("projects"); 78 | 79 | let mut projects: Vec = vec![]; 80 | 81 | // Query all documents in the collection. 82 | let mut cursor = coll.find(doc! {"user_id": user_id}, None).await.unwrap(); 83 | 84 | // Iterate over the results of the cursor. 85 | while let Some(result) = cursor.next().await { 86 | match result { 87 | Ok(document) => { 88 | let project = from_document(document)?; 89 | projects.push(project); 90 | } 91 | Err(error) => { 92 | println!("Error to find doc: {}", error); 93 | } 94 | } 95 | } 96 | 97 | Ok(projects) 98 | } 99 | -------------------------------------------------------------------------------- /backend/src/users/cred.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU32; 2 | use ring::{digest, pbkdf2}; 3 | 4 | static PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256; 5 | 6 | // The salt should have a user-specific component so that an attacker 7 | // cannot crack one password for multiple users. 8 | async fn salt(username: &str) -> Vec { 9 | let salt_component: [u8; 16] = [ 10 | // This value was generated from a secure PRNG. 11 | 0xd6, 0x26, 0x98, 0xda, 0xf4, 0xdc, 0x50, 0x52, 0x24, 0xf2, 0x27, 0xd1, 12 | 0xfe, 0x39, 0x01, 0x8a, 13 | ]; 14 | 15 | let mut salt = 16 | Vec::with_capacity(salt_component.len() + username.as_bytes().len()); 17 | 18 | salt.extend(salt_component.as_ref()); 19 | salt.extend(username.as_bytes()); 20 | 21 | salt 22 | } 23 | 24 | pub async fn cred_encode(username: &str, password: &str) -> String { 25 | const CREDENTIAL_LEN: usize = digest::SHA256_OUTPUT_LEN; 26 | type Credential = [u8; CREDENTIAL_LEN]; 27 | 28 | let salt = salt(username).await; 29 | 30 | let mut cred: Credential = [0u8; CREDENTIAL_LEN]; 31 | pbkdf2::derive( 32 | PBKDF2_ALG, 33 | NonZeroU32::new(100_000).unwrap(), 34 | &salt, 35 | password.as_bytes(), 36 | &mut cred, 37 | ); 38 | 39 | base64::encode(&cred) 40 | } 41 | 42 | pub async fn cred_verify( 43 | username: &str, 44 | attempted_password: &str, 45 | actual_cred: &str, 46 | ) -> bool { 47 | let salt = salt(username).await; 48 | let actual_cred_decode = base64::decode(actual_cred.as_bytes()).unwrap(); 49 | 50 | pbkdf2::verify( 51 | PBKDF2_ALG, 52 | NonZeroU32::new(100_000).unwrap(), 53 | &salt, 54 | attempted_password.as_bytes(), 55 | &actual_cred_decode, 56 | ) 57 | .is_ok() 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/users/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod services; 3 | 4 | mod cred; 5 | -------------------------------------------------------------------------------- /backend/src/users/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use mongodb::bson::oid::ObjectId; 3 | 4 | use crate::util::constant::GqlResult; 5 | use crate::dbs::mongo::DataSource; 6 | use crate::projects::models::Project; 7 | use crate::projects::services::all_projects_by_user; 8 | 9 | #[derive(Serialize, Deserialize, Clone)] 10 | pub struct User { 11 | pub _id: ObjectId, 12 | pub email: String, 13 | pub username: String, 14 | pub cred: String, 15 | } 16 | 17 | #[async_graphql::Object] 18 | impl User { 19 | pub async fn id(&self) -> ObjectId { 20 | self._id.clone() 21 | } 22 | 23 | pub async fn email(&self) -> &str { 24 | self.email.as_str() 25 | } 26 | 27 | pub async fn username(&self) -> &str { 28 | self.username.as_str() 29 | } 30 | 31 | pub async fn projects( 32 | &self, 33 | ctx: &async_graphql::Context<'_>, 34 | ) -> GqlResult> { 35 | let db = ctx.data_unchecked::().db_budshome.clone(); 36 | all_projects_by_user(db, self._id.clone()).await 37 | } 38 | } 39 | #[derive(Serialize, Deserialize, async_graphql::InputObject)] 40 | pub struct NewUser { 41 | pub email: String, 42 | pub username: String, 43 | pub cred: String, 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize)] 47 | pub struct SignInfo { 48 | pub email: String, 49 | pub username: String, 50 | pub token: String, 51 | } 52 | 53 | #[async_graphql::Object] 54 | impl SignInfo { 55 | pub async fn email(&self) -> &str { 56 | self.email.as_str() 57 | } 58 | 59 | pub async fn username(&self) -> &str { 60 | self.username.as_str() 61 | } 62 | 63 | pub async fn token(&self) -> &str { 64 | self.token.as_str() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/users/services.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::StreamExt; 2 | use mongodb::{ 3 | Database, 4 | bson::{Document, doc, to_document, from_document}, 5 | }; 6 | use async_graphql::{Error, ErrorExtensions}; 7 | use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; 8 | 9 | use crate::util::{ 10 | constant::{CFG, GqlResult}, 11 | common::{Claims, token_data}, 12 | }; 13 | use crate::users::models::{User, NewUser, SignInfo}; 14 | 15 | // Find user info by email 16 | pub async fn get_user_by_email(db: Database, email: &str) -> GqlResult { 17 | let coll = db.collection::("users"); 18 | 19 | let exist_document = coll.find_one(doc! {"email": email}, None).await; 20 | 21 | if let Ok(user_document_exist) = exist_document { 22 | if let Some(user_document) = user_document_exist { 23 | let user: User = from_document(user_document)?; 24 | Ok(user) 25 | } else { 26 | Err(Error::new("Email not found").extend_with(|err, eev| { 27 | eev.set("details", err.message.as_str()) 28 | })) 29 | } 30 | } else { 31 | Err(Error::new("Error searching mongodb") 32 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 33 | } 34 | } 35 | 36 | // Find user info by username 37 | pub async fn get_user_by_username( 38 | db: Database, 39 | username: &str, 40 | ) -> GqlResult { 41 | let coll = db.collection::("users"); 42 | 43 | let exist_document = coll.find_one(doc! {"username": username}, None).await; 44 | 45 | if let Ok(user_document_exist) = exist_document { 46 | if let Some(user_document) = user_document_exist { 47 | let user: User = from_document(user_document)?; 48 | Ok(user) 49 | } else { 50 | Err(Error::new("Username not found").extend_with(|err, eev| { 51 | eev.set("details", err.message.as_str()) 52 | })) 53 | } 54 | } else { 55 | Err(Error::new("Error searching mongodb") 56 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 57 | } 58 | } 59 | 60 | // Create new user 61 | pub async fn user_register( 62 | db: Database, 63 | mut new_user: NewUser, 64 | ) -> GqlResult { 65 | let coll = db.collection::("users"); 66 | 67 | new_user.email = new_user.email.to_lowercase(); 68 | new_user.username = new_user.username.to_lowercase(); 69 | 70 | if self::get_user_by_email(db.clone(), &new_user.email).await.is_ok() { 71 | Err(Error::new("email exists") 72 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 73 | } else if self::get_user_by_username(db.clone(), &new_user.username) 74 | .await 75 | .is_ok() 76 | { 77 | Err(Error::new("username exists") 78 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 79 | } else { 80 | new_user.cred = 81 | super::cred::cred_encode(&new_user.username, &new_user.cred).await; 82 | let new_user_document = to_document(&new_user)?; 83 | 84 | // Insert into a MongoDB collection 85 | coll.insert_one(new_user_document, None) 86 | .await 87 | .expect("Failed to insert into a MongoDB collection!"); 88 | 89 | self::get_user_by_email(db.clone(), &new_user.email).await 90 | } 91 | } 92 | 93 | // User sign in 94 | pub async fn user_sign_in( 95 | db: Database, 96 | unknown_user: NewUser, 97 | ) -> GqlResult { 98 | unknown_user.email.to_lowercase(); 99 | unknown_user.username.to_lowercase(); 100 | 101 | let user_res; 102 | match regex::Regex::new(r"(@)").unwrap().is_match(&unknown_user.email) { 103 | true => { 104 | user_res = 105 | self::get_user_by_email(db.clone(), &unknown_user.email).await; 106 | } 107 | false => { 108 | user_res = 109 | self::get_user_by_username(db.clone(), &unknown_user.username) 110 | .await; 111 | } 112 | } 113 | 114 | if let Ok(user) = user_res { 115 | if super::cred::cred_verify( 116 | &user.username, 117 | &unknown_user.cred, 118 | &user.cred, 119 | ) 120 | .await 121 | { 122 | let mut header = Header::default(); 123 | // header.kid = Some("signing_key".to_owned()); 124 | header.alg = Algorithm::HS512; 125 | 126 | let site_key = CFG.get("SITE_KEY").unwrap().as_bytes(); 127 | let claim_exp = 128 | CFG.get("CLAIM_EXP").unwrap().parse::().unwrap(); 129 | let claims = Claims { 130 | email: user.email.to_owned(), 131 | username: user.username.to_owned(), 132 | exp: claim_exp, 133 | }; 134 | 135 | let token = match encode( 136 | &header, 137 | &claims, 138 | &EncodingKey::from_secret(site_key), 139 | ) { 140 | Ok(t) => t, 141 | Err(error) => Err(Error::new(format!( 142 | "Error to encode token: {}", 143 | error 144 | )) 145 | .extend_with(|err, eev| { 146 | eev.set("details", err.message.as_str()) 147 | }))?, 148 | }; 149 | 150 | let sign_info = SignInfo { 151 | email: user.email, 152 | username: user.username, 153 | token: token, 154 | }; 155 | Ok(sign_info) 156 | } else { 157 | Err(Error::new("Invalid credential").extend_with(|err, eev| { 158 | eev.set("details", err.message.as_str()) 159 | })) 160 | } 161 | } else { 162 | Err(Error::new("User not exist") 163 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 164 | } 165 | } 166 | 167 | // Find all users 168 | pub async fn all_users(db: Database, token: &str) -> GqlResult> { 169 | let token_data = token_data(token).await; 170 | if token_data.is_ok() { 171 | let coll = db.collection::("users"); 172 | 173 | let mut users: Vec = vec![]; 174 | 175 | // Query all documents in the collection. 176 | let mut cursor = coll.find(None, None).await.unwrap(); 177 | 178 | // Iterate over the results of the cursor. 179 | while let Some(result) = cursor.next().await { 180 | match result { 181 | Ok(document) => { 182 | let user = from_document(document)?; 183 | users.push(user); 184 | } 185 | Err(error) => { 186 | Err(Error::new(format!("Error to find doc: {}", error)) 187 | .extend_with(|err, eev| { 188 | eev.set("details", err.message.as_str()) 189 | }))? 190 | } 191 | } 192 | } 193 | 194 | Ok(users) 195 | } else { 196 | Err(Error::new(format!("{}", token_data.err().unwrap())) 197 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 198 | } 199 | } 200 | 201 | // Change user password 202 | pub async fn user_change_password( 203 | db: Database, 204 | cur_password: &str, 205 | new_password: &str, 206 | token: &str, 207 | ) -> GqlResult { 208 | let token_data = token_data(token).await; 209 | if let Ok(data) = token_data { 210 | let email = data.claims.email; 211 | let user_res = self::get_user_by_email(db.clone(), &email).await; 212 | if let Ok(mut user) = user_res { 213 | if super::cred::cred_verify( 214 | &user.username, 215 | cur_password, 216 | &user.cred, 217 | ) 218 | .await 219 | { 220 | user.cred = 221 | super::cred::cred_encode(&user.username, new_password) 222 | .await; 223 | 224 | let coll = db.collection::("users"); 225 | coll.update_one( 226 | doc! {"_id": &user._id}, 227 | doc! {"$set": {"cred": &user.cred}}, 228 | None, 229 | ) 230 | .await 231 | .expect("Failed to update a MongoDB collection!"); 232 | 233 | Ok(user) 234 | } else { 235 | Err(Error::new("Error verifying current password").extend_with( 236 | |err, eev| eev.set("details", err.message.as_str()), 237 | )) 238 | } 239 | } else { 240 | Err(Error::new("User not exist").extend_with(|err, eev| { 241 | eev.set("details", err.message.as_str()) 242 | })) 243 | } 244 | } else { 245 | Err(Error::new(format!("{}", token_data.err().unwrap())) 246 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 247 | } 248 | } 249 | 250 | // Update user profile 251 | pub async fn user_update_profile( 252 | db: Database, 253 | new_user: NewUser, 254 | token: &str, 255 | ) -> GqlResult { 256 | let token_data = token_data(token).await; 257 | if let Ok(data) = token_data { 258 | let email = data.claims.email; 259 | let user_res = self::get_user_by_email(db.clone(), &email).await; 260 | if let Ok(mut user) = user_res { 261 | let coll = db.collection::("users"); 262 | 263 | user.email = new_user.email.to_lowercase(); 264 | user.username = new_user.username.to_lowercase(); 265 | 266 | let user_document = to_document(&user)?; 267 | 268 | coll.find_one_and_replace( 269 | doc! {"_id": &user._id}, 270 | user_document, 271 | None, 272 | ) 273 | .await 274 | .expect("Failed to replace a MongoDB collection!"); 275 | 276 | Ok(user) 277 | } else { 278 | Err(Error::new("User not exist").extend_with(|err, eev| { 279 | eev.set("details", err.message.as_str()) 280 | })) 281 | } 282 | } else { 283 | Err(Error::new(format!("{}", token_data.err().unwrap())) 284 | .extend_with(|err, eev| eev.set("details", err.message.as_str()))) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /backend/src/util/common.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use jsonwebtoken::{ 3 | decode, TokenData, Algorithm, DecodingKey, Validation, errors::Error, 4 | }; 5 | 6 | use crate::util::constant::CFG; 7 | 8 | #[derive(Debug, Serialize, Deserialize)] 9 | pub struct Claims { 10 | pub email: String, 11 | pub username: String, 12 | pub exp: usize, 13 | } 14 | 15 | pub async fn token_data(token: &str) -> Result, Error> { 16 | let site_key = CFG.get("SITE_KEY").unwrap().as_bytes(); 17 | 18 | let data = decode::( 19 | token, 20 | &DecodingKey::from_secret(site_key), 21 | &Validation::new(Algorithm::HS512), 22 | ); 23 | 24 | data 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/util/constant.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use lazy_static::lazy_static; 3 | use std::collections::HashMap; 4 | 5 | pub type GqlResult = std::result::Result; 6 | 7 | lazy_static! { 8 | // CFG variables defined in .env file 9 | pub static ref CFG: HashMap<&'static str, String> = { 10 | dotenv().ok(); 11 | 12 | let mut map = HashMap::new(); 13 | 14 | map.insert( 15 | "ADDRESS", 16 | dotenv::var("ADDRESS").expect("Expected ADDRESS to be set in env!"), 17 | ); 18 | map.insert( 19 | "PORT", 20 | dotenv::var("PORT").expect("Expected PORT to be set in env!"), 21 | ); 22 | 23 | map.insert( 24 | "GRAPHQL_PATH", 25 | dotenv::var("GRAPHQL_PATH").expect("Expected GRAPHQL_PATH to be set in env!"), 26 | ); 27 | map.insert( 28 | "GRAPHIQL_PATH", 29 | dotenv::var("GRAPHIQL_PATH").expect("Expected GRAPHIQL_PATH to be set in env!"), 30 | ); 31 | 32 | map.insert( 33 | "MONGODB_URI", 34 | dotenv::var("MONGODB_URI").expect("Expected MONGODB_URI to be set in env!"), 35 | ); 36 | map.insert( 37 | "MONGODB_BUDSHOME", 38 | dotenv::var("MONGODB_BUDSHOME").expect("Expected MONGODB_BUDSHOME to be set in env!"), 39 | ); 40 | 41 | map.insert( 42 | "SITE_KEY", 43 | dotenv::var("SITE_KEY").expect("Expected SITE_KEY to be set in env!"), 44 | ); 45 | map.insert( 46 | "CLAIM_EXP", 47 | dotenv::var("CLAIM_EXP").expect("Expected CLAIM_EXP to be set in env!"), 48 | ); 49 | 50 | map 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constant; 2 | pub mod common; 3 | -------------------------------------------------------------------------------- /data/budshome.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Source Server Type : MongoDB 3 | Source Server Version : 40404 4 | */ 5 | 6 | 7 | // ---------------------------- 8 | // Collection structure for projects 9 | // ---------------------------- 10 | db.getCollection("projects").drop(); 11 | db.createCollection("projects"); 12 | 13 | // ---------------------------- 14 | // Documents of projects 15 | // ---------------------------- 16 | db.getCollection("projects").insert([ { 17 | _id: ObjectId("5ff318c100e6569b006a2475"), 18 | "user_id": ObjectId("5ff82b2c0076cc8b00e5cddb"), 19 | subject: "测试项目", 20 | website: "https://budshome.com" 21 | } ]); 22 | db.getCollection("projects").insert([ { 23 | _id: ObjectId("5ff318ee00436493006a2476"), 24 | "user_id": ObjectId("5ff82b2c0076cc8b00e5cddb"), 25 | subject: "测试项目2", 26 | website: "https://budshome.com" 27 | } ]); 28 | db.getCollection("projects").insert([ { 29 | _id: ObjectId("5ff319120023102e006a2477"), 30 | "user_id": ObjectId("5ff83f4b00e8fda000e5cddc"), 31 | subject: "测试项目3", 32 | website: "https://budshome.com" 33 | } ]); 34 | 35 | // ---------------------------- 36 | // Collection structure for users 37 | // ---------------------------- 38 | db.getCollection("users").drop(); 39 | db.createCollection("users"); 40 | 41 | // ---------------------------- 42 | // Documents of users 43 | // ---------------------------- 44 | db.getCollection("users").insert([ { 45 | _id: ObjectId("5ff82b2c0076cc8b00e5cddb"), 46 | email: "ok@budshome.com", 47 | username: "我谁24ok32", 48 | cred: "bOCMU4h9aO4o6jP3cujPDQi+b3Ig6JhZcLPWFpqbnog=" 49 | } ]); 50 | db.getCollection("users").insert([ { 51 | _id: ObjectId("5ff83f4b00e8fda000e5cddc"), 52 | email: "oka@budshome.com", 53 | username: "我s谁24ok32", 54 | cred: "+d/XvT9JE/zjt6R/IAkwwwpk8q6y2Jhv3EvRH/UHEYE=" 55 | } ]); 56 | db.getCollection("users").insert([ { 57 | _id: ObjectId("5ffd710400b6b84e000349f8"), 58 | email: "oka2@budshome.com", 59 | username: "我2s谁24ok32", 60 | cred: "801v0TV0h1XGMmCVTwItWjieGhI4hrGRAgNcutK8IW0=" 61 | } ]); 62 | db.getCollection("users").insert([ { 63 | _id: ObjectId("5ffdb3fa00bbdf3a007a2988"), 64 | email: "afasf@budshome.com", 65 | username: "哈哈", 66 | cred: "P38V7+1Q5sjuKvaZEXnXQqI9SiY6ZMisB8QfUOP91Ao=" 67 | } ]); 68 | db.getCollection("users").insert([ { 69 | _id: ObjectId("600b7a2700e7c21500d6cc1e"), 70 | email: "oka22@budshome.com", 71 | username: "我22s谁24ok32", 72 | cred: "fF7BBhTWbTGmC4Tu1rcw93D5S+G57WeDtzVmQjz0jro=" 73 | } ]); 74 | db.getCollection("users").insert([ { 75 | _id: ObjectId("600b7a5300adcd7900d6cc1f"), 76 | email: "iok@budshome.com", 77 | username: "我是ok", 78 | cred: "peCwspEaVw3HB05ObIpnGxgK2VSQOCmgxjzFEOY+fk0=" 79 | } ]); 80 | db.getCollection("users").insert([ { 81 | _id: ObjectId("600b8064000a5ca30024199e"), 82 | email: "iok2@budshome.com", 83 | username: "我是ok2", 84 | cred: "SJs0tA07rSN+4AHvLuN9zgZkihqJ+5no+lSax8DR8uE=" 85 | } ]); 86 | db.getCollection("users").insert([ { 87 | _id: ObjectId("60894ddb00dac15e00911d8b"), 88 | email: "ooo@ooo.com", 89 | username: "哈哈哈", 90 | cred: "P38V7+1Q5sjuKvaZEXnXQqI9SiY6ZMisB8QfUOP91Ao=" 91 | } ]); 92 | db.getCollection("users").insert([ { 93 | _id: ObjectId("608954d900136b6c0041ae09"), 94 | email: "budshome@budshome.com", 95 | username: "我是谁", 96 | cred: "P38V7+1Q5sjuKvaZEXnXQqI9SiY6ZMisB8QfUOP91Ao=" 97 | } ]); 98 | -------------------------------------------------------------------------------- /data/graphiql.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/data/graphiql.jpg -------------------------------------------------------------------------------- /data/handlebars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/data/handlebars.jpg -------------------------------------------------------------------------------- /data/yew.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/data/yew.jpg -------------------------------------------------------------------------------- /frontend-handlebars/.env.example: -------------------------------------------------------------------------------- 1 | ADDRESS=127.0.0.1 2 | PORT=3000 3 | 4 | GRAPHQL_PORT=8000 5 | GRAPHQL_PATH=graphql 6 | GRAPHIQL_PATH=graphiql 7 | -------------------------------------------------------------------------------- /frontend-handlebars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend-handlebars" 3 | version = "0.1.0" 4 | authors = ["zzy <9809920@qq.com>"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | async-std = { version = "1.10.0", features = ["attributes"] } 9 | tide = "0.16.0" 10 | 11 | dotenv = "0.15.0" 12 | lazy_static = "1.4.0" 13 | 14 | serde = { version = "1.0.130", features = ["derive"] } 15 | serde_json = "1.0.68" 16 | 17 | surf = "2.3.1" 18 | graphql_client = "0.10.0" 19 | handlebars = { version = "4.1.3", features = ["script_helper"] } 20 | 21 | [dev-dependencies] 22 | rhai = "1.0.6" 23 | -------------------------------------------------------------------------------- /frontend-handlebars/README.md: -------------------------------------------------------------------------------- 1 | # Web Application Server - handlebars 2 | 3 | ``` Bash 4 | git clone https://github.com/zzy/tide-async-graphql-mongodb.git 5 | cd tide-async-graphql-mongodb 6 | cargo build 7 | 8 | cd frontend-handlebars 9 | ``` 10 | 11 | Rename file `.env.example` to `.env`, or put the environment variables into a `.env` file: 12 | 13 | ``` 14 | ADDRESS=127.0.0.1 15 | PORT=3000 16 | 17 | GRAPHQL_PORT=8000 18 | GRAPHQL_PATH=graphql 19 | GRAPHIQL_PATH=graphiql 20 | ``` 21 | 22 | ## Build & Run: 23 | 24 | ``` Bash 25 | cargo run 26 | ``` 27 | Then connect to http://127.0.0.1:3000 with browser. 28 | 29 | ![Client Image](../data/handlebars.jpg) 30 | 31 | ## How to Test & Run `rhai scripts` 32 | 33 | You could use `rhai-repl` to test your rhai code, and use `rhai-run` to run it. `rhai-repl.rs` and `rhai-run.rs` are in the folder `frontend-handlebars/scripts`, please copy them into `frontend-handlebars/examples` folder, then test or run rhai code with command: 34 | 35 | ``` bash 36 | cargo run --example / 37 | ``` 38 | 39 | If you would want to install the rhai tool, use the command 40 | 41 | ``` bash 42 | cargo install --path . --example / 43 | ``` 44 | 45 | then test rhai code using `rhai-repl`, and run scripts using the `rhai-run`: 46 | 47 | ``` bash 48 | rhai-run ./scripts/script_to_run.rhai 49 | ``` 50 | 51 | ## Contributing 52 | 53 | You are welcome in contributing to this project. 54 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/all_projects.graphql: -------------------------------------------------------------------------------- 1 | query AllProjects { 2 | allProjects { 3 | id 4 | userId 5 | subject 6 | website 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/all_users.graphql: -------------------------------------------------------------------------------- 1 | query AllUsers($token: String!) { 2 | allUsers( 3 | token: $token 4 | ) { 5 | id 6 | email 7 | username 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend-handlebars/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | mutation: MutationRoot 4 | } 5 | 6 | # Directs the executor to query only when the field exists. 7 | directive @ifdef on FIELD 8 | 9 | type MutationRoot { 10 | userRegister(newUser: NewUser!): User! 11 | userChangePassword( 12 | curPassword: String! 13 | newPassword: String! 14 | token: String! 15 | ): User! 16 | userUpdateProfile(newUser: NewUser!, token: String!): User! 17 | addProject(newProject: NewProject!): Project! 18 | } 19 | 20 | input NewProject { 21 | userId: ObjectId! 22 | subject: String! 23 | website: String! 24 | } 25 | 26 | input NewUser { 27 | email: String! 28 | username: String! 29 | cred: String! 30 | } 31 | 32 | scalar ObjectId 33 | 34 | type Project { 35 | id: ObjectId! 36 | userId: ObjectId! 37 | subject: String! 38 | website: String! 39 | } 40 | 41 | type QueryRoot { 42 | getUserByEmail(email: String!): User! 43 | getUserByUsername(username: String!): User! 44 | userSignIn(unknownUser: NewUser!): SignInfo! 45 | allUsers(token: String!): [User!]! 46 | allProjects: [Project!]! 47 | allProjectsByUser(userId: ObjectId!): [Project!]! 48 | } 49 | 50 | type SignInfo { 51 | email: String! 52 | username: String! 53 | token: String! 54 | } 55 | 56 | type User { 57 | id: ObjectId! 58 | email: String! 59 | username: String! 60 | projects: [Project!]! 61 | } 62 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/length.rhai: -------------------------------------------------------------------------------- 1 | let username = params[0]; 2 | 3 | username.len() 4 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/rhai-repl.rs: -------------------------------------------------------------------------------- 1 | use rhai::{Dynamic, Engine, EvalAltResult, Module, Scope, AST}; 2 | 3 | use std::{ 4 | env, 5 | fs::File, 6 | io::{stdin, stdout, Read, Write}, 7 | path::Path, 8 | process::exit, 9 | }; 10 | 11 | /// Pretty-print error. 12 | fn print_error(input: &str, mut err: EvalAltResult) { 13 | let lines: Vec<_> = input.trim().split('\n').collect(); 14 | let pos = err.take_position(); 15 | 16 | let line_no = if lines.len() > 1 { 17 | if pos.is_none() { 18 | "".to_string() 19 | } else { 20 | format!("{}: ", pos.line().unwrap()) 21 | } 22 | } else { 23 | "".to_string() 24 | }; 25 | 26 | // Print error position 27 | if pos.is_none() { 28 | // No position 29 | println!("{}", err); 30 | } else { 31 | // Specific position - print line text 32 | println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); 33 | 34 | // Display position marker 35 | println!( 36 | "{0:>1$} {2}", 37 | "^", 38 | line_no.len() + pos.position().unwrap(), 39 | err 40 | ); 41 | } 42 | } 43 | 44 | /// Print help text. 45 | fn print_help() { 46 | println!("help => print this help"); 47 | println!("quit, exit => quit"); 48 | println!("scope => print all variables in the scope"); 49 | #[cfg(feature = "metadata")] 50 | println!("functions => print all functions defined"); 51 | println!("ast => print the last AST (optimized)"); 52 | println!("astu => print the last raw, un-optimized AST"); 53 | println!(r"end a line with '\' to continue to the next line."); 54 | println!(); 55 | } 56 | 57 | fn main() { 58 | let title = format!("Rhai REPL tool (version {})", env!("CARGO_PKG_VERSION")); 59 | println!("{}", title); 60 | println!("{0:=<1$}", "", title.len()); 61 | print_help(); 62 | 63 | // Initialize scripting engine 64 | let mut engine = Engine::new(); 65 | 66 | #[cfg(not(feature = "no_module"))] 67 | #[cfg(not(feature = "no_std"))] 68 | { 69 | // Load init scripts 70 | let mut contents = String::new(); 71 | let mut has_init_scripts = false; 72 | 73 | for filename in env::args().skip(1) { 74 | let filename = match Path::new(&filename).canonicalize() { 75 | Err(err) => { 76 | eprintln!("Error script file path: {}\n{}", filename, err); 77 | exit(1); 78 | } 79 | Ok(f) => f, 80 | }; 81 | 82 | contents.clear(); 83 | 84 | let mut f = match File::open(&filename) { 85 | Err(err) => { 86 | eprintln!( 87 | "Error reading script file: {}\n{}", 88 | filename.to_string_lossy(), 89 | err 90 | ); 91 | exit(1); 92 | } 93 | Ok(f) => f, 94 | }; 95 | 96 | if let Err(err) = f.read_to_string(&mut contents) { 97 | println!( 98 | "Error reading script file: {}\n{}", 99 | filename.to_string_lossy(), 100 | err 101 | ); 102 | exit(1); 103 | } 104 | 105 | let module = match engine 106 | .compile(&contents) 107 | .map_err(|err| err.into()) 108 | .and_then(|mut ast| { 109 | ast.set_source(filename.to_string_lossy().to_string()); 110 | Module::eval_ast_as_new(Default::default(), &ast, &engine) 111 | }) { 112 | Err(err) => { 113 | let filename = filename.to_string_lossy(); 114 | 115 | eprintln!("{:=<1$}", "", filename.len()); 116 | eprintln!("{}", filename); 117 | eprintln!("{:=<1$}", "", filename.len()); 118 | eprintln!(""); 119 | 120 | print_error(&contents, *err); 121 | exit(1); 122 | } 123 | Ok(m) => m, 124 | }; 125 | 126 | engine.register_global_module(module.into()); 127 | 128 | has_init_scripts = true; 129 | 130 | println!("Script '{}' loaded.", filename.to_string_lossy()); 131 | } 132 | 133 | if has_init_scripts { 134 | println!(); 135 | } 136 | } 137 | 138 | // Setup Engine 139 | #[cfg(not(feature = "no_optimize"))] 140 | engine.set_optimization_level(rhai::OptimizationLevel::None); 141 | 142 | // Set a file module resolver without caching 143 | #[cfg(not(feature = "no_module"))] 144 | #[cfg(not(feature = "no_std"))] 145 | { 146 | let mut resolver = rhai::module_resolvers::FileModuleResolver::new(); 147 | resolver.enable_cache(false); 148 | engine.set_module_resolver(resolver); 149 | } 150 | 151 | // Make Engine immutable 152 | let engine = engine; 153 | 154 | // Create scope 155 | let mut scope = Scope::new(); 156 | 157 | // REPL loop 158 | let mut input = String::new(); 159 | let mut main_ast: AST = Default::default(); 160 | let mut ast_u: AST = Default::default(); 161 | let mut ast: AST = Default::default(); 162 | 163 | 'main_loop: loop { 164 | print!("rhai-repl> "); 165 | stdout().flush().expect("couldn't flush stdout"); 166 | 167 | input.clear(); 168 | 169 | loop { 170 | match stdin().read_line(&mut input) { 171 | Ok(0) => break 'main_loop, 172 | Ok(_) => (), 173 | Err(err) => panic!("input error: {}", err), 174 | } 175 | 176 | let line = input.as_str().trim_end(); 177 | 178 | // Allow line continuation 179 | if line.ends_with('\\') { 180 | let len = line.len(); 181 | input.truncate(len - 1); 182 | input.push('\n'); 183 | } else { 184 | break; 185 | } 186 | 187 | print!("> "); 188 | stdout().flush().expect("couldn't flush stdout"); 189 | } 190 | 191 | let script = input.trim(); 192 | 193 | if script.is_empty() { 194 | continue; 195 | } 196 | 197 | // Implement standard commands 198 | match script { 199 | "help" => { 200 | print_help(); 201 | continue; 202 | } 203 | "exit" | "quit" => break, // quit 204 | "scope" => { 205 | scope 206 | .iter_raw() 207 | .enumerate() 208 | .for_each(|(i, (name, constant, value))| { 209 | #[cfg(not(feature = "no_closure"))] 210 | let value_is_shared = if value.is_shared() { " (shared" } else { "" }; 211 | #[cfg(feature = "no_closure")] 212 | let value_is_shared = ""; 213 | 214 | println!( 215 | "[{}] {}{}{} = {:?}", 216 | i + 1, 217 | if constant { "const " } else { "" }, 218 | name, 219 | value_is_shared, 220 | *value.read_lock::().unwrap(), 221 | ) 222 | }); 223 | println!(); 224 | continue; 225 | } 226 | "astu" => { 227 | // print the last un-optimized AST 228 | println!("{:#?}\n", ast_u); 229 | continue; 230 | } 231 | "ast" => { 232 | // print the last AST 233 | println!("{:#?}\n", ast); 234 | continue; 235 | } 236 | #[cfg(feature = "metadata")] 237 | "functions" => { 238 | // print a list of all registered functions 239 | engine 240 | .gen_fn_signatures(false) 241 | .into_iter() 242 | .for_each(|f| println!("{}", f)); 243 | 244 | #[cfg(not(feature = "no_function"))] 245 | main_ast.iter_functions().for_each(|f| println!("{}", f)); 246 | 247 | println!(); 248 | continue; 249 | } 250 | // "json" => { 251 | // println!( 252 | // "{}", 253 | // engine 254 | // .gen_fn_metadata_with_ast_to_json(&main_ast, true) 255 | // .unwrap() 256 | // ); 257 | // continue; 258 | // } 259 | _ => (), 260 | } 261 | 262 | match engine 263 | .compile_with_scope(&scope, &script) 264 | .map_err(Into::into) 265 | .and_then(|r| { 266 | ast_u = r.clone(); 267 | 268 | #[cfg(not(feature = "no_optimize"))] 269 | { 270 | ast = engine.optimize_ast(&scope, r, rhai::OptimizationLevel::Simple); 271 | } 272 | 273 | #[cfg(feature = "no_optimize")] 274 | { 275 | ast = r; 276 | } 277 | 278 | // Merge the AST into the main 279 | main_ast += ast.clone(); 280 | 281 | // Evaluate 282 | engine.eval_ast_with_scope::(&mut scope, &main_ast) 283 | }) { 284 | Ok(result) if !result.is::<()>() => { 285 | println!("=> {:?}", result); 286 | println!(); 287 | } 288 | Ok(_) => (), 289 | Err(err) => { 290 | println!(); 291 | print_error(&input, *err); 292 | println!(); 293 | } 294 | } 295 | 296 | // Throw away all the statements, leaving only the functions 297 | main_ast.clear_statements(); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /frontend-handlebars/scripts/rhai-run.rs: -------------------------------------------------------------------------------- 1 | use rhai::{Engine, EvalAltResult, Position}; 2 | 3 | #[cfg(not(feature = "no_optimize"))] 4 | use rhai::OptimizationLevel; 5 | 6 | use std::{env, fs::File, io::Read, path::Path, process::exit}; 7 | 8 | fn eprint_error(input: &str, mut err: EvalAltResult) { 9 | fn eprint_line(lines: &[&str], pos: Position, err_msg: &str) { 10 | let line = pos.line().unwrap(); 11 | let line_no = format!("{}: ", line); 12 | 13 | eprintln!("{}{}", line_no, lines[line - 1]); 14 | eprintln!( 15 | "{:>1$} {2}", 16 | "^", 17 | line_no.len() + pos.position().unwrap(), 18 | err_msg 19 | ); 20 | eprintln!(""); 21 | } 22 | 23 | let lines: Vec<_> = input.split('\n').collect(); 24 | 25 | // Print error 26 | let pos = err.take_position(); 27 | 28 | if pos.is_none() { 29 | // No position 30 | eprintln!("{}", err); 31 | } else { 32 | // Specific position 33 | eprint_line(&lines, pos, &err.to_string()) 34 | } 35 | } 36 | 37 | fn main() { 38 | let mut contents = String::new(); 39 | 40 | for filename in env::args().skip(1) { 41 | let filename = match Path::new(&filename).canonicalize() { 42 | Err(err) => { 43 | eprintln!("Error script file path: {}\n{}", filename, err); 44 | exit(1); 45 | } 46 | Ok(f) => f, 47 | }; 48 | 49 | let mut engine = Engine::new(); 50 | 51 | #[cfg(not(feature = "no_optimize"))] 52 | engine.set_optimization_level(OptimizationLevel::Full); 53 | 54 | let mut f = match File::open(&filename) { 55 | Err(err) => { 56 | eprintln!( 57 | "Error reading script file: {}\n{}", 58 | filename.to_string_lossy(), 59 | err 60 | ); 61 | exit(1); 62 | } 63 | Ok(f) => f, 64 | }; 65 | 66 | contents.clear(); 67 | 68 | if let Err(err) = f.read_to_string(&mut contents) { 69 | eprintln!( 70 | "Error reading script file: {}\n{}", 71 | filename.to_string_lossy(), 72 | err 73 | ); 74 | exit(1); 75 | } 76 | 77 | let contents = if contents.starts_with("#!") { 78 | // Skip shebang 79 | &contents[contents.find('\n').unwrap_or(0)..] 80 | } else { 81 | &contents[..] 82 | }; 83 | 84 | if let Err(err) = engine 85 | .compile(contents) 86 | .map_err(|err| Box::new(err.into()) as Box) 87 | .and_then(|mut ast| { 88 | ast.set_source(filename.to_string_lossy().to_string()); 89 | engine.consume_ast(&ast) 90 | }) 91 | { 92 | let filename = filename.to_string_lossy(); 93 | 94 | eprintln!("{:=<1$}", "", filename.len()); 95 | eprintln!("{}", filename); 96 | eprintln!("{:=<1$}", "", filename.len()); 97 | eprintln!(""); 98 | 99 | eprint_error(contents, *err); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /frontend-handlebars/src/main.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | mod routes; 3 | 4 | use crate::util::constant::CFG; 5 | 6 | #[async_std::main] 7 | async fn main() -> Result<(), std::io::Error> { 8 | // tide logger 9 | tide::log::start(); 10 | 11 | // Initialize the application with state. 12 | // Something in Tide State 13 | let app_state = State {}; 14 | let mut app = tide::with_state(app_state); 15 | // app = push_res(app).await; 16 | routes::push_res(&mut app).await; 17 | 18 | app.listen(format!( 19 | "{}:{}", 20 | CFG.get("ADDRESS").unwrap(), 21 | CFG.get("PORT").unwrap() 22 | )) 23 | .await?; 24 | 25 | Ok(()) 26 | } 27 | 28 | // Tide application scope state. 29 | #[derive(Clone)] 30 | pub struct State {} 31 | -------------------------------------------------------------------------------- /frontend-handlebars/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | use tide::{self, Server, Request}; 2 | use serde_json::json; 3 | 4 | pub mod users; 5 | pub mod projects; 6 | 7 | use crate::{State, util::common::Tpl}; 8 | use crate::routes::{users::user_index, projects::project_index}; 9 | 10 | // pub async fn push_res(mut app: Server) -> Server { 11 | pub async fn push_res(app: &mut Server) { 12 | app.at("/static").serve_dir("./static").unwrap(); 13 | 14 | //environment variables defined in .env file 15 | app.at("/").get(index); 16 | app.at("users").get(user_index); 17 | app.at("projects").get(project_index); 18 | 19 | // app 20 | } 21 | 22 | async fn index(_req: Request) -> tide::Result { 23 | let index: Tpl = Tpl::new("index").await; 24 | 25 | // make data and render it 26 | let data = json!({"app_name": "frontend-handlebars / tide-async-graphql-mongodb", "author": "我是谁?"}); 27 | 28 | index.render(&data).await 29 | } 30 | -------------------------------------------------------------------------------- /frontend-handlebars/src/routes/projects.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::{GraphQLQuery, Response}; 2 | use tide::Request; 3 | 4 | // use crate::State; 5 | use crate::{ 6 | State, 7 | util::common::{gql_uri, Tpl}, 8 | }; 9 | 10 | type ObjectId = String; 11 | 12 | #[derive(GraphQLQuery)] 13 | #[graphql( 14 | schema_path = "./graphql/schema.graphql", 15 | query_path = "./graphql/all_projects.graphql", 16 | response_derives = "Debug" 17 | )] 18 | struct AllProjects; 19 | 20 | pub async fn project_index(_req: Request) -> tide::Result { 21 | let project_index: Tpl = Tpl::new("projects/index").await; 22 | 23 | // make data and render it 24 | let build_query = AllProjects::build_query(all_projects::Variables {}); 25 | let query = serde_json::json!(build_query); 26 | 27 | let resp_body: Response = 28 | surf::post(&gql_uri().await).body(query).recv_json().await.unwrap(); 29 | 30 | let resp_data = resp_body.data.expect("missing response data"); 31 | 32 | project_index.render(&resp_data).await 33 | } 34 | -------------------------------------------------------------------------------- /frontend-handlebars/src/routes/users.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::{GraphQLQuery, Response}; 2 | use tide::Request; 3 | 4 | // use crate::State; 5 | use crate::{ 6 | State, 7 | util::common::{gql_uri, rhai_dir, Tpl}, 8 | }; 9 | 10 | type ObjectId = String; 11 | 12 | #[derive(GraphQLQuery)] 13 | #[graphql( 14 | schema_path = "./graphql/schema.graphql", 15 | query_path = "./graphql/all_users.graphql", 16 | response_derives = "Debug" 17 | )] 18 | struct AllUsers; 19 | 20 | pub async fn user_index(_req: Request) -> tide::Result { 21 | let mut user_index: Tpl = Tpl::new("users/index").await; 22 | user_index 23 | .reg 24 | .register_script_helper_file( 25 | "length", 26 | format!("{}{}", rhai_dir().await, "length.rhai"), 27 | ) 28 | .unwrap(); 29 | 30 | // make data and render it 31 | let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6ImlvazJAYnVkc2hvbWUuY29tIiwidXNlcm5hbWUiOiLmiJHmmK9vazIiLCJleHAiOjEwMDAwMDAwMDAwfQ.Gk98TjaFPpyW2Vdunn-pVqSPizP_zzTr89psBTE6zzfLQStUnBEXA2k0yVrS0CHBt9bHLLcFgmo4zYiioRBzBg"; 32 | let build_query = AllUsers::build_query(all_users::Variables { 33 | token: token.to_string(), 34 | }); 35 | let query = serde_json::json!(build_query); 36 | 37 | let resp_body: Response = 38 | surf::post(&gql_uri().await).body(query).recv_json().await.unwrap(); 39 | 40 | let resp_data = resp_body.data.expect("missing response data"); 41 | 42 | user_index.render(&resp_data).await 43 | } 44 | -------------------------------------------------------------------------------- /frontend-handlebars/src/util/common.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use tide::{ 3 | Response, StatusCode, Body, 4 | {http::mime::HTML}, 5 | }; 6 | use handlebars::Handlebars; 7 | 8 | use crate::util::constant::CFG; 9 | 10 | pub async fn gql_uri() -> String { 11 | let address = CFG.get("ADDRESS").unwrap(); 12 | let gql_port = CFG.get("GRAPHQL_PORT").unwrap(); 13 | let gql_path = CFG.get("GRAPHQL_PATH").unwrap(); 14 | 15 | format!("http://{}:{}/{}", address, gql_port, gql_path) 16 | } 17 | 18 | pub async fn rhai_dir() -> String { 19 | format!("./{}/", "scripts") 20 | } 21 | 22 | pub struct Tpl<'tpl> { 23 | pub name: String, 24 | pub reg: Handlebars<'tpl>, 25 | } 26 | 27 | impl<'tpl> Tpl<'tpl> { 28 | pub async fn new(rel_path: &str) -> Tpl<'tpl> { 29 | let tpl_name = &rel_path.replace("/", "_"); 30 | let abs_path = format!("./templates/{}.html", rel_path); 31 | 32 | // create the handlebars registry 33 | let mut hbs_reg = Handlebars::new(); 34 | // enable dev mode for template reloading 35 | // hbs_reg.set_dev_mode(true); 36 | // register template from a file and assign a name to it 37 | hbs_reg.register_template_file(tpl_name, abs_path).unwrap(); 38 | 39 | Tpl { name: tpl_name.to_string(), reg: hbs_reg } 40 | } 41 | 42 | pub async fn render(&self, data: &T) -> tide::Result 43 | where 44 | T: Serialize, 45 | { 46 | let mut resp = Response::new(StatusCode::Ok); 47 | resp.set_content_type(HTML); 48 | resp.set_body(Body::from_string( 49 | self.reg.render(&self.name, data).unwrap(), 50 | )); 51 | 52 | Ok(resp.into()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend-handlebars/src/util/constant.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use lazy_static::lazy_static; 3 | use std::collections::HashMap; 4 | 5 | lazy_static! { 6 | // CFG variables defined in .env file 7 | pub static ref CFG: HashMap<&'static str, String> = { 8 | dotenv().ok(); 9 | 10 | let mut map = HashMap::new(); 11 | 12 | map.insert( 13 | "ADDRESS", 14 | dotenv::var("ADDRESS").expect("Expected ADDRESS to be set in env!"), 15 | ); 16 | map.insert( 17 | "PORT", 18 | dotenv::var("PORT").expect("Expected PORT to be set in env!"), 19 | ); 20 | 21 | map.insert( 22 | "GRAPHQL_PORT", 23 | dotenv::var("GRAPHQL_PORT").expect("Expected GRAPHQL_PORT to be set in env!"), 24 | ); 25 | map.insert( 26 | "GRAPHQL_PATH", 27 | dotenv::var("GRAPHQL_PATH").expect("Expected GRAPHQL_PATH to be set in env!"), 28 | ); 29 | map.insert( 30 | "GRAPHIQL_PATH", 31 | dotenv::var("GRAPHIQL_PATH").expect("Expected GRAPHIQL_PATH to be set in env!"), 32 | ); 33 | 34 | map 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /frontend-handlebars/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constant; 2 | pub mod common; 3 | -------------------------------------------------------------------------------- /frontend-handlebars/static/css/style.css: -------------------------------------------------------------------------------- 1 | .logo-title { 2 | line-height: 40px; 3 | display:flex; 4 | width: 500px; 5 | } 6 | -------------------------------------------------------------------------------- /frontend-handlebars/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/frontend-handlebars/static/favicon.png -------------------------------------------------------------------------------- /frontend-handlebars/static/imgs/budshome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/frontend-handlebars/static/imgs/budshome.png -------------------------------------------------------------------------------- /frontend-handlebars/static/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/frontend-handlebars/static/js/.gitkeep -------------------------------------------------------------------------------- /frontend-handlebars/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ app_name }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 | {{ app_name }} 22 |
23 |

-- {{ author }}

24 | 25 | 38 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/projects/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all projects 6 | 7 | 8 | 9 | 10 | 11 | 12 | frontend-handlebars / tide-async-graphql-mongodb 13 |

all projects

14 | 15 |
    16 | {{#each allProjects as |p|}} 17 |
  • {{p.subject}}
  • 18 | 23 | {{/each}} 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /frontend-handlebars/templates/users/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all users 6 | 7 | 8 | 9 | 10 | 11 | 12 | frontend-handlebars / tide-async-graphql-mongodb 13 |

all users

14 | 15 |
    16 | {{#each allUsers as |u|}} 17 |
  • {{u.username}} - (length: {{length u.username}})
  • 18 |
      19 |
    • {{ u.id }}
    • 20 |
    • {{ u.email }}
    • 21 |
    22 | {{/each}} 23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend-yew/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend-yew" 3 | version = "0.1.0" 4 | authors = ["zzy <9809920@qq.com>"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | wasm-bindgen = "0.2.78" 9 | wasm-bindgen-futures = "0.4.28" 10 | wee_alloc = "0.4.5" 11 | console_error_panic_hook = "0.1.6" 12 | 13 | yew = "0.18.0" 14 | yew-router = "0.15.0" 15 | 16 | graphql_client = "0.10.0" 17 | serde = { version = "1.0.130", features = ["derive"] } 18 | serde_json = "1.0.68" 19 | anyhow = "1.0.44" 20 | 21 | toml = "0.5.8" 22 | lazy_static = "1.4.0" 23 | -------------------------------------------------------------------------------- /frontend-yew/README.md: -------------------------------------------------------------------------------- 1 | # Web Application Server - yew 2 | 3 | ``` Bash 4 | git clone https://github.com/zzy/tide-async-graphql-mongodb.git 5 | cd tide-async-graphql-mongodb 6 | cargo build 7 | 8 | cd frontend-yew 9 | ``` 10 | 11 | ## Build & Run: 12 | 13 | ``` Bash 14 | cargo install trunk wasm-bindgen-cli 15 | 16 | trunk build 17 | trunk serve --release 18 | ``` 19 | Then connect to http://127.0.0.1:3001 with browser. 20 | 21 | ![Client Image](../data/yew.jpg) 22 | 23 | ## Contributing 24 | 25 | You are welcome in contributing to this project. 26 | -------------------------------------------------------------------------------- /frontend-yew/assets/css/style.css: -------------------------------------------------------------------------------- 1 | .home { 2 | font-size: xx-large; 3 | font-weight: bolder; 4 | } 5 | 6 | .placeholder { 7 | padding: 5px; 8 | } 9 | -------------------------------------------------------------------------------- /frontend-yew/assets/css/style.sass: -------------------------------------------------------------------------------- 1 | $users-color: blue 2 | 3 | .users 4 | background-color: $users-color; 5 | -------------------------------------------------------------------------------- /frontend-yew/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | .logo-title { 2 | line-height: 40px; 3 | display:flex; 4 | align-items: center; 5 | } 6 | 7 | .nav { 8 | line-height: 30px; 9 | display:flex; 10 | align-items: center; 11 | margin-bottom: 20px; 12 | font-weight: bold; 13 | } 14 | 15 | $projects-color: green; 16 | 17 | .projects { 18 | background-color: $projects-color; 19 | } 20 | -------------------------------------------------------------------------------- /frontend-yew/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/frontend-yew/assets/favicon.png -------------------------------------------------------------------------------- /frontend-yew/assets/imgs/budshome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/frontend-yew/assets/imgs/budshome.png -------------------------------------------------------------------------------- /frontend-yew/assets/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzy/tide-async-graphql-mongodb/5e79a6312e8248bc7b8cad7c10103c69d2cf0c01/frontend-yew/assets/js/.gitkeep -------------------------------------------------------------------------------- /frontend-yew/cfg.toml: -------------------------------------------------------------------------------- 1 | [gql] 2 | addr = "127.0.0.1" 3 | port = 8000 4 | path = "graphql" 5 | -------------------------------------------------------------------------------- /frontend-yew/graphql/all_projects.graphql: -------------------------------------------------------------------------------- 1 | query AllProjects { 2 | allProjects { 3 | id 4 | userId 5 | subject 6 | website 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend-yew/graphql/all_users.graphql: -------------------------------------------------------------------------------- 1 | query AllUsers($token: String!) { 2 | allUsers( 3 | token: $token 4 | ) { 5 | id 6 | email 7 | username 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend-yew/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | mutation: MutationRoot 4 | } 5 | 6 | # Directs the executor to query only when the field exists. 7 | directive @ifdef on FIELD 8 | 9 | type MutationRoot { 10 | userRegister(newUser: NewUser!): User! 11 | userChangePassword( 12 | curPassword: String! 13 | newPassword: String! 14 | token: String! 15 | ): User! 16 | userUpdateProfile(newUser: NewUser!, token: String!): User! 17 | addProject(newProject: NewProject!): Project! 18 | } 19 | 20 | input NewProject { 21 | userId: ObjectId! 22 | subject: String! 23 | website: String! 24 | } 25 | 26 | input NewUser { 27 | email: String! 28 | username: String! 29 | cred: String! 30 | } 31 | 32 | scalar ObjectId 33 | 34 | type Project { 35 | id: ObjectId! 36 | userId: ObjectId! 37 | subject: String! 38 | website: String! 39 | } 40 | 41 | type QueryRoot { 42 | getUserByEmail(email: String!): User! 43 | getUserByUsername(username: String!): User! 44 | userSignIn(unknownUser: NewUser!): SignInfo! 45 | allUsers(token: String!): [User!]! 46 | allProjects: [Project!]! 47 | allProjectsByUser(userId: ObjectId!): [Project!]! 48 | } 49 | 50 | type SignInfo { 51 | email: String! 52 | username: String! 53 | token: String! 54 | } 55 | 56 | type User { 57 | id: ObjectId! 58 | email: String! 59 | username: String! 60 | projects: [Project!]! 61 | } 62 | -------------------------------------------------------------------------------- /frontend-yew/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | frontend-yew - tide-async-graphql-mongodb 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /frontend-yew/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | #[global_allocator] 4 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 5 | 6 | mod util; 7 | mod pages; 8 | 9 | use console_error_panic_hook::set_once as set_panic_hook; 10 | use yew::prelude::*; 11 | use yew_router::prelude::*; 12 | use yew_router::components::RouterAnchor; 13 | 14 | use pages::{home::Home, users::Users, projects::Projects}; 15 | 16 | #[derive(Switch, Debug, Clone, PartialEq)] 17 | pub enum Route { 18 | #[to = "/users"] 19 | Users, 20 | #[to = "/projects"] 21 | Projects, 22 | #[to = "/"] 23 | Home, 24 | } 25 | 26 | fn switch(switch: Route) -> Html { 27 | match switch { 28 | Route::Users => { 29 | html! { } 30 | } 31 | Route::Projects => { 32 | html! { } 33 | } 34 | Route::Home => { 35 | html! { } 36 | } 37 | } 38 | } 39 | 40 | struct App; 41 | 42 | impl Component for App { 43 | type Message = (); 44 | type Properties = (); 45 | 46 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 47 | Self 48 | } 49 | 50 | fn update(&mut self, _: Self::Message) -> ShouldRender { 51 | false 52 | } 53 | 54 | fn change(&mut self, _: Self::Properties) -> ShouldRender { 55 | false 56 | } 57 | 58 | fn view(&self) -> Html { 59 | type Anchor = RouterAnchor; 60 | 61 | let home_cls = "nav"; 62 | 63 | html! { 64 | <> 65 |
66 | 67 | { "frontend-yew / tide-async-graphql-mongodb" } 68 |
69 |
70 | 71 | { "用户列表" } 72 | 73 | { " - " } 74 | 75 | { "项目列表" } 76 | 77 | { " - " } 78 | 79 | { "主页" } 80 | 81 |
82 |
83 | render=Router::render(switch) /> 84 |
85 | 86 | } 87 | } 88 | } 89 | 90 | fn main() { 91 | set_panic_hook(); 92 | 93 | yew::start_app::(); 94 | } 95 | -------------------------------------------------------------------------------- /frontend-yew/src/pages/home.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | pub struct Home; 4 | 5 | impl Component for Home { 6 | type Message = (); 7 | type Properties = (); 8 | 9 | fn create(_props: Self::Properties, _link: ComponentLink) -> Self { 10 | Self 11 | } 12 | 13 | fn update(&mut self, _msg: Self::Message) -> ShouldRender { 14 | false 15 | } 16 | 17 | fn change(&mut self, _props: Self::Properties) -> ShouldRender { 18 | false 19 | } 20 | 21 | fn view(&self) -> Html { 22 | let home_cls = "home"; 23 | 24 | html! { 25 |
26 |

{ "Rust + WebAssembly" }

27 |

{ "使用 yew 构建 WebAssembly 标准的 web 前端" }

28 |
29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend-yew/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod home; 2 | pub mod users; 3 | pub mod projects; 4 | -------------------------------------------------------------------------------- /frontend-yew/src/pages/projects.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use wasm_bindgen_futures::spawn_local; 3 | 4 | use yew::{format::Json, prelude::*}; 5 | use yew::services::fetch::{FetchService, FetchTask, Request, Response}; 6 | 7 | use graphql_client::GraphQLQuery; 8 | use serde_json::{Value, from_str}; 9 | 10 | use crate::util::{constant::ObjectId, common::gql_uri}; 11 | 12 | //////////////////////////////////////////////////// 13 | // Fetch projects data use `yew::services::fetch` // 14 | //////////////////////////////////////////////////// 15 | 16 | #[derive(GraphQLQuery)] 17 | #[graphql( 18 | schema_path = "./graphql/schema.graphql", 19 | query_path = "./graphql/all_projects.graphql", 20 | response_derives = "Debug" 21 | )] 22 | struct AllProjects; 23 | 24 | #[derive(Debug)] 25 | pub enum Msg { 26 | PassRequest, 27 | ReceiveResponse(Result, anyhow::Error>), 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct Projects { 32 | fetch_task: Option, 33 | list: Option>, 34 | link: ComponentLink, 35 | error: Option, 36 | } 37 | 38 | impl Projects { 39 | fn view_fetching(&self) -> Html { 40 | if self.fetch_task.is_some() { 41 | html! {

{ "Fetching data..." }

} 42 | } else { 43 | html! {

} 44 | } 45 | } 46 | 47 | fn view_data(&self) -> Html { 48 | match self.list { 49 | Some(ref list) => { 50 | let projects = list.iter().map(|project| { 51 | html! { 52 |
53 |
  • 54 | { &project["subject"].as_str().unwrap() } 55 |
  • 56 | 65 |
    66 | } 67 | }); 68 | 69 | html! { 70 |
      71 | { for projects } 72 |
    73 | } 74 | } 75 | None => { 76 | html! { 77 |

    78 | { "No data." } 79 |

    80 | } 81 | } 82 | } 83 | } 84 | 85 | fn view_error(&self) -> Html { 86 | if let Some(ref error) = self.error { 87 | html! {

    { error.clone() }

    } 88 | } else { 89 | html! {} 90 | } 91 | } 92 | } 93 | 94 | impl Component for Projects { 95 | type Message = Msg; 96 | type Properties = (); 97 | 98 | fn create(_props: Self::Properties, link: ComponentLink) -> Self { 99 | Self { fetch_task: None, list: None, link, error: None } 100 | } 101 | 102 | fn rendered(&mut self, first_render: bool) { 103 | let link = self.link.clone(); 104 | if first_render { 105 | spawn_local(async move { 106 | link.send_message(Msg::PassRequest); 107 | }); 108 | } 109 | } 110 | 111 | fn update(&mut self, msg: Self::Message) -> bool { 112 | match msg { 113 | Msg::PassRequest => { 114 | // build graphql query body 115 | let build_query = 116 | AllProjects::build_query(all_projects::Variables {}); 117 | let query = Json(&build_query); 118 | 119 | // build the request 120 | let request = Request::post(&gql_uri()) 121 | .body(query) 122 | .expect("Could not build request."); 123 | 124 | // construct a callback 125 | let callback = self.link.callback( 126 | |response: Response>| { 127 | let resp_body = response.into_body(); 128 | let resp_str = resp_body.as_ref().unwrap(); 129 | 130 | let projects_value: Value = 131 | from_str(&resp_str).unwrap(); 132 | let projects_vec = projects_value["data"] 133 | ["allProjects"] 134 | .as_array() 135 | .unwrap() 136 | .to_owned(); 137 | 138 | Msg::ReceiveResponse(Ok(projects_vec)) 139 | }, 140 | ); 141 | 142 | // pass the request and callback to the fetch service 143 | let task = FetchService::fetch(request, callback) 144 | .expect("failed to start request"); 145 | 146 | // store the task so it isn't canceled immediately 147 | self.fetch_task = Some(task); 148 | 149 | // redraw so that the page displays a 'fetching...' message 150 | true 151 | } 152 | Msg::ReceiveResponse(data) => { 153 | match data { 154 | Ok(projects_vec) => { 155 | self.list = Some(projects_vec); 156 | } 157 | Err(error) => self.error = Some(error.to_string()), 158 | } 159 | self.fetch_task = None; 160 | 161 | // redraw so that the page displays projects data 162 | true 163 | } 164 | } 165 | } 166 | 167 | fn change(&mut self, _props: Self::Properties) -> ShouldRender { 168 | let link = self.link.clone(); 169 | spawn_local(async move { 170 | link.send_message(Msg::PassRequest); 171 | }); 172 | 173 | false 174 | } 175 | 176 | fn view(&self) -> Html { 177 | html! { 178 | <> 179 |

    { "all projects" }

    180 | 181 | { self.view_fetching() } 182 | { self.view_data() } 183 | { self.view_error() } 184 | 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /frontend-yew/src/pages/users.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::GraphQLQuery; 2 | use serde_json::Value; 3 | use std::fmt::Debug; 4 | use wasm_bindgen::{prelude::*, JsCast}; 5 | use wasm_bindgen_futures::{spawn_local, JsFuture}; 6 | use yew::web_sys::{Request, RequestInit, RequestMode, Response}; 7 | use yew::{html, Component, ComponentLink, Html, ShouldRender}; 8 | 9 | use crate::util::{constant::ObjectId, common::gql_uri}; 10 | 11 | ///////////////////////////////////////// 12 | // Fetch users data use `yew::web_sys` // 13 | ///////////////////////////////////////// 14 | 15 | #[derive(Debug, Clone, PartialEq)] 16 | pub struct FetchError { 17 | err: JsValue, 18 | } 19 | 20 | impl From for FetchError { 21 | fn from(value: JsValue) -> Self { 22 | Self { err: value } 23 | } 24 | } 25 | 26 | #[derive(GraphQLQuery)] 27 | #[graphql( 28 | schema_path = "./graphql/schema.graphql", 29 | query_path = "./graphql/all_users.graphql", 30 | response_derives = "Debug" 31 | )] 32 | struct AllUsers; 33 | 34 | async fn fetch_users() -> Result, FetchError> { 35 | let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6ImFzZmZhQGRzYWZhLmNvbSIsInVzZXJuYW1lIjoi5a-G56CBMTExIiwiZXhwIjoxMDAwMDAwMDAwMH0.NyEN13J5trkn9OlRqWv2xMHshysR9QPWclo_-q1cbF4y_9rbkpSI6ern-GgKIh_ED0Czk98M1fJ6tzLczbdptg"; 36 | let build_query = AllUsers::build_query(all_users::Variables { 37 | token: token.to_string(), 38 | }); 39 | let query = serde_json::json!(build_query); 40 | 41 | let mut req_opts = RequestInit::new(); 42 | req_opts.method("POST"); 43 | req_opts.body(Some(&JsValue::from_str(&query.to_string()))); 44 | req_opts.mode(RequestMode::Cors); // 可以不写,默认为 Cors 45 | 46 | let request = Request::new_with_str_and_init(&gql_uri(), &req_opts)?; 47 | 48 | let window = yew::utils::window(); 49 | let resp_value = 50 | JsFuture::from(window.fetch_with_request(&request)).await?; 51 | let resp: Response = resp_value.dyn_into().unwrap(); 52 | let resp_text = JsFuture::from(resp.text()?).await?; 53 | 54 | let users_str = resp_text.as_string().unwrap(); 55 | let users_value: Value = serde_json::from_str(&users_str).unwrap(); 56 | let users_vec = 57 | users_value["data"]["allUsers"].as_array().unwrap().to_owned(); 58 | 59 | Ok(users_vec) 60 | } 61 | 62 | pub struct Users { 63 | list: Vec, 64 | link: ComponentLink, 65 | } 66 | 67 | pub enum Msg { 68 | UpdateList(Vec), 69 | } 70 | 71 | impl Component for Users { 72 | type Message = Msg; 73 | type Properties = (); 74 | 75 | fn create(_props: Self::Properties, link: ComponentLink) -> Self { 76 | Self { list: Vec::new(), link } 77 | } 78 | 79 | fn rendered(&mut self, first_render: bool) { 80 | let link = self.link.clone(); 81 | if first_render { 82 | spawn_local(async move { 83 | let res = fetch_users().await; 84 | link.send_message(Msg::UpdateList(res.unwrap())) 85 | }); 86 | } 87 | } 88 | 89 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 90 | match msg { 91 | Msg::UpdateList(res) => { 92 | self.list = res; 93 | true 94 | } 95 | } 96 | } 97 | 98 | fn change(&mut self, _props: Self::Properties) -> ShouldRender { 99 | false 100 | } 101 | 102 | fn view(&self) -> Html { 103 | let users = self.list.iter().map(|user| { 104 | html! { 105 |
    106 |
  • 107 | 108 | { &user["username"].as_str().unwrap() } 109 | { " - length: " } 110 | { &user["username"].as_str().unwrap().len() } 111 | 112 |
  • 113 |
      114 |
    • { &user["id"].as_str().unwrap() }
    • 115 |
    • { &user["email"].as_str().unwrap() }
    • 116 |
    117 |
    118 | } 119 | }); 120 | 121 | html! { 122 | <> 123 |

    { "all users" }

    124 |
      125 | { for users } 126 |
    127 | 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /frontend-yew/src/util/common.rs: -------------------------------------------------------------------------------- 1 | use crate::util::constant::CFG; 2 | 3 | pub fn gql_uri() -> String { 4 | let addr = CFG.get("addr").unwrap(); 5 | let port = CFG.get("port").unwrap(); 6 | let path = CFG.get("path").unwrap(); 7 | 8 | format!("http://{}:{}/{}", addr, port, path) 9 | } 10 | -------------------------------------------------------------------------------- /frontend-yew/src/util/constant.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use toml::from_str; 3 | use serde::Deserialize; 4 | use lazy_static::lazy_static; 5 | 6 | pub type ObjectId = String; 7 | 8 | lazy_static! { 9 | // CFG variables defined in cfg.toml file 10 | pub static ref CFG: HashMap<&'static str, String> = { 11 | let cfg_str = include_str!("../../cfg.toml"); 12 | let config: Config = from_str(cfg_str).unwrap(); 13 | 14 | let mut map = HashMap::new(); 15 | 16 | map.insert("addr", config.gql.addr); 17 | map.insert("port", config.gql.port.to_string()); 18 | map.insert("path",config.gql.path); 19 | 20 | map 21 | }; 22 | } 23 | 24 | #[derive(Deserialize)] 25 | struct Config { 26 | gql: Gql, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | struct Gql { 31 | addr: String, 32 | port: u16, 33 | path: String, 34 | } 35 | -------------------------------------------------------------------------------- /frontend-yew/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constant; 2 | pub mod common; 3 | -------------------------------------------------------------------------------- /frontend-yew/trunk.toml: -------------------------------------------------------------------------------- 1 | [serve] 2 | port = 4000 3 | open = true 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | 3 | max_width = 80 4 | array_width = 50 5 | indent_style = "Block" 6 | reorder_imports = false 7 | reorder_modules = false 8 | merge_derives = false 9 | use_small_heuristics = "Max" 10 | 11 | error_on_line_overflow = true 12 | error_on_unformatted = true 13 | 14 | ignore = [] 15 | --------------------------------------------------------------------------------