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