finish admin tasks
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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'); | ||||||
|   | |||||||
| @@ -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,21 +94,57 @@ 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() |                     return Ok(user); | ||||||
|             .to_string(); |                 } | ||||||
|  |  | ||||||
|         if password_hash == user.pw { |                 Err(LoginError::InvalidAuthenticationCombo) | ||||||
|             return Ok(user); |             } | ||||||
|  |             None => Err(LoginError::NoPasswordSet(user)), | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|         Err(LoginError::InvalidAuthenticationCombo) |     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 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -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
									
								
							
							
						
						
									
										9
									
								
								src/rest/admin/mod.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										61
									
								
								src/rest/admin/user.rs
									
									
									
									
									
										Normal 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] | ||||||
|  | } | ||||||
| @@ -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] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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()) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								templates/admin/user/index.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								templates/admin/user/index.html.tera
									
									
									
									
									
										Normal 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 %} | ||||||
							
								
								
									
										8
									
								
								templates/auth/set-pw.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								templates/auth/set-pw.html.tera
									
									
									
									
									
										Normal 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> | ||||||
| @@ -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 %} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user