feat: enable login via email magic link
This commit is contained in:
parent
8def2bfaf3
commit
b4fd4cea05
|
|
@ -19,11 +19,12 @@ use gotrue::params::{
|
||||||
MagicLinkParams,
|
MagicLinkParams,
|
||||||
};
|
};
|
||||||
use gotrue_entity::dto::{GotrueTokenResponse, SignUpResponse, UpdateGotrueUserParams, User};
|
use gotrue_entity::dto::{GotrueTokenResponse, SignUpResponse, UpdateGotrueUserParams, User};
|
||||||
use gotrue_entity::error::GoTrueError;
|
use tracing::info;
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/login", post(login_handler))
|
.route("/signin", post(sign_in_handler))
|
||||||
|
.route("/signup", post(sign_up_handler))
|
||||||
.route("/login_refresh/:refresh_token", post(login_refresh_handler))
|
.route("/login_refresh/:refresh_token", post(login_refresh_handler))
|
||||||
.route("/logout", post(logout_handler))
|
.route("/logout", post(logout_handler))
|
||||||
|
|
||||||
|
|
@ -112,18 +113,14 @@ pub async fn open_app_handler(session: UserSession) -> Result<HeaderMap, WebApiE
|
||||||
// to the target user
|
// to the target user
|
||||||
pub async fn invite_handler(
|
pub async fn invite_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
session: UserSession,
|
|
||||||
Form(param): Form<WebApiInviteUserRequest>,
|
Form(param): Form<WebApiInviteUserRequest>,
|
||||||
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
||||||
state
|
state
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.magic_link(
|
.magic_link(&MagicLinkParams {
|
||||||
&session.token.access_token,
|
email: param.email,
|
||||||
&MagicLinkParams {
|
..Default::default()
|
||||||
email: param.email,
|
})
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(WebApiResponse::<()>::from_str("Invitation sent".into()))
|
Ok(WebApiResponse::<()>::from_str("Invitation sent".into()))
|
||||||
}
|
}
|
||||||
|
|
@ -275,13 +272,18 @@ pub async fn login_refresh_handler(
|
||||||
|
|
||||||
// login and set the cookie
|
// login and set the cookie
|
||||||
// sign up if not exist
|
// sign up if not exist
|
||||||
pub async fn login_handler(
|
pub async fn sign_in_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
Form(param): Form<WebApiLoginRequest>,
|
Form(param): Form<WebApiLoginRequest>,
|
||||||
) -> Result<(CookieJar, HeaderMap, WebApiResponse<()>), WebApiError<'static>> {
|
) -> Result<(CookieJar, HeaderMap, WebApiResponse<()>), WebApiError<'static>> {
|
||||||
|
if param.password.is_empty() {
|
||||||
|
let res = send_magic_link(State(state), ¶m.email).await?;
|
||||||
|
return Ok((CookieJar::new(), HeaderMap::new(), res));
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to sign in with email and password
|
// Attempt to sign in with email and password
|
||||||
let token_res = state
|
let token = state
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.token(&gotrue::grant::Grant::Password(
|
.token(&gotrue::grant::Grant::Password(
|
||||||
gotrue::grant::PasswordGrant {
|
gotrue::grant::PasswordGrant {
|
||||||
|
|
@ -289,48 +291,36 @@ pub async fn login_handler(
|
||||||
password: param.password.to_owned(),
|
password: param.password.to_owned(),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
match token_res {
|
session_login(State(state), token, jar).await
|
||||||
Ok(token) => session_login(State(state), token, jar).await, // login success
|
}
|
||||||
Err(err) => match &err {
|
|
||||||
GoTrueError::ClientError(client_err) => {
|
|
||||||
match (
|
|
||||||
client_err.error.as_str(),
|
|
||||||
client_err.error_description.as_deref(),
|
|
||||||
) {
|
|
||||||
// Email not exist or wrong password
|
|
||||||
("invalid_grant", Some("Invalid login credentials")) => {
|
|
||||||
let sign_up_res = state
|
|
||||||
.gotrue_client
|
|
||||||
.sign_up_with_referrer(¶m.email, ¶m.password, Some("/"))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match sign_up_res {
|
pub async fn sign_up_handler(
|
||||||
Ok(resp) => match resp {
|
State(state): State<AppState>,
|
||||||
// when GOTRUE_MAILER_AUTOCONFIRM=true, auto sign in
|
jar: CookieJar,
|
||||||
SignUpResponse::Authenticated(token) => {
|
Form(param): Form<WebApiLoginRequest>,
|
||||||
session_login(State(state), token, jar).await
|
) -> Result<(CookieJar, HeaderMap, WebApiResponse<()>), WebApiError<'static>> {
|
||||||
},
|
if param.password.is_empty() {
|
||||||
SignUpResponse::NotAuthenticated(user) => match user.identities {
|
let res = send_magic_link(State(state), ¶m.email).await?;
|
||||||
Some(_identities) => {
|
return Ok((CookieJar::new(), HeaderMap::new(), res));
|
||||||
// new user, awaiting email verification
|
}
|
||||||
Ok((
|
|
||||||
jar,
|
let sign_up_res = state
|
||||||
HeaderMap::new(),
|
.gotrue_client
|
||||||
WebApiResponse::<()>::from_str("Email Verification Sent".into()),
|
.sign_up_with_referrer(¶m.email, ¶m.password, Some("/"))
|
||||||
))
|
.await?;
|
||||||
},
|
|
||||||
None => Err(err.into()), // user exists but sign in password not correct
|
match sign_up_res {
|
||||||
},
|
// when GOTRUE_MAILER_AUTOCONFIRM=true, auto sign in
|
||||||
},
|
SignUpResponse::Authenticated(token) => session_login(State(state), token, jar).await,
|
||||||
Err(err) => Err(err.into()),
|
SignUpResponse::NotAuthenticated(user) => {
|
||||||
}
|
info!("user signed up and not authenticated: {:?}", user);
|
||||||
},
|
Ok((
|
||||||
_ => Err(err.into()),
|
jar,
|
||||||
}
|
HeaderMap::new(),
|
||||||
},
|
WebApiResponse::<()>::from_str("Email Verification Sent".into()),
|
||||||
_ => Err(err.into()),
|
))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -382,6 +372,22 @@ async fn session_login(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_magic_link(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
email: &str,
|
||||||
|
) -> Result<WebApiResponse<()>, WebApiError<'static>> {
|
||||||
|
Ok(
|
||||||
|
state
|
||||||
|
.gotrue_client
|
||||||
|
.magic_link(&MagicLinkParams {
|
||||||
|
email: email.to_owned(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_base_url(header_map: &HeaderMap) -> String {
|
fn get_base_url(header_map: &HeaderMap) -> String {
|
||||||
let scheme = get_header_value_or_default(header_map, "x-scheme", "http");
|
let scheme = get_header_value_or_default(header_map, "x-scheme", "http");
|
||||||
let host = get_header_value_or_default(header_map, "host", "localhost");
|
let host = get_header_value_or_default(header_map, "host", "localhost");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div>
|
<div>
|
||||||
<h4>Please enter the following information to create new SSO</h4>
|
<h4>Please enter the following information to create new SSO</h4>
|
||||||
<form hx-post="/web-api/admin/sso" hx-target="#none">
|
<form hx-post="/web-api/admin/sso">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Email</td>
|
<td>Email</td>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
{% include "user_details.html" %}
|
{% include "user_details.html" %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<form hx-put="/web-api/admin/user/{{ user.id|escape }}" hx-target="#none">
|
<form hx-put="/web-api/admin/user/{{ user.id|escape }}">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Set Password:</td>
|
<td>Set Password:</td>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div>
|
<div>
|
||||||
<h3>Password Change</h3>
|
<h3>Password Change</h3>
|
||||||
<form hx-post="/web-api/change_password" hx-target="#none">
|
<form hx-post="/web-api/change_password">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>New Password:</td>
|
<td>New Password:</td>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div id="create-user">
|
<div id="create-user">
|
||||||
<h4>Please enter the following information to create a new user</h4>
|
<h4>Please enter the following information to create a new user</h4>
|
||||||
<form hx-post="/web-api/admin/user" hx-target="#none">
|
<form hx-post="/web-api/admin/user">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Email:</td>
|
<td>Email:</td>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div id="invite-user">
|
<div id="invite-user">
|
||||||
<h4>Please enter the following email invite a new user</h4>
|
<h4>Please enter the following email invite a new user</h4>
|
||||||
<form hx-post="/web-api/invite" hx-target="#none">
|
<form hx-post="/web-api/invite">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Email:</td>
|
<td>Email:</td>
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,31 @@
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function findButton(event) {
|
||||||
|
let button = event.target.querySelector(".button");
|
||||||
|
if (button) {
|
||||||
|
return button;
|
||||||
|
} else {
|
||||||
|
return event.target.closest(".button");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.body.addEventListener("htmx:beforeRequest", function (event) {
|
document.body.addEventListener("htmx:beforeRequest", function (event) {
|
||||||
const closeButton = event.target.querySelector(".button");
|
const closeButton = findButton(event);
|
||||||
closeButton.classList.add("loading-button");
|
if (closeButton) {
|
||||||
closeButton.disabled = true;
|
closeButton.classList.add("loading-button");
|
||||||
|
closeButton.disabled = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener("htmx:afterRequest", function (evt) {
|
document.body.addEventListener("htmx:afterRequest", function (event) {
|
||||||
const closeButton = event.target.querySelector(".button");
|
const closeButton = findButton(event);
|
||||||
closeButton.classList.remove("loading-button");
|
if (closeButton) {
|
||||||
closeButton.disabled = false;
|
closeButton.classList.remove("loading-button");
|
||||||
|
closeButton.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
const detail = evt.detail;
|
const detail = event.detail;
|
||||||
if (detail.failed) {
|
if (detail.failed) {
|
||||||
const xhr = detail.xhr;
|
const xhr = detail.xhr;
|
||||||
displayHttpFail(xhr.status, xhr.statusText, xhr.responseText);
|
displayHttpFail(xhr.status, xhr.statusText, xhr.responseText);
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Email Login</h3>
|
<h3>Email Login</h3>
|
||||||
<form hx-post="/web-api/login" hx-target="#none">
|
<form>
|
||||||
<table style="width: 100%">
|
<table style="width: 100%">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Email:</td>
|
<td>Email</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Password:</td>
|
<td>Password  </td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
|
|
@ -44,19 +44,35 @@
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="********"
|
placeholder="********(optional)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<button
|
|
||||||
class="button cyan"
|
<small style="color: #888"
|
||||||
type="submit"
|
><i>
|
||||||
style="width: 100%; margin: 16px auto 0; padding: 8px 8px"
|
(Magic link will be sent to email if password is not provided)
|
||||||
id="submitBtn"
|
</i></small
|
||||||
>
|
>
|
||||||
Sign In / Sign Up
|
<div style="display: flex; margin: 8px 0px">
|
||||||
</button>
|
<button
|
||||||
|
hx-post="/web-api/signin"
|
||||||
|
class="button cyan"
|
||||||
|
type="submit"
|
||||||
|
style="width: 100%; padding: 8px 8px"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
hx-post="/web-api/signup"
|
||||||
|
class="button purple"
|
||||||
|
type="submit"
|
||||||
|
style="width: 100%; padding: 8px 8px"
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Load OAuth Providers if configured -->
|
<!-- Load OAuth Providers if configured -->
|
||||||
|
|
|
||||||
|
|
@ -374,23 +374,16 @@ impl Client {
|
||||||
pub async fn invite(&self, email: &str) -> Result<(), AppResponseError> {
|
pub async fn invite(&self, email: &str) -> Result<(), AppResponseError> {
|
||||||
self
|
self
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
.magic_link(
|
.magic_link(&MagicLinkParams {
|
||||||
&self.access_token()?,
|
email: email.to_owned(),
|
||||||
&MagicLinkParams {
|
..Default::default()
|
||||||
email: email.to_owned(),
|
})
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn create_magic_link(
|
pub async fn create_user(&self, email: &str, password: &str) -> Result<User, AppResponseError> {
|
||||||
&self,
|
|
||||||
email: &str,
|
|
||||||
password: &str,
|
|
||||||
) -> Result<User, AppResponseError> {
|
|
||||||
Ok(
|
Ok(
|
||||||
self
|
self
|
||||||
.gotrue_client
|
.gotrue_client
|
||||||
|
|
|
||||||
|
|
@ -224,14 +224,11 @@ impl Client {
|
||||||
to_gotrue_result(resp).await
|
to_gotrue_result(resp).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn magic_link(
|
pub async fn magic_link(&self, magic_link_params: &MagicLinkParams) -> Result<(), GoTrueError> {
|
||||||
&self,
|
|
||||||
access_token: &str,
|
|
||||||
magic_link_params: &MagicLinkParams,
|
|
||||||
) -> Result<(), GoTrueError> {
|
|
||||||
let url = format!("{}/magiclink", self.base_url);
|
let url = format!("{}/magiclink", self.base_url);
|
||||||
let resp = self
|
let resp = self
|
||||||
.http_client_with_auth(Method::POST, &url, access_token)
|
.client
|
||||||
|
.request(Method::POST, &url)
|
||||||
.json(&magic_link_params)
|
.json(&magic_link_params)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue