aboutsummaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/Cargo.toml17
-rw-r--r--backend/Rocket.toml2
-rw-r--r--backend/migrations/2021-12-13-145111_users/down.sql1
-rw-r--r--backend/migrations/2021-12-13-145111_users/up.sql6
-rw-r--r--backend/migrations/2021-12-13-151129_sessions/down.sql1
-rw-r--r--backend/migrations/2021-12-13-151129_sessions/up.sql5
-rw-r--r--backend/src/db/mod.rs2
-rw-r--r--backend/src/db/sessions.rs46
-rw-r--r--backend/src/db/users.rs106
-rw-r--r--backend/src/main.rs27
-rw-r--r--backend/src/routes/mod.rs1
-rw-r--r--backend/src/routes/users.rs100
-rw-r--r--backend/src/schema.rs20
-rw-r--r--backend/tests/common.rs0
14 files changed, 326 insertions, 8 deletions
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
index 42933e5..b44b5ad 100644
--- a/backend/Cargo.toml
+++ b/backend/Cargo.toml
@@ -6,11 +6,18 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-rocket = "0.4.10"
-diesel = { version = "1.4.4", features = ["postgres"] }
+rocket = { version= "0.5.0-rc.1", features = ["json"] }
+diesel = { version = "1.4.4", features = ["postgres", "r2d2"] }
dotenv = "0.15.0"
+rust-argon2 = "0.8"
+rand = "0.8.4"
+serde = { version = "1.0", features = ["derive"] }
+serde_bytes = "0.11"
+chrono = { version = "0.4", features = ["serde"] }
+serde_json = "1.0"
+base64 = "0.13.0"
-[dependencies.rocket_contrib]
-version = "0.4.10"
-default-features = false
+
+[dependencies.rocket_sync_db_pools]
+version = "0.1.0-rc.1"
features = ["diesel_postgres_pool"] \ No newline at end of file
diff --git a/backend/Rocket.toml b/backend/Rocket.toml
new file mode 100644
index 0000000..40635de
--- /dev/null
+++ b/backend/Rocket.toml
@@ -0,0 +1,2 @@
+[debug.databases.postgresql_database]
+url = "postgresql://planetwars:planetwars@localhost/planetwars" \ No newline at end of file
diff --git a/backend/migrations/2021-12-13-145111_users/down.sql b/backend/migrations/2021-12-13-145111_users/down.sql
new file mode 100644
index 0000000..441087a
--- /dev/null
+++ b/backend/migrations/2021-12-13-145111_users/down.sql
@@ -0,0 +1 @@
+DROP TABLE users; \ No newline at end of file
diff --git a/backend/migrations/2021-12-13-145111_users/up.sql b/backend/migrations/2021-12-13-145111_users/up.sql
new file mode 100644
index 0000000..6ec7e01
--- /dev/null
+++ b/backend/migrations/2021-12-13-145111_users/up.sql
@@ -0,0 +1,6 @@
+CREATE TABLE users(
+ id SERIAL PRIMARY KEY,
+ username VARCHAR(52) NOT NULL,
+ password_salt BYTEA NOT NULL,
+ password_hash BYTEA NOT NULL
+); \ No newline at end of file
diff --git a/backend/migrations/2021-12-13-151129_sessions/down.sql b/backend/migrations/2021-12-13-151129_sessions/down.sql
new file mode 100644
index 0000000..54d1e93
--- /dev/null
+++ b/backend/migrations/2021-12-13-151129_sessions/down.sql
@@ -0,0 +1 @@
+DROP TABLE sessions; \ No newline at end of file
diff --git a/backend/migrations/2021-12-13-151129_sessions/up.sql b/backend/migrations/2021-12-13-151129_sessions/up.sql
new file mode 100644
index 0000000..f8ec21b
--- /dev/null
+++ b/backend/migrations/2021-12-13-151129_sessions/up.sql
@@ -0,0 +1,5 @@
+CREATE TABLE sessions (
+ id serial PRIMARY KEY,
+ user_id integer REFERENCES users(id) NOT NULL,
+ token VARCHAR(255) NOT NULL UNIQUE
+) \ No newline at end of file
diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs
new file mode 100644
index 0000000..b6e3efc
--- /dev/null
+++ b/backend/src/db/mod.rs
@@ -0,0 +1,2 @@
+pub mod sessions;
+pub mod users;
diff --git a/backend/src/db/sessions.rs b/backend/src/db/sessions.rs
new file mode 100644
index 0000000..0cc3f1a
--- /dev/null
+++ b/backend/src/db/sessions.rs
@@ -0,0 +1,46 @@
+use super::users::User;
+use crate::schema::{sessions, users};
+use base64;
+use diesel::PgConnection;
+use diesel::{insert_into, prelude::*, Insertable, RunQueryDsl};
+use rand::{self, Rng};
+
+#[derive(Insertable)]
+#[table_name = "sessions"]
+struct NewSession {
+ token: String,
+ user_id: i32,
+}
+
+#[derive(Queryable, Debug, PartialEq)]
+pub struct Session {
+ pub id: i32,
+ pub user_id: i32,
+ pub token: String,
+}
+
+pub fn create_session(user: &User, conn: &PgConnection) -> Session {
+ let new_session = NewSession {
+ token: gen_session_token(),
+ user_id: user.user_id,
+ };
+ let session = insert_into(sessions::table)
+ .values(&new_session)
+ .get_result::<Session>(conn)
+ .unwrap();
+
+ return session;
+}
+
+pub fn find_user_by_session(token: &str, conn: &PgConnection) -> QueryResult<(Session, User)> {
+ sessions::table
+ .inner_join(users::table)
+ .filter(sessions::token.eq(&token))
+ .first::<(Session, User)>(conn)
+}
+
+pub fn gen_session_token() -> String {
+ let mut rng = rand::thread_rng();
+ let token: [u8; 32] = rng.gen();
+ return base64::encode(&token);
+}
diff --git a/backend/src/db/users.rs b/backend/src/db/users.rs
new file mode 100644
index 0000000..c06e5b3
--- /dev/null
+++ b/backend/src/db/users.rs
@@ -0,0 +1,106 @@
+use crate::{schema::users, DbConn};
+use argon2;
+use diesel::{prelude::*, PgConnection};
+use rand::Rng;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Deserialize)]
+pub struct Credentials<'a> {
+ pub username: &'a str,
+ pub password: &'a str,
+}
+
+#[derive(Insertable)]
+#[table_name = "users"]
+pub struct NewUser<'a> {
+ pub username: &'a str,
+ pub password_hash: &'a [u8],
+ pub password_salt: &'a [u8],
+}
+
+#[derive(Queryable, Debug)]
+pub struct User {
+ pub user_id: i32,
+ pub username: String,
+ pub password_salt: Vec<u8>,
+ pub password_hash: Vec<u8>,
+}
+
+// TODO: make this configurable somewhere
+fn argon2_config() -> argon2::Config<'static> {
+ argon2::Config {
+ variant: argon2::Variant::Argon2i,
+ version: argon2::Version::Version13,
+ mem_cost: 4096,
+ time_cost: 3,
+ lanes: 1,
+ thread_mode: argon2::ThreadMode::Sequential,
+ // TODO: set a secret
+ secret: &[],
+ ad: &[],
+ hash_length: 32,
+ }
+}
+
+pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResult<User> {
+ let argon_config = argon2_config();
+
+ let salt: [u8; 32] = rand::thread_rng().gen();
+ let hash = argon2::hash_raw(credentials.password.as_bytes(), &salt, &argon_config).unwrap();
+ let new_user = NewUser {
+ username: &credentials.username,
+ password_salt: &salt,
+ password_hash: &hash,
+ };
+ diesel::insert_into(users::table)
+ .values(&new_user)
+ .get_result::<User>(conn)
+}
+
+pub fn authenticate_user(credentials: &Credentials, db_conn: &PgConnection) -> Option<User> {
+ let user = users::table
+ .filter(users::username.eq(&credentials.username))
+ .first::<User>(db_conn)
+ .unwrap();
+
+ let password_matches = argon2::verify_raw(
+ credentials.password.as_bytes(),
+ &user.password_salt,
+ &user.password_hash,
+ &argon2_config(),
+ )
+ .unwrap();
+
+ if password_matches {
+ return Some(user);
+ } else {
+ return None;
+ }
+}
+
+#[test]
+fn test_argon() {
+ let credentials = Credentials {
+ username: "piepkonijn",
+ password: "geheim123",
+ };
+ let argon_config = argon2_config();
+
+ let salt: [u8; 32] = rand::thread_rng().gen();
+ let hash = argon2::hash_raw(credentials.password.as_bytes(), &salt, &argon_config).unwrap();
+ let new_user = NewUser {
+ username: &credentials.username,
+ password_hash: &hash,
+ password_salt: &salt,
+ };
+
+ let password_matches = argon2::verify_raw(
+ credentials.password.as_bytes(),
+ &new_user.password_salt,
+ &new_user.password_hash,
+ &argon2_config(),
+ )
+ .unwrap();
+
+ assert!(password_matches);
+}
diff --git a/backend/src/main.rs b/backend/src/main.rs
index fd0ff2e..6ee54ec 100644
--- a/backend/src/main.rs
+++ b/backend/src/main.rs
@@ -1,15 +1,36 @@
#![feature(proc_macro_hygiene, decl_macro)]
+use rocket::{Build, Rocket};
+use rocket_sync_db_pools::database;
+
#[macro_use]
extern crate rocket;
#[macro_use]
-extern crate rocket_contrib;
+extern crate diesel;
+
+mod db;
+mod routes;
+mod schema;
+
+#[database("postgresql_database")]
+pub struct DbConn(diesel::PgConnection);
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
-fn main() {
- rocket::ignite().mount("/", routes![index]).launch();
+#[launch]
+fn rocket() -> Rocket<Build> {
+ rocket::build()
+ .mount(
+ "/",
+ routes![
+ index,
+ routes::users::register,
+ routes::users::login,
+ routes::users::current_user,
+ ],
+ )
+ .attach(DbConn::fairing())
}
diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs
new file mode 100644
index 0000000..913bd46
--- /dev/null
+++ b/backend/src/routes/mod.rs
@@ -0,0 +1 @@
+pub mod users;
diff --git a/backend/src/routes/users.rs b/backend/src/routes/users.rs
new file mode 100644
index 0000000..274b712
--- /dev/null
+++ b/backend/src/routes/users.rs
@@ -0,0 +1,100 @@
+use crate::db::{sessions, users};
+use crate::{
+ db::users::{Credentials, User},
+ DbConn,
+};
+use rocket::serde::json::Json;
+use serde::{Deserialize, Serialize};
+
+use rocket::http::Status;
+use rocket::request::{self, FromRequest, Outcome, Request};
+
+#[derive(Debug)]
+pub enum AuthTokenError {
+ BadCount,
+ Missing,
+ Invalid,
+}
+
+// TODO: error handling and proper lifetimes
+#[rocket::async_trait]
+impl<'r> FromRequest<'r> for User {
+ type Error = AuthTokenError;
+
+ async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
+ let keys: Vec<_> = request.headers().get("Authorization").collect();
+ let token = match keys.len() {
+ 0 => return Outcome::Failure((Status::BadRequest, AuthTokenError::Missing)),
+ 1 => keys[0].to_string(),
+ _ => return Outcome::Failure((Status::BadRequest, AuthTokenError::BadCount)),
+ };
+ let db = request.guard::<DbConn>().await.unwrap();
+ let (_session, user) = db
+ .run(move |conn| sessions::find_user_by_session(&token, conn))
+ .await
+ .unwrap();
+ Outcome::Success(user)
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct UserData {
+ pub user_id: i32,
+ pub username: String,
+}
+
+impl From<User> for UserData {
+ fn from(user: User) -> Self {
+ UserData {
+ user_id: user.user_id,
+ username: user.username,
+ }
+ }
+}
+
+#[derive(Deserialize)]
+pub struct RegistrationParams {
+ pub username: String,
+ pub password: String,
+}
+
+#[post("/register", data = "<params>")]
+pub async fn register(db_conn: DbConn, params: Json<RegistrationParams>) -> Json<UserData> {
+ db_conn
+ .run(move |conn| {
+ let credentials = Credentials {
+ username: &params.username,
+ password: &params.password,
+ };
+ let user = users::create_user(&credentials, conn).unwrap();
+ Json(user.into())
+ })
+ .await
+}
+
+#[derive(Deserialize)]
+pub struct LoginParams {
+ pub username: String,
+ pub password: String,
+}
+
+#[post("/login", data = "<params>")]
+pub async fn login(db_conn: DbConn, params: Json<LoginParams>) -> String {
+ db_conn
+ .run(move |conn| {
+ let credentials = Credentials {
+ username: &params.username,
+ password: &params.password,
+ };
+ // TODO: handle failures
+ let user = users::authenticate_user(&credentials, conn).unwrap();
+ let session = sessions::create_session(&user, conn);
+ return session.token;
+ })
+ .await
+}
+
+#[get("/users/me")]
+pub async fn current_user(user: User) -> Json<UserData> {
+ Json(user.into())
+}
diff --git a/backend/src/schema.rs b/backend/src/schema.rs
new file mode 100644
index 0000000..04ecbd7
--- /dev/null
+++ b/backend/src/schema.rs
@@ -0,0 +1,20 @@
+table! {
+ sessions (id) {
+ id -> Int4,
+ user_id -> Int4,
+ token -> Varchar,
+ }
+}
+
+table! {
+ users (id) {
+ id -> Int4,
+ username -> Varchar,
+ password_salt -> Bytea,
+ password_hash -> Bytea,
+ }
+}
+
+joinable!(sessions -> users (user_id));
+
+allow_tables_to_appear_in_same_query!(sessions, users,);
diff --git a/backend/tests/common.rs b/backend/tests/common.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/backend/tests/common.rs