Add cookie based auth, file serving and uploading
This commit is contained in:
78
Cargo.lock
generated
78
Cargo.lock
generated
@@ -430,6 +430,7 @@ dependencies = [
|
|||||||
"matchit",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
|
"multer",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
@@ -468,22 +469,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-extra"
|
name = "axum-extra"
|
||||||
version = "0.9.3"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733"
|
checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
"cookie",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"headers",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
|
"multer",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"serde",
|
"serde",
|
||||||
"tower 0.4.13",
|
"tower 0.5.1",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -500,6 +503,21 @@ dependencies = [
|
|||||||
"syn 2.0.72",
|
"syn 2.0.72",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-range"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1c30398a7f716ebdd7f3c8a4f7a7a6df48a30e002007fd57b2a7a00fac864bd"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
|
"bytes",
|
||||||
|
"futures",
|
||||||
|
"http-body",
|
||||||
|
"pin-project",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-test"
|
name = "axum-test"
|
||||||
version = "16.2.0"
|
version = "16.2.0"
|
||||||
@@ -1269,6 +1287,15 @@ version = "0.3.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "english-to-cron"
|
name = "english-to-cron"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1632,6 +1659,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
|
"axum-range",
|
||||||
"chrono",
|
"chrono",
|
||||||
"fluent-templates",
|
"fluent-templates",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
@@ -1644,6 +1673,8 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower 0.5.1",
|
||||||
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unic-langid",
|
"unic-langid",
|
||||||
@@ -1760,6 +1791,30 @@ dependencies = [
|
|||||||
"hashbrown 0.14.5",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bytes",
|
||||||
|
"headers-core",
|
||||||
|
"http 1.1.0",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
|
dependencies = [
|
||||||
|
"http 1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2479,6 +2534,23 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multer"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.1.0",
|
||||||
|
"httparse",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"spin",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
@@ -20,6 +20,8 @@ migration = { path = "migration" }
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1.33.0", default-features = false }
|
tokio = { version = "1.33.0", default-features = false }
|
||||||
|
tower = { version = "0.5.1", features = ["util"] }
|
||||||
|
tower-http = { version = "0.6.1", features = ["fs", "trace"] }
|
||||||
async-trait = "0.1.74"
|
async-trait = "0.1.74"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
@@ -31,7 +33,7 @@ sea-orm = { version = "1.1.0", features = [
|
|||||||
"macros",
|
"macros",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
axum = "0.7.5"
|
axum = { version = "0.7.5", features = ["multipart"] }
|
||||||
include_dir = "0.7"
|
include_dir = "0.7"
|
||||||
uuid = { version = "1.6.0", features = ["v4"] }
|
uuid = { version = "1.6.0", features = ["v4"] }
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
|
||||||
@@ -39,6 +41,8 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
|
|||||||
# view engine i18n
|
# view engine i18n
|
||||||
fluent-templates = { version = "0.8.0", features = ["tera"] }
|
fluent-templates = { version = "0.8.0", features = ["tera"] }
|
||||||
unic-langid = "0.9.4"
|
unic-langid = "0.9.4"
|
||||||
|
axum-range = "0.4.0"
|
||||||
|
axum-extra = { version = "0.9.4", features = ["multipart", "typed-header", "cookie"] }
|
||||||
# /view engine
|
# /view engine
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
16
assets/views/website/login.html
Normal file
16
assets/views/website/login.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "website/base.html" %} {% block content %}
|
||||||
|
<div class="w-full mt-16"></div>
|
||||||
|
<form
|
||||||
|
class="text-black"
|
||||||
|
method="post"
|
||||||
|
enctype="application/x-www-form-urlencoded"
|
||||||
|
action="/api/auth/login"
|
||||||
|
>
|
||||||
|
<label class="text-white" for="email">Email:</label>
|
||||||
|
<input type="text" id="email" name="email" required />
|
||||||
|
<label class="text-white" for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required />
|
||||||
|
<button class="text-white" type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content%}
|
@@ -136,3 +136,7 @@ auth:
|
|||||||
secret: PqRwLF2rhHe8J22oBeHy
|
secret: PqRwLF2rhHe8J22oBeHy
|
||||||
# Token expiration time in seconds
|
# Token expiration time in seconds
|
||||||
expiration: 604800 # 7 days
|
expiration: 604800 # 7 days
|
||||||
|
location:
|
||||||
|
from: Cookie
|
||||||
|
name: token
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ mod m20241029_235230_skills;
|
|||||||
mod m20241030_002154_jobs;
|
mod m20241030_002154_jobs;
|
||||||
mod m20241030_024340_projects;
|
mod m20241030_024340_projects;
|
||||||
mod m20241030_024830_data;
|
mod m20241030_024830_data;
|
||||||
|
mod m20241102_204505_add_file_name_to_data;
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -15,6 +16,7 @@ impl MigratorTrait for Migrator {
|
|||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
vec![
|
vec![
|
||||||
// inject-below
|
// inject-below
|
||||||
|
Box::new(m20241102_204505_add_file_name_to_data::Migration),
|
||||||
Box::new(m20241030_024830_data::Migration),
|
Box::new(m20241030_024830_data::Migration),
|
||||||
Box::new(m20241030_024340_projects::Migration),
|
Box::new(m20241030_024340_projects::Migration),
|
||||||
Box::new(m20241030_002154_jobs::Migration),
|
Box::new(m20241030_002154_jobs::Migration),
|
||||||
|
35
migration/src/m20241102_204505_add_file_name_to_data.rs
Normal file
35
migration/src/m20241102_204505_add_file_name_to_data.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Data {
|
||||||
|
Table,
|
||||||
|
FileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Data::Table)
|
||||||
|
.add_column_if_not_exists(string(Data::FileName))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Data::Table)
|
||||||
|
.drop_column(Data::FileName)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
12
src/app.rs
12
src/app.rs
@@ -8,6 +8,7 @@ use loco_rs::{
|
|||||||
controller::AppRoutes,
|
controller::AppRoutes,
|
||||||
db::{self, truncate_table},
|
db::{self, truncate_table},
|
||||||
environment::Environment,
|
environment::Environment,
|
||||||
|
storage::{self, Storage},
|
||||||
task::Tasks,
|
task::Tasks,
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
@@ -47,6 +48,7 @@ impl Hooks for App {
|
|||||||
|
|
||||||
fn routes(_ctx: &AppContext) -> AppRoutes {
|
fn routes(_ctx: &AppContext) -> AppRoutes {
|
||||||
AppRoutes::with_default_routes() // controller routes below
|
AppRoutes::with_default_routes() // controller routes below
|
||||||
|
.add_route(controllers::data::routes())
|
||||||
.add_route(controllers::auth::routes())
|
.add_route(controllers::auth::routes())
|
||||||
.add_route(controllers::website::routes())
|
.add_route(controllers::website::routes())
|
||||||
}
|
}
|
||||||
@@ -56,8 +58,18 @@ impl Hooks for App {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn after_context(ctx: AppContext) -> Result<AppContext> {
|
||||||
|
let store = storage::drivers::local::new_with_prefix("uploads").map_err(Box::from)?;
|
||||||
|
|
||||||
|
Ok(AppContext {
|
||||||
|
storage: Storage::single(store).into(),
|
||||||
|
..ctx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn register_tasks(tasks: &mut Tasks) {
|
fn register_tasks(tasks: &mut Tasks) {
|
||||||
tasks.register(tasks::seed::SeedData);
|
tasks.register(tasks::seed::SeedData);
|
||||||
|
tasks.register(tasks::create_user::CreateUserData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn truncate(db: &DatabaseConnection) -> Result<()> {
|
async fn truncate(db: &DatabaseConnection) -> Result<()> {
|
||||||
|
@@ -1,14 +1,16 @@
|
|||||||
use axum::debug_handler;
|
use axum::debug_handler;
|
||||||
|
use axum::http::header;
|
||||||
|
use axum::http::HeaderValue;
|
||||||
|
use axum::response::Redirect;
|
||||||
|
use cookie::Cookie;
|
||||||
|
use cookie::CookieJar;
|
||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
mailers::auth::AuthMailer,
|
mailers::auth::AuthMailer,
|
||||||
models::{
|
models::{_entities::users, users::LoginParams},
|
||||||
_entities::users,
|
views::auth::CurrentResponse,
|
||||||
users::{LoginParams, RegisterParams},
|
|
||||||
},
|
|
||||||
views::auth::{CurrentResponse, LoginResponse},
|
|
||||||
};
|
};
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct VerifyParams {
|
pub struct VerifyParams {
|
||||||
@@ -26,37 +28,6 @@ pub struct ResetParams {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register function creates a new user with the given parameters and sends a
|
|
||||||
/// welcome email to the user
|
|
||||||
#[debug_handler]
|
|
||||||
async fn register(
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
Json(params): Json<RegisterParams>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let res = users::Model::create_with_password(&ctx.db, ¶ms).await;
|
|
||||||
|
|
||||||
let user = match res {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::info!(
|
|
||||||
message = err.to_string(),
|
|
||||||
user_email = ¶ms.email,
|
|
||||||
"could not register user",
|
|
||||||
);
|
|
||||||
return format::json(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = user
|
|
||||||
.into_active_model()
|
|
||||||
.set_email_verification_sent(&ctx.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
AuthMailer::send_welcome(&ctx, &user).await?;
|
|
||||||
|
|
||||||
format::json(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify register user. if the user not verified his email, he can't login to
|
/// Verify register user. if the user not verified his email, he can't login to
|
||||||
/// the system.
|
/// the system.
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
@@ -121,7 +92,11 @@ async fn reset(State(ctx): State<AppContext>, Json(params): Json<ResetParams>) -
|
|||||||
|
|
||||||
/// Creates a user login and returns a token
|
/// Creates a user login and returns a token
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -> Result<Response> {
|
async fn login(
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
jar: CookieJar,
|
||||||
|
Form(params): Form<LoginParams>,
|
||||||
|
) -> Result<Response> {
|
||||||
let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?;
|
let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?;
|
||||||
|
|
||||||
let valid = user.verify_password(¶ms.password);
|
let valid = user.verify_password(¶ms.password);
|
||||||
@@ -136,7 +111,32 @@ async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -
|
|||||||
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
|
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
|
||||||
.or_else(|_| unauthorized("unauthorized!"))?;
|
.or_else(|_| unauthorized("unauthorized!"))?;
|
||||||
|
|
||||||
format::json(LoginResponse::new(&user, &token))
|
let cookie = Cookie::build(("token", token.clone()))
|
||||||
|
.path("/")
|
||||||
|
.http_only(true)
|
||||||
|
.secure(true)
|
||||||
|
.same_site(cookie::SameSite::Lax);
|
||||||
|
let cookie = jar.add(cookie);
|
||||||
|
|
||||||
|
let cookie = cookie.get("token").map(|c| c.value().to_owned());
|
||||||
|
|
||||||
|
let mut response = Redirect::to("/").into_response();
|
||||||
|
match cookie {
|
||||||
|
Some(token) => {
|
||||||
|
let cookie = format!("token={}; Path=/; HttpOnly; Secure; SameSite=Lax", token);
|
||||||
|
let authorization_header = format!("Bearer {}", token);
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.append(header::SET_COOKIE, HeaderValue::from_str(&cookie).unwrap());
|
||||||
|
response.headers_mut().append(
|
||||||
|
header::AUTHORIZATION,
|
||||||
|
HeaderValue::from_str(&authorization_header).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
@@ -145,13 +145,24 @@ async fn current(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Respo
|
|||||||
format::json(CurrentResponse::new(&user))
|
format::json(CurrentResponse::new(&user))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn logout() -> impl IntoResponse {
|
||||||
|
let cookie = "token=''; Path=/; HttpOnly; Secure; SameSite=Lax";
|
||||||
|
let mut response = Redirect::to("/").into_response();
|
||||||
|
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.append(header::SET_COOKIE, HeaderValue::from_str(&cookie).unwrap());
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
Routes::new()
|
Routes::new()
|
||||||
.prefix("/api/auth")
|
.prefix("/api/auth")
|
||||||
.add("/register", post(register))
|
|
||||||
.add("/verify", post(verify))
|
.add("/verify", post(verify))
|
||||||
.add("/login", post(login))
|
.add("/login", post(login))
|
||||||
.add("/forgot", post(forgot))
|
.add("/forgot", post(forgot))
|
||||||
.add("/reset", post(reset))
|
.add("/reset", post(reset))
|
||||||
.add("/current", get(current))
|
.add("/current", get(current))
|
||||||
|
.add("/logout", get(logout))
|
||||||
}
|
}
|
||||||
|
39
src/controllers/data.rs
Normal file
39
src/controllers/data.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#![allow(clippy::missing_errors_doc)]
|
||||||
|
#![allow(clippy::unnecessary_struct_initialization)]
|
||||||
|
#![allow(clippy::unused_async)]
|
||||||
|
use axum::extract::Multipart;
|
||||||
|
use loco_rs::prelude::*;
|
||||||
|
use tokio::fs::File;
|
||||||
|
|
||||||
|
use axum_range::KnownSize;
|
||||||
|
use axum_range::Ranged;
|
||||||
|
|
||||||
|
use axum_extra::headers::Range;
|
||||||
|
use axum_extra::TypedHeader;
|
||||||
|
|
||||||
|
use crate::services;
|
||||||
|
|
||||||
|
pub async fn get_data(
|
||||||
|
auth: auth::JWT,
|
||||||
|
range: Option<TypedHeader<Range>>,
|
||||||
|
Path(file_name): Path<String>,
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
) -> Result<Ranged<KnownSize<File>>> {
|
||||||
|
services::data::serve_data_file(&auth, range, &file_name, &ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_data(
|
||||||
|
auth: auth::JWT,
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
payload: Multipart,
|
||||||
|
) -> Result<Response> {
|
||||||
|
services::data::add(&auth, &ctx, payload).await?;
|
||||||
|
format::html("<h1>File uploaded successfully</h1>")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Routes {
|
||||||
|
Routes::new()
|
||||||
|
.prefix("api/data/")
|
||||||
|
.add("/upload", post(upload_data))
|
||||||
|
.add("/:file_name", get(get_data))
|
||||||
|
}
|
@@ -1,2 +1,4 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod website;
|
pub mod website;
|
||||||
|
|
||||||
|
pub mod data;
|
@@ -12,6 +12,12 @@ pub async fn render_index(
|
|||||||
views::website::index(v, &ctx).await
|
views::website::index(v, &ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub async fn render_login(ViewEngine(v): ViewEngine<TeraView>) -> impl IntoResponse {
|
||||||
Routes::new().add("/", get(render_index))
|
views::website::login(v).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Routes {
|
||||||
|
Routes::new()
|
||||||
|
.add("/", get(render_index))
|
||||||
|
.add("/login", get(render_login))
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ pub struct Model {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub file_url: String,
|
pub file_url: String,
|
||||||
pub protected: bool,
|
pub protected: bool,
|
||||||
|
pub file_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
105
src/services/data.rs
Normal file
105
src/services/data.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::models::_entities::data::{self, ActiveModel, Entity, Model};
|
||||||
|
use crate::models::users::users;
|
||||||
|
use axum::extract::Multipart;
|
||||||
|
use axum_extra::headers::Range;
|
||||||
|
use axum_extra::TypedHeader;
|
||||||
|
use loco_rs::prelude::*;
|
||||||
|
use tokio::fs::File;
|
||||||
|
|
||||||
|
use axum_range::KnownSize;
|
||||||
|
use axum_range::Ranged;
|
||||||
|
|
||||||
|
pub async fn get_data_by_file_name(file_name: &str, ctx: &AppContext) -> ModelResult<Model> {
|
||||||
|
let data = Entity::find()
|
||||||
|
.filter(data::Column::FileName.eq(file_name))
|
||||||
|
.one(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
data.ok_or_else(|| ModelError::EntityNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve_data_file(
|
||||||
|
auth: &auth::JWT,
|
||||||
|
range: Option<TypedHeader<Range>>,
|
||||||
|
file_name: &str,
|
||||||
|
ctx: &AppContext,
|
||||||
|
) -> Result<Ranged<KnownSize<File>>> {
|
||||||
|
let data = get_data_by_file_name(&file_name, &ctx).await?;
|
||||||
|
if data.protected {
|
||||||
|
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => return unauthorized("Unauthorized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::open(&data.file_url).await?;
|
||||||
|
let body = KnownSize::file(file).await?;
|
||||||
|
let range = range.map(|TypedHeader(range)| range);
|
||||||
|
Ok(Ranged::new(range, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add(auth: &auth::JWT, ctx: &AppContext, mut payload: Multipart) -> ModelResult<Model> {
|
||||||
|
let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||||
|
let mut protected = None;
|
||||||
|
let mut file_name = None;
|
||||||
|
|
||||||
|
while let Some(field) = payload
|
||||||
|
.next_field()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get next field".into()))?
|
||||||
|
{
|
||||||
|
let name = field
|
||||||
|
.name()
|
||||||
|
.ok_or_else(|| ModelError::Any("Failed to get field name".into()))?;
|
||||||
|
match name {
|
||||||
|
"protected" => {
|
||||||
|
let value = field
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get text".into()))?
|
||||||
|
.parse::<bool>()
|
||||||
|
.map_err(|_| ModelError::Any("Failed to parse bool".into()))?;
|
||||||
|
protected = Some(value);
|
||||||
|
}
|
||||||
|
"file" => {
|
||||||
|
file_name = match field.file_name() {
|
||||||
|
Some(file_name) => Some(String::from(file_name)),
|
||||||
|
None => return Err(ModelError::Any("Failed to get file name".into())),
|
||||||
|
};
|
||||||
|
|
||||||
|
if file_name.is_none() {
|
||||||
|
return Err(ModelError::Any("Failed to get file name".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = PathBuf::from("uploads").join(file_name.as_ref().unwrap());
|
||||||
|
|
||||||
|
let content = field
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ModelError::Any("Failed to get bytes".into()))?;
|
||||||
|
|
||||||
|
match ctx.storage.as_ref().upload(path.as_path(), &content).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => return Err(ModelError::Any("Failed to save file to storage".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let protected =
|
||||||
|
protected.ok_or_else(|| ModelError::Any("Protected field is required".into()))?;
|
||||||
|
let file_name = file_name.ok_or_else(|| ModelError::Any("File field is required".into()))?;
|
||||||
|
|
||||||
|
let mut item = ActiveModel {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
item.protected = Set(protected);
|
||||||
|
item.file_name = Set(file_name);
|
||||||
|
|
||||||
|
let item = item.insert(&ctx.db).await?;
|
||||||
|
Ok(item)
|
||||||
|
}
|
@@ -1,2 +1,3 @@
|
|||||||
|
pub mod data;
|
||||||
pub mod jobs;
|
pub mod jobs;
|
||||||
pub mod skills;
|
pub mod skills;
|
||||||
|
39
src/tasks/create_user.rs
Normal file
39
src/tasks/create_user.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
|
use crate::models::users::{self};
|
||||||
|
|
||||||
|
pub struct CreateUserData;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Task for CreateUserData {
|
||||||
|
fn task(&self) -> TaskInfo {
|
||||||
|
TaskInfo {
|
||||||
|
name: "create_user".to_string(),
|
||||||
|
detail: "Task for creating a new user".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {
|
||||||
|
let username = vars.cli_arg("username")?;
|
||||||
|
let email = vars.cli_arg("email")?;
|
||||||
|
let password = vars.cli_arg("password")?;
|
||||||
|
|
||||||
|
let user = users::Model::create_with_password(
|
||||||
|
&app_context.db,
|
||||||
|
&users::RegisterParams {
|
||||||
|
name: username.to_string(),
|
||||||
|
email: email.to_string(),
|
||||||
|
password: password.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
user_id = user.id,
|
||||||
|
user_email = &user.email,
|
||||||
|
"User created successfully",
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +1,2 @@
|
|||||||
|
pub mod create_user;
|
||||||
pub mod seed;
|
pub mod seed;
|
||||||
|
@@ -12,3 +12,7 @@ pub async fn index(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoRe
|
|||||||
data!({ "skills": skills, "jobs": jobs }),
|
data!({ "skills": skills, "jobs": jobs }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn login(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
||||||
|
format::render().view(&v, "website/login.html", data!({}))
|
||||||
|
}
|
||||||
|
17
tests/requests/data.rs
Normal file
17
tests/requests/data.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use gabrielkaszewski_rs::app::App;
|
||||||
|
use loco_rs::testing;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn can_get_data() {
|
||||||
|
testing::request::<App, _, _>(|request, _ctx| async move {
|
||||||
|
let res = request.get("/data/").await;
|
||||||
|
assert_eq!(res.status_code(), 200);
|
||||||
|
|
||||||
|
// you can assert content like this:
|
||||||
|
// assert_eq!(res.text(), "content");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
@@ -1,2 +1,4 @@
|
|||||||
mod auth;
|
mod auth;
|
||||||
mod prepare_data;
|
mod prepare_data;
|
||||||
|
|
||||||
|
pub mod data;
|
Reference in New Issue
Block a user