├── migrations ├── .gitkeep └── 2020-05-03-131537_create_accounts │ ├── down.sql │ └── up.sql ├── .gitignore ├── .env ├── src ├── account_requests │ ├── db │ │ ├── schema.rs │ │ └── models.rs │ └── db.rs ├── main.rs └── account_requests.rs ├── static ├── util.js ├── style.css ├── delacc.html ├── login.html ├── newacc.html └── chpass.html ├── diesel.toml ├── Cargo.toml ├── templates └── welcome.html └── README.md /migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | cookie-key -------------------------------------------------------------------------------- /migrations/2020-05-03-131537_create_accounts/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE accounts -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mysql://mysql:flyingiguanas314@localhost/actix_web_example 2 | -------------------------------------------------------------------------------- /migrations/2020-05-03-131537_create_accounts/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE accounts ( 2 | username VARCHAR(255) PRIMARY KEY, 3 | password_hash BINARY(32) NOT NULL 4 | ) -------------------------------------------------------------------------------- /src/account_requests/db/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | accounts (username) { 3 | username -> Varchar, 4 | password_hash -> Binary, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /static/util.js: -------------------------------------------------------------------------------- 1 | function get(id) { 2 | return document.getElementById(id); 3 | } 4 | 5 | function getval(id) { 6 | return document.getElementById(id).value; 7 | } -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/account_requests/db/schema.rs" 6 | -------------------------------------------------------------------------------- /src/account_requests/db/models.rs: -------------------------------------------------------------------------------- 1 | use super::schema::accounts; 2 | 3 | #[derive(Queryable, Insertable)] 4 | pub struct Account { 5 | pub username: String, 6 | pub password_hash: Vec, 7 | } 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-example" 3 | version = "0.1.0" 4 | authors = ["Srinivasa "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | actix-web = "2.0.0" 9 | actix-rt = "1.1.1" 10 | diesel = { version = "1.4.5", features = ["mysql","r2d2"] } 11 | dotenv = "0.15.0" 12 | serde = "1.0.115" 13 | sha2 = "0.9.1" 14 | tera = "1.5.0" 15 | actix-files = "0.2.2" 16 | actix-identity = "0.2.1" 17 | rand = "0.7.3" 18 | bytes = "0.5.6" 19 | -------------------------------------------------------------------------------- /templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | Delete Account 10 |    11 | Change Password 12 |    13 | Logout 14 |
15 |
16 | Welcome {{ name }}! 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | .center-box { 2 | position: absolute; 3 | left: 50%; 4 | top: 50%; 5 | -webkit-transform: translate(-50%, -50%); 6 | transform: translate(-50%, -50%); 7 | text-align: center; 8 | } 9 | 10 | .form { 11 | border: 1px solid; 12 | padding: 18px; 13 | background-color: #eeeeee; 14 | font-size: 18px; 15 | border-radius: 5px; 16 | } 17 | 18 | body { 19 | font-size: 17px; 20 | font-family: sans-serif; 21 | background-color: #ffeeff; 22 | } 23 | 24 | button { 25 | background-color: greenyellow; 26 | border-radius: 4px; 27 | border: 1px solid; 28 | } 29 | 30 | .row { 31 | display: flex; 32 | align-items: center; 33 | } 34 | 35 | input { 36 | margin-left: 15px; 37 | border: 1px solid grey; 38 | border-radius: 3px; 39 | font-size: 18px; 40 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # actix-web-example 2 | `actix-web-example` is a complete website created using [actix-web](https://actix.rs/). 3 | It uses [diesel](https://diesel.rs/) for interacting with the database. 4 | 5 | The following actions can be done 6 | * Create an account 7 | * Login 8 | * Delete an account 9 | * Change password 10 | * Logout 11 | 12 | ## Installation 13 | 1. This uses MySql (or Maria DB) so make sure that MySql (or Maria DB) is installed and running. 14 | Feel free to change the code to use any other database. 15 | 2. Diesel is also required. To install it 16 | ``` 17 | cargo install diesel_cli --no-default-features --features mysql 18 | ``` 19 | 3. Change .env to be of the form given below (Refer .env in this repo as an example) 20 | ``` 21 | DATABASE_URL=mysql://username:password@localhost/actix_web_example 22 | ``` 23 | 4. Run the following commands 24 | ``` 25 | diesel setup 26 | diesel migration run 27 | ``` 28 | 29 | ## Running the server 30 | ``` 31 | cargo run 32 | ``` 33 | 34 | ## Web Client 35 | [http://localhost:8000/](http://localhost:8000/) 36 | 37 | -------------------------------------------------------------------------------- /static/delacc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 29 | 30 | 31 | 32 |
33 |

Type your password to delete your account

34 |
35 |
36 | : 37 |
38 |
39 | 40 |
41 |
42 | 43 |

44 | Go Back 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /src/account_requests/db.rs: -------------------------------------------------------------------------------- 1 | mod schema; 2 | pub use schema::accounts::dsl::{self, accounts}; 3 | pub mod models; 4 | 5 | use actix_web::{error::BlockingError, web::block, web::Data}; 6 | 7 | pub use diesel::prelude::*; 8 | use diesel::r2d2::{self, ConnectionManager, PooledConnection}; 9 | 10 | pub type DbPool = r2d2::Pool>; 11 | 12 | pub async fn change_password( 13 | username: String, 14 | newpasshash: Vec, 15 | conn: PooledConnection>, 16 | ) { 17 | block(move || { 18 | diesel::update(accounts.filter(dsl::username.eq(username))) 19 | .set(dsl::password_hash.eq(newpasshash)) 20 | .execute(&conn) 21 | }) 22 | .await 23 | .expect("Unable to change password"); 24 | } 25 | 26 | pub async fn delete_user( 27 | username: String, 28 | conn: PooledConnection>, 29 | ) { 30 | block(move || diesel::delete(accounts.filter(dsl::username.eq(username))).execute(&conn)) 31 | .await 32 | .expect("Could not delete user"); 33 | } 34 | 35 | pub async fn get_user_from_database( 36 | username: String, 37 | conn: PooledConnection>, 38 | ) -> Result, BlockingError> { 39 | block(move || { 40 | accounts 41 | .filter(dsl::username.eq(username)) 42 | .load::(&conn) 43 | }) 44 | .await 45 | } 46 | 47 | pub async fn get_connection(pool: Data) -> PooledConnection> { 48 | block(move || pool.get()).await.expect("Could not get db connection") 49 | } 50 | -------------------------------------------------------------------------------- /static/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 31 | 32 | 33 | 34 |
35 |
36 | Login 37 | 38 |

39 | 40 |
41 | 42 |
43 | 44 |
45 | 46 |
47 | : 48 |
49 | 50 |
51 | 52 | 53 |
54 |
55 | 56 |

57 | Create an account 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /static/newacc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | Create a new account 44 | 45 |

46 | 47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 | : 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /static/chpass.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | Change Password 46 | 47 |

48 | 49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 | : 57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 | 68 |
69 |
70 | Go Back 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod account_requests; 2 | use account_requests::*; 3 | 4 | use actix_files::{Files, NamedFile}; 5 | use actix_identity::{CookieIdentityPolicy, Identity, IdentityService}; 6 | use actix_web::{get, http, web::Data, App, HttpResponse, HttpServer, Responder}; 7 | 8 | #[macro_use] 9 | extern crate diesel; 10 | use diesel::r2d2::{self, ConnectionManager}; 11 | use diesel::MysqlConnection; 12 | 13 | use io::prelude::*; 14 | use std::{fs, io}; 15 | use tera::Tera; 16 | 17 | #[get("/")] 18 | async fn index(tmpl: Data, id: Identity) -> impl Responder { 19 | if id.identity().is_some() { 20 | let mut ctx = tera::Context::new(); 21 | ctx.insert("name", &id.identity().unwrap()); 22 | HttpResponse::Ok() 23 | .content_type("text/html") 24 | .body(tmpl.render("welcome.html", &ctx).expect("Template error")) 25 | } else { 26 | HttpResponse::Found() 27 | .header(http::header::LOCATION, "/static/login.html") 28 | .finish() 29 | } 30 | } 31 | 32 | #[get("/logout")] 33 | async fn logout(id: Identity) -> impl Responder { 34 | id.forget(); 35 | NamedFile::open("static/login.html") 36 | } 37 | 38 | #[actix_rt::main] 39 | async fn main() -> io::Result<()> { 40 | dotenv::dotenv().ok(); 41 | let conn_url = std::env::var("DATABASE_URL").expect("Failed to get value of DATABASE_URL"); 42 | 43 | let private_key = match fs::read("cookie-key") { 44 | Ok(bytes) => bytes, 45 | Err(e) => { 46 | if e.kind() == io::ErrorKind::NotFound { 47 | let mut f = 48 | fs::File::create("cookie-key").expect("Unable to create cookie key file"); 49 | let key: [u8; 32] = rand::random(); 50 | 51 | f.write_all(&key).expect("Unable to write to file"); 52 | key.to_vec() 53 | } else { 54 | panic!(e) 55 | } 56 | } 57 | }; 58 | 59 | let manager = ConnectionManager::::new(&conn_url); 60 | let pool = r2d2::Pool::builder() 61 | .build(manager) 62 | .expect("Failed to create pool"); 63 | 64 | HttpServer::new(move || { 65 | let tera = Tera::new("templates/*.html").expect("Failed to parse template files"); 66 | App::new() 67 | .wrap(IdentityService::new( 68 | CookieIdentityPolicy::new(&private_key) 69 | .name("actix-web-example") 70 | .secure(false) 71 | .max_age(31_556_952), 72 | )) 73 | .data(pool.clone()) 74 | .data(tera) 75 | .service(index) 76 | .service(create_account) 77 | .service(confirm_delacc) 78 | .service(login_request) 79 | .service(chpass_request) 80 | .service(Files::new("/static", "static/")) 81 | .service(logout) 82 | }) 83 | .bind("127.0.0.1:8000")? 84 | .run() 85 | .await 86 | } 87 | -------------------------------------------------------------------------------- /src/account_requests.rs: -------------------------------------------------------------------------------- 1 | mod db; 2 | use db::*; 3 | 4 | use actix_identity::Identity; 5 | use actix_web::{post, web::block, web::Data, web::Json, Responder}; 6 | 7 | use diesel::insert_into; 8 | use serde::{Deserialize, Serialize}; 9 | use sha2::{Digest, Sha256}; 10 | 11 | fn sha256(s: &str) -> Vec { 12 | let mut hasher = Sha256::new(); 13 | hasher.update(s); 14 | hasher.finalize().to_vec() 15 | } 16 | 17 | #[derive(Serialize, Deserialize)] 18 | pub struct RequestParams { 19 | username: String, 20 | password: String, 21 | } 22 | 23 | #[post("/acc_create")] 24 | pub async fn create_account(pool: Data, params: Json) -> impl Responder { 25 | let pass_hash = sha256(¶ms.password); 26 | let conn = get_connection(pool).await; 27 | 28 | match block(move || { 29 | insert_into(accounts) 30 | .values(&models::Account { 31 | username: params.username.clone(), 32 | password_hash: pass_hash, 33 | }) 34 | .execute(&conn) 35 | }) 36 | .await 37 | { 38 | Ok(_) => "Account created! Login", 39 | Err(_) => "Username exists", 40 | } 41 | } 42 | 43 | #[post("/login_request")] 44 | pub async fn login_request( 45 | id: Identity, 46 | pool: Data, 47 | params: Json, 48 | ) -> impl Responder { 49 | let username = params.username.clone(); 50 | 51 | match get_user_from_database(username, get_connection(pool).await).await { 52 | Ok(result) => match result.len() { 53 | 0 => "No such user", 54 | _ => { 55 | if result[0].password_hash == sha256(¶ms.password) { 56 | id.remember(params.username.clone()); 57 | "LOGIN_SUCCESS" 58 | } else { 59 | "Wrong Password" 60 | } 61 | } 62 | }, 63 | Err(e) => panic!(e), 64 | } 65 | } 66 | 67 | #[derive(Serialize, Deserialize)] 68 | pub struct ChangePassParams { 69 | oldpass: String, 70 | newpass: String, 71 | } 72 | 73 | #[post("/chpass_request")] 74 | pub async fn chpass_request( 75 | id: Identity, 76 | pool: Data, 77 | params: Json, 78 | ) -> impl Responder { 79 | let pool1 = pool.clone(); 80 | 81 | match id.identity() { 82 | None => "Invalid session", 83 | Some(username) => { 84 | let name = username.clone(); 85 | match get_user_from_database(username, get_connection(pool).await).await { 86 | Ok(result) => { 87 | if result[0].password_hash == sha256(¶ms.oldpass) { 88 | change_password(name, sha256(¶ms.newpass), get_connection(pool1).await).await; 89 | "Password changed" 90 | } else { 91 | "Wrong Password" 92 | } 93 | } 94 | Err(e) => panic!(e), 95 | } 96 | } 97 | } 98 | } 99 | 100 | #[post("/confirm_delacc")] 101 | pub async fn confirm_delacc( 102 | id: Identity, 103 | pool: Data, 104 | body: bytes::Bytes, 105 | ) -> impl Responder { 106 | let password = String::from_utf8_lossy(&body); 107 | 108 | match id.identity() { 109 | None => "Invalid session", 110 | Some(username) => { 111 | let pool1 = pool.clone(); 112 | let name = username.clone(); 113 | let conn = get_connection(pool1).await; 114 | 115 | match block(move || { 116 | accounts 117 | .filter(dsl::username.eq(name)) 118 | .load::(&conn) 119 | }) 120 | .await 121 | { 122 | Ok(result) => { 123 | if result[0].password_hash == sha256(&password) { 124 | delete_user(username, get_connection(pool).await).await; 125 | id.forget(); 126 | "Account deleted" 127 | } else { 128 | "Wrong Password" 129 | } 130 | } 131 | Err(e) => panic!(e), 132 | } 133 | } 134 | } 135 | } 136 | --------------------------------------------------------------------------------