add: postgres support
chore: restructure
This commit is contained in:
parent
cda879f6df
commit
b6fe2fba37
58 changed files with 3403 additions and 603 deletions
1
crates/app/src/routes/api/mod.rs
Normal file
1
crates/app/src/routes/api/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod v1;
|
102
crates/app/src/routes/api/v1/auth/images.rs
Normal file
102
crates/app/src/routes/api/v1/auth/images.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use axum::{Extension, body::Body, extract::Path, response::IntoResponse};
|
||||
use pathbufd::PathBufD;
|
||||
use std::{
|
||||
fs::{File, exists},
|
||||
io::Read,
|
||||
};
|
||||
|
||||
use crate::State;
|
||||
|
||||
pub fn read_image(path: PathBufD) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
for byte in File::open(path).unwrap().bytes() {
|
||||
bytes.push(byte.unwrap())
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Get a profile's avatar image
|
||||
/// `/api/v1/auth/profile/{id}/avatar`
|
||||
pub async fn avatar_request(
|
||||
Path(username): Path<String>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
|
||||
let user = match data.get_user_by_username(&username).await {
|
||||
Ok(ua) => ua,
|
||||
Err(_) => {
|
||||
return (
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-avatar.svg",
|
||||
]))),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let path =
|
||||
PathBufD::current().extend(&["avatars", &data.0.dirs.media, &format!("{}.avif", &user.id)]);
|
||||
|
||||
if !exists(&path).unwrap() {
|
||||
return (
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-avatar.svg",
|
||||
]))),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
[("Content-Type", "image/avif")],
|
||||
Body::from(read_image(path)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get a profile's banner image
|
||||
/// `/api/v1/auth/profile/{id}/banner`
|
||||
pub async fn banner_request(
|
||||
Path(username): Path<String>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
|
||||
let user = match data.get_user_by_username(&username).await {
|
||||
Ok(ua) => ua,
|
||||
Err(_) => {
|
||||
return (
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-banner.svg",
|
||||
]))),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let path =
|
||||
PathBufD::current().extend(&["avatars", &data.0.dirs.media, &format!("{}.avif", &user.id)]);
|
||||
|
||||
if !exists(&path).unwrap() {
|
||||
return (
|
||||
[("Content-Type", "image/svg+xml")],
|
||||
Body::from(read_image(PathBufD::current().extend(&[
|
||||
data.0.dirs.media.as_str(),
|
||||
"images",
|
||||
"default-banner.svg",
|
||||
]))),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
[("Content-Type", "image/avif")],
|
||||
Body::from(read_image(path)),
|
||||
)
|
||||
}
|
127
crates/app/src/routes/api/v1/auth/mod.rs
Normal file
127
crates/app/src/routes/api/v1/auth/mod.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
pub mod images;
|
||||
|
||||
use super::AuthProps;
|
||||
use crate::{
|
||||
State, get_user_from_token,
|
||||
model::{ApiReturn, Error, auth::User},
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
http::{HeaderMap, HeaderValue},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
|
||||
/// `/api/v1/auth/register`
|
||||
pub async fn register_request(
|
||||
headers: HeaderMap,
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Json(props): Json<AuthProps>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = get_user_from_token!((jar, data) <optional>);
|
||||
|
||||
if user.is_some() {
|
||||
return (
|
||||
None,
|
||||
Json(ApiReturn {
|
||||
ok: false,
|
||||
message: Error::AlreadyAuthenticated.to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// get real ip
|
||||
let real_ip = headers
|
||||
.get(data.0.security.real_ip_header.to_owned())
|
||||
.unwrap_or(&HeaderValue::from_static(""))
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
// ...
|
||||
let mut user = User::new(props.username, props.password);
|
||||
let (initial_token, t) = User::create_token(&real_ip);
|
||||
user.tokens.push(t);
|
||||
|
||||
// return
|
||||
match data.create_user(user).await {
|
||||
Ok(_) => (
|
||||
Some([(
|
||||
"Set-Cookie",
|
||||
format!(
|
||||
"__Secure-atto-token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
|
||||
initial_token,
|
||||
60 * 60 * 24 * 365
|
||||
),
|
||||
)]),
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "User created".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
),
|
||||
Err(e) => (None, Json(e.into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// `/api/v1/auth/login`
|
||||
pub async fn login_request(
|
||||
headers: HeaderMap,
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Json(props): Json<AuthProps>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = get_user_from_token!((jar, data) <optional>);
|
||||
|
||||
if user.is_some() {
|
||||
return (None, Json(Error::AlreadyAuthenticated.into()));
|
||||
}
|
||||
|
||||
// get real ip
|
||||
let real_ip = headers
|
||||
.get(data.0.security.real_ip_header.to_owned())
|
||||
.unwrap_or(&HeaderValue::from_static(""))
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
// verify password
|
||||
let user = match data.get_user_by_username(&props.username).await {
|
||||
Ok(ua) => ua,
|
||||
Err(_) => return (None, Json(Error::IncorrectPassword.into())),
|
||||
};
|
||||
|
||||
if !user.check_password(props.password) {
|
||||
return (None, Json(Error::IncorrectPassword.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.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: (),
|
||||
}),
|
||||
)
|
||||
}
|
28
crates/app/src/routes/api/v1/mod.rs
Normal file
28
crates/app/src/routes/api/v1/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
pub mod auth;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
// global
|
||||
.route("/auth/register", post(auth::register_request))
|
||||
.route("/auth/login", post(auth::login_request))
|
||||
// profile
|
||||
.route(
|
||||
"/auth/profile/{id}/avatar",
|
||||
get(auth::images::avatar_request),
|
||||
)
|
||||
.route(
|
||||
"/auth/profile/{id}/banner",
|
||||
get(auth::images::banner_request),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AuthProps {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
22
crates/app/src/routes/assets.rs
Normal file
22
crates/app/src/routes/assets.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use axum::response::IntoResponse;
|
||||
|
||||
/// `/css/style.css`
|
||||
pub async fn style_css_request() -> impl IntoResponse {
|
||||
([("Content-Type", "text/css")], crate::assets::STYLE_CSS)
|
||||
}
|
||||
|
||||
/// `/js/atto.js`
|
||||
pub async fn atto_js_request() -> impl IntoResponse {
|
||||
(
|
||||
[("Content-Type", "text/javascript")],
|
||||
crate::assets::ATTO_JS,
|
||||
)
|
||||
}
|
||||
|
||||
/// `/js/atto.js`
|
||||
pub async fn loader_js_request() -> impl IntoResponse {
|
||||
(
|
||||
[("Content-Type", "text/javascript")],
|
||||
crate::assets::LOADER_JS,
|
||||
)
|
||||
}
|
25
crates/app/src/routes/mod.rs
Normal file
25
crates/app/src/routes/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
pub mod api;
|
||||
pub mod assets;
|
||||
pub mod pages;
|
||||
|
||||
use crate::config::Config;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, get_service},
|
||||
};
|
||||
|
||||
pub fn routes(config: &Config) -> Router {
|
||||
Router::new()
|
||||
// assets
|
||||
.route("/css/style.css", get(assets::style_css_request))
|
||||
.route("/js/atto.js", get(assets::atto_js_request))
|
||||
.route("/js/loader.js", get(assets::loader_js_request))
|
||||
.nest_service(
|
||||
"/static",
|
||||
get_service(tower_http::services::ServeDir::new(&config.dirs.assets)),
|
||||
)
|
||||
// api
|
||||
.nest("/api/v1", api::v1::routes())
|
||||
// pages
|
||||
.merge(pages::routes())
|
||||
}
|
39
crates/app/src/routes/pages/auth.rs
Normal file
39
crates/app/src/routes/pages/auth.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use crate::{State, assets::initial_context, get_user_from_token};
|
||||
use axum::{
|
||||
Extension,
|
||||
response::{Html, IntoResponse, Redirect},
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
|
||||
/// `/auth/login`
|
||||
pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = get_user_from_token!((jar, data.0) <optional>);
|
||||
|
||||
if user.is_some() {
|
||||
return Err(Redirect::to("/"));
|
||||
}
|
||||
|
||||
let mut context = initial_context(&data.0.0, &user);
|
||||
Ok(Html(
|
||||
data.1.render("auth/login.html", &mut context).unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
/// `/auth/register`
|
||||
pub async fn register_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = get_user_from_token!((jar, data.0) <optional>);
|
||||
|
||||
if user.is_some() {
|
||||
return Err(Redirect::to("/"));
|
||||
}
|
||||
|
||||
let mut context = initial_context(&data.0.0, &user);
|
||||
Ok(Html(
|
||||
data.1.render("auth/register.html", &mut context).unwrap(),
|
||||
))
|
||||
}
|
15
crates/app/src/routes/pages/misc.rs
Normal file
15
crates/app/src/routes/pages/misc.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use crate::{State, assets::initial_context, get_user_from_token};
|
||||
use axum::{
|
||||
Extension,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
|
||||
/// `/`
|
||||
pub async fn index_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = get_user_from_token!((jar, data.0) <optional>);
|
||||
|
||||
let mut context = initial_context(&data.0.0, &user);
|
||||
Html(data.1.render("misc/index.html", &mut context).unwrap())
|
||||
}
|
13
crates/app/src/routes/pages/mod.rs
Normal file
13
crates/app/src/routes/pages/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
pub mod auth;
|
||||
pub mod misc;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
// misc
|
||||
.route("/", get(misc::index_request))
|
||||
// auth
|
||||
.route("/auth/register", get(auth::register_request))
|
||||
.route("/auth/login", get(auth::login_request))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue