220 lines
8.4 KiB
Rust
220 lines
8.4 KiB
Rust
use based::{
|
|
auth::{Session, Sessions, User, csrf::CSRF},
|
|
page::htmx_link,
|
|
request::{RequestContext, StringResponse, api::to_uuid, respond_html},
|
|
};
|
|
use maud::{PreEscaped, html};
|
|
use rocket::{
|
|
FromForm,
|
|
form::Form,
|
|
get,
|
|
http::{Cookie, CookieJar},
|
|
post,
|
|
response::Redirect,
|
|
};
|
|
|
|
use super::render;
|
|
|
|
#[get("/login")]
|
|
pub async fn login(ctx: RequestContext) -> StringResponse {
|
|
let content = html! {
|
|
div class="min-w-screen justify-center flex items-center" {
|
|
div class="bg-gray-800 shadow-lg rounded-lg p-8 max-w-sm w-full" {
|
|
h2 class="text-2xl font-bold text-center text-gray-100 mb-6" { "Login" };
|
|
form action="/login" method="POST" {
|
|
div class="mb-4" {
|
|
label for="email" class="block text-sm font-medium text-gray-400" { "Email" };
|
|
input type="text" id="username" name="username" placeholder="Username" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
|
|
};
|
|
div class="mb-4" {
|
|
label for="password" class="block text-sm font-medium text-gray-400" { "Password" };
|
|
input type="password" id="password" name="password" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
|
|
};
|
|
button type="submit" class="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400" { "Login" };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
render(content, "Login", ctx).await
|
|
}
|
|
|
|
#[derive(FromForm)]
|
|
pub struct LoginForm {
|
|
username: String,
|
|
password: String,
|
|
}
|
|
|
|
#[post("/login", data = "<form>")]
|
|
pub async fn login_post(form: Form<LoginForm>, cookies: &CookieJar<'_>) -> Redirect {
|
|
if let Some(user) = User::login(&form.username, &form.password).await {
|
|
let session_cookie = Cookie::build(("session", user.0.token.to_string()))
|
|
.path("/")
|
|
.http_only(true)
|
|
.max_age(rocket::time::Duration::days(7))
|
|
.build();
|
|
|
|
cookies.add(session_cookie);
|
|
|
|
Redirect::to("/")
|
|
} else {
|
|
Redirect::to("/login")
|
|
}
|
|
}
|
|
|
|
#[get("/end_session/<session_id>?<csrf>")]
|
|
pub async fn end_session(session_id: &str, csrf: &str, user: User) -> StringResponse {
|
|
if user.verify_csrf(csrf).await {
|
|
user.end_session(&to_uuid(session_id).unwrap()).await;
|
|
respond_html(
|
|
html! {
|
|
(user.update_csrf().await)
|
|
}
|
|
.into_string(),
|
|
)
|
|
} else {
|
|
respond_html(html! { p { "Invalid CSRF" }}.into_string())
|
|
}
|
|
}
|
|
|
|
#[get("/new_api_key?<csrf>&<session_name>")]
|
|
pub async fn new_api_key(user: User, csrf: &str, session_name: &str) -> StringResponse {
|
|
if user.verify_csrf(csrf).await {
|
|
if session_name.is_empty() {
|
|
return respond_html(
|
|
html! {
|
|
div id="next_session" {};
|
|
(user.update_csrf().await)
|
|
}
|
|
.into_string(),
|
|
);
|
|
}
|
|
|
|
let api = user.api_key(session_name).await;
|
|
respond_html(
|
|
html! {
|
|
li class="justify-between items-center bg-gray-50 p-4 rounded shadow" {
|
|
span class="text-gray-700" { (api.name.unwrap_or_default()) };
|
|
br {};
|
|
span class="text-red-500" { (api.token) };
|
|
};
|
|
|
|
div id="next_session" {};
|
|
(user.update_csrf().await)
|
|
}
|
|
.into_string(),
|
|
)
|
|
} else {
|
|
respond_html(
|
|
html! {
|
|
p { "CSRF!" };
|
|
}
|
|
.into_string(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[get("/account")]
|
|
pub async fn account_page(user: User, ctx: RequestContext) -> StringResponse {
|
|
let sessions = user.list_sessions().await;
|
|
|
|
let content = html! {
|
|
main class="w-full mt-6 rounded-lg shadow-md p-6" {
|
|
section class="mb-6" {
|
|
h2 class="text-xl font-semibold mb-2" { (user.username) };
|
|
};
|
|
|
|
(htmx_link("/passwd", "mb-6 bg-green-500 text-white py-2 px-6 rounded hover:bg-green-600", "", html! { "Change Password" }))
|
|
|
|
section class="mb-6 mt-6" {
|
|
h3 class="text-lg font-semibold mb-4" { "Active Sessions" };
|
|
ul class="space-y-4" id="sessions-list" {
|
|
|
|
@for ses in sessions {
|
|
(build_session_block(&ses, &user).await)
|
|
};
|
|
|
|
div id="next_session" {};
|
|
|
|
}
|
|
}
|
|
|
|
form class="flex" hx-get="/new_api_key" hx-target="#next_session"
|
|
hx-swap="outerHTML" {
|
|
input type="text" name="session_name" placeholder="API Key Name" class="text-black bg-gray-100 px-4 py-2 rounded mr-4" {};
|
|
input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {};
|
|
button class="bg-green-500 text-white py-2 px-6 rounded hover:bg-green-600" { "New API Key" };
|
|
}
|
|
};
|
|
};
|
|
|
|
render(content, "Account", ctx).await
|
|
}
|
|
|
|
pub async fn build_session_block(ses: &Session, user: &User) -> PreEscaped<String> {
|
|
html! {
|
|
li class="flex justify-between items-center bg-gray-50 p-4 rounded shadow" {
|
|
span class="text-gray-700" { (ses.name.clone().unwrap_or("Session".to_string())) };
|
|
|
|
form hx-target="closest li" hx-swap="outerHTML" hx-get=(format!("/end_session/{}", ses.id)) {
|
|
input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {};
|
|
button class="bg-red-500 text-white py-1 px-4 rounded hover:bg-red-600"
|
|
{ "Kill" };
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(FromForm)]
|
|
pub struct PasswordChangeForm {
|
|
password_old: String,
|
|
password_new: String,
|
|
password_new_repeat: String,
|
|
csrf: String,
|
|
}
|
|
|
|
#[post("/passwd", data = "<form>")]
|
|
pub async fn change_password_post(form: Form<PasswordChangeForm>, user: User) -> Redirect {
|
|
if form.password_new != form.password_new_repeat {
|
|
return Redirect::to("/passwd");
|
|
}
|
|
|
|
if !user.verify_csrf(&form.csrf).await {
|
|
return Redirect::to("/passwd");
|
|
}
|
|
|
|
user.passwd(&form.password_old, &form.password_new)
|
|
.await
|
|
.unwrap();
|
|
|
|
Redirect::to("/account")
|
|
}
|
|
|
|
#[get("/passwd")]
|
|
pub async fn change_password(ctx: RequestContext, user: User) -> StringResponse {
|
|
let content = html! {
|
|
div class="min-w-screen justify-center flex items-center" {
|
|
div class="bg-gray-800 shadow-lg rounded-lg p-8 max-w-sm w-full" {
|
|
h2 class="text-2xl font-bold text-center text-gray-100 mb-6" { "Change Password" };
|
|
form action="/passwd" method="POST" {
|
|
div class="mb-4" {
|
|
label for="password_old" class="block text-sm font-medium text-gray-400" { "Old Password" };
|
|
input type="password" id="password_old" name="password_old" placeholder="Old Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
|
|
};
|
|
div class="mb-4" {
|
|
label for="password_new" class="block text-sm font-medium text-gray-400" { "New Password" };
|
|
input type="password" id="password_new" name="password_new" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
|
|
};
|
|
div class="mb-4" {
|
|
label for="password_new_repeat" class="block text-sm font-medium text-gray-400" { "Repeat new Password" };
|
|
input type="password" id="password_new_repeat" name="password_new_repeat" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
|
|
};
|
|
input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {};
|
|
button type="submit" class="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400" { "Login" };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
render(content, "Change Password", ctx).await
|
|
}
|