261 lines
7.7 KiB
Rust
261 lines
7.7 KiB
Rust
use std::{collections::HashMap, str::FromStr};
|
|
|
|
use axum::{
|
|
body::Full,
|
|
extract::Path,
|
|
handler::Handler,
|
|
http::{header, HeaderValue, StatusCode},
|
|
response::{self, Html, IntoResponse, Response},
|
|
routing, Extension, Form, Json, Router,
|
|
};
|
|
use mime_guess::mime;
|
|
use tower_cookies::{Cookie, Cookies};
|
|
|
|
use crate::svc::{
|
|
auth::{AuthError, Claims},
|
|
profiles,
|
|
};
|
|
|
|
use super::{
|
|
servek::{Server, ServerError},
|
|
CreateProfileRequest, LoginRequest, NavType, Notification, Redirect, WithNav,
|
|
};
|
|
use rust_embed::RustEmbed;
|
|
|
|
const AUTH_COOKIE_NAME: &str = "flabk_token";
|
|
|
|
#[derive(RustEmbed)]
|
|
#[folder = "static"]
|
|
struct StaticData;
|
|
|
|
impl Server {
|
|
pub(super) fn register_html(&self, router: &Router) -> Router {
|
|
router
|
|
.clone()
|
|
.route("/favicon.svg", routing::get(Self::favicon))
|
|
.route("/", routing::get(Self::index))
|
|
.route("/login", routing::get(Self::login_page).post(Self::login))
|
|
.route("/logout", routing::get(Self::logout))
|
|
.route(
|
|
"/signup",
|
|
routing::get(Self::signup_page).post(Self::create_user),
|
|
)
|
|
.route("/@/:username", routing::get(Self::profile))
|
|
.route("/static/*file", routing::get(Self::static_handler))
|
|
.fallback(routing::get(Self::handler_404))
|
|
}
|
|
|
|
fn from_cookie(&self, cookie: Option<Cookie>) -> Result<WithNav<Option<Claims>>, ServerError> {
|
|
if let Some(cookie) = cookie {
|
|
let claims = self.auth.get_claims(cookie.value().to_owned())?;
|
|
Ok(WithNav::new(Some(claims.clone()), claims.into()))
|
|
} else {
|
|
Ok(WithNav {
|
|
nav_type: NavType::LoggedOut,
|
|
obj: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn index(
|
|
Extension(srv): Extension<Server>,
|
|
cookies: Cookies,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
let user = srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?;
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(srv.hb.render("index", &user)?),
|
|
))
|
|
}
|
|
|
|
async fn favicon() -> impl IntoResponse {
|
|
(
|
|
StatusCode::OK,
|
|
(
|
|
[(
|
|
header::CONTENT_TYPE,
|
|
HeaderValue::from_static(mime::IMAGE_SVG.as_ref()),
|
|
)],
|
|
axum::body::boxed(Full::from(
|
|
include_bytes!("../../flabk_icon_placeholder.svg").as_ref(),
|
|
)),
|
|
),
|
|
)
|
|
}
|
|
|
|
async fn handler_404(
|
|
Extension(srv): Extension<Server>,
|
|
cookies: Cookies,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(
|
|
srv.hb
|
|
.render("404", &srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?)?,
|
|
),
|
|
))
|
|
}
|
|
|
|
async fn logout(
|
|
Extension(srv): Extension<Server>,
|
|
cookies: Cookies,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(srv.hb.render(
|
|
"redirect",
|
|
&Redirect {
|
|
location: "/".to_owned(),
|
|
},
|
|
)?),
|
|
))
|
|
}
|
|
|
|
fn login_page_with(
|
|
&self,
|
|
tag_name: String,
|
|
message: String,
|
|
) -> Result<response::Html<String>, ServerError> {
|
|
Ok(self
|
|
.hb
|
|
.render(
|
|
"login",
|
|
&serde_json::json!(Notification { message, tag_name }),
|
|
)
|
|
.map(|html| response::Html(html))?)
|
|
}
|
|
|
|
async fn login_page(
|
|
Extension(srv): Extension<Server>,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(srv.hb.render("login", &serde_json::json!(()))?),
|
|
))
|
|
}
|
|
|
|
async fn login(
|
|
cookies: Cookies,
|
|
Extension(srv): Extension<Server>,
|
|
Form(login): Form<LoginRequest>,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
if login.username == "" || login.password == "" {
|
|
return Ok((
|
|
StatusCode::BAD_REQUEST,
|
|
srv.login_page_with(
|
|
"error-partial".to_owned(),
|
|
"credentials required".to_owned(),
|
|
)?,
|
|
));
|
|
}
|
|
let token = srv.auth.login(login.username, login.password).await?;
|
|
|
|
if cookies.get(AUTH_COOKIE_NAME).is_some() {
|
|
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
|
}
|
|
cookies.add(Cookie::new(AUTH_COOKIE_NAME, token));
|
|
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(srv.hb.render(
|
|
"redirect",
|
|
&Redirect {
|
|
location: "/".to_owned(),
|
|
},
|
|
)?),
|
|
))
|
|
}
|
|
|
|
async fn profile(
|
|
cookies: Cookies,
|
|
Extension(srv): Extension<Server>,
|
|
Path(username): Path<String>,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(srv.hb.render(
|
|
"profile",
|
|
&WithNav::new(
|
|
srv.profiler.profile(username).await?,
|
|
srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?.nav_type,
|
|
),
|
|
)?),
|
|
))
|
|
}
|
|
|
|
async fn create_user(
|
|
Extension(srv): Extension<Server>,
|
|
Form(body): Form<CreateProfileRequest>,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
if body.username == "" {
|
|
return srv.signup_page_with("error-partial".to_owned(), "empty username".to_owned());
|
|
}
|
|
if body.password == "" {
|
|
return srv.signup_page_with("error-partial".to_owned(), "empty password".to_owned());
|
|
}
|
|
if body.email == "" {
|
|
return srv.signup_page_with("error-partial".to_owned(), "empty email".to_owned());
|
|
}
|
|
srv.profiler
|
|
.create_user(body.username, body.password, body.email)
|
|
.await?;
|
|
srv.signup_page_with("success-partial".to_owned(), "signup successful".to_owned())
|
|
}
|
|
|
|
async fn signup_page(
|
|
Extension(srv): Extension<Server>,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(srv.hb.render("signup", &serde_json::json!(()))?),
|
|
))
|
|
}
|
|
|
|
fn signup_page_with(
|
|
&self,
|
|
tag_name: String,
|
|
message: String,
|
|
) -> Result<impl IntoResponse, ServerError> {
|
|
Ok((
|
|
StatusCode::OK,
|
|
response::Html(self.hb.render(
|
|
"signup",
|
|
&serde_json::json!(Notification { message, tag_name }),
|
|
)?),
|
|
))
|
|
}
|
|
|
|
async fn static_handler(Path(mut path): Path<String>) -> impl IntoResponse {
|
|
path.remove(0);
|
|
println!("getting path: {}", path);
|
|
StaticFile(path)
|
|
}
|
|
}
|
|
|
|
pub struct StaticFile<T>(pub T);
|
|
|
|
impl<T> IntoResponse for StaticFile<T>
|
|
where
|
|
T: Into<String>,
|
|
{
|
|
fn into_response(self) -> axum::response::Response {
|
|
let path = self.0.into();
|
|
|
|
match StaticData::get(path.as_str()) {
|
|
Some(content) => {
|
|
let body = axum::body::boxed(Full::from(content.data));
|
|
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
Response::builder()
|
|
.header(header::CONTENT_TYPE, mime.as_ref())
|
|
.body(body)
|
|
.unwrap()
|
|
}
|
|
None => Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body(axum::body::boxed(Full::from("404")))
|
|
.unwrap(),
|
|
}
|
|
}
|
|
}
|