diff --git a/README.md b/README.md index 0baa02a..e892525 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ # TODO - [x] User login -- [ ] Admin - - [ ] User - - [ ] User passwort zurücksetzen - - [ ] Cox + admin setzen +- [x] Admin + - [x] User + - [x] User passwort zurücksetzen + - [x] Cox + admin + guest setzen - [ ] Ausfahrten - [ ] CRUD planned_event - [ ] CRUD trip_details diff --git a/migration.sql b/migration.sql index efa2fe0..5020307 100644 --- a/migration.sql +++ b/migration.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS "user" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" text NOT NULL UNIQUE, - "pw" text NOT NULL DEFAULT "", + "pw" text, "is_cox" boolean NOT NULL DEFAULT FALSE, "is_admin" boolean NOT NULL DEFAULT FALSE, "is_guest" boolean NOT NULL DEFAULT TRUE diff --git a/seeds.sql b/seeds.sql index 4673b33..51710b6 100644 --- a/seeds.sql +++ b/seeds.sql @@ -1,4 +1,5 @@ INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('admin', false, true, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); -INSERT INTO "user" (name, is_cox, is_admin, is_guest) VALUES('rower', false, false, false); -INSERT INTO "user" (name, is_cox, is_admin, is_guest) VALUES('guest', false, false, true); -INSERT INTO "user" (name, is_cox, is_admin, is_guest) VALUES('cox', true, false, false); +INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('rower', false, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('guest', false, false, true, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); +INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('cox', true, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "user" (name) VALUES('new'); diff --git a/src/model/user.rs b/src/model/user.rs index 8faf63d..46d0b59 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -10,19 +10,37 @@ use sqlx::{FromRow, SqlitePool}; #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct User { - id: i64, - name: String, - pw: String, + pub id: i64, + pub name: String, + pw: Option, is_cox: bool, is_admin: bool, is_guest: bool, } +pub struct AdminUser { + user: User, +} + +impl TryFrom for AdminUser { + type Error = LoginError; + + fn try_from(user: User) -> Result { + if user.is_admin { + Ok(AdminUser { user }) + } else { + Err(LoginError::NotAnAdmin) + } + } +} + #[derive(Debug)] pub enum LoginError { SqlxError(sqlx::Error), InvalidAuthenticationCombo, NotLoggedIn, + NotAnAdmin, + NoPasswordSet(User), } impl From for LoginError { @@ -31,6 +49,35 @@ impl From for LoginError { } } impl User { + pub async fn update(&self, db: &SqlitePool, is_cox: bool, is_admin: bool, is_guest: bool) { + sqlx::query!( + "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ? where id = ?", + is_cox, + is_admin, + is_guest, + self.id + ) + .execute(db) + .await + .unwrap(); //TODO: fixme + } + + pub async fn find_by_id(db: &SqlitePool, id: i32) -> Result { + let user: User = sqlx::query_as!( + User, + " +SELECT id, name, pw, is_cox, is_admin, is_guest +FROM user +WHERE id like ? + ", + id + ) + .fetch_one(db) + .await?; + + Ok(user) + } + async fn find_by_name(db: &SqlitePool, name: String) -> Result { let user: User = sqlx::query_as!( User, @@ -47,21 +94,57 @@ WHERE name like ? Ok(user) } + fn get_hashed_pw(pw: String) -> String { + let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap(); + let argon2 = Argon2::default(); + argon2 + .hash_password(&pw.as_bytes(), &salt) + .unwrap() + .to_string() + } + pub async fn login(db: &SqlitePool, name: String, pw: String) -> Result { let user = User::find_by_name(db, name).await?; - let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap(); - let argon2 = Argon2::default(); - let password_hash = argon2 - .hash_password(&pw.as_bytes(), &salt) - .unwrap() - .to_string(); + match user.pw.clone() { + Some(user_pw) => { + let password_hash = Self::get_hashed_pw(pw); + if password_hash == user_pw { + return Ok(user); + } - if password_hash == user.pw { - return Ok(user); + Err(LoginError::InvalidAuthenticationCombo) + } + None => Err(LoginError::NoPasswordSet(user)), } + } - Err(LoginError::InvalidAuthenticationCombo) + pub async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as!( + User, + " +SELECT id, name, pw, is_cox, is_admin, is_guest +FROM user + " + ) + .fetch_all(db) + .await + .unwrap() //TODO: fixme + } + + pub async fn reset_pw(&self, db: &SqlitePool) { + sqlx::query!("UPDATE user SET pw = null where id = ?", self.id) + .execute(db) + .await + .unwrap(); //TODO: fixme + } + + pub async fn update_pw(&self, db: &SqlitePool, pw: String) { + let pw = Self::get_hashed_pw(pw); + sqlx::query!("UPDATE user SET pw = ? where id = ?", pw, self.id) + .execute(db) + .await + .unwrap(); //TODO: fixme } } @@ -80,6 +163,24 @@ impl<'r> FromRequest<'r> for User { } } +#[async_trait] +impl<'r> FromRequest<'r> for AdminUser { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + match req.cookies().get_private("loggedin_user") { + Some(user) => { + let user: User = serde_json::from_str(&user.value()).unwrap(); //TODO: fixme + match user.try_into() { + Ok(user) => Outcome::Success(user), + Err(_) => Outcome::Failure((Status::Unauthorized, LoginError::NotAnAdmin)), + } + } + None => Outcome::Failure((Status::Unauthorized, LoginError::NotLoggedIn)), + } + } +} + #[cfg(test)] mod test { use crate::testdb; diff --git a/src/rest/admin/mod.rs b/src/rest/admin/mod.rs new file mode 100644 index 0000000..1056ce6 --- /dev/null +++ b/src/rest/admin/mod.rs @@ -0,0 +1,9 @@ +use rocket::Route; + +pub mod user; + +pub fn routes() -> Vec { + let mut ret = Vec::new(); + ret.append(&mut user::routes()); + ret +} diff --git a/src/rest/admin/user.rs b/src/rest/admin/user.rs new file mode 100644 index 0000000..7c944d2 --- /dev/null +++ b/src/rest/admin/user.rs @@ -0,0 +1,61 @@ +use crate::model::user::{AdminUser, User}; +use rocket::{ + form::Form, + get, post, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::{context, Template}; +use sqlx::SqlitePool; + +#[get("/user")] +async fn index(db: &State, _admin: AdminUser) -> Template { + let users = User::all(db).await; + Template::render("admin/user/index", context!(users)) +} + +#[get("/user//reset-pw")] +async fn resetpw(db: &State, _admin: AdminUser, user: i32) -> Flash { + let user = User::find_by_id(db, user).await; + match user { + Ok(user) => { + user.reset_pw(db).await; + Flash::success( + Redirect::to("/admin/user"), + format!("Successfully reset pw of {}", user.name), + ) + } + Err(_) => Flash::error(Redirect::to("/admin/user"), "User does not exist"), + } +} + +#[derive(FromForm)] +struct UserEditForm { + id: i32, + is_guest: bool, + is_cox: bool, + is_admin: bool, +} + +#[post("/user", data = "")] +async fn update(db: &State, data: Form) -> Flash { + let user = User::find_by_id(db, data.id).await; + let user = match user { + Ok(user) => user, + Err(_) => { + return Flash::error( + Redirect::to("/admin/user"), + format!("User with ID {} does not exist!", data.id), + ) + } + }; + + user.update(db, data.is_cox, data.is_admin, data.is_guest) + .await; + + Flash::success(Redirect::to("/admin/user"), "Successfully updated user") +} + +pub fn routes() -> Vec { + routes![index, resetpw, update] +} diff --git a/src/rest/auth.rs b/src/rest/auth.rs index 746c7e6..8f403af 100644 --- a/src/rest/auth.rs +++ b/src/rest/auth.rs @@ -7,11 +7,11 @@ use rocket::{ response::{Flash, Redirect}, routes, FromForm, Route, State, }; -use rocket_dyn_templates::{tera, Template}; +use rocket_dyn_templates::{context, tera, Template}; use serde_json::json; use sqlx::SqlitePool; -use crate::model::user::User; +use crate::model::user::{LoginError, User}; #[get("/")] async fn index(flash: Option>) -> Template { @@ -41,6 +41,12 @@ async fn login( //TODO: be able to use ? for login. This would get rid of the following match clause. let user = match user { Ok(user) => user, + Err(LoginError::NoPasswordSet(user)) => { + return Flash::warning( + Redirect::to(format!("/auth/set-pw/{}", user.id)), + "Setze ein neues Passwort", + ); + } Err(_) => { return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort"); } @@ -52,6 +58,53 @@ async fn login( Flash::success(Redirect::to("/"), "Login erfolgreich") } +#[get("/set-pw/")] +async fn setpw(userid: i32) -> Template { + Template::render("auth/set-pw", context!(userid)) +} + +#[derive(FromForm)] +struct UpdatePw { + userid: i32, + password: String, + password_confirm: String, +} + +#[post("/set-pw", data = "")] +async fn updatepw( + db: &State, + updatepw: Form, + cookies: &CookieJar<'_>, +) -> Flash { + let user = User::find_by_id(db, updatepw.userid).await; + let user = match user { + Ok(user) => user, + Err(_) => { + return Flash::error( + Redirect::to("/auth"), + format!("User with ID {} does not exist!", updatepw.userid), + ) + } + }; + + if updatepw.password != updatepw.password_confirm { + return Flash::error( + Redirect::to(format!("/auth/set-pw/{}", updatepw.userid)), + "Passwörter stimmen nicht überein! Bitte probiere es nochmal", + ); + } + + user.update_pw(db, updatepw.password.clone()).await; + + let user_json: String = format!("{}", json!(user)); + cookies.add_private(Cookie::new("loggedin_user", user_json)); + + Flash::success( + Redirect::to("/"), + "Passwort erfolgreich gesetzt. Du bist nun eingeloggt.", + ) +} + #[get("/logout")] async fn logout(cookies: &CookieJar<'_>, _user: User) -> Flash { cookies.remove_private(Cookie::named("loggedin_user")); @@ -60,5 +113,5 @@ async fn logout(cookies: &CookieJar<'_>, _user: User) -> Flash { } pub fn routes() -> Vec { - routes![index, login, logout] + routes![index, login, logout, setpw, updatepw] } diff --git a/src/rest/mod.rs b/src/rest/mod.rs index 9208a5d..59ef60e 100644 --- a/src/rest/mod.rs +++ b/src/rest/mod.rs @@ -4,6 +4,7 @@ use sqlx::SqlitePool; use crate::model::user::User; +mod admin; mod auth; #[get("/")] @@ -21,6 +22,7 @@ pub fn start(db: SqlitePool) -> Rocket { .manage(db) .mount("/", routes![index]) .mount("/auth", auth::routes()) + .mount("/admin", admin::routes()) .register("/", catchers![unauthorized_error]) .attach(Template::fairing()) } diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera new file mode 100644 index 0000000..4f45d0d --- /dev/null +++ b/templates/admin/user/index.html.tera @@ -0,0 +1,22 @@ +{% extends "base" %} + +{% block content %} + +

Users

+ +{% for user in users %} +
+ + Name: {{ user.name }}
+ Guest
+ Cox
+ Admin
+ {% if user.pw %} + RESET PW + {% endif %} + +
+
+{% endfor %} + +{% endblock content %} diff --git a/templates/auth/set-pw.html.tera b/templates/auth/set-pw.html.tera new file mode 100644 index 0000000..34af874 --- /dev/null +++ b/templates/auth/set-pw.html.tera @@ -0,0 +1,8 @@ +

Passwort setzen

+ +
+ + + + +
diff --git a/templates/index.html.tera b/templates/index.html.tera index bfc7bf6..3348466 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -4,6 +4,10 @@ {% if loggedin_user %} Hi {{ loggedin_user.name }}. LOGOUT + + {% if loggedin_user.is_admin %} + USER + {% endif %} {% endif %} {% if flash %}