implement user registration and other user related routes, with htmx error messages. (work in progress)

This commit is contained in:
Douze Bé 2024-10-23 18:54:44 +02:00
parent 84bffc5f72
commit f9941ed06a
15 changed files with 424 additions and 67 deletions

View file

@ -6,7 +6,7 @@
<title>{% block title %}{% endblock title %}</title> <title>{% block title %}{% endblock title %}</title>
<script src="https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"></script> <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 %} {% block head %}
{% endblock head %} {% endblock head %}

View 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 %}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
Login Index
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
@ -14,5 +14,8 @@ Login
hello user {{user.name}} hello user {{user.name}}
</div> </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> </div>
{% endblock content %} {% endblock content %}

View 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 %}

View file

@ -6,7 +6,7 @@ Login
{% block content %} {% block content %}
<!-- <!--
This example requires some changes to your config: This requires some changes to tailwind config:
``` ```
// tailwind.config.js // 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="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"> <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"> <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>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <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> <div>
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label> <label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
<div class="mt-2"> <div class="mt-2">
@ -46,7 +42,7 @@ Login
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label> <label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
<div class="text-sm"> <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> </div>
<div class="mt-2"> <div class="mt-2">
@ -54,6 +50,8 @@ Login
</div> </div>
</div> </div>
<div id="alert-message"></div>
<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> <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> </div>
@ -61,7 +59,7 @@ Login
<p class="mt-10 text-center text-sm text-gray-500"> <p class="mt-10 text-center text-sm text-gray-500">
No account yet? 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> </p>
</div> </div>
</div> </div>

View file

@ -1,17 +1,56 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
Login Register a new account
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8"> <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"> <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"> <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"> <h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Register a new account</h2>
Register a new account
</h2>
</div> </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> </div>
{% endblock content %} {% endblock content %}

View file

@ -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 %}

View 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 %}

View file

@ -31,6 +31,7 @@ async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
#[debug_handler] #[debug_handler]
pub async fn list( pub async fn list(
_auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
) -> Result<Response> { ) -> Result<Response> {
@ -43,6 +44,7 @@ pub async fn list(
#[debug_handler] #[debug_handler]
pub async fn new( pub async fn new(
_auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(_ctx): State<AppContext>, State(_ctx): State<AppContext>,
) -> Result<Response> { ) -> Result<Response> {
@ -51,6 +53,7 @@ pub async fn new(
#[debug_handler] #[debug_handler]
pub async fn update( pub async fn update(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
Json(params): Json<Params>, Json(params): Json<Params>,
@ -64,6 +67,7 @@ pub async fn update(
#[debug_handler] #[debug_handler]
pub async fn edit( pub async fn edit(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
@ -74,6 +78,7 @@ pub async fn edit(
#[debug_handler] #[debug_handler]
pub async fn show( pub async fn show(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
@ -84,6 +89,7 @@ pub async fn show(
#[debug_handler] #[debug_handler]
pub async fn add( pub async fn add(
_auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
Json(params): Json<Params>, Json(params): Json<Params>,
@ -97,7 +103,10 @@ pub async fn add(
} }
#[debug_handler] #[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?; load_item(&ctx, id).await?.delete(&ctx.db).await?;
format::empty() format::empty()
} }

View file

@ -6,34 +6,53 @@ use loco_rs::prelude::*;
use axum::{ use axum::{
debug_handler, debug_handler,
extract::State, extract::State,
extract::Query, //extract::Query,
response::{IntoResponse, Redirect}, response::{/*IntoResponse,*/ Redirect},
http::StatusCode, http::StatusCode,
Json, //Json,
Form}; Form};
use axum_extra::extract::cookie::{CookieJar, Cookie}; use axum_extra::extract::cookie::{CookieJar, Cookie};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use loco_rs::{ use loco_rs::{
app::AppContext, app::AppContext,
controller::middleware,
Result, Result,
}; };
use crate::{ use crate::{
mailers::auth::AuthMailer,
models::{ models::{
users, _entities::users,
//users::{LoginParams, RegisterParams}, users::RegisterParams,
}, },
views, views,
controllers::middleware::auth_no_error, 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] #[debug_handler]
pub async fn home( pub async fn home(
auth: auth_no_error::JWTWithUserOpt<users::Model>, auth: auth_no_error::JWTWithUserOpt<users::Model>,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(_ctx): State<AppContext>,
) -> Result<Response> { ) -> Result<Response> {
println!("{:?}",auth); println!("{:?}",auth);
@ -49,52 +68,197 @@ pub async fn home(
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[debug_handler]
pub struct LoginParams { pub async fn register(
pub email: Option<String>, ViewEngine(v): ViewEngine<TeraView>,
pub password: Option<String>, 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, &params).await;
let user = match res {
Ok(user) => user,
Err(err) => {
tracing::info!(
message = err.to_string(),
user_email = &params.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 = &params.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] #[debug_handler]
pub async fn login( 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>, State(ctx): State<AppContext>,
jar: CookieJar, jar: CookieJar,
Form(params): Form<LoginParams>) -> Result<(CookieJar, Redirect), StatusCode> { Form(form_params): Form<LoginFormParams>)
println!("Auth: {:?}",auth); -> Result<(CookieJar, impl IntoResponse)> {
println!("Form parameters: {:?}",params); match form_params {
println!("Cookie jar: {:?}",jar); LoginFormParams{
email: Some(email),
match params { password: Some(password)} => {
LoginParams{email: Some(email), password: Some(password)} => { let Ok(user) = users::Model::find_by_email(&ctx.db, &email).await else {
let user = users::Model::find_by_email(&ctx.db, &email).await return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
.or_else(|_| Err(StatusCode::UNAUTHORIZED))?; };
let valid = user.verify_password(&password); let valid = user.verify_password(&password);
if !valid { 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() let Ok(jwt_secret) = ctx.config.get_jwt_config() else {
.or_else(|_| Err(StatusCode::UNAUTHORIZED))?; return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
let token = user };
let Ok(token) = user
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration) .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
.or_else(|_| Err(StatusCode::UNAUTHORIZED))?; else {
Ok(( return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
};
Ok((
// the updated jar must be returned for the changes // the updated jar must be returned for the changes
// to be included in the response // to be included in the response
jar.add(Cookie::new("token", token)), 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 { pub fn routes() -> Routes {
Routes::new() Routes::new()
//.prefix("homes")
.add("/", get(home)) .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))
} }

View file

@ -10,7 +10,6 @@ use crate::{
models::_entities::servers::{ActiveModel, Column, Entity, Model}, models::_entities::servers::{ActiveModel, Column, Entity, Model},
views, views,
}; };
use crate::models::users;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Params { pub struct Params {
@ -32,14 +31,10 @@ async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
#[debug_handler] #[debug_handler]
pub async fn list( pub async fn list(
auth: auth::JWT, _auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
) -> Result<Response> { ) -> 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() let item = Entity::find()
.order_by(Column::Id, Order::Desc) .order_by(Column::Id, Order::Desc)
.all(&ctx.db) .all(&ctx.db)
@ -49,6 +44,7 @@ pub async fn list(
#[debug_handler] #[debug_handler]
pub async fn new( pub async fn new(
_auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(_ctx): State<AppContext>, State(_ctx): State<AppContext>,
) -> Result<Response> { ) -> Result<Response> {
@ -57,6 +53,7 @@ pub async fn new(
#[debug_handler] #[debug_handler]
pub async fn update( pub async fn update(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
Json(params): Json<Params>, Json(params): Json<Params>,
@ -70,6 +67,7 @@ pub async fn update(
#[debug_handler] #[debug_handler]
pub async fn edit( pub async fn edit(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
@ -80,6 +78,7 @@ pub async fn edit(
#[debug_handler] #[debug_handler]
pub async fn show( pub async fn show(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
@ -90,6 +89,7 @@ pub async fn show(
#[debug_handler] #[debug_handler]
pub async fn add( pub async fn add(
_auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
Json(params): Json<Params>, Json(params): Json<Params>,
@ -103,7 +103,10 @@ pub async fn add(
} }
#[debug_handler] #[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?; load_item(&ctx, id).await?.delete(&ctx.db).await?;
format::empty() format::empty()
} }

View file

@ -10,7 +10,7 @@ use crate::{
models::_entities::services::{ActiveModel, Column, Entity, Model}, models::_entities::services::{ActiveModel, Column, Entity, Model},
views, views,
}; };
use crate::models::users;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Params { pub struct Params {
@ -34,7 +34,7 @@ async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
#[debug_handler] #[debug_handler]
pub async fn list( pub async fn list(
_user: auth::ApiToken<users::Model>, _auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
) -> Result<Response> { ) -> Result<Response> {
@ -47,6 +47,7 @@ pub async fn list(
#[debug_handler] #[debug_handler]
pub async fn new( pub async fn new(
_auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(_ctx): State<AppContext>, State(_ctx): State<AppContext>,
) -> Result<Response> { ) -> Result<Response> {
@ -55,6 +56,7 @@ pub async fn new(
#[debug_handler] #[debug_handler]
pub async fn update( pub async fn update(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
Json(params): Json<Params>, Json(params): Json<Params>,
@ -68,6 +70,7 @@ pub async fn update(
#[debug_handler] #[debug_handler]
pub async fn edit( pub async fn edit(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
@ -78,6 +81,7 @@ pub async fn edit(
#[debug_handler] #[debug_handler]
pub async fn show( pub async fn show(
_auth: auth::JWT,
Path(id): Path<i32>, Path(id): Path<i32>,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
@ -88,6 +92,7 @@ pub async fn show(
#[debug_handler] #[debug_handler]
pub async fn add( pub async fn add(
_auth: auth::JWT,
ViewEngine(v): ViewEngine<TeraView>, ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
Json(params): Json<Params>, Json(params): Json<Params>,
@ -101,7 +106,10 @@ pub async fn add(
} }
#[debug_handler] #[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?; load_item(&ctx, id).await?.delete(&ctx.db).await?;
format::empty() format::empty()
} }

View file

@ -1,10 +1,10 @@
--- ---
- id: 1 - id: 1
pid: 11111111-1111-1111-1111-111111111111 pid: b7b311f1-85ba-459e-9ffb-abcad784bc98
email: test@nixin.local.com email: test@nixin.local.com
password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc" password: "$argon2id$v=19$m=19456,t=2,p=1$vZAE4BQRuY+diRder4RiNA$X1TY4uFQwvwP4Cty6I53TB/kRckx/O1VpAW/DGYR+a4"
api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758 api_key: lo-9bb66c1c-10ac-46ef-ad07-76fc63d24718
name: test name: test
created_at: "2023-11-12T12:34:56.789Z" created_at: "2024-10-22T11:59:54+00:00"
updated_at: "2023-11-12T12:34:56.789Z" updated_at: "2024-10-22T11:59:54+00:00"

View file

@ -2,6 +2,15 @@ use loco_rs::prelude::*;
use crate::models::users; 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. /// Display the login form.
/// ///
/// # Errors /// # 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})) 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}))
}

View file

@ -13,6 +13,7 @@ in
openssl openssl
pkg-config pkg-config
rustup rustup
cargo-watch
]; ];
RUSTC_VERSION = overrides.toolchain.channel; RUSTC_VERSION = overrides.toolchain.channel;
# https://github.com/rust-lang/rust-bindgen#environment-variables # https://github.com/rust-lang/rust-bindgen#environment-variables