finish admin tasks

This commit is contained in:
philipp 2023-04-04 10:44:14 +02:00
parent 202964bfa4
commit 3dfc64071c
11 changed files with 284 additions and 23 deletions

View File

@ -49,10 +49,10 @@
# TODO # TODO
- [x] User login - [x] User login
- [ ] Admin - [x] Admin
- [ ] User - [x] User
- [ ] User passwort zurücksetzen - [x] User passwort zurücksetzen
- [ ] Cox + admin setzen - [x] Cox + admin + guest setzen
- [ ] Ausfahrten - [ ] Ausfahrten
- [ ] CRUD planned_event - [ ] CRUD planned_event
- [ ] CRUD trip_details - [ ] CRUD trip_details

View File

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS "user" ( CREATE TABLE IF NOT EXISTS "user" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE, "name" text NOT NULL UNIQUE,
"pw" text NOT NULL DEFAULT "", "pw" text,
"is_cox" boolean NOT NULL DEFAULT FALSE, "is_cox" boolean NOT NULL DEFAULT FALSE,
"is_admin" boolean NOT NULL DEFAULT FALSE, "is_admin" boolean NOT NULL DEFAULT FALSE,
"is_guest" boolean NOT NULL DEFAULT TRUE "is_guest" boolean NOT NULL DEFAULT TRUE

View File

@ -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, 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, 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) VALUES('guest', false, false, true); 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) VALUES('cox', true, false, false); 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');

View File

