add: postgres support

chore: restructure
This commit is contained in:
trisua 2025-03-22 22:17:47 -04:00
parent cda879f6df
commit b6fe2fba37
58 changed files with 3403 additions and 603 deletions

View file

@ -0,0 +1 @@
pub mod v1;

View 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)),
)
}

View 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: (),
}),
)
}

View 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,
}

View 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,
)
}

View 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())
}

View 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(),
))
}

View 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())
}

View 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))
}