Add 'Remember Me' feature to login (extends session to 30 days)
All checks were successful
Build MIPS Binary / build (push) Successful in 4m7s

This commit is contained in:
spinline
2026-02-07 19:05:52 +03:00
parent 9b18b97c49
commit 575cfa4b38
2 changed files with 28 additions and 4 deletions

View File

@@ -13,6 +13,8 @@ use time::Duration;
pub struct LoginRequest { pub struct LoginRequest {
username: String, username: String,
password: String, password: String,
#[serde(default)]
remember_me: bool,
} }
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
@@ -61,8 +63,13 @@ pub async fn login_handler(
rand::thread_rng().sample(Alphanumeric) as char rand::thread_rng().sample(Alphanumeric) as char
}).collect(); }).collect();
// Expires in 30 days // Expiration: 30 days if remember_me is true, else 1 day
let expires_in = 60 * 60 * 24 * 30; let expires_in = if payload.remember_me {
60 * 60 * 24 * 30
} else {
60 * 60 * 24
};
let expires_at = time::OffsetDateTime::now_utc().unix_timestamp() + expires_in; let expires_at = time::OffsetDateTime::now_utc().unix_timestamp() + expires_in;
if let Err(e) = state.db.create_session(user_id, &token, expires_at).await { if let Err(e) = state.db.create_session(user_id, &token, expires_at).await {
@@ -70,13 +77,14 @@ pub async fn login_handler(
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create session").into_response(); return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create session").into_response();
} }
let cookie = Cookie::build(("auth_token", token)) let mut cookie = Cookie::build(("auth_token", token))
.path("/") .path("/")
.http_only(true) .http_only(true)
.same_site(SameSite::Lax) .same_site(SameSite::Lax)
.max_age(Duration::seconds(expires_in))
.build(); .build();
cookie.set_max_age(Duration::seconds(expires_in));
tracing::info!("Session created and cookie set for user: {}", payload.username); tracing::info!("Session created and cookie set for user: {}", payload.username);
(StatusCode::OK, jar.add(cookie), Json(UserResponse { username: payload.username })).into_response() (StatusCode::OK, jar.add(cookie), Json(UserResponse { username: payload.username })).into_response()
} }

View File

@@ -6,12 +6,14 @@ use serde::Serialize;
struct LoginRequest { struct LoginRequest {
username: String, username: String,
password: String, password: String,
remember_me: bool,
} }
#[component] #[component]
pub fn Login() -> impl IntoView { pub fn Login() -> impl IntoView {
let (username, set_username) = create_signal(String::new()); let (username, set_username) = create_signal(String::new());
let (password, set_password) = create_signal(String::new()); let (password, set_password) = create_signal(String::new());
let (remember_me, set_remember_me) = create_signal(false);
let (error, set_error) = create_signal(Option::<String>::None); let (error, set_error) = create_signal(Option::<String>::None);
let (loading, set_loading) = create_signal(false); let (loading, set_loading) = create_signal(false);
@@ -26,6 +28,7 @@ pub fn Login() -> impl IntoView {
let req = LoginRequest { let req = LoginRequest {
username: username.get(), username: username.get(),
password: password.get(), password: password.get(),
remember_me: remember_me.get(),
}; };
let client = gloo_net::http::Request::post("/api/auth/login") let client = gloo_net::http::Request::post("/api/auth/login")
@@ -89,6 +92,19 @@ pub fn Login() -> impl IntoView {
/> />
</div> </div>
<div class="form-control mt-4">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-sm"
prop:checked=remember_me
on:change=move |ev| set_remember_me.set(event_target_checked(&ev))
disabled=move || loading.get()
/>
<span class="label-text">"Beni Hatırla"</span>
</label>
</div>
<Show when=move || error.get().is_some()> <Show when=move || error.get().is_some()>
<div class="alert alert-error mt-4 text-sm py-2"> <div class="alert alert-error mt-4 text-sm py-2">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>