@ -10,19 +10,37 @@ use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct User { pub struct User {
id: i64, pub id: i64,
name: String, pub name: String,
pw: String, pw: Option<String>,
is_cox: bool, is_cox: bool,
is_admin: bool, is_admin: bool,
is_guest: bool, is_guest: bool,
} }
pub struct AdminUser {
user: User,
}
impl TryFrom<User> for AdminUser {
type Error = LoginError;
fn try_from(user: User) -> Result<Self, Self::Error> {
if user.is_admin {
Ok(AdminUser { user })
} else {
Err(LoginError::NotAnAdmin)
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum LoginError { pub enum LoginError {
SqlxError(sqlx::Error), SqlxError(sqlx::Error),
InvalidAuthenticationCombo, InvalidAuthenticationCombo,
NotLoggedIn, NotLoggedIn,
NotAnAdmin,
NoPasswordSet(User),
} }
impl From<sqlx::Error> for LoginError { impl From<sqlx::Error> for LoginError {
@ -31,6 +49,35 @@ impl From<sqlx::Error> for LoginError {
} }
} }
impl User { 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<Self, sqlx::Error> {
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<Self, sqlx::Error> { async fn find_by_name(db: &SqlitePool, name: String) -> Result<Self, sqlx::Error> {
let user: User = sqlx::query_as!( let user: User = sqlx::query_as!(
User, User,
@ -47,22 +94,58 @@ WHERE name like ?
Ok(user) 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<Self, LoginError> { pub async fn login(db: &SqlitePool, name: String, pw: String) -> Result<Self, LoginError> {
let user = User::find_by_name(db, name).await?; let user = User::find_by_name(db, name).await?;
let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap(); match user.pw.clone() {
let argon2 = Argon2::default(); Some(user_pw) => {
let password_hash = argon2 let password_hash = Self::get_hashed_pw(pw);
.hash_password(&pw.as_bytes(), &salt) if password_hash == user_pw {
.unwrap()
.to_string();
if password_hash == user.pw {
return Ok(user); return Ok(user);
} }
Err(LoginError::InvalidAuthenticationCombo) Err(LoginError::InvalidAuthenticationCombo)
} }
None => Err(LoginError::NoPasswordSet(user)),
}
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
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
}
} }
#[async_trait] #[async_trait]
@ -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<Self, Self::Error> {
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)] #[cfg(test)]
mod test { mod test {
use crate::testdb; use crate::testdb;

9
src/rest/admin/mod.rs Normal file
View File

@ -0,0 +1,9 @@
use rocket::Route;
pub mod user;
pub fn routes() -> Vec<Route> {
let mut ret = Vec::new();
ret.append(&mut user::routes());
ret
}

61
src/rest/admin/user.rs Normal file
View File

@ -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<SqlitePool>, _admin: AdminUser) -> Template {
let users = User::all(db).await;
Template::render("admin/user/index", context!(users))
}
#[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
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 = "<data>")]
async fn update(db: &State<SqlitePool>, data: Form<UserEditForm>) -> Flash<Redirect> {
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<Route> {
routes![index, resetpw, update]
}

View File

@ -7,11 +7,11 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, FromForm, Route, State, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{tera, Template}; use rocket_dyn_templates::{context, tera, Template};
use serde_json::json; use serde_json::json;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::user::User; use crate::model::user::{LoginError, User};
#[get("/")] #[get("/")]
async fn index(flash: Option<FlashMessage<'_>>) -> Template { async fn index(flash: Option<FlashMessage<'_>>) -> Template {
@ -41,6 +41,12 @@ async fn login(
//TODO: be able to use ? for login. This would get rid of the following match clause. //TODO: be able to use ? for login. This would get rid of the following match clause.
let user = match user { let user = match user {
Ok(user) => user, Ok(user) => user,
Err(LoginError::NoPasswordSet(user)) => {
return Flash::warning(
Redirect::to(format!("/auth/set-pw/{}", user.id)),
"Setze ein neues Passwort",
);
}
Err(_) => { Err(_) => {
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort"); return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort");
} }
@ -52,6 +58,53 @@ async fn login(
Flash::success(Redirect::to("/"), "Login erfolgreich") Flash::success(Redirect::to("/"), "Login erfolgreich")
} }
#[get("/set-pw/<userid>")]
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 = "<updatepw>")]
async fn updatepw(
db: &State<SqlitePool>,
updatepw: Form<UpdatePw>,
cookies: &CookieJar<'_>,
) -> Flash<Redirect> {
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")] #[get("/logout")]
async fn logout(cookies: &CookieJar<'_>, _user: User) -> Flash<Redirect> { async fn logout(cookies: &CookieJar<'_>, _user: User) -> Flash<Redirect> {
cookies.remove_private(Cookie::named("loggedin_user")); cookies.remove_private(Cookie::named("loggedin_user"));
@ -60,5 +113,5 @@ async fn logout(cookies: &CookieJar<'_>, _user: User) -> Flash<Redirect> {
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, login, logout] routes![index, login, logout, setpw, updatepw]
} }

View File

@ -4,6 +4,7 @@ use sqlx::SqlitePool;
use crate::model::user::User; use crate::model::user::User;
mod admin;
mod auth; mod auth;
#[get("/")] #[get("/")]
@ -21,6 +22,7 @@ pub fn start(db: SqlitePool) -> Rocket<Build> {
.manage(db) .manage(db)
.mount("/", routes![index]) .mount("/", routes![index])
.mount("/auth", auth::routes()) .mount("/auth", auth::routes())
.mount("/admin", admin::routes())
.register("/", catchers![unauthorized_error]) .register("/", catchers![unauthorized_error])
.attach(Template::fairing()) .attach(Template::fairing())
} }

View File

@ -0,0 +1,22 @@
{% extends "base" %}
{% block content %}
<h1>Users</h1>
{% for user in users %}
<form action="/admin/user" method="post">
<input type="hidden" name="id" value="{{ user.id }}" />
Name: {{ user.name }}<br />
Guest <input type="checkbox" name="is_guest" {% if user.is_guest %} checked="true"{% endif %} /><br />
Cox <input type="checkbox" name="is_cox" {% if user.is_cox %} checked="true"{% endif %} /><br />
Admin <input type="checkbox" name="is_admin" {% if user.is_admin %} checked="true"{% endif %} /><br />
{% if user.pw %}
<a href="/admin/user/{{ user.id }}/reset-pw">RESET PW</a>
{% endif %}
<input type="submit" />
</form>
<hr />
{% endfor %}
{% endblock content %}

View File

@ -0,0 +1,8 @@
<h1>Passwort setzen</h1>
<form action="/auth/set-pw" method="post">
<input type="hidden" name="userid" value="{{ userid }}" />
<input type="password" name="password" placeholder="PW" />
<input type="password" name="password_confirm" placeholder="Confirm PW"/>
<input type="submit" />
</form>

View File

@ -4,6 +4,10 @@
{% if loggedin_user %} {% if loggedin_user %}
Hi {{ loggedin_user.name }}. <a href="/auth/logout">LOGOUT</a> Hi {{ loggedin_user.name }}. <a href="/auth/logout">LOGOUT</a>
{% if loggedin_user.is_admin %}
<a href="/admin/user">USER</a>
{% endif %}
{% endif %} {% endif %}
{% if flash %} {% if flash %}