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>
|
<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 %}
|
||||||
|
|
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" %}
|
{% 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 %}
|
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 %}
|
{% 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>
|
||||||
|
|
|
@ -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 %}
|
|
@ -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]
|
#[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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, ¶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]
|
#[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 {
|
||||||
|
return Ok((jar, views::home::error(&v,"Login failed: invalid email or password")));
|
||||||
|
};
|
||||||
Ok((
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue