diff --git a/backend/Cargo.lock b/backend/Cargo.lock index c79987c..e4c2e52 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. [[package]] name = "adler32" version = "1.0.3" @@ -246,6 +248,18 @@ name = "fuchsia-zircon-sys" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "google-signin" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hyper 0.10.15 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-rustls 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gzip-header" version = "0.2.0" @@ -259,6 +273,35 @@ name = "httparse" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "hyper" +version = "0.10.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper-rustls" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hyper 0.10.15 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki-roots 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "idna" version = "0.1.5" @@ -274,6 +317,11 @@ name = "itoa" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "lazy_static" version = "1.2.0" @@ -619,6 +667,17 @@ dependencies = [ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ring" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.47 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rouille" version = "3.0.0" @@ -656,6 +715,19 @@ dependencies = [ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rustls" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ryu" version = "0.2.7" @@ -671,6 +743,15 @@ name = "safemem" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "sct" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "semver" version = "0.9.0" @@ -835,6 +916,11 @@ dependencies = [ "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "twoway" version = "0.1.8" @@ -843,6 +929,11 @@ dependencies = [ "memchr 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ucd-util" version = "0.1.3" @@ -890,6 +981,11 @@ dependencies = [ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "untrusted" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "url" version = "1.7.2" @@ -928,6 +1024,7 @@ dependencies = [ "diesel 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "google-signin 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "rouille 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)", @@ -936,6 +1033,24 @@ dependencies = [ "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "webpki" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "webpki-roots" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winapi" version = "0.3.6" @@ -987,10 +1102,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum filetime 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a2df5c1a8c4be27e7707789dc42ae65976e60b394afd293d1419ab915833e646" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum google-signin 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2a88c191e9a56f44f057a9e49b330f9eb2164f865472ca3777b631f51d1e46" "checksum gzip-header 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9e59524a909fe98bb6c6f2cf1f27f2f6772887a496bf4c68cae0d94f884586" "checksum httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e8734b0cfd3bc3e101ec59100e101c2eecd19282202e87808b3037b442777a83" +"checksum hyper 0.10.15 (registry+https://github.com/rust-lang/crates.io-index)" = "df0caae6b71d266b91b4a83111a61d2b94ed2e2bea024c532b933dcff867e58c" +"checksum hyper-rustls 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "71f7b2e5858ab9e19771dc361159f09ee5031734a6f7471fe0947db0238d92b7" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" +"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" "checksum libc 0.2.47 (registry+https://github.com/rust-lang/crates.io-index)" = "48450664a984b25d5b479554c29cc04e3150c97aa4c01da5604a2d4ed9151476" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" @@ -1032,12 +1151,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f" "checksum regex-syntax 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4e47a2ed29da7a9e1960e1639e7a982e6edc6d49be308a3b02daf511504a16d1" "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" +"checksum ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" "checksum rouille 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "112568052ec17fa26c6c11c40acbb30d3ad244bf3d6da0be181f5e7e42e5004f" "checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "942b71057b31981152970d57399c25f72e27a6ee0d207a669d8304cabf44705b" "checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9" +"checksum sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cb8f61f9e6eadd062a71c380043d28036304a4706b3c4dd001ff3387ed00745a" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)" = "534b8b91a95e0f71bca3ed5824752d558da048d4248c91af873b63bd60519752" @@ -1057,7 +1179,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum tiny_http 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1661fa0a44c95d01604bd05c66732a446c657efb62b5164a7a083a3b552b4951" +"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" "checksum twoway 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" "checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" "checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" @@ -1065,11 +1189,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +"checksum untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" "checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)" = "17d7967316d8411ca3b01821ee6c332bde138ba4363becdb492f12e514daa17f" +"checksum webpki-roots 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "85d1f408918fd590908a70d36b7ac388db2edc221470333e4d6e5b598e44cabf" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 697c6cd..eddc757 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] rouille = "3.0.0" +google-signin = "0.3.0" diesel = { version = "1.3.3", features = ["mysql"] } diesel_migrations = "1.4.0" dotenv = "0.13.0" diff --git a/backend/README.md b/backend/README.md index 26697bf..888af15 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,12 +4,43 @@ Coded in Rust, manages database manipulation using AJAX requests from frontend. ### Dependencies: * [Rouille 3.0.0](https://github.com/tomaka/rouille) * [Diesel 1.3.3](https://github.com/diesel-rs/diesel) +* [Google Sign-In 0.3.0](https://github.com/wyyerd/google-signin-rs) * [dotenv 0.13.0](https://github.com/sgrif/rust-dotenv) * [serde 1.0](https://github.com/serde-rs/serde) * [serde_json 1.0](https://github.com/serde-rs/json) * [log 0.4](https://github.com/rust-lang-nursery/log) * [simplelog](https://github.com/drakulix/simplelog.rs) +### Authentication and Authorization + +#### Authentication and Authorization Flow +1. On the frontend, a sign-in button that redirects to Google sign-in calls `onSignIn()` afterwards to set a cookie for the `id_token` of the logged in user. + * That cookie has an expire date, and will delete itself from the browser once that date has passed. + * If a user on the frontend prompts an action that attempts to access the `id_token` cookie and it is not present, a login is automatically prompted. +2. The frontend generates an xmlHTTP request over HTTPS with `id_token` in the header. + * Sending the ID token over HTTP exposes the user's token to packet sniffing vulnerabilities, allowing sniffers to impersonate the user by submitting unauthentic requests with the unencrypted token. +3. The backend attempts authentication before processing the requests (some requests might need authorization, some might not). + * The token is sent back to Google's servers with our services information, Google does their own verification and sends back valid user data. +4. The email is taken from the Google user's data and cross checked with the `users` database. + * NOTE: AN EMAIL IS NECESSARY FOR AUTHORIZED REQUESTS +5. The request is processed and just before execution of the request, the backend checks for authorization on the found user if needed. + * A `user_access` request is made to verify authorization + +#### Making Authorized API Calls + +To make an authorized API call, a valid ID token from Google's sign-in services must be present in the HTTPS request. + +Add the following script imports to HTML pages that make requests. + +`` + +`` + + +And add the following to JavaScript functions that make xmlHTTP requests + +`mlhttp.setRequestHeader("id_token", getID_Token());` + ### API Calls `GET /users` diff --git a/backend/diesel.toml b/backend/diesel.toml index e0be62f..3333d2b 100644 --- a/backend/diesel.toml +++ b/backend/diesel.toml @@ -3,4 +3,4 @@ # see diesel.rs/guides/configuring-diesel-cli [print_schema] -file = "src/schema.rs \ No newline at end of file +file = "src/schema.rs" diff --git a/backend/migrations/2019-01-13-203149_create_users/down.sql b/backend/migrations/2019-01-13-203149_create_users/down.sql index c8b9a69..dc3714b 100644 --- a/backend/migrations/2019-01-13-203149_create_users/down.sql +++ b/backend/migrations/2019-01-13-203149_create_users/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -DROP TABLE users \ No newline at end of file +DROP TABLE users; diff --git a/backend/migrations/2019-01-13-203149_create_users/up.sql b/backend/migrations/2019-01-13-203149_create_users/up.sql index 2968d06..d853a64 100644 --- a/backend/migrations/2019-01-13-203149_create_users/up.sql +++ b/backend/migrations/2019-01-13-203149_create_users/up.sql @@ -5,4 +5,11 @@ CREATE TABLE users ( last_name VARCHAR(255) NOT NULL, banner_id INT(9) UNSIGNED NOT NULL, email VARCHAR(255) -) \ No newline at end of file +); + +INSERT INTO users (first_name, last_name, banner_id) VALUES ("root", "root", 0); +UPDATE users +SET + id = 0 +WHERE + first_name="root" AND last_name="root" AND banner_id=0 AND id != 0; diff --git a/backend/migrations/2019-03-08-171503_create_access/down.sql b/backend/migrations/2019-03-08-171503_create_access/down.sql new file mode 100644 index 0000000..fac3ecb --- /dev/null +++ b/backend/migrations/2019-03-08-171503_create_access/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP TABLE user_access; +DROP TABLE access; diff --git a/backend/migrations/2019-03-08-171503_create_access/up.sql b/backend/migrations/2019-03-08-171503_create_access/up.sql new file mode 100644 index 0000000..42c9ff0 --- /dev/null +++ b/backend/migrations/2019-03-08-171503_create_access/up.sql @@ -0,0 +1,53 @@ +-- Your SQL goes here +CREATE TABLE access ( + id SERIAL PRIMARY KEY, + access_name VARCHAR(255) NOT NULL +); + +INSERT INTO access (access_name) VALUES + ("RootAccess"), + + ("GetUsers"), + ("CreateUsers"), + ("UpdateUsers"), + ("DeleteUsers"), + + ("GetAccess"), + ("CreateAccess"), + ("UpdateAccess"), + ("DeleteAccess"), + + ("GetUserAccess"), + ("CreateUserAccess"), + ("UpdateUserAccess"), + ("DeleteUserAccess"), + + ("GetChemical"), + ("CreateChemical"), + ("UpdateChemical"), + ("DeleteChemical"), + + ("GetChemicalInventory"), + ("CreateChemicalInventory"), + ("UpdateChemicalInventory"), + ("DeleteChemicalInventory"); + +CREATE TABLE user_access ( + permission_id SERIAL PRIMARY KEY, + access_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + FOREIGN KEY (access_id) + REFERENCES access(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (user_id) + REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + permission_level VARCHAR(255) +); + +INSERT INTO user_access(access_id, user_id, permission_level) + SELECT (select access.id as access_id from access where access.name = "RootAccess"), + (select users.id as user_id from users where user.id = 0), + (select "RootAccess" as permission_level); diff --git a/backend/migrations/2019-04-15-150025_chemicals/down.sql b/backend/migrations/2019-04-15-150025_chemicals/down.sql new file mode 100644 index 0000000..62a3643 --- /dev/null +++ b/backend/migrations/2019-04-15-150025_chemicals/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP chemical_inventory; +DROP chemical; diff --git a/backend/migrations/2019-04-15-150025_chemicals/up.sql b/backend/migrations/2019-04-15-150025_chemicals/up.sql new file mode 100644 index 0000000..b34a79f --- /dev/null +++ b/backend/migrations/2019-04-15-150025_chemicals/up.sql @@ -0,0 +1,30 @@ +-- Your SQL goes here +CREATE TABLE chemical ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + purpose VARCHAR(1023) NOT NULL, + company_name VARCHAR(255) NOT NULL, + ingredients VARCHAR(1023) NOT NULL, + manual_link VARCHAR(1023) NOT NULL +); + +CREATE TABLE chemical_inventory ( + id SERIAL PRIMARY KEY, + purchaser_id BIGINT UNSIGNED NOT NULL, + custodian_id BIGINT UNSIGNED NOT NULL, + chemical_id BIGINT UNSIGNED NOT NULL, + storage_location VARCHAR(255) NOT NULL, + amount VARCHAR(255) NOT NULL, + FOREIGN KEY (purchaser_id) + REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (custodian_id) + REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY (chemical_id) + REFERENCES chemical(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); diff --git a/backend/src/access.rs b/backend/src/access.rs new file mode 100644 index 0000000..849c906 --- /dev/null +++ b/backend/src/access.rs @@ -0,0 +1,3 @@ +pub mod models; +pub mod requests; +pub mod schema; diff --git a/backend/src/access/models.rs b/backend/src/access/models.rs new file mode 100644 index 0000000..2d0881c --- /dev/null +++ b/backend/src/access/models.rs @@ -0,0 +1,228 @@ +use diesel::Queryable; + +use rouille::router; + +use serde::Deserialize; +use serde::Serialize; + +use url::form_urlencoded; + +use log::{trace, warn}; + +use crate::errors::{WebdevError, WebdevErrorKind}; + +use crate::search::{NullableSearch, Search}; + +use super::schema::{access, user_access}; + +#[derive(Queryable, Serialize, Deserialize)] +pub struct Access { + pub id: u64, + pub access_name: String, +} + +#[derive(Insertable, Serialize, Deserialize)] +#[table_name = "access"] +pub struct NewAccess { + pub access_name: String, +} + +#[derive(AsChangeset, Serialize, Deserialize)] +#[table_name = "access"] +pub struct PartialAccess { + pub access_name: Option, +} + +pub enum AccessRequest { + GetAccess(u64), //id of access name searched + CreateAccess(NewAccess), //new access type of some name to be created + UpdateAccess(u64, PartialAccess), //Contains id to be changed to new access_name + DeleteAccess(u64), //if of access to be deleted +} + +impl AccessRequest { + pub fn from_rouille(request: &rouille::Request) -> Result { + trace!("Creating AccessRequest from {:#?}", request); + + router!(request, + (GET) (/{id: u64}) => { + Ok(AccessRequest::GetAccess(id)) + }, + + (POST) (/) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let new_access: NewAccess = serde_json::from_reader(request_body)?; + + Ok(AccessRequest::CreateAccess(new_access)) + }, + + (POST) (/{id: u64}) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let update_access: PartialAccess = serde_json::from_reader(request_body)?; + + Ok(AccessRequest::UpdateAccess(id, update_access)) + }, + + (DELETE) (/{id: u64}) => { + Ok(AccessRequest::DeleteAccess(id)) + }, + + _ => { + warn!("Could not create an access request for the given rouille request"); + Err(WebdevError::new(WebdevErrorKind::NotFound)) + } + ) //end router + } +} + +pub enum AccessResponse { + OneAccess(Access), + NoResponse, +} + +impl AccessResponse { + pub fn to_rouille(self) -> rouille::Response { + match self { + AccessResponse::OneAccess(access) => rouille::Response::json(&access), + AccessResponse::NoResponse => rouille::Response::empty_204(), + } + } +} + +#[derive(Queryable, Serialize, Deserialize)] +pub struct UserAccess { + pub permission_id: u64, + pub access_id: u64, + pub user_id: u64, + pub permission_level: Option, +} + +#[derive(Insertable, Serialize, Deserialize)] +#[table_name = "user_access"] +pub struct NewUserAccess { + pub access_id: u64, + pub user_id: u64, + pub permission_level: Option, +} + +#[derive(AsChangeset, Serialize, Deserialize)] +#[table_name = "user_access"] +pub struct PartialUserAccess { + pub access_id: Option, + pub user_id: Option, + pub permission_level: Option>, +} + +pub struct SearchUserAccess { + pub access_id: Search, + pub user_id: Search, + pub permission_level: NullableSearch, +} + +pub enum UserAccessRequest { + SearchAccess(SearchUserAccess), //list of users with access id or (?) name + GetAccess(u64), //get individual access entry from its id + CheckAccess(u64, String), //entry allowing user of user_id to perform action of action_id + CreateAccess(NewUserAccess), //entry to add to database + UpdateAccess(u64, PartialUserAccess), //entry to update with new information + DeleteAccess(u64), //entry to delete from database +} + +impl UserAccessRequest { + pub fn from_rouille(request: &rouille::Request) -> Result { + trace!("Creating UserAccessRequest from {:#?}", request); + + let url_queries = form_urlencoded::parse(request.raw_query_string().as_bytes()); + + router!(request, + (GET) (/) => { + let mut access_id_search = Search::NoSearch; + let mut user_id_search = Search::NoSearch; + let mut permission_level_search = NullableSearch::NoSearch; + + for (field, query) in url_queries { + match field.as_ref() as &str { + "access_id" => access_id_search = Search::from_query(query.as_ref())?, + "user_id" => user_id_search = Search::from_query(query.as_ref())?, + "permission_level" => permission_level_search = NullableSearch::from_query(query.as_ref())?, + _ => return Err(WebdevError::new(WebdevErrorKind::Format)), + } + } + + Ok(UserAccessRequest::SearchAccess(SearchUserAccess { + access_id: access_id_search, + user_id: user_id_search, + permission_level: permission_level_search, + })) + }, + + (GET) (/{permission_id: u64}) => { + Ok(UserAccessRequest::GetAccess(permission_id)) + }, + + (GET) (/{user_id:u64}/{access_name: String}) => { + Ok(UserAccessRequest::CheckAccess(user_id, access_name)) + }, + + (POST) (/) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let new_user_access: NewUserAccess = serde_json::from_reader(request_body)?; + + Ok(UserAccessRequest::CreateAccess(new_user_access)) + }, + + (POST) (/{id: u64}) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let update_user_access: PartialUserAccess = serde_json::from_reader(request_body)?; + + Ok(UserAccessRequest::UpdateAccess(id, update_user_access)) + }, + + (DELETE) (/{id: u64}) => { + Ok(UserAccessRequest::DeleteAccess(id)) + }, + + _ => { + warn!("Could not create a user access request for the given rouille request"); + Err(WebdevError::new(WebdevErrorKind::NotFound)) + } + ) //end router + } +} + +pub enum UserAccessResponse { + AccessState(bool), + ManyUserAccess(JoinedUserAccessList), + OneUserAccess(UserAccess), + NoResponse, +} + +impl UserAccessResponse { + pub fn to_rouille(self) -> rouille::Response { + match self { + UserAccessResponse::AccessState(state) => { + rouille::Response::text(if state { "true" } else { "false" }) + } + UserAccessResponse::ManyUserAccess(user_accesses) => { + rouille::Response::json(&user_accesses) + } + UserAccessResponse::OneUserAccess(user_access) => rouille::Response::json(&user_access), + UserAccessResponse::NoResponse => rouille::Response::empty_204(), + } + } +} + +#[derive(Queryable, Serialize, Deserialize)] +pub struct JoinedUserAccess { + pub permission_id: u64, + pub user_id: u64, + pub access_id: u64, + pub first_name: String, + pub last_name: String, + pub banner_id: u32, +} + +#[derive(Serialize, Deserialize)] +pub struct JoinedUserAccessList { + pub entries: Vec, +} diff --git a/backend/src/access/requests.rs b/backend/src/access/requests.rs new file mode 100644 index 0000000..a36f7c3 --- /dev/null +++ b/backend/src/access/requests.rs @@ -0,0 +1,394 @@ +use diesel; +use diesel::mysql::types::Unsigned; +use diesel::mysql::Mysql; +use diesel::mysql::MysqlConnection; +use diesel::query_builder::AsQuery; +use diesel::query_builder::BoxedSelectStatement; +use diesel::types; +use diesel::ExpressionMethods; +use diesel::NullableExpressionMethods; +use diesel::QueryDsl; +use diesel::RunQueryDsl; +use diesel::TextExpressionMethods; + +use google_signin; + +use crate::errors::{WebdevError, WebdevErrorKind}; + +use crate::search::{NullableSearch, Search}; + +use super::models::{ + Access, AccessRequest, AccessResponse, JoinedUserAccess, JoinedUserAccessList, NewAccess, + NewUserAccess, PartialAccess, PartialUserAccess, SearchUserAccess, UserAccess, + UserAccessRequest, UserAccessResponse, +}; + +use crate::users::models::{SearchUser, User, UserList, UserRequest, UserResponse}; +use crate::users::requests::handle_user; + +use super::schema::access as access_schema; +use super::schema::user_access as user_access_schema; +use crate::users::schema::users as users_schema; + +pub fn get_user(id_token: &str, database_connection: &MysqlConnection) -> Option { + let mut client = google_signin::Client::new(); + client.audiences.push(String::from( + "743489940041-kd5b4r3l4tiohluea0omifnhdf7db78t.apps.googleusercontent.com", + )); + + let id_info = client.verify(id_token); + + match id_info { + Ok(info) => match info.email { + Some(email) => { + let search_request = UserRequest::SearchUsers(SearchUser { + first_name: Search::NoSearch, + last_name: Search::NoSearch, + banner_id: Search::NoSearch, + email: NullableSearch::Exact(email), + }); + match handle_user(search_request, Some(0), database_connection) { + Ok(found_user) => match found_user { + UserResponse::OneUser(user) => Some(user.id), + UserResponse::ManyUsers(user_list) => Some(user_list.users[0].id), + UserResponse::NoResponse => None, + }, + Err(_e) => None, + } + } + None => None, + }, + Err(_e) => None, + } +} + +pub fn check_to_run( + requesting_user_id: Option, + access_type: &str, + database_connection: &MysqlConnection, +) -> Result<(), WebdevError> { + match requesting_user_id { + Some(user_id) => { + match check_user_access(user_id, String::from(access_type), database_connection) { + Ok(access) => { + if access { + Ok(()) + } else { + Err(WebdevError::new(WebdevErrorKind::AccessDenied)) + } + } + Err(e) => Err(e), + } + } + None => Err(WebdevError::new(WebdevErrorKind::AccessDenied)), + } +} + +pub fn handle_access( + request: AccessRequest, + requesting_user: Option, + database_connection: &MysqlConnection, +) -> Result { + match request { + AccessRequest::GetAccess(id) => { + match check_to_run(requesting_user, "GetAccess", database_connection) { + Ok(()) => get_access(id, database_connection).map(|a| AccessResponse::OneAccess(a)), + Err(e) => Err(e), + } + } + AccessRequest::CreateAccess(access) => { + match check_to_run(requesting_user, "CreateAccess", database_connection) { + Ok(()) => { + create_access(access, database_connection).map(|a| AccessResponse::OneAccess(a)) + } + Err(e) => Err(e), + } + } + AccessRequest::UpdateAccess(id, access) => { + match check_to_run(requesting_user, "UpdateAccess", database_connection) { + Ok(()) => update_access(id, access, database_connection) + .map(|_| AccessResponse::NoResponse), + Err(e) => Err(e), + } + } + AccessRequest::DeleteAccess(id) => { + match check_to_run(requesting_user, "DeleteAccess", database_connection) { + Ok(()) => { + delete_access(id, database_connection).map(|_| AccessResponse::NoResponse) + } + Err(e) => Err(e), + } + } + } +} + +fn get_access(id: u64, database_connection: &MysqlConnection) -> Result { + let mut found_access = access_schema::table + .filter(access_schema::id.eq(id)) + .load::(database_connection)?; + + match found_access.pop() { + Some(access) => Ok(access), + None => Err(WebdevError::new(WebdevErrorKind::NotFound)), + } +} + +fn create_access( + access: NewAccess, + database_connection: &MysqlConnection, +) -> Result { + diesel::insert_into(access_schema::table) + .values(access) + .execute(database_connection)?; + + no_arg_sql_function!(last_insert_id, Unsigned); + + let mut inserted_accesses = access_schema::table + .filter(access_schema::id.eq(last_insert_id)) + //.filter(diesel::dsl::sql("id = LAST_INSERT_ID()")) + .load::(database_connection)?; + + if let Some(inserted_access) = inserted_accesses.pop() { + Ok(inserted_access) + } else { + Err(WebdevError::new(WebdevErrorKind::Database)) + } +} + +fn update_access( + id: u64, + access: PartialAccess, + database_connection: &MysqlConnection, +) -> Result<(), WebdevError> { + diesel::update(access_schema::table) + .filter(access_schema::id.eq(id)) + .set(&access) + .execute(database_connection)?; + Ok(()) +} + +fn delete_access(id: u64, database_connection: &MysqlConnection) -> Result<(), WebdevError> { + diesel::delete(access_schema::table.filter(access_schema::id.eq(id))) + .execute(database_connection)?; + + Ok(()) +} + +pub fn handle_user_access( + request: UserAccessRequest, + requesting_user: Option, + database_connection: &MysqlConnection, +) -> Result { + match request { + UserAccessRequest::SearchAccess(user_access) => { + match check_to_run(requesting_user, "GetUserAccess", database_connection) { + Ok(()) => search_user_access(user_access, database_connection) + .map(|u| UserAccessResponse::ManyUserAccess(u)), + Err(e) => Err(e), + } + } + UserAccessRequest::GetAccess(permission_id) => { + match check_to_run(requesting_user, "GetUserAccess", database_connection) { + Ok(()) => get_user_access(permission_id, database_connection) + .map(|a| UserAccessResponse::OneUserAccess(a)), + Err(e) => Err(e), + } + } + UserAccessRequest::CheckAccess(user_id, access_name) => { + check_user_access(user_id, access_name, database_connection) + .map(|s| UserAccessResponse::AccessState(s)) + } + UserAccessRequest::CreateAccess(user_access) => { + match check_to_run(requesting_user, "CreateUserAccess", database_connection) { + Ok(()) => create_user_access(user_access, database_connection) + .map(|a| UserAccessResponse::OneUserAccess(a)), + Err(e) => Err(e), + } + } + UserAccessRequest::UpdateAccess(id, user_access) => { + match check_to_run(requesting_user, "UpdateUserAccess", database_connection) { + Ok(()) => update_user_access(id, user_access, database_connection) + .map(|_| UserAccessResponse::NoResponse), + Err(e) => Err(e), + } + } + UserAccessRequest::DeleteAccess(id) => { + match check_to_run(requesting_user, "DeleteUserAccess", database_connection) { + Ok(()) => delete_user_access(id, database_connection) + .map(|_| UserAccessResponse::NoResponse), + Err(e) => Err(e), + } + } + } +} + +fn search_user_access( + user_access_search: SearchUserAccess, + database_connection: &MysqlConnection, +) -> Result { + let mut user_access_query = user_access_schema::table + .inner_join(access_schema::table) + .inner_join(users_schema::table) + .select(( + user_access_schema::permission_id, + users_schema::id, + access_schema::id, + users_schema::first_name, + users_schema::last_name, + users_schema::banner_id, + )) + .into_boxed::(); + + match user_access_search.access_id { + Search::Partial(s) => { + user_access_query = user_access_query.filter(user_access_schema::access_id.eq(s)) + } + + Search::Exact(s) => { + user_access_query = user_access_query.filter(user_access_schema::access_id.eq(s)) + } + + Search::NoSearch => {} + } + + match user_access_search.user_id { + Search::Partial(s) => { + user_access_query = user_access_query.filter(user_access_schema::user_id.eq(s)) + } + + Search::Exact(s) => { + user_access_query = user_access_query.filter(user_access_schema::user_id.eq(s)) + } + + Search::NoSearch => {} + } + + match user_access_search.permission_level { + NullableSearch::Partial(s) => { + user_access_query = user_access_query + .filter(user_access_schema::permission_level.like(format!("{}%", s))) + } + + NullableSearch::Exact(s) => { + user_access_query = user_access_query.filter(user_access_schema::permission_level.eq(s)) + } + + NullableSearch::Some => { + user_access_query = + user_access_query.filter(user_access_schema::permission_level.is_not_null()); + } + + NullableSearch::None => { + user_access_query = + user_access_query.filter(user_access_schema::permission_level.is_null()); + } + + NullableSearch::NoSearch => {} + } + + let found_access_entries = user_access_query.load::(database_connection)?; + + let joined_list = JoinedUserAccessList { + entries: found_access_entries, + }; + + Ok(joined_list) +} + +fn get_user_access( + permission_id: u64, + database_connection: &MysqlConnection, +) -> Result { + let mut found_user_accesses = user_access_schema::table + .filter(user_access_schema::permission_id.eq(permission_id)) + .load::(database_connection)?; + + match found_user_accesses.pop() { + Some(found_user_access) => Ok(found_user_access), + None => Err(WebdevError::new(WebdevErrorKind::NotFound)), + } +} + +fn check_user_access( + user_id: u64, + access_name: String, + database_connection: &MysqlConnection, +) -> Result { + if access_name != "RootAccess" { + match check_user_access(user_id, String::from("RootAccess"), database_connection) { + Ok(access) => { + if access { + return Ok(true); + } + } + Err(_e) => {} + } + } + + let found_user_accesses = user_access_schema::table + .inner_join(access_schema::table) + .select((user_access_schema::user_id, access_schema::access_name)) + .filter(user_access_schema::user_id.eq(user_id)) + .filter(access_schema::access_name.eq(access_name)) + .execute(database_connection)?; + + if found_user_accesses != 0 { + Ok(true) + } else { + Ok(false) + } +} + +fn create_user_access( + user_access: NewUserAccess, + database_connection: &MysqlConnection, +) -> Result { + //find if permission currently exists, should not duplicate (user_id, access_id) pairs + let found_user_accesses = user_access_schema::table + .filter(user_access_schema::user_id.eq(user_access.user_id)) + .filter(user_access_schema::access_id.eq(user_access.access_id)) + .execute(database_connection)?; + + if found_user_accesses != 0 { + return Err(WebdevError::new(WebdevErrorKind::Database)); + } + + //permission most definitely does not exist at this point + + diesel::insert_into(user_access_schema::table) + .values(user_access) + .execute(database_connection)?; + + no_arg_sql_function!(last_insert_id, Unsigned); + + let mut inserted_accesses = user_access_schema::table + .filter(user_access_schema::permission_id.eq(last_insert_id)) + //.filter(diesel::dsl::sql("permission_id = LAST_INSERT_ID()")) + .load::(database_connection)?; + + if let Some(inserted_access) = inserted_accesses.pop() { + Ok(inserted_access) + } else { + Err(WebdevError::new(WebdevErrorKind::Database)) + } +} + +fn update_user_access( + id: u64, + user_access: PartialUserAccess, + database_connection: &MysqlConnection, +) -> Result<(), WebdevError> { + diesel::update(user_access_schema::table) + .filter(user_access_schema::permission_id.eq(id)) + .set(&user_access) + .execute(database_connection)?; + + Ok(()) +} + +fn delete_user_access(id: u64, database_connection: &MysqlConnection) -> Result<(), WebdevError> { + diesel::delete(user_access_schema::table.filter(user_access_schema::permission_id.eq(id))) + .execute(database_connection)?; + + Ok(()) +} diff --git a/backend/src/access/schema.rs b/backend/src/access/schema.rs new file mode 100644 index 0000000..57bf738 --- /dev/null +++ b/backend/src/access/schema.rs @@ -0,0 +1,22 @@ +use crate::users::schema::users; + +table! { + access (id) { + id -> Unsigned, + access_name -> Varchar, + } +} + +table! { + user_access (permission_id) { + permission_id -> Unsigned, + access_id -> Unsigned, + user_id -> Unsigned, + permission_level -> Nullable, + } +} + +joinable!(user_access -> access (access_id)); +joinable!(user_access -> users (user_id)); + +allow_tables_to_appear_in_same_query!(access, user_access, users,); diff --git a/backend/src/bin/csv_user_import.rs b/backend/src/bin/csv_user_import.rs index 92bfaf5..b30e22d 100644 --- a/backend/src/bin/csv_user_import.rs +++ b/backend/src/bin/csv_user_import.rs @@ -1,104 +1,103 @@ -use log::debug; -use log::error; -use log::info; -use log::trace; -use log::warn; -use diesel::prelude::*; -use diesel::MysqlConnection; -use dotenv::dotenv; -use csv; -use web_dev::users::models::{NewUser,UserRequest}; -use web_dev::users::requests; -use serde::Deserialize; -use serde::Serialize; - -#[derive(Serialize, Deserialize, Debug)] -//Struct to take the data from the csv -struct Csv_User { - #[serde(rename = "Banner ID")] - banner_id: i32, - #[serde(rename = "Last Name")] - last_name: String, - #[serde(rename = "First Name")] - first_name: String, - #[serde(rename = "Email")] - email: String, - #[serde(rename = "Year")] - year: String, - #[serde(rename = "Department")] - department: String, -} -fn main(){ - //Diesel things - dotenv().ok(); - - simplelog::TermLogger::init(simplelog::LevelFilter::Trace, simplelog::Config::default()) - .unwrap(); - - info!("Connecting to database"); - - let database_url = match env::var("DATABASE_URL") { - Ok(url) => url, - Err(e) => { - error!("Could not read DATABASE_URL environment variable"); - return; - } - }; - - debug!("Connecting to {}", database_url); - - let connection = match MysqlConnection::establish(&database_url) { - Ok(c) => c, - Err(e) => { - error!("Could not connect to database: {}", e); - return; - } - }; - - debug!("Connected to database"); - //Get file name and path from args - use std::env; - let arg = env::args().nth(1); - let filename = match arg { - Some(name) => name, - None => { - error!("Needs a filename"); - return; - } - }; - debug!("{}", filename); - //Import the csv into an iterator - let mut user_count = 0; - let all_users_result = csv::Reader::from_path(filename); - let mut all_users = match all_users_result{ - Ok(data) => data, - Err(e) => { - error!("Bad file. Error {}",e); - return; - } - }; - //Go through each item in the iterator - for result in all_users.deserialize(){ - //Check to see if it's valid - let csv_user: Csv_User = match result{ - Ok(data) => data, - Err(e) => { - error!("Bad data, {:?}", e); - return; - } - }; - //Convert the user data from the csv and create a New User from it - let new_user:NewUser = NewUser{ - first_name: csv_user.first_name, - last_name: csv_user.last_name, - email: Some(csv_user.email), - banner_id: csv_user.banner_id as u32, - }; - //Import new user into database - let import_user = UserRequest::CreateUser(new_user); - requests::handle_user(import_user, &connection); - user_count = user_count+1; - } - info!("Imported {} user(s)",user_count); -} - +use csv; +use diesel::prelude::*; +use diesel::MysqlConnection; +use dotenv::dotenv; +use log::debug; +use log::error; +use log::info; +use log::trace; +use log::warn; +use serde::Deserialize; +use serde::Serialize; +use web_dev::users::models::{NewUser, UserRequest}; +use web_dev::users::requests; + +#[derive(Serialize, Deserialize, Debug)] +//Struct to take the data from the csv +struct Csv_User { + #[serde(rename = "Banner ID")] + banner_id: i32, + #[serde(rename = "Last Name")] + last_name: String, + #[serde(rename = "First Name")] + first_name: String, + #[serde(rename = "Email")] + email: String, + #[serde(rename = "Year")] + year: String, + #[serde(rename = "Department")] + department: String, +} +fn main() { + //Diesel things + dotenv().ok(); + + simplelog::TermLogger::init(simplelog::LevelFilter::Trace, simplelog::Config::default()) + .unwrap(); + + info!("Connecting to database"); + + let database_url = match env::var("DATABASE_URL") { + Ok(url) => url, + Err(e) => { + error!("Could not read DATABASE_URL environment variable"); + return; + } + }; + + debug!("Connecting to {}", database_url); + + let connection = match MysqlConnection::establish(&database_url) { + Ok(c) => c, + Err(e) => { + error!("Could not connect to database: {}", e); + return; + } + }; + + debug!("Connected to database"); + //Get file name and path from args + use std::env; + let arg = env::args().nth(1); + let filename = match arg { + Some(name) => name, + None => { + error!("Needs a filename"); + return; + } + }; + debug!("{}", filename); + //Import the csv into an iterator + let mut user_count = 0; + let all_users_result = csv::Reader::from_path(filename); + let mut all_users = match all_users_result { + Ok(data) => data, + Err(e) => { + error!("Bad file. Error {}", e); + return; + } + }; + //Go through each item in the iterator + for result in all_users.deserialize() { + //Check to see if it's valid + let csv_user: Csv_User = match result { + Ok(data) => data, + Err(e) => { + error!("Bad data, {:?}", e); + return; + } + }; + //Convert the user data from the csv and create a New User from it + let new_user: NewUser = NewUser { + first_name: csv_user.first_name, + last_name: csv_user.last_name, + email: Some(csv_user.email), + banner_id: csv_user.banner_id as u32, + }; + //Import new user into database + let import_user = UserRequest::CreateUser(new_user); + requests::handle_user(import_user, Some(0), &connection); + user_count = user_count + 1; + } + info!("Imported {} user(s)", user_count); +} diff --git a/backend/src/chemicals.rs b/backend/src/chemicals.rs new file mode 100644 index 0000000..849c906 --- /dev/null +++ b/backend/src/chemicals.rs @@ -0,0 +1,3 @@ +pub mod models; +pub mod requests; +pub mod schema; diff --git a/backend/src/chemicals/models.rs b/backend/src/chemicals/models.rs new file mode 100644 index 0000000..7667865 --- /dev/null +++ b/backend/src/chemicals/models.rs @@ -0,0 +1,284 @@ +use diesel::Queryable; + +use rouille::router; + +use serde::Deserialize; +use serde::Serialize; + +use url::form_urlencoded; + +use log::{trace, warn}; + +use crate::errors::{WebdevError, WebdevErrorKind}; + +use crate::search::{NullableSearch, Search}; + +use super::schema::{chemical, chemical_inventory}; + +#[derive(Queryable, Serialize, Deserialize)] +pub struct Chemical { + pub id: u64, + pub name: String, + pub purpose: String, + pub company_name: String, + pub ingredients: String, + pub manual_link: String, +} + +#[derive(Insertable, Serialize, Deserialize)] +#[table_name = "chemical"] +pub struct NewChemical { + pub name: String, + pub purpose: String, + pub company_name: String, + pub ingredients: String, + pub manual_link: String, +} + +#[derive(AsChangeset, Serialize, Deserialize)] +#[table_name = "chemical"] +pub struct PartialChemical { + pub name: Option, + pub purpose: Option, + pub company_name: Option, + pub ingredients: Option, + pub manual_link: Option, +} + +pub struct SearchChemical { + pub name: Search, + pub purpose: Search, + pub company_name: Search, + pub ingredients: Search, + pub manual_link: Search, +} + +#[derive(Serialize, Deserialize)] +pub struct ChemicalList { + pub chemicals: Vec, +} + +pub enum ChemicalRequest { + Search(SearchChemical), + GetChemical(u64), //id of access name searched + CreateChemical(NewChemical), //new access type of some name to be created + UpdateChemical(u64, PartialChemical), //Contains id to be changed to new access_name + DeleteChemical(u64), //if of access to be deleted +} + +impl ChemicalRequest { + pub fn from_rouille(request: &rouille::Request) -> Result { + trace!("Creating ChemicalRequest from {:#?}", request); + + let url_queries = form_urlencoded::parse(request.raw_query_string().as_bytes()); + + router!(request, + (GET) (/) => { + let mut name_search = Search::NoSearch; + let mut purpose_search = Search::NoSearch; + let mut company_name_search = Search::NoSearch; + let mut ingredients_search = Search::NoSearch; + let mut manual_link_search = Search::NoSearch; + + for (field, query) in url_queries { + match field.as_ref() as &str { + "name" => name_search = Search::from_query(query.as_ref())?, + "purpose" => purpose_search = Search::from_query(query.as_ref())?, + "company_name" => company_name_search = Search::from_query(query.as_ref())?, + "ingredients" => ingredients_search = Search::from_query(query.as_ref())?, + "manual_link" => manual_link_search = Search::from_query(query.as_ref())?, + _ => return Err(WebdevError::new(WebdevErrorKind::Format)), + } + } + + Ok(ChemicalRequest::Search(SearchChemical { + name: name_search, + purpose: purpose_search, + company_name: company_name_search, + ingredients: ingredients_search, + manual_link: manual_link_search, + })) + }, + + (GET) (/{id: u64}) => { + Ok(ChemicalRequest::GetChemical(id)) + }, + + (POST) (/) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let new_chemical: NewChemical = serde_json::from_reader(request_body)?; + + Ok(ChemicalRequest::CreateChemical(new_chemical)) + }, + + (POST) (/{id: u64}) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let update_chemical: PartialChemical = serde_json::from_reader(request_body)?; + + Ok(ChemicalRequest::UpdateChemical(id, update_chemical)) + }, + + (DELETE) (/{id: u64}) => { + Ok(ChemicalRequest::DeleteChemical(id)) + }, + + _ => { + warn!("Could not create a chemical request for the given rouille request"); + Err(WebdevError::new(WebdevErrorKind::NotFound)) + } + ) //end router + } +} + +pub enum ChemicalResponse { + OneChemical(Chemical), + ManyChemical(ChemicalList), + NoResponse, +} + +impl ChemicalResponse { + pub fn to_rouille(self) -> rouille::Response { + match self { + ChemicalResponse::OneChemical(chemical) => rouille::Response::json(&chemical), + ChemicalResponse::ManyChemical(chemicals) => { + rouille::Response::json(&chemicals.chemicals) + } + ChemicalResponse::NoResponse => rouille::Response::empty_204(), + } + } +} + +#[derive(Queryable, Serialize, Deserialize)] +pub struct ChemicalInventory { + pub id: u64, + pub purchaser_id: u64, + pub custodian_id: u64, + pub chemical_id: u64, + pub storage_location: String, + pub amount: String, +} + +#[derive(Insertable, Serialize, Deserialize)] +#[table_name = "chemical_inventory"] +pub struct NewChemicalInventory { + pub purchaser_id: u64, + pub custodian_id: u64, + pub chemical_id: u64, + pub storage_location: String, + pub amount: String, +} + +#[derive(AsChangeset, Serialize, Deserialize)] +#[table_name = "chemical_inventory"] +pub struct PartialChemicalInventory { + pub purchaser_id: Option, + pub custodian_id: Option, + pub chemical_id: Option, + pub storage_location: Option, + pub amount: Option, +} + +pub struct SearchChemicalInventory { + pub purchaser_id: Search, + pub custodian_id: Search, + pub chemical_id: Search, + pub storage_location: Search, + pub amount: Search, +} + +#[derive(Serialize, Deserialize)] +pub struct ChemicalInventoryList { + pub entries: Vec, +} + +pub enum ChemicalInventoryRequest { + SearchInventory(SearchChemicalInventory), + GetInventory(u64), + CreateInventory(NewChemicalInventory), + UpdateInventory(u64, PartialChemicalInventory), + DeleteInventory(u64), +} + +impl ChemicalInventoryRequest { + pub fn from_rouille( + request: &rouille::Request, + ) -> Result { + trace!("Creating ChemicalInvntoryRequest from {:#?}", request); + + let url_queries = form_urlencoded::parse(request.raw_query_string().as_bytes()); + + router!(request, + (GET) (/) => { + let mut purchaser_id_search = Search::NoSearch; + let mut custodian_id_search = Search::NoSearch; + let mut chemical_id_search = Search::NoSearch; + let mut storage_location_search = Search::NoSearch; + let mut amount_search = Search::NoSearch; + + for (field, query) in url_queries { + match field.as_ref() as &str { + "purchaser_id" => purchaser_id_search = Search::from_query(query.as_ref())?, + "custodian_id" => custodian_id_search = Search::from_query(query.as_ref())?, + "chemical_id" => chemical_id_search = Search::from_query(query.as_ref())?, + "storage_location" => storage_location_search = Search::from_query(query.as_ref())?, + "amount" => amount_search = Search::from_query(query.as_ref())?, + _ => return Err(WebdevError::new(WebdevErrorKind::Format)), + } + } + + Ok(ChemicalInventoryRequest::SearchInventory(SearchChemicalInventory { + purchaser_id: purchaser_id_search, + custodian_id: custodian_id_search, + chemical_id: chemical_id_search, + storage_location: storage_location_search, + amount: amount_search, + })) + }, + + (GET) (/{permission_id: u64}) => { + Ok(ChemicalInventoryRequest::GetInventory(permission_id)) + }, + + (POST) (/) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let new_chemical_inventory: NewChemicalInventory = serde_json::from_reader(request_body)?; + + Ok(ChemicalInventoryRequest::CreateInventory(new_chemical_inventory)) + }, + + (POST) (/{id: u64}) => { + let request_body = request.data().ok_or(WebdevError::new(WebdevErrorKind::Format))?; + let update_chemical_inventory: PartialChemicalInventory = serde_json::from_reader(request_body)?; + + Ok(ChemicalInventoryRequest::UpdateInventory(id, update_chemical_inventory)) + }, + + (DELETE) (/{id: u64}) => { + Ok(ChemicalInventoryRequest::DeleteInventory(id)) + }, + + _ => { + warn!("Could not create a chemical inventory request for the given rouille request"); + Err(WebdevError::new(WebdevErrorKind::NotFound)) + } + ) //end router + } +} + +pub enum ChemicalInventoryResponse { + OneInventoryEntry(ChemicalInventory), + ManyInventoryEntries(ChemicalInventoryList), + NoResponse, +} + +impl ChemicalInventoryResponse { + pub fn to_rouille(self) -> rouille::Response { + match self { + ChemicalInventoryResponse::OneInventoryEntry(entry) => rouille::Response::json(&entry), + ChemicalInventoryResponse::ManyInventoryEntries(entries) => { + rouille::Response::json(&entries.entries) + } + ChemicalInventoryResponse::NoResponse => rouille::Response::empty_204(), + } + } +} diff --git a/backend/src/chemicals/requests.rs b/backend/src/chemicals/requests.rs new file mode 100644 index 0000000..fbc6d19 --- /dev/null +++ b/backend/src/chemicals/requests.rs @@ -0,0 +1,395 @@ +use diesel; +use diesel::mysql::types::Unsigned; +use diesel::mysql::Mysql; +use diesel::mysql::MysqlConnection; +use diesel::query_builder::AsQuery; +use diesel::query_builder::BoxedSelectStatement; +use diesel::types; +use diesel::ExpressionMethods; +use diesel::NullableExpressionMethods; +use diesel::QueryDsl; +use diesel::RunQueryDsl; +use diesel::TextExpressionMethods; + +use crate::errors::{WebdevError, WebdevErrorKind}; + +use crate::search::Search; + +use super::models::{ + Chemical, ChemicalInventory, ChemicalInventoryList, ChemicalInventoryRequest, + ChemicalInventoryResponse, ChemicalList, ChemicalRequest, ChemicalResponse, NewChemical, + NewChemicalInventory, PartialChemical, PartialChemicalInventory, SearchChemical, + SearchChemicalInventory, +}; + +use super::schema::chemical as chemical_schema; +use super::schema::chemical_inventory as chemical_inventory_schema; + +use crate::access::requests::check_to_run; + +pub fn handle_chemical( + request: ChemicalRequest, + requesting_user: Option, + database_connection: &MysqlConnection, +) -> Result { + match request { + ChemicalRequest::Search(chemical) => { + match check_to_run(requesting_user, "GetChemical", database_connection) { + Ok(()) => search_chemical(chemical, database_connection) + .map(|c| ChemicalResponse::ManyChemical(c)), + Err(e) => Err(e), + } + } + ChemicalRequest::GetChemical(id) => { + match check_to_run(requesting_user, "GetChemical", database_connection) { + Ok(()) => { + get_chemical(id, database_connection).map(|c| ChemicalResponse::OneChemical(c)) + } + Err(e) => Err(e), + } + } + ChemicalRequest::CreateChemical(chemical) => { + match check_to_run(requesting_user, "CreateChemical", database_connection) { + Ok(()) => create_chemical(chemical, database_connection) + .map(|c| ChemicalResponse::OneChemical(c)), + Err(e) => Err(e), + } + } + ChemicalRequest::UpdateChemical(id, chemical) => { + match check_to_run(requesting_user, "UpdateChemical", database_connection) { + Ok(()) => update_chemical(id, chemical, database_connection) + .map(|_| ChemicalResponse::NoResponse), + Err(e) => Err(e), + } + } + ChemicalRequest::DeleteChemical(id) => { + match check_to_run(requesting_user, "DeleteChemical", database_connection) { + Ok(()) => { + delete_chemical(id, database_connection).map(|_| ChemicalResponse::NoResponse) + } + Err(e) => Err(e), + } + } + } +} + +fn search_chemical( + chemical_search: SearchChemical, + database_connection: &MysqlConnection, +) -> Result { + let mut chemical_query = chemical_schema::table.as_query().into_boxed(); + + match chemical_search.name { + Search::Partial(s) => { + chemical_query = chemical_query.filter(chemical_schema::name.like(format!("{}%", s))) + } + + Search::Exact(s) => chemical_query = chemical_query.filter(chemical_schema::name.eq(s)), + + Search::NoSearch => {} + } + + match chemical_search.purpose { + Search::Partial(s) => { + chemical_query = chemical_query.filter(chemical_schema::purpose.like(format!("{}%", s))) + } + + Search::Exact(s) => chemical_query = chemical_query.filter(chemical_schema::purpose.eq(s)), + + Search::NoSearch => {} + } + + match chemical_search.company_name { + Search::Partial(s) => { + chemical_query = + chemical_query.filter(chemical_schema::company_name.like(format!("{}%", s))) + } + + Search::Exact(s) => { + chemical_query = chemical_query.filter(chemical_schema::company_name.eq(s)) + } + + Search::NoSearch => {} + } + + match chemical_search.ingredients { + Search::Partial(s) => { + chemical_query = + chemical_query.filter(chemical_schema::ingredients.like(format!("{}%", s))) + } + + Search::Exact(s) => { + chemical_query = chemical_query.filter(chemical_schema::ingredients.eq(s)) + } + + Search::NoSearch => {} + } + + match chemical_search.manual_link { + Search::Partial(s) => { + chemical_query = + chemical_query.filter(chemical_schema::manual_link.like(format!("{}%", s))) + } + + Search::Exact(s) => { + chemical_query = chemical_query.filter(chemical_schema::manual_link.eq(s)) + } + + Search::NoSearch => {} + } + + let found_chemicals = chemical_query.load::(database_connection)?; + let chemical_list = ChemicalList { + chemicals: found_chemicals, + }; + + Ok(chemical_list) +} + +fn get_chemical(id: u64, database_connection: &MysqlConnection) -> Result { + let mut found_chemical = chemical_schema::table + .filter(chemical_schema::id.eq(id)) + .load::(database_connection)?; + + match found_chemical.pop() { + Some(chemical) => Ok(chemical), + None => Err(WebdevError::new(WebdevErrorKind::NotFound)), + } +} + +fn create_chemical( + chemical: NewChemical, + database_connection: &MysqlConnection, +) -> Result { + diesel::insert_into(chemical_schema::table) + .values(chemical) + .execute(database_connection)?; + + no_arg_sql_function!(last_insert_id, Unsigned); + + let mut inserted_chemicals = chemical_schema::table + .filter(chemical_schema::id.eq(last_insert_id)) + .load::(database_connection)?; + + if let Some(inserted_chemical) = inserted_chemicals.pop() { + Ok(inserted_chemical) + } else { + Err(WebdevError::new(WebdevErrorKind::Database)) + } +} + +fn update_chemical( + id: u64, + chemical: PartialChemical, + database_connection: &MysqlConnection, +) -> Result<(), WebdevError> { + diesel::update(chemical_schema::table) + .filter(chemical_schema::id.eq(id)) + .set(&chemical) + .execute(database_connection)?; + Ok(()) +} + +fn delete_chemical(id: u64, database_connection: &MysqlConnection) -> Result<(), WebdevError> { + diesel::delete(chemical_schema::table.filter(chemical_schema::id.eq(id))) + .execute(database_connection)?; + + Ok(()) +} + +pub fn handle_chemical_inventory( + request: ChemicalInventoryRequest, + requesting_user: Option, + database_connection: &MysqlConnection, +) -> Result { + match request { + ChemicalInventoryRequest::SearchInventory(inventory) => { + match check_to_run(requesting_user, "GetChemicalInventory", database_connection) { + Ok(()) => search_chemical_inventory(inventory, database_connection) + .map(|c| ChemicalInventoryResponse::ManyInventoryEntries(c)), + Err(e) => Err(e), + } + } + ChemicalInventoryRequest::GetInventory(id) => { + match check_to_run(requesting_user, "GetChemicalInventory", database_connection) { + Ok(()) => get_chemical_inventory(id, database_connection) + .map(|c| ChemicalInventoryResponse::OneInventoryEntry(c)), + Err(e) => Err(e), + } + } + ChemicalInventoryRequest::CreateInventory(inventory) => { + match check_to_run( + requesting_user, + "CreateChemicalInventory", + database_connection, + ) { + Ok(()) => create_chemical_inventory(inventory, database_connection) + .map(|c| ChemicalInventoryResponse::OneInventoryEntry(c)), + Err(e) => Err(e), + } + } + ChemicalInventoryRequest::UpdateInventory(id, inventory) => { + match check_to_run( + requesting_user, + "UpdateChemicalInventory", + database_connection, + ) { + Ok(()) => update_chemical_inventory(id, inventory, database_connection) + .map(|_| ChemicalInventoryResponse::NoResponse), + Err(e) => Err(e), + } + } + ChemicalInventoryRequest::DeleteInventory(id) => { + match check_to_run( + requesting_user, + "DeleteChemicalInventory", + database_connection, + ) { + Ok(()) => delete_chemical_inventory(id, database_connection) + .map(|_| ChemicalInventoryResponse::NoResponse), + Err(e) => Err(e), + } + } + } +} + +fn search_chemical_inventory( + chemical_inventory_search: SearchChemicalInventory, + database_connection: &MysqlConnection, +) -> Result { + let mut chemical_inventory_query = chemical_inventory_schema::table.as_query().into_boxed(); + + match chemical_inventory_search.purchaser_id { + Search::Partial(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::purchaser_id.eq(s)) + } + + Search::Exact(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::purchaser_id.eq(s)) + } + + Search::NoSearch => {} + } + + match chemical_inventory_search.custodian_id { + Search::Partial(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::custodian_id.eq(s)) + } + + Search::Exact(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::custodian_id.eq(s)) + } + + Search::NoSearch => {} + } + + match chemical_inventory_search.chemical_id { + Search::Partial(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::chemical_id.eq(s)) + } + + Search::Exact(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::chemical_id.eq(s)) + } + + Search::NoSearch => {} + } + + match chemical_inventory_search.storage_location { + Search::Partial(s) => { + chemical_inventory_query = chemical_inventory_query + .filter(chemical_inventory_schema::storage_location.like(format!("{}%", s))) + } + + Search::Exact(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::storage_location.eq(s)) + } + + Search::NoSearch => {} + } + + match chemical_inventory_search.amount { + Search::Partial(s) => { + chemical_inventory_query = chemical_inventory_query + .filter(chemical_inventory_schema::amount.like(format!("{}%", s))) + } + + Search::Exact(s) => { + chemical_inventory_query = + chemical_inventory_query.filter(chemical_inventory_schema::amount.eq(s)) + } + + Search::NoSearch => {} + } + + let found_entries = chemical_inventory_query.load::(database_connection)?; + let inventory_list = ChemicalInventoryList { + entries: found_entries, + }; + + Ok(inventory_list) +} + +fn get_chemical_inventory( + id: u64, + database_connection: &MysqlConnection, +) -> Result { + let mut found_inventory = chemical_inventory_schema::table + .filter(chemical_inventory_schema::id.eq(id)) + .load::(database_connection)?; + + match found_inventory.pop() { + Some(entry) => Ok(entry), + None => Err(WebdevError::new(WebdevErrorKind::NotFound)), + } +} + +fn create_chemical_inventory( + inventory: NewChemicalInventory, + database_connection: &MysqlConnection, +) -> Result { + diesel::insert_into(chemical_inventory_schema::table) + .values(inventory) + .execute(database_connection)?; + + no_arg_sql_function!(last_insert_id, Unsigned); + + let mut inserted_inventory_entries = chemical_inventory_schema::table + .filter(chemical_inventory_schema::id.eq(last_insert_id)) + .load::(database_connection)?; + + if let Some(inserted_entry) = inserted_inventory_entries.pop() { + Ok(inserted_entry) + } else { + Err(WebdevError::new(WebdevErrorKind::Database)) + } +} + +fn update_chemical_inventory( + id: u64, + inventory: PartialChemicalInventory, + database_connection: &MysqlConnection, +) -> Result<(), WebdevError> { + diesel::update(chemical_inventory_schema::table) + .filter(chemical_inventory_schema::id.eq(id)) + .set(&inventory) + .execute(database_connection)?; + Ok(()) +} + +fn delete_chemical_inventory( + id: u64, + database_connection: &MysqlConnection, +) -> Result<(), WebdevError> { + diesel::delete(chemical_inventory_schema::table.filter(chemical_inventory_schema::id.eq(id))) + .execute(database_connection)?; + + Ok(()) +} diff --git a/backend/src/chemicals/schema.rs b/backend/src/chemicals/schema.rs new file mode 100644 index 0000000..fce54a4 --- /dev/null +++ b/backend/src/chemicals/schema.rs @@ -0,0 +1,30 @@ +use crate::users::schema::users; + +table! { + chemical (id) { + id -> Unsigned, + name -> Varchar, + purpose -> Varchar, + company_name -> Varchar, + ingredients -> Varchar, + manual_link -> Varchar, + } +} + +table! { + chemical_inventory (id) { + id -> Unsigned, + purchaser_id -> Unsigned, + custodian_id -> Unsigned, + chemical_id -> Unsigned, + storage_location -> Varchar, + amount -> Varchar, + } +} + +//Cant seem to do this because of multiple points to users, need explicit on clause in queries +//joinable!(chemical_inventory -> users (purchaser_id)); +//joinable!(chemical_inventory -> users (custodian_id)); +joinable!(chemical_inventory -> chemical (chemical_id)); + +allow_tables_to_appear_in_same_query!(chemical, chemical_inventory, users,); diff --git a/backend/src/errors.rs b/backend/src/errors.rs index 2968a12..ff09001 100644 --- a/backend/src/errors.rs +++ b/backend/src/errors.rs @@ -7,6 +7,7 @@ use crate::search::SearchParseError; pub enum WebdevErrorKind { Database, Format, + AccessDenied, NotFound, } @@ -21,6 +22,7 @@ impl std::fmt::Display for WebdevError { match self.kind { WebdevErrorKind::Database => write!(f, "Database error!"), WebdevErrorKind::Format => write!(f, "Format error!"), + WebdevErrorKind::AccessDenied => write!(f, "Accessed denied!"), WebdevErrorKind::NotFound => write!(f, "Not found!"), } } @@ -95,6 +97,9 @@ impl From for rouille::Response { rouille::Response::text(e.to_string()).with_status_code(404) } WebdevErrorKind::Format => rouille::Response::text(e.to_string()).with_status_code(400), + WebdevErrorKind::AccessDenied => { + rouille::Response::text(e.to_string()).with_status_code(401) + } WebdevErrorKind::Database => { rouille::Response::text(e.to_string()).with_status_code(500) } diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 2fdbd10..659e56e 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,10 +1,10 @@ -#[macro_use] -extern crate diesel; - -#[macro_use] -extern crate diesel_migrations; - - -pub mod errors; -pub mod users; -pub mod search; +#[macro_use] +extern crate diesel; + +#[macro_use] +extern crate diesel_migrations; + +pub mod access; +pub mod errors; +pub mod search; +pub mod users; diff --git a/backend/src/main.rs b/backend/src/main.rs index d0ee78b..00b75ae 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,6 +1,12 @@ #[macro_use] extern crate diesel_migrations; +#[macro_use] +extern crate diesel; + +#[macro_use] +extern crate google_signin; + use std::env; use std::sync::Mutex; use std::thread; @@ -16,12 +22,25 @@ use diesel::MysqlConnection; use dotenv::dotenv; +mod access; +mod chemicals; +mod errors; +mod search; +mod users; + use web_dev::errors::WebdevError; use web_dev::errors::WebdevErrorKind; use web_dev::users::models::UserRequest; use web_dev::users::requests::handle_user; +use access::models::{AccessRequest, UserAccessRequest}; +use access::requests::get_user; +use access::requests::{handle_access, handle_user_access}; + +use chemicals::models::{ChemicalInventoryRequest, ChemicalRequest}; +use chemicals::requests::{handle_chemical, handle_chemical_inventory}; + embed_migrations!(); fn main() { @@ -56,7 +75,9 @@ fn main() { debug!("Connected to database"); info!("Running migrations"); - embedded_migrations::run(&connection); + if let Err(e) = embedded_migrations::run(&connection) { + warn!("Could not run migrations: {}", e); + } let connection_mutex = Mutex::new(connection); @@ -99,14 +120,65 @@ fn handle_request( request: &rouille::Request, database_connection: &MysqlConnection, ) -> rouille::Response { + let mut requesting_user = None; + + if let Some(id_token) = request.header("id_token") { + requesting_user = get_user(id_token, database_connection); + } + if let Some(user_request) = request.remove_prefix("/users") { match UserRequest::from_rouille(&user_request) { Err(err) => rouille::Response::from(err), - Ok(user_request) => match handle_user(user_request, database_connection) { - Ok(user_response) => user_response.to_rouille(), + Ok(user_request) => { + match handle_user(user_request, requesting_user, database_connection) { + Ok(user_response) => user_response.to_rouille(), + Err(err) => rouille::Response::from(err), + } + } + } + } else if let Some(access_request) = request.remove_prefix("/access") { + match AccessRequest::from_rouille(&access_request) { + Err(err) => rouille::Response::from(err), + Ok(access_request) => { + match handle_access(access_request, requesting_user, database_connection) { + Ok(access_response) => access_response.to_rouille(), + Err(err) => rouille::Response::from(err), + } + } + } + } else if let Some(user_access_request) = request.remove_prefix("/user_access") { + match UserAccessRequest::from_rouille(&user_access_request) { + Err(err) => rouille::Response::from(err), + Ok(user_access_request) => { + match handle_user_access(user_access_request, requesting_user, database_connection) + { + Ok(user_access_response) => user_access_response.to_rouille(), + Err(err) => rouille::Response::from(err), + } + } + } + } else if let Some(chem_inventory_request_url) = request.remove_prefix("/chemical_inventory") { + match ChemicalInventoryRequest::from_rouille(&chem_inventory_request_url) { + Err(err) => rouille::Response::from(err), + Ok(chem_inventory_request) => match handle_chemical_inventory( + chem_inventory_request, + requesting_user, + database_connection, + ) { + Ok(chem_inventory_response) => chem_inventory_response.to_rouille(), Err(err) => rouille::Response::from(err), }, } + } else if let Some(chemical_request_url) = request.remove_prefix("/chemical") { + match ChemicalRequest::from_rouille(&chemical_request_url) { + Err(err) => rouille::Response::from(err), + Ok(chemical_request) => { + match handle_chemical(chemical_request, requesting_user, database_connection) { + Ok(chemical_response) => chemical_response.to_rouille(), + Err(err) => rouille::Response::from(err), + } + } + } } else { rouille::Response::empty_404() } diff --git a/backend/src/users.rs b/backend/src/users.rs index 24543b5..295ee30 100644 --- a/backend/src/users.rs +++ b/backend/src/users.rs @@ -1,6 +1,6 @@ pub mod models; pub mod requests; -mod schema; +pub mod schema; use self::schema::users as users_schema; use diesel::expression::AsExpression; diff --git a/backend/src/users/requests.rs b/backend/src/users/requests.rs index 795cf15..b4baf98 100644 --- a/backend/src/users/requests.rs +++ b/backend/src/users/requests.rs @@ -8,10 +8,10 @@ use diesel::QueryDsl; use diesel::RunQueryDsl; use diesel::TextExpressionMethods; -use log::trace; +use log::error; use log::info; +use log::trace; use log::warn; -use log::error; use crate::errors::WebdevError; use crate::errors::WebdevErrorKind; @@ -19,6 +19,8 @@ use crate::errors::WebdevErrorKind; use crate::search::NullableSearch; use crate::search::Search; +use crate::access::requests::check_to_run; + use crate::users::models::{ NewUser, PartialUser, SearchUser, User, UserList, UserRequest, UserResponse, }; @@ -26,23 +28,43 @@ use crate::users::schema::users as users_schema; pub fn handle_user( request: UserRequest, + requested_user: Option, database_connection: &MysqlConnection, ) -> Result { match request { UserRequest::SearchUsers(user) => { - search_users(user, database_connection).map(|u| UserResponse::ManyUsers(u)) + match check_to_run(requested_user, "GetUsers", database_connection) { + Ok(()) => { + search_users(user, database_connection).map(|u| UserResponse::ManyUsers(u)) + } + Err(e) => Err(e), + } } UserRequest::GetUser(id) => { - get_user(id, database_connection).map(|u| UserResponse::OneUser(u)) + match check_to_run(requested_user, "GetUsers", database_connection) { + Ok(()) => get_user(id, database_connection).map(|u| UserResponse::OneUser(u)), + Err(e) => Err(e), + } } UserRequest::CreateUser(user) => { - create_user(user, database_connection).map(|u| UserResponse::OneUser(u)) + match check_to_run(requested_user, "CreateUsers", database_connection) { + Ok(()) => create_user(user, database_connection).map(|u| UserResponse::OneUser(u)), + Err(e) => Err(e), + } } UserRequest::UpdateUser(id, user) => { - update_user(id, user, database_connection).map(|_| UserResponse::NoResponse) + match check_to_run(requested_user, "DeleteUsers", database_connection) { + Ok(()) => { + update_user(id, user, database_connection).map(|_| UserResponse::NoResponse) + } + Err(e) => Err(e), + } } UserRequest::DeleteUser(id) => { - delete_user(id, database_connection).map(|_| UserResponse::NoResponse) + match check_to_run(requested_user, "GetUsers", database_connection) { + Ok(()) => delete_user(id, database_connection).map(|_| UserResponse::NoResponse), + Err(e) => Err(e), + } } } } @@ -58,9 +80,7 @@ fn search_users( users_query = users_query.filter(users_schema::first_name.like(format!("{}%", s))) } - Search::Exact(s) => { - users_query = users_query.filter(users_schema::first_name.eq(s)) - } + Search::Exact(s) => users_query = users_query.filter(users_schema::first_name.eq(s)), Search::NoSearch => {} } @@ -70,9 +90,7 @@ fn search_users( users_query = users_query.filter(users_schema::last_name.like(format!("{}%", s))) } - Search::Exact(s) => { - users_query = users_query.filter(users_schema::last_name.eq(s)) - } + Search::Exact(s) => users_query = users_query.filter(users_schema::last_name.eq(s)), Search::NoSearch => {} } @@ -84,9 +102,7 @@ fn search_users( users_query = users_query.filter(users_schema::banner_id.eq(s)) } - Search::Exact(s) => { - users_query = users_query.filter(users_schema::banner_id.eq(s)) - } + Search::Exact(s) => users_query = users_query.filter(users_schema::banner_id.eq(s)), Search::NoSearch => {} } @@ -96,9 +112,7 @@ fn search_users( users_query = users_query.filter(users_schema::email.like(format!("{}%", s))) } - NullableSearch::Exact(s) => { - users_query = users_query.filter(users_schema::email.eq(s)) - } + NullableSearch::Exact(s) => users_query = users_query.filter(users_schema::email.eq(s)), NullableSearch::Some => { users_query = users_query.filter(users_schema::email.is_not_null()); diff --git a/backend/start.sh b/backend/start.sh new file mode 100755 index 0000000..56b86ee --- /dev/null +++ b/backend/start.sh @@ -0,0 +1 @@ +docker run -it --net=host server diff --git a/frontend/www/access/signin.js b/frontend/www/access/signin.js new file mode 100644 index 0000000..a9e73a9 --- /dev/null +++ b/frontend/www/access/signin.js @@ -0,0 +1,46 @@ +function onSignIn(googleUser) { + // The ID token needed to be passed to the backend is saved as a cookie + var auth_response = googleUser.getAuthResponse(); + var id_token = auth_response.id_token; + var expire_ms = auth_response.expires_in*1000; //expires_in is the # of seconds from now until the id_token expires + var d = new Date(); + d.setTime(d.getTime() + expire_ms); + var expires = " expires="+d.toUTCString()+";"; + document.cookies = "id_token="+id_token+";"+expires+" path=/;"; + console.log(document.cookies); +} + +//For requests that need user authentication, add the following lines before the xmlhttp request is sent +//IN of page w/function: +// +// +//IN function: +// xmlhttp.setRequestHeader("id_token", getID_Token()); +function getID_Token(first_run = true) { + var name = "id_token="; + var value = ""; + var cookies_array = document.cookie.split(';'); + for(var i = 0; i < cookies_array.length; i++) { + var c = cookies_array[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + value = c.substring(name.length, c.length); + break; + } + } + + if (value === "" && first_run) { + var auth = gapi.auth2.getAuthInstance(); + auth.signin().then(onSignIn); + return getID_Token(false); + } + + return value; +} + +function signOut() { + var auth = gapi.auth2.getAuthInstance(); + auth2.signOut(); +}