better projects upload and mobile design
This commit is contained in:
@@ -109,7 +109,7 @@ async fn login(
|
||||
|
||||
let token = user
|
||||
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
|
||||
.or_else(|_| unauthorized("unauthorized!"))?;
|
||||
.or_else(|_| unauthorized("jwt error"))?;
|
||||
|
||||
let cookie = Cookie::build(("token", token.clone()))
|
||||
.path("/")
|
||||
|
@@ -1,6 +1,8 @@
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::unnecessary_struct_initialization)]
|
||||
#![allow(clippy::unused_async)]
|
||||
use axum::extract::Multipart;
|
||||
use format::redirect;
|
||||
use loco_rs::prelude::*;
|
||||
|
||||
use crate::{
|
||||
@@ -39,8 +41,24 @@ async fn create_project(
|
||||
format::json(&project)
|
||||
}
|
||||
|
||||
async fn create_project_with_thumbnails(
|
||||
auth: auth::JWT,
|
||||
State(ctx): State<AppContext>,
|
||||
payload: Multipart,
|
||||
) -> Result<Response> {
|
||||
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
||||
Ok(_) => {}
|
||||
Err(_) => return unauthorized("Unauthorized"),
|
||||
}
|
||||
|
||||
services::projects::add_project_with_thumbnails_multipart(&auth, &ctx, payload).await?;
|
||||
|
||||
redirect("/projects")
|
||||
}
|
||||
|
||||
pub fn routes() -> Routes {
|
||||
Routes::new()
|
||||
.prefix("api/projects/")
|
||||
.add("/", post(create_project))
|
||||
.add("/upload", post(create_project_with_thumbnails))
|
||||
}
|
||||
|
13
src/services/auth.rs
Normal file
13
src/services/auth.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use loco_rs::prelude::*;
|
||||
|
||||
use crate::models::users;
|
||||
|
||||
pub async fn is_logged_in(
|
||||
auth: &auth::JWT,
|
||||
ctx: &AppContext,
|
||||
) -> Result<bool> {
|
||||
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::models::_entities::data::{self, ActiveModel, Entity, Model};
|
||||
use crate::models::users::users;
|
||||
use crate::shared::get_file_name_with_extension::get_file_name_with_extension_from_field;
|
||||
use axum::extract::Multipart;
|
||||
use axum_extra::headers::Range;
|
||||
use axum_extra::TypedHeader;
|
||||
@@ -129,10 +130,7 @@ pub async fn add(
|
||||
protected = Some(value);
|
||||
}
|
||||
"file" => {
|
||||
let og_file_name = field
|
||||
.file_name()
|
||||
.ok_or_else(|| ModelError::Any("Failed to get file name".into()))?;
|
||||
let ext = String::from(og_file_name.split('.').last().unwrap_or("txt"));
|
||||
let (og_file_name, ext) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?;
|
||||
|
||||
let temp_file_name = if uuid_name {
|
||||
let temp_file_name = uuid::Uuid::new_v4().to_string();
|
||||
|
@@ -1,7 +1,8 @@
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::QueryOrder;
|
||||
|
||||
use crate::{models::{
|
||||
_entities::jobs::{Entity, Model},
|
||||
_entities::jobs::{Column, Entity, Model},
|
||||
jobs::JobWithTechnologies,
|
||||
}, shared::get_technologies_from_string::get_technologies_from_string};
|
||||
|
||||
@@ -11,7 +12,9 @@ pub async fn get_all_jobs(ctx: &AppContext) -> Result<Vec<Model>> {
|
||||
}
|
||||
|
||||
pub async fn get_all_jobs_with_technologies(ctx: &AppContext) -> Result<Vec<JobWithTechnologies>> {
|
||||
let jobs = Entity::find().all(&ctx.db).await?;
|
||||
let jobs = Entity::find()
|
||||
.order_by_asc(Column::StartDate)
|
||||
.all(&ctx.db).await?;
|
||||
let jobs_with_technologies = jobs
|
||||
.into_iter()
|
||||
.map(|job| {
|
||||
|
@@ -2,4 +2,5 @@ pub mod data;
|
||||
pub mod jobs;
|
||||
pub mod skills;
|
||||
pub mod website;
|
||||
pub mod projects;
|
||||
pub mod projects;
|
||||
pub mod auth;
|
@@ -1,13 +1,15 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use axum::extract::Multipart;
|
||||
use loco_rs::prelude::*;
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
_entities::{
|
||||
project_thumbnails,
|
||||
projects::{self, ActiveModel, Entity, Model},
|
||||
data, project_thumbnails, projects::{self, ActiveModel, Entity, Model}
|
||||
},
|
||||
projects::{get_category_from_string, get_string_from_category, CreateProject, ProjectDto, UpdateProject},
|
||||
}, services::data::add_data_file_from_path, shared::get_technologies_from_string::get_technologies_from_string
|
||||
projects::{get_category_from_string, get_string_from_category, CreateProject, ProjectDto, UpdateProject}, users,
|
||||
}, services::data::add_data_file_from_path, shared::{get_file_name_with_extension::get_file_name_with_extension_from_field, get_technologies_from_string::get_technologies_from_string}
|
||||
};
|
||||
|
||||
use super::data::delete_data_by_id;
|
||||
@@ -62,13 +64,39 @@ pub async fn get_all_projects_dto(ctx: &AppContext) -> Result<Vec<ProjectDto>> {
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
let thumbnails_ids = projects_with_thumbnails
|
||||
.iter()
|
||||
.map(|(_, thumbnails)| {
|
||||
thumbnails
|
||||
.iter()
|
||||
.map(|thumbnail| thumbnail.data_id)
|
||||
.collect::<Vec<i32>>()
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<i32>>();
|
||||
|
||||
let thumbnails_data = data::Entity::find()
|
||||
.filter(model::query::condition().is_in(data::Column::Id, thumbnails_ids).build())
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
let thumbnails_map = thumbnails_data
|
||||
.into_iter()
|
||||
.map(|thumbnail| (thumbnail.id, thumbnail))
|
||||
.collect::<std::collections::HashMap<i32, data::Model>>();
|
||||
|
||||
let projects_dto = projects_with_thumbnails
|
||||
.into_iter()
|
||||
.map(|(project, thumbnails)| {
|
||||
let thumbnails = thumbnails
|
||||
.into_iter()
|
||||
.map(|thumbnail| thumbnail.data_id.to_string())
|
||||
.map(|thumbnail| {
|
||||
let thumbnail_data = thumbnails_map.get(&thumbnail.data_id).unwrap();
|
||||
let url = format!("/api/data/{}", thumbnail_data.file_name);
|
||||
url
|
||||
})
|
||||
.collect();
|
||||
|
||||
ProjectDto {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
@@ -189,6 +217,176 @@ pub async fn add_project_with_thumbnails(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_project_with_thumbnails_multipart(
|
||||
auth: &auth::JWT,
|
||||
ctx: &AppContext,
|
||||
mut payload: Multipart,
|
||||
) -> Result<()> {
|
||||
let _current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
||||
|
||||
let mut project_name = None;
|
||||
let mut short_description = None;
|
||||
let mut description = None;
|
||||
let mut category = None;
|
||||
let mut github_url = None;
|
||||
let mut download_url = None;
|
||||
let mut visit_url = None;
|
||||
let mut technologies = None;
|
||||
|
||||
let mut thumbnails_file_names = Vec::new();
|
||||
let mut thumbnails = Vec::new();
|
||||
|
||||
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 {
|
||||
"name" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
project_name = Some(value);
|
||||
}
|
||||
"short_description" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
short_description = Some(value);
|
||||
}
|
||||
"description" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
description = Some(value);
|
||||
}
|
||||
"category" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
category = Some(value);
|
||||
}
|
||||
"github_url" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
github_url = Some(value);
|
||||
}
|
||||
"download_url" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
download_url = Some(value);
|
||||
}
|
||||
"visit_url" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
visit_url = Some(value);
|
||||
}
|
||||
"technologies" => {
|
||||
let value = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get text".into()))?;
|
||||
technologies = Some(value);
|
||||
}
|
||||
"thumbnail" => {
|
||||
let (_, ext) = get_file_name_with_extension_from_field(&field, "txt").map_err(|_| ModelError::Any("Failed to get file name".into()))?;
|
||||
|
||||
let file_name = uuid::Uuid::new_v4().to_string();
|
||||
let file_name = format!("{}.{}", file_name, ext);
|
||||
|
||||
let data_content = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|_| ModelError::Any("Failed to get bytes".into()))?;
|
||||
|
||||
thumbnails_file_names.push(file_name);
|
||||
thumbnails.push(data_content);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let category = category.map(|s| get_category_from_string(&s));
|
||||
|
||||
let project = CreateProject {
|
||||
name: project_name.ok_or_else(|| ModelError::Any("Name field is required".into()))?,
|
||||
short_description: short_description.ok_or_else(|| ModelError::Any("Short description field is required".into()))?,
|
||||
description: description,
|
||||
category: category.ok_or_else(|| ModelError::Any("Category field is required".into()))?,
|
||||
github_url: github_url,
|
||||
download_url: download_url,
|
||||
visit_url: visit_url,
|
||||
technologies: technologies.ok_or_else(|| ModelError::Any("Technologies field is required".into()))?.split(",").map(|s| s.to_string()).collect(),
|
||||
};
|
||||
|
||||
let txn = ctx.db.begin().await?;
|
||||
|
||||
let item = ActiveModel {
|
||||
name: Set(project.name),
|
||||
short_description: Set(project.short_description),
|
||||
description: Set(project.description),
|
||||
category: Set(get_string_from_category(&project.category)),
|
||||
github_url: Set(project.github_url),
|
||||
download_url: Set(project.download_url),
|
||||
visit_url: Set(project.visit_url),
|
||||
technology: Set(project.technologies.join(",")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let item = item.insert(&txn).await?;
|
||||
|
||||
let project_id = item.id;
|
||||
|
||||
for (thumbnail_file_name, thumbnail) in thumbnails_file_names.iter().zip(thumbnails.iter()) {
|
||||
let thumbnail_data = data::ActiveModel {
|
||||
file_name: Set(thumbnail_file_name.clone()),
|
||||
file_url: Set(format!("uploads/{}", thumbnail_file_name)),
|
||||
protected: Set(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let thumbnail_data = thumbnail_data.insert(&txn).await?;
|
||||
let path = PathBuf::from(thumbnail_file_name);
|
||||
match ctx
|
||||
.storage
|
||||
.as_ref()
|
||||
.upload(
|
||||
path.as_path(),
|
||||
thumbnail,
|
||||
)
|
||||
.await {
|
||||
Ok(_) => {
|
||||
let thumbnail = project_thumbnails::ActiveModel {
|
||||
project_id: Set(project_id),
|
||||
data_id: Set(thumbnail_data.id),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
thumbnail.insert(&txn).await?;
|
||||
},
|
||||
Err(_) => return Err(Error::Any("Failed to save file to storage".into())),
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_project(
|
||||
ctx: &AppContext,
|
||||
id: i32,
|
||||
|
@@ -1,8 +1,11 @@
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::QueryOrder;
|
||||
|
||||
use crate::models::_entities::skills::{Entity, Model};
|
||||
use crate::models::_entities::skills::{Column, Entity, Model};
|
||||
|
||||
pub async fn get_all_skills(ctx: &AppContext) -> Result<Vec<Model>> {
|
||||
let skills = Entity::find().all(&ctx.db).await?;
|
||||
let skills = Entity::find()
|
||||
.order_by_asc(Column::Name)
|
||||
.all(&ctx.db).await?;
|
||||
Ok(skills)
|
||||
}
|
||||
|
13
src/shared/get_file_name_with_extension.rs
Normal file
13
src/shared/get_file_name_with_extension.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use axum::extract::multipart::Field;
|
||||
use loco_rs::prelude::*;
|
||||
|
||||
pub fn get_file_name_with_extension_from_field(
|
||||
field: &Field<'_>,
|
||||
default_extension: &str,
|
||||
) -> Result<(String, String)> {
|
||||
let file_name = field.file_name().ok_or_else(|| ModelError::Any("Failed to get file name".into()))?;
|
||||
let mut parts = file_name.split('.').collect::<Vec<&str>>();
|
||||
let extension = parts.pop().unwrap_or(default_extension);
|
||||
let file_name = parts.join(".");
|
||||
Ok((file_name, extension.to_string()))
|
||||
}
|
@@ -1 +1,2 @@
|
||||
pub mod get_technologies_from_string;
|
||||
pub mod get_technologies_from_string;
|
||||
pub mod get_file_name_with_extension;
|
@@ -41,6 +41,6 @@ impl CurrentResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
||||
pub async fn login(v: impl ViewRenderer) -> Result<impl IntoResponse> {
|
||||
format::render().view(&v, "website/login.html", data!({}))
|
||||
}
|
||||
|
Reference in New Issue
Block a user