generated from t/malachite
add: login
This commit is contained in:
parent
ce9ce4f635
commit
8c86dd6cda
13 changed files with 407 additions and 25 deletions
191
src/routes/api/auth.rs
Normal file
191
src/routes/api/auth.rs
Normal file
|
@ -0,0 +1,191 @@
|
|||
use crate::{State, get_user_from_token};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
extract::{Path, Query},
|
||||
http::{HeaderMap, HeaderValue},
|
||||
response::{IntoResponse, Redirect},
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use serde::Deserialize;
|
||||
use tetratto_core::model::{ApiReturn, Error, addr::RemoteAddr, auth::User};
|
||||
use tetratto_shared::hash::hash;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginProps {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub totp: String,
|
||||
}
|
||||
|
||||
/// `/api/v1/auth/login`
|
||||
pub async fn login_request(
|
||||
headers: HeaderMap,
|
||||
// jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Json(props): Json<LoginProps>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
// let user = get_user_from_token!(jar, data);
|
||||
|
||||
// if user.is_some() {
|
||||
// return (None, Json(Error::AlreadyAuthenticated.into()));
|
||||
// }
|
||||
|
||||
// get real ip
|
||||
let real_ip = headers
|
||||
.get(data.2.0.0.security.real_ip_header.to_owned())
|
||||
.unwrap_or(&HeaderValue::from_static(""))
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
// check for ip ban
|
||||
if data
|
||||
.2
|
||||
.get_ipban_by_addr(&RemoteAddr::from(real_ip.as_str()))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return (None, Json(Error::NotAllowed.into()));
|
||||
}
|
||||
|
||||
// verify password
|
||||
let user = match data
|
||||
.2
|
||||
.get_user_by_username_no_cache(&props.username.to_lowercase())
|
||||
.await
|
||||
{
|
||||
Ok(ua) => ua,
|
||||
Err(_) => return (None, Json(Error::IncorrectPassword.into())),
|
||||
};
|
||||
|
||||
if !user.check_password(props.password) {
|
||||
return (None, Json(Error::IncorrectPassword.into()));
|
||||
}
|
||||
|
||||
// verify totp code
|
||||
if !data.2.check_totp(&user, &props.totp) {
|
||||
return (None, Json(Error::NotAllowed.into()));
|
||||
}
|
||||
|
||||
// update tokens
|
||||
let mut new_tokens = user.tokens.clone();
|
||||
let (unhashed_token_id, token) = User::create_token(&real_ip);
|
||||
new_tokens.push(token);
|
||||
|
||||
if let Err(e) = data.2.update_user_tokens(user.id, new_tokens).await {
|
||||
return (None, Json(e.into()));
|
||||
}
|
||||
|
||||
// ...
|
||||
(
|
||||
Some([(
|
||||
"Set-Cookie",
|
||||
format!(
|
||||
"__Secure-atto-token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
|
||||
unhashed_token_id,
|
||||
60 * 60 * 24 * 365
|
||||
),
|
||||
)]),
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: unhashed_token_id,
|
||||
payload: (),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// `/api/v1/auth/logout`
|
||||
pub async fn logout_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data.2) {
|
||||
Some(ua) => ua,
|
||||
None => return (None, Json(Error::NotAllowed.into())),
|
||||
};
|
||||
|
||||
// update tokens
|
||||
let token = jar
|
||||
.get("__Secure-atto-token")
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.replace("__Secure-atto-token=", "");
|
||||
|
||||
let mut new_tokens = user.tokens.clone();
|
||||
new_tokens.remove(
|
||||
new_tokens
|
||||
.iter()
|
||||
.position(|t| t.1 == hash(token.to_string()))
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
if let Err(e) = data.2.update_user_tokens(user.id, new_tokens).await {
|
||||
return (None, Json(e.into()));
|
||||
}
|
||||
|
||||
// ...
|
||||
(
|
||||
Some([(
|
||||
"Set-Cookie",
|
||||
format!(
|
||||
"__Secure-atto-token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0",
|
||||
"refresh",
|
||||
),
|
||||
)]),
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Goodbye!".to_string(),
|
||||
payload: Some(user.username.clone()),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetTokenQuery {
|
||||
#[serde(default)]
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Set the current user token.
|
||||
pub async fn set_token_request(Query(props): Query<SetTokenQuery>) -> impl IntoResponse {
|
||||
(
|
||||
{
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
headers.insert(
|
||||
"Set-Cookie",
|
||||
format!(
|
||||
"__Secure-atto-token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
|
||||
props.token,
|
||||
60* 60 * 24 * 365
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
headers
|
||||
},
|
||||
Redirect::to("/"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if the given user has TOTP enabled.
|
||||
pub async fn check_totp_request(
|
||||
Path(username): Path<String>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match data.2.get_user_by_username(&username).await {
|
||||
Ok(u) => u,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Success".to_string(),
|
||||
payload: Some(!user.totp.is_empty()),
|
||||
})
|
||||
}
|
|
@ -1,13 +1,24 @@
|
|||
pub mod auth;
|
||||
pub mod chats;
|
||||
pub mod messages;
|
||||
|
||||
use axum::routing::{Router, delete, post, put};
|
||||
use axum::routing::{Router, delete, get, post, put};
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
// auth
|
||||
.route("/auth/login", post(auth::login_request))
|
||||
.route("/auth/logout", post(auth::logout_request))
|
||||
.route("/auth/set_token", get(auth::set_token_request))
|
||||
.route(
|
||||
"/auth/user/{username}/check_totp",
|
||||
get(auth::check_totp_request),
|
||||
)
|
||||
// chats
|
||||
.route("/chats", post(chats::create_request))
|
||||
.route("/chats/{id}/leave", post(chats::leave_request))
|
||||
.route("/chats/{id}/info", post(chats::update_info_request))
|
||||
// messages
|
||||
.route("/messages", post(messages::create_request))
|
||||
.route("/messages/{id}", delete(messages::delete_request))
|
||||
.route("/messages/{id}", put(messages::update_content_request))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::config::Config;
|
||||
use axum::{Router, routing::get_service};
|
||||
use tera::Context;
|
||||
use tetratto_core::model::auth::User;
|
||||
|
||||
pub mod api;
|
||||
pub mod pages;
|
||||
|
@ -16,10 +17,12 @@ pub fn routes() -> Router {
|
|||
.nest("/api/v1", api::routes())
|
||||
}
|
||||
|
||||
pub fn default_context(config: &Config, build_code: &str) -> Context {
|
||||
pub fn default_context(config: &Config, build_code: &str, user: &Option<User>) -> Context {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert("name", &config.name);
|
||||
ctx.insert("theme_color", &config.theme_color);
|
||||
ctx.insert("build_code", &build_code);
|
||||
ctx.insert("user", &user);
|
||||
ctx.insert("config", &config);
|
||||
ctx
|
||||
}
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
use crate::{State, routes::default_context};
|
||||
use crate::{State, get_user_from_token, routes::default_context};
|
||||
use axum::{
|
||||
Extension,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::Error;
|
||||
|
||||
pub async fn not_found_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
pub async fn not_found_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||
let user = get_user_from_token!(jar, data.2);
|
||||
|
||||
let mut ctx = default_context(&data.0.0, &build_code);
|
||||
let mut ctx = default_context(&data.0.0, &build_code, &user);
|
||||
ctx.insert(
|
||||
"error",
|
||||
&Error::GeneralNotFound("page".to_string()).to_string(),
|
||||
|
@ -16,10 +21,28 @@ pub async fn not_found_request(Extension(data): Extension<State>) -> impl IntoRe
|
|||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
|
||||
pub async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
pub async fn index_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||
let user = get_user_from_token!(jar, data.2);
|
||||
|
||||
Html(
|
||||
tera.render("index.lisp", &default_context(&data.0.0, &build_code))
|
||||
.unwrap(),
|
||||
tera.render(
|
||||
"index.lisp",
|
||||
&default_context(&data.0.0, &build_code, &user),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||
let user = get_user_from_token!(jar, data.2);
|
||||
|
||||
Html(
|
||||
tera.render(
|
||||
"login.lisp",
|
||||
&default_context(&data.0.0, &build_code, &user),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,5 +3,7 @@ pub mod misc;
|
|||
use axum::routing::{Router, get};
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new().route("/", get(misc::index_request))
|
||||
Router::new()
|
||||
.route("/", get(misc::index_request))
|
||||
.route("/login", get(misc::login_request))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue