implement user registration and other user related routes, with htmx error messages. (work in progress)
This commit is contained in:
parent
84bffc5f72
commit
f9941ed06a
15 changed files with 424 additions and 67 deletions
|
@ -6,7 +6,7 @@
|
|||
<title>{% block title %}{% endblock title %}</title>
|
||||
|
||||
<script src="https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>
|
||||
{% block head %}
|
||||
|
||||
{% endblock head %}
|
||||
|
|
15
nixin_farm_ssr/assets/views/home/error.html
Normal file
15
nixin_farm_ssr/assets/views/home/error.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Login
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div id="alert-message"
|
||||
class="bg-red-100 border-l-4 border-red-500 text-red-800 rounded-b px-4 py-1 shadow-md"
|
||||
role="alert">
|
||||
<p class="font-bold">Error</p>
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Login
|
||||
Index
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -14,5 +14,8 @@ Login
|
|||
hello user {{user.name}}
|
||||
</div>
|
||||
|
||||
<p class="mt-10 text-center text-sm text-gray-500">
|
||||
<a href="/logout" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Logout</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock content %}
|
15
nixin_farm_ssr/assets/views/home/information.html
Normal file
15
nixin_farm_ssr/assets/views/home/information.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Login
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div id="alert-message"
|
||||
class="bg-green-100 border-l-4 border-green-500 text-green-800 rounded-b px-4 py-1 shadow-md"
|
||||
role="alert">
|
||||
<p class="font-bold">Information</p>
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -6,7 +6,7 @@ Login
|
|||
|
||||
{% block content %}
|
||||
<!--
|
||||
This example requires some changes to your config:
|
||||
This requires some changes to tailwind config:
|
||||
|
||||
```
|
||||
// tailwind.config.js
|
||||
|
@ -19,14 +19,6 @@ Login
|
|||
}
|
||||
```
|
||||
-->
|
||||
<!--
|
||||
This example requires updating your template:
|
||||
|
||||
```
|
||||
<html class="h-full bg-white">
|
||||
<body class="h-full">
|
||||
```
|
||||
-->
|
||||
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img class="mx-auto h-12 w-auto" src="https://nixin.distrilab.eu/logo-nixin.svg" alt="NixiN">
|
||||
|
@ -34,7 +26,11 @@ Login
|
|||
</div>
|
||||
|
||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form class="space-y-6" action="/login" method="POST">
|
||||
<form class="space-y-6" action="/login" method="POST"
|
||||
hx-post="/login"
|
||||
hx-target="#alert-message"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
|
||||
<div class="mt-2">
|
||||
|
@ -46,7 +42,7 @@ Login
|
|||
<div class="flex items-center justify-between">
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
|
||||
<div class="text-sm">
|
||||
<a href="#" class="font-semibold text-indigo-600 hover:text-indigo-500">Forgot password?</a>
|
||||
<a href="/resetpwd" class="font-semibold text-indigo-600 hover:text-indigo-500">Forgot password?</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
|
@ -54,6 +50,8 @@ Login
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-message"></div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
|
||||
</div>
|
||||
|
@ -61,7 +59,7 @@ Login
|
|||
|
||||
<p class="mt-10 text-center text-sm text-gray-500">
|
||||
No account yet?
|
||||
<a href="#" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Register a new account</a>
|
||||
<a href="/register" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Register a new account</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,17 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Login
|
||||
Register a new account
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img class="mx-auto h-12 w-auto" src="https://nixin.distrilab.eu/logo-nixin.svg" alt="NixiN">
|
||||
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
Register a new account
|
||||
</h2>
|
||||
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Register a new account</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form class="space-y-6" action="/register" method="POST"
|
||||
hx-post="/register"
|
||||
hx-target="#alert-message"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium leading-6 text-gray-900">Name</label>
|
||||
<div class="mt-2">
|
||||
<input id="name" name="name" type="text" autocomplete="name" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
|
||||
<div class="mt-2">
|
||||
<input id="email" name="email" type="email" autocomplete="email" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-message"></div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Register new account</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="mt-10 text-center text-sm text-gray-500">
|
||||
Already registered?
|
||||
<a href="/login" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -0,0 +1,42 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Reset your account password
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img class="mx-auto h-12 w-auto" src="https://nixin.distrilab.eu/logo-nixin.svg" alt="NixiN">
|
||||
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Reset your account password</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form class="space-y-6" action="/resetpwd" method="POST"
|
||||
hx-post="/resetpwd"
|
||||
hx-target="#alert-message"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="This will send an email to the provided address if a corresponding account exists">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
|
||||
<div class="mt-2">
|
||||
<input id="email" name="email" type="email" autocomplete="email" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
<div id="alert-message"></div>
|
||||
<div>
|
||||
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Send password reset email</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<p class="mt-10 text-center text-sm text-gray-500">
|
||||
<a href="/login" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Go back to the login page</a>
|
||||
</p>
|
||||
<p class="mt-10 text-center text-sm text-gray-500">
|
||||
No account yet?
|
||||
<a href="/register" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Register a new account</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
15
nixin_farm_ssr/assets/views/home/warning.html
Normal file
15
nixin_farm_ssr/assets/views/home/warning.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Login
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div id="alert-message"
|
||||
class="bg-orange-100 border-l-4 border-orange-500 text-orange-800 rounded-b px-4 py-1 shadow-md"
|
||||
role="alert">
|
||||
<p class="font-bold">Warning</p>
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -31,6 +31,7 @@ async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn list(
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
|
@ -43,6 +44,7 @@ pub async fn list(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn new(
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(_ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
|
@ -51,6 +53,7 @@ pub async fn new(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn update(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(params): Json<Params>,
|
||||
|
@ -64,6 +67,7 @@ pub async fn update(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn edit(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
|
@ -74,6 +78,7 @@ pub async fn edit(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn show(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
|
@ -84,6 +89,7 @@ pub async fn show(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn add(
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(params): Json<Params>,
|
||||
|
@ -97,7 +103,10 @@ pub async fn add(
|
|||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
|
||||
pub async fn remove(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>) -> Result<Response> {
|
||||
load_item(&ctx, id).await?.delete(&ctx.db).await?;
|
||||
format::empty()
|
||||
}
|
||||
|
|
|
@ -6,34 +6,53 @@ use loco_rs::prelude::*;
|
|||
use axum::{
|
||||
debug_handler,
|
||||
extract::State,
|
||||
extract::Query,
|
||||
response::{IntoResponse, Redirect},
|
||||
//extract::Query,
|
||||
response::{/*IntoResponse,*/ Redirect},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
//Json,
|
||||
Form};
|
||||
use axum_extra::extract::cookie::{CookieJar, Cookie};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use loco_rs::{
|
||||
app::AppContext,
|
||||
controller::middleware,
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mailers::auth::AuthMailer,
|
||||
models::{
|
||||
users,
|
||||
//users::{LoginParams, RegisterParams},
|
||||
_entities::users,
|
||||
users::RegisterParams,
|
||||
},
|
||||
views,
|
||||
controllers::middleware::auth_no_error,
|
||||
};
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResetpwdFormParams {
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginFormParams {
|
||||
pub email: Option<String>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterFormParams {
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn home(
|
||||
auth: auth_no_error::JWTWithUserOpt<users::Model>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
State(_ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
println!("{:?}",auth);
|
||||
|
||||
|
@ -49,52 +68,197 @@ pub async fn home(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginParams {
|
||||
pub email: Option<String>,
|
||||
pub password: Option<String>,
|
||||
#[debug_handler]
|
||||
pub async fn register(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(_ctx): State<AppContext>) -> Result<Response> {
|
||||
views::home::register(&v)
|
||||
}
|
||||
|
||||
/// Creates a user login and returns a token
|
||||
#[debug_handler]
|
||||
pub async fn do_register(
|
||||
//auth: auth_no_error::JWTWithUserOpt<users::Model>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
//jar: CookieJar,
|
||||
Form(form_params): Form<RegisterFormParams>)
|
||||
-> Result<Response>
|
||||
{
|
||||
let params: RegisterParams = match form_params {
|
||||
RegisterFormParams{
|
||||
name: Some(name),
|
||||
email: Some(email),
|
||||
password: Some(password)}
|
||||
=> {
|
||||
RegisterParams{name, email, password}
|
||||
}
|
||||
_ => {
|
||||
return views::home::error(&v,"registration failed: missing name, email or password");
|
||||
}
|
||||
};
|
||||
|
||||
let res = users::Model::create_with_password(&ctx.db, ¶ms).await;
|
||||
let user = match res {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
tracing::info!(
|
||||
message = err.to_string(),
|
||||
user_email = ¶ms.email,
|
||||
"could not register user",
|
||||
);
|
||||
return views::home::error(&v,&format!("registration failed: {}",err.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let user = match user
|
||||
.into_active_model()
|
||||
.set_email_verification_sent(&ctx.db)
|
||||
.await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
tracing::info!(
|
||||
message = err.to_string(),
|
||||
user_email = ¶ms.email,
|
||||
"could not register user",
|
||||
);
|
||||
return views::home::error(&v,&format!("registration failed: {}",err.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let res = AuthMailer::send_welcome(&ctx, &user).await;
|
||||
match res {
|
||||
Ok(()) => {
|
||||
return format::redirect("/login");
|
||||
}
|
||||
Err(err) => {
|
||||
return views::home::error(&v,&format!("failed to send welcome email: {}",err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn login(
|
||||
auth: auth_no_error::JWTWithUserOpt<users::Model>,
|
||||
//ViewEngine(v): ViewEngine<TeraView>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(_ctx): State<AppContext>) -> Result<Response> {
|
||||
views::home::login(&v)
|
||||
}
|
||||
|
||||
/// Validate a user login and add a token cookie
|
||||
//ToDo: Try to return the JWT token in header instead of cookie
|
||||
// example to add headers to a response, return a `(HeaderMap, impl IntoResponse)`
|
||||
// async fn with_headers() -> impl IntoResponse {
|
||||
// let mut headers = HeaderMap::new();
|
||||
// headers.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap());
|
||||
// (headers, "foo")
|
||||
// }
|
||||
#[debug_handler]
|
||||
pub async fn do_login(
|
||||
//auth: auth_no_error::JWTWithUserOpt<users::Model>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
jar: CookieJar,
|
||||
Form(params): Form<LoginParams>) -> Result<(CookieJar, Redirect), StatusCode> {
|
||||
println!("Auth: {:?}",auth);
|
||||
println!("Form parameters: {:?}",params);
|
||||
println!("Cookie jar: {:?}",jar);
|
||||
|
||||
match params {
|
||||
LoginParams{email: Some(email), password: Some(password)} => {
|
||||
let user = users::Model::find_by_email(&ctx.db, &email).await
|
||||
.or_else(|_| Err(StatusCode::UNAUTHORIZED))?;
|
||||
Form(form_params): Form<LoginFormParams>)
|
||||
-> Result<(CookieJar, impl IntoResponse)> {
|
||||
match form_params {
|
||||
LoginFormParams{
|
||||
email: Some(email),
|
||||
password: Some(password)} => {
|
||||
let Ok(user) = users::Model::find_by_email(&ctx.db, &email).await else {
|
||||
return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
|
||||
};
|
||||
let valid = user.verify_password(&password);
|
||||
if !valid {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
|
||||
}
|
||||
let jwt_secret = ctx.config.get_jwt_config()
|
||||
.or_else(|_| Err(StatusCode::UNAUTHORIZED))?;
|
||||
let token = user
|
||||
let Ok(jwt_secret) = ctx.config.get_jwt_config() else {
|
||||
return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
|
||||
};
|
||||
let Ok(token) = user
|
||||
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
|
||||
.or_else(|_| Err(StatusCode::UNAUTHORIZED))?;
|
||||
Ok((
|
||||
else {
|
||||
return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
|
||||
};
|
||||
Ok((
|
||||
// the updated jar must be returned for the changes
|
||||
// to be included in the response
|
||||
jar.add(Cookie::new("token", token)),
|
||||
Redirect::to("/"),))
|
||||
Ok(Redirect::to("/").into_response()),))
|
||||
}
|
||||
_ => {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
return Ok((jar, views::home::error(&v,"Login failed: you need to provide an email and a password")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the user login token cookie
|
||||
#[debug_handler]
|
||||
pub async fn logout(
|
||||
State(_ctx): State<AppContext>,
|
||||
jar: CookieJar)
|
||||
-> Result<(CookieJar, Redirect), StatusCode> {
|
||||
|
||||
println!("Cookie jar: {:?}",jar);
|
||||
Ok((jar.remove(Cookie::from("token")), Redirect::to("/"),))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn resetpwd(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(_ctx): State<AppContext>) -> Result<Response> {
|
||||
views::home::resetpwd(&v)
|
||||
}
|
||||
|
||||
/// Validate a user login and add a token cookie
|
||||
#[debug_handler]
|
||||
pub async fn do_resetpwd(
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
Form(form_params): Form<ResetpwdFormParams>)
|
||||
-> Result<Response> {
|
||||
|
||||
let Some(email) = form_params.email else {
|
||||
return views::home::error(&v,"Password reset failed: missing email");
|
||||
};
|
||||
|
||||
let Ok(user) = users::Model::find_by_email(&ctx.db, &email).await else {
|
||||
// we don't want to expose our users email. if the email is invalid we
|
||||
// should still be returning success to the caller
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
return views::home::info(&v,"If an account exists for this address an email has been sent with the information on how to reset your password");
|
||||
};
|
||||
|
||||
|
||||
let res = user
|
||||
.into_active_model()
|
||||
.set_forgot_password_sent(&ctx.db)
|
||||
.await;
|
||||
match res {
|
||||
Ok(user) => {
|
||||
let res = AuthMailer::forgot_password(&ctx, &user).await;
|
||||
match res {
|
||||
Ok(()) => {
|
||||
return views::home::info(&v,"If an account exists for this address an email has been sent with the information on how to reset your password");
|
||||
}
|
||||
Err(err) => {
|
||||
return views::home::error(&v,&format!("Failed to send welcome email: {}",err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return views::home::error(&v,&format!("Failed to prepare password reset email: {}",err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
//.prefix("homes")
|
||||
.add("/", get(home))
|
||||
.add("/login", post(login))
|
||||
.add("/login", post(do_login))
|
||||
.add("/login", get(login))
|
||||
.add("/register", post(do_register))
|
||||
.add("/register", get(register))
|
||||
.add("/resetpwd", post(do_resetpwd))
|
||||
.add("/resetpwd", get(resetpwd))
|
||||
.add("/logout", get(logout))
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ use crate::{
|
|||
models::_entities::servers::{ActiveModel, Column, Entity, Model},
|
||||
views,
|
||||
};
|
||||
use crate::models::users;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Params {
|
||||
|
@ -32,14 +31,10 @@ async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn list(
|
||||
auth: auth::JWT,
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
// we only want to make sure user exists, so we name the variable
|
||||
// with a `_` in front to remove the warning about unused variable
|
||||
let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||
|
||||
let item = Entity::find()
|
||||
.order_by(Column::Id, Order::Desc)
|
||||
.all(&ctx.db)
|
||||
|
@ -49,6 +44,7 @@ pub async fn list(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn new(
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(_ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
|
@ -57,6 +53,7 @@ pub async fn new(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn update(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(params): Json<Params>,
|
||||
|
@ -70,6 +67,7 @@ pub async fn update(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn edit(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
|
@ -80,6 +78,7 @@ pub async fn edit(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn show(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
|
@ -90,6 +89,7 @@ pub async fn show(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn add(
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(params): Json<Params>,
|
||||
|
@ -103,7 +103,10 @@ pub async fn add(
|
|||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
|
||||
pub async fn remove(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>) -> Result<Response> {
|
||||
load_item(&ctx, id).await?.delete(&ctx.db).await?;
|
||||
format::empty()
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
models::_entities::services::{ActiveModel, Column, Entity, Model},
|
||||
views,
|
||||
};
|
||||
use crate::models::users;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Params {
|
||||
|
@ -34,7 +34,7 @@ async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn list(
|
||||
_user: auth::ApiToken<users::Model>,
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
|
@ -47,6 +47,7 @@ pub async fn list(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn new(
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(_ctx): State<AppContext>,
|
||||
) -> Result<Response> {
|
||||
|
@ -55,6 +56,7 @@ pub async fn new(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn update(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(params): Json<Params>,
|
||||
|
@ -68,6 +70,7 @@ pub async fn update(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn edit(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
|
@ -78,6 +81,7 @@ pub async fn edit(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn show(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
|
@ -88,6 +92,7 @@ pub async fn show(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn add(
|
||||
_auth: auth::JWT,
|
||||
ViewEngine(v): ViewEngine<TeraView>,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(params): Json<Params>,
|
||||
|
@ -101,7 +106,10 @@ pub async fn add(
|
|||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
|
||||
pub async fn remove(
|
||||
_auth: auth::JWT,
|
||||
Path(id): Path<i32>,
|
||||
State(ctx): State<AppContext>) -> Result<Response> {
|
||||
load_item(&ctx, id).await?.delete(&ctx.db).await?;
|
||||
format::empty()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
- id: 1
|
||||
pid: 11111111-1111-1111-1111-111111111111
|
||||
pid: b7b311f1-85ba-459e-9ffb-abcad784bc98
|
||||
email: test@nixin.local.com
|
||||
password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc"
|
||||
api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758
|
||||
password: "$argon2id$v=19$m=19456,t=2,p=1$vZAE4BQRuY+diRder4RiNA$X1TY4uFQwvwP4Cty6I53TB/kRckx/O1VpAW/DGYR+a4"
|
||||
api_key: lo-9bb66c1c-10ac-46ef-ad07-76fc63d24718
|
||||
name: test
|
||||
created_at: "2023-11-12T12:34:56.789Z"
|
||||
updated_at: "2023-11-12T12:34:56.789Z"
|
||||
created_at: "2024-10-22T11:59:54+00:00"
|
||||
updated_at: "2024-10-22T11:59:54+00:00"
|
||||
|
||||
|
|
|
@ -2,6 +2,15 @@ use loco_rs::prelude::*;
|
|||
|
||||
use crate::models::users;
|
||||
|
||||
/// Display the register form.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When there is an issue with rendering the view.
|
||||
pub fn register(v: &impl ViewRenderer) -> Result<Response> {
|
||||
format::render().view(v, "home/register.html", data!({}))
|
||||
}
|
||||
|
||||
/// Display the login form.
|
||||
///
|
||||
/// # Errors
|
||||
|
@ -20,3 +29,39 @@ pub fn index(v: &impl ViewRenderer, user: &users::Model) -> Result<Response> {
|
|||
format::render().view(v, "home/index.html", data!({"user": user}))
|
||||
}
|
||||
|
||||
/// Display the password reset page
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When there is an issue with rendering the view.
|
||||
pub fn resetpwd(v: &impl ViewRenderer) -> Result<Response> {
|
||||
format::render().view(v, "home/resetpwd.html", data!({}))
|
||||
}
|
||||
|
||||
/// return an alert message fragment of type error
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When there is an issue with rendering the view.
|
||||
pub fn error(v: &impl ViewRenderer, message: &str) -> Result<Response> {
|
||||
format::render().view(v, "home/error.html", data!({"message": message}))
|
||||
}
|
||||
|
||||
/// return an alert message fragment of type warning
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When there is an issue with rendering the view.
|
||||
pub fn warn(v: &impl ViewRenderer, message: &str) -> Result<Response> {
|
||||
format::render().view(v, "home/warning.html", data!({"message": message}))
|
||||
}
|
||||
|
||||
/// return an alert message fragment of type information
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// When there is an issue with rendering the view.
|
||||
pub fn info(v: &impl ViewRenderer, message: &str) -> Result<Response> {
|
||||
format::render().view(v, "home/information.html", data!({"message": message}))
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ in
|
|||
openssl
|
||||
pkg-config
|
||||
rustup
|
||||
cargo-watch
|
||||
];
|
||||
RUSTC_VERSION = overrides.toolchain.channel;
|
||||
# https://github.com/rust-lang/rust-bindgen#environment-variables
|
||||
|
|
Loading…
Reference in a new issue