Compare commits
5 Commits
c22050005c
...
ac27cba766
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac27cba766 | ||
|
|
61d0bbe4d1 | ||
|
|
405ca0d2ea
|
||
|
|
95863fccd0
|
||
|
|
b4e9fd499f
|
4
.env
4
.env
@@ -1 +1,3 @@
|
||||
DATABASE_URL=postgres://user:password@localhost/diesel_demo
|
||||
MONGOURI=mongodb://jheuel:bla@localhost/?retryWrites=true&w=majority
|
||||
API_ROOT=http://localhost:8080
|
||||
SECRET=mila-likes-the-ol-moonwalk-and-that-is-how-she-rolls-bla-bla-bla
|
||||
|
||||
1700
Cargo.lock
generated
1700
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,12 +11,18 @@ default-run = "backend"
|
||||
actix-web = "4"
|
||||
actix-web-lab = "^0"
|
||||
actix-files = "0.6"
|
||||
actix-cors = "0.6.4"
|
||||
actix-session = { version = "0.7.2", features = ["cookie-session"] }
|
||||
env_logger = "0.9.0"
|
||||
log = "0.4"
|
||||
diesel = { version = "1.4.8", features = ["postgres", "r2d2"] }
|
||||
diesel_migrations = "1.4"
|
||||
dotenv = "0.15.0"
|
||||
walkdir = "2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
common = { path = "../common" }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
||||
[dependencies.mongodb]
|
||||
version = "2.2.0"
|
||||
default-features = false
|
||||
features = ["async-std-runtime"]
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
1
backend/src/api/mod.rs
Normal file
1
backend/src/api/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod user_api;
|
||||
186
backend/src/api/user_api.rs
Normal file
186
backend/src/api/user_api.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use crate::{models::user_model::User, repository::mongodb_repo::MongoRepo};
|
||||
use actix_session::Session;
|
||||
use actix_web::{
|
||||
delete, get, post, put,
|
||||
web::{Data, Json, Path},
|
||||
HttpResponse,
|
||||
};
|
||||
use mongodb::bson::oid::ObjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UserWrapper {
|
||||
user: User,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UserWithToken {
|
||||
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<ObjectId>,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UserTokenWrapper {
|
||||
user: UserWithToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Login {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct LoginWrapper {
|
||||
user: Login,
|
||||
}
|
||||
|
||||
// Return an opaque 500 while preserving the error's root cause for logging.
|
||||
fn e500<T>(e: T) -> actix_web::Error
|
||||
where
|
||||
T: std::fmt::Debug + std::fmt::Display + 'static,
|
||||
{
|
||||
actix_web::error::ErrorInternalServerError(e)
|
||||
}
|
||||
|
||||
#[get("/api/user")]
|
||||
pub async fn is_logged_in(
|
||||
db: Data<MongoRepo>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
if let Some(user_id) = session.get::<String>("user_id").map_err(e500)? {
|
||||
if let Ok(user) = db.get_user(&user_id).await {
|
||||
let token = user.username.clone();
|
||||
log::info!("success");
|
||||
return Ok(HttpResponse::Ok().json(UserTokenWrapper {
|
||||
user: UserWithToken {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
token,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::Unauthorized().body("Not logged in"))
|
||||
}
|
||||
|
||||
#[post("/api/user")]
|
||||
pub async fn create_user(db: Data<MongoRepo>, request: Json<UserWrapper>) -> HttpResponse {
|
||||
let data = User {
|
||||
id: None,
|
||||
username: request.user.username.to_owned(),
|
||||
email: request.user.email.to_owned(),
|
||||
password: request.user.password.to_owned(),
|
||||
};
|
||||
let user_detail = db.create_user(data).await;
|
||||
match user_detail {
|
||||
Ok(user) => HttpResponse::Ok().json(user),
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/users/login")]
|
||||
pub async fn login(
|
||||
db: Data<MongoRepo>,
|
||||
request: Json<LoginWrapper>,
|
||||
session: Session,
|
||||
) -> HttpResponse {
|
||||
let user = db.get_user_from_email(&request.user.email).await;
|
||||
let Ok(user) = user else {
|
||||
return HttpResponse::Unauthorized().body("Login failed");
|
||||
};
|
||||
if user.password != request.user.password {
|
||||
return HttpResponse::Unauthorized().body("Login failed");
|
||||
}
|
||||
|
||||
let Some(user_id) = user.id else {
|
||||
return HttpResponse::Unauthorized().body("Login failed");
|
||||
};
|
||||
|
||||
session.renew();
|
||||
if let Err(e) = session.insert("user_id", user_id.to_string()) {
|
||||
return HttpResponse::InternalServerError().body(e.to_string());
|
||||
}
|
||||
|
||||
let token = user.username.clone();
|
||||
HttpResponse::Ok().json(UserTokenWrapper {
|
||||
user: UserWithToken {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
token,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/api/user/{id}")]
|
||||
pub async fn get_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
|
||||
let id = path.into_inner();
|
||||
if id.is_empty() {
|
||||
return HttpResponse::BadRequest().body("invalid ID");
|
||||
}
|
||||
let user_detail = db.get_user(&id).await;
|
||||
match user_detail {
|
||||
Ok(user) => HttpResponse::Ok().json(user),
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/api/user/{id}")]
|
||||
pub async fn update_user(
|
||||
db: Data<MongoRepo>,
|
||||
path: Path<String>,
|
||||
request: Json<UserWrapper>,
|
||||
) -> HttpResponse {
|
||||
let id = path.into_inner();
|
||||
if id.is_empty() {
|
||||
return HttpResponse::BadRequest().body("invalid ID");
|
||||
};
|
||||
let data = User {
|
||||
id: Some(ObjectId::parse_str(&id).unwrap()),
|
||||
username: request.user.username.to_owned(),
|
||||
email: request.user.email.to_owned(),
|
||||
password: request.user.password.to_owned(),
|
||||
};
|
||||
let update_result = db.update_user(&id, data).await;
|
||||
match update_result {
|
||||
Ok(update) => {
|
||||
if update.matched_count == 1 {
|
||||
let updated_user_info = db.get_user(&id).await;
|
||||
match updated_user_info {
|
||||
Ok(user) => HttpResponse::Ok().json(user),
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::NotFound().body("No user found with specified ID")
|
||||
}
|
||||
}
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/api/user/{id}")]
|
||||
pub async fn delete_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
|
||||
let id = path.into_inner();
|
||||
if id.is_empty() {
|
||||
return HttpResponse::BadRequest().body("invalid ID");
|
||||
};
|
||||
let result = db.delete_user(&id).await;
|
||||
match result {
|
||||
Ok(res) => {
|
||||
if res.deleted_count == 1 {
|
||||
HttpResponse::Ok().json("User successfully deleted!")
|
||||
} else {
|
||||
HttpResponse::NotFound().json("User with specified ID not found!")
|
||||
}
|
||||
}
|
||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -1,236 +1,236 @@
|
||||
extern crate diesel;
|
||||
use backend::create_picture;
|
||||
use backend::establish_connection;
|
||||
use backend::models::NewPicture;
|
||||
// extern crate diesel;
|
||||
// use backend::create_picture;
|
||||
// use backend::establish_connection;
|
||||
// use backend::models::NewPicture;
|
||||
|
||||
// use backend::*;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use std::{fs, process::Command};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
// // use backend::*;
|
||||
// use std::ffi::OsStr;
|
||||
// use std::path::Path;
|
||||
// use std::time::UNIX_EPOCH;
|
||||
// use std::{fs, process::Command};
|
||||
// use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PhotoExif {
|
||||
#[serde(default, alias = "FocalLength")]
|
||||
focal_length: Option<String>,
|
||||
#[serde(default, alias = "ShutterSpeed")]
|
||||
shutter_speed: Option<String>,
|
||||
#[serde(alias = "ImageWidth")]
|
||||
width: i32,
|
||||
#[serde(alias = "ImageHeight")]
|
||||
height: i32,
|
||||
#[serde(default, alias = "Make")]
|
||||
make: Option<String>,
|
||||
#[serde(default, alias = "Model")]
|
||||
model: Option<String>,
|
||||
#[serde(default, alias = "LensID")]
|
||||
lens: Option<String>,
|
||||
#[serde(default, alias = "Orientation")]
|
||||
orientation: Option<String>,
|
||||
#[serde(default, alias = "FNumber")]
|
||||
fnumber: Option<f64>,
|
||||
#[serde(default, alias = "ExposureProgram")]
|
||||
exposure_program: Option<String>,
|
||||
#[serde(default, alias = "CreateDate")]
|
||||
created_at: Option<i32>,
|
||||
#[serde(default, alias = "ISO")]
|
||||
iso: Option<i32>,
|
||||
#[serde(default = "MaybeString::default", alias = "ExposureCompensation")]
|
||||
exposure_compensation: MaybeString,
|
||||
}
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// struct PhotoExif {
|
||||
// #[serde(default, alias = "FocalLength")]
|
||||
// focal_length: Option<String>,
|
||||
// #[serde(default, alias = "ShutterSpeed")]
|
||||
// shutter_speed: Option<String>,
|
||||
// #[serde(alias = "ImageWidth")]
|
||||
// width: i32,
|
||||
// #[serde(alias = "ImageHeight")]
|
||||
// height: i32,
|
||||
// #[serde(default, alias = "Make")]
|
||||
// make: Option<String>,
|
||||
// #[serde(default, alias = "Model")]
|
||||
// model: Option<String>,
|
||||
// #[serde(default, alias = "LensID")]
|
||||
// lens: Option<String>,
|
||||
// #[serde(default, alias = "Orientation")]
|
||||
// orientation: Option<String>,
|
||||
// #[serde(default, alias = "FNumber")]
|
||||
// fnumber: Option<f64>,
|
||||
// #[serde(default, alias = "ExposureProgram")]
|
||||
// exposure_program: Option<String>,
|
||||
// #[serde(default, alias = "CreateDate")]
|
||||
// created_at: Option<i32>,
|
||||
// #[serde(default, alias = "ISO")]
|
||||
// iso: Option<i32>,
|
||||
// #[serde(default = "MaybeString::default", alias = "ExposureCompensation")]
|
||||
// exposure_compensation: MaybeString,
|
||||
// }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum MaybeString {
|
||||
Number(i32),
|
||||
Str(String),
|
||||
None,
|
||||
}
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// #[serde(untagged)]
|
||||
// enum MaybeString {
|
||||
// Number(i32),
|
||||
// Str(String),
|
||||
// None,
|
||||
// }
|
||||
|
||||
impl MaybeString {
|
||||
fn default() -> MaybeString {
|
||||
MaybeString::None
|
||||
}
|
||||
fn to_opt_string(&self) -> Option<String> {
|
||||
if let MaybeString::Str(exp_comp) = &self {
|
||||
Some(exp_comp.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
// impl MaybeString {
|
||||
// fn default() -> MaybeString {
|
||||
// MaybeString::None
|
||||
// }
|
||||
// fn to_opt_string(&self) -> Option<String> {
|
||||
// if let MaybeString::Str(exp_comp) = &self {
|
||||
// Some(exp_comp.clone())
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fn is_hidden(entry: &DirEntry) -> bool {
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
// fn is_hidden(entry: &DirEntry) -> bool {
|
||||
// entry
|
||||
// .file_name()
|
||||
// .to_str()
|
||||
// .map(|s| s.starts_with('.'))
|
||||
// .unwrap_or(false)
|
||||
// }
|
||||
|
||||
fn is_image(entry: &DirEntry) -> bool {
|
||||
let allowed_extensions = ["cr2", "cr3", "jpg", "jpeg"];
|
||||
// fn is_image(entry: &DirEntry) -> bool {
|
||||
// let allowed_extensions = ["cr2", "cr3", "jpg", "jpeg"];
|
||||
|
||||
let extension = if let Some(ext) = entry.path().extension() {
|
||||
ext
|
||||
} else {
|
||||
OsStr::new("")
|
||||
};
|
||||
// let extension = if let Some(ext) = entry.path().extension() {
|
||||
// ext
|
||||
// } else {
|
||||
// OsStr::new("")
|
||||
// };
|
||||
|
||||
if allowed_extensions
|
||||
.iter()
|
||||
.all(|&v| v != extension.to_ascii_lowercase())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
// if allowed_extensions
|
||||
// .iter()
|
||||
// .all(|&v| v != extension.to_ascii_lowercase())
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
|
||||
static PICTURE_PATH: &str = "./pictures";
|
||||
static LIBRARY_PATH: &str = "./examples";
|
||||
// static PICTURE_PATH: &str = "./pictures";
|
||||
// static LIBRARY_PATH: &str = "./examples";
|
||||
|
||||
fn main() {
|
||||
let connection = establish_connection();
|
||||
// let connection = establish_connection();
|
||||
|
||||
WalkDir::new(LIBRARY_PATH)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| !e.file_type().is_dir())
|
||||
.filter(|e| !is_hidden(e))
|
||||
.filter(is_image)
|
||||
.into_iter()
|
||||
.for_each(|path| {
|
||||
let thumbnail = if let Ok(t) = extract_preview(path.path()) {
|
||||
t
|
||||
} else {
|
||||
println!("Could not create thumbnail");
|
||||
return;
|
||||
};
|
||||
let thumbnail = std::path::PathBuf::from(thumbnail.strip_prefix(PICTURE_PATH).unwrap());
|
||||
// WalkDir::new(LIBRARY_PATH)
|
||||
// .into_iter()
|
||||
// .filter_map(Result::ok)
|
||||
// .filter(|e| !e.file_type().is_dir())
|
||||
// .filter(|e| !is_hidden(e))
|
||||
// .filter(is_image)
|
||||
// .into_iter()
|
||||
// .for_each(|path| {
|
||||
// let thumbnail = if let Ok(t) = extract_preview(path.path()) {
|
||||
// t
|
||||
// } else {
|
||||
// println!("Could not create thumbnail");
|
||||
// return;
|
||||
// };
|
||||
// let thumbnail = std::path::PathBuf::from(thumbnail.strip_prefix(PICTURE_PATH).unwrap());
|
||||
|
||||
let output = Command::new("exiftool")
|
||||
.arg("-j")
|
||||
.arg("-d")
|
||||
.arg("%s")
|
||||
.arg(path.path())
|
||||
.output()
|
||||
.expect("failed to execute exiftool");
|
||||
let pel: Vec<PhotoExif> = serde_json::from_slice(&output.stdout).unwrap();
|
||||
let pe = &pel[0];
|
||||
// let output = Command::new("exiftool")
|
||||
// .arg("-j")
|
||||
// .arg("-d")
|
||||
// .arg("%s")
|
||||
// .arg(path.path())
|
||||
// .output()
|
||||
// .expect("failed to execute exiftool");
|
||||
// let pel: Vec<PhotoExif> = serde_json::from_slice(&output.stdout).unwrap();
|
||||
// let pe = &pel[0];
|
||||
|
||||
println!("pe = {}", serde_json::to_string_pretty(pe).unwrap());
|
||||
// println!("pe = {}", serde_json::to_string_pretty(pe).unwrap());
|
||||
|
||||
let created_at: Option<i32> = if let Some(c) = pe.created_at {
|
||||
Some(c)
|
||||
} else {
|
||||
let metadata = fs::metadata(&path.path()).unwrap();
|
||||
if let Ok(time) = metadata.created() {
|
||||
Some(
|
||||
time.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
println!("Not supported on this platform or filesystem");
|
||||
None
|
||||
}
|
||||
};
|
||||
// let created_at: Option<i32> = if let Some(c) = pe.created_at {
|
||||
// Some(c)
|
||||
// } else {
|
||||
// let metadata = fs::metadata(&path.path()).unwrap();
|
||||
// if let Ok(time) = metadata.created() {
|
||||
// Some(
|
||||
// time.duration_since(UNIX_EPOCH)
|
||||
// .unwrap()
|
||||
// .as_secs()
|
||||
// .try_into()
|
||||
// .unwrap(),
|
||||
// )
|
||||
// } else {
|
||||
// println!("Not supported on this platform or filesystem");
|
||||
// None
|
||||
// }
|
||||
// };
|
||||
|
||||
let filepath = path.path().to_string_lossy().into_owned();
|
||||
// let filepath = path.path().to_string_lossy().into_owned();
|
||||
|
||||
let new_picture = NewPicture {
|
||||
filepath: filepath.clone(),
|
||||
created_at,
|
||||
focal_length: pe.focal_length.clone(),
|
||||
shutter_speed: pe.shutter_speed.clone(),
|
||||
width: pe.width,
|
||||
height: pe.height,
|
||||
make: pe.make.clone(),
|
||||
model: pe.model.clone(),
|
||||
lens: pe.lens.clone(),
|
||||
orientation: pe.orientation.clone(),
|
||||
fnumber: pe.fnumber,
|
||||
iso: pe.iso,
|
||||
exposure_program: pe.exposure_program.clone(),
|
||||
exposure_compensation: pe.exposure_compensation.to_opt_string(),
|
||||
thumbnail: Some(thumbnail.into_os_string().into_string().unwrap()),
|
||||
};
|
||||
// let new_picture = NewPicture {
|
||||
// filepath: filepath.clone(),
|
||||
// created_at,
|
||||
// focal_length: pe.focal_length.clone(),
|
||||
// shutter_speed: pe.shutter_speed.clone(),
|
||||
// width: pe.width,
|
||||
// height: pe.height,
|
||||
// make: pe.make.clone(),
|
||||
// model: pe.model.clone(),
|
||||
// lens: pe.lens.clone(),
|
||||
// orientation: pe.orientation.clone(),
|
||||
// fnumber: pe.fnumber,
|
||||
// iso: pe.iso,
|
||||
// exposure_program: pe.exposure_program.clone(),
|
||||
// exposure_compensation: pe.exposure_compensation.to_opt_string(),
|
||||
// thumbnail: Some(thumbnail.into_os_string().into_string().unwrap()),
|
||||
// };
|
||||
|
||||
let pic = create_picture(&connection, new_picture);
|
||||
println!("Created picture with filepath={} and id={}", filepath, pic);
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_preview(path: &Path) -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||||
let file_name = if let Some(p) = path.file_name() {
|
||||
p
|
||||
} else {
|
||||
OsStr::new("")
|
||||
};
|
||||
let parent = if let Some(p) = path.parent() {
|
||||
p
|
||||
} else {
|
||||
Path::new(LIBRARY_PATH)
|
||||
};
|
||||
let relative_parent = parent
|
||||
.strip_prefix(LIBRARY_PATH)
|
||||
.expect("Could not remove prefix");
|
||||
|
||||
let thumb_path = Path::new(PICTURE_PATH).join(relative_parent);
|
||||
|
||||
if !thumb_path.exists() {
|
||||
fs::create_dir_all(&thumb_path).unwrap_or_else(|e| {
|
||||
panic!("Could not create directory {}: {}", thumb_path.display(), e)
|
||||
});
|
||||
}
|
||||
|
||||
let mut thumbnail = thumb_path.join(file_name);
|
||||
thumbnail.set_extension("jpg");
|
||||
|
||||
let extension = path.extension().unwrap();
|
||||
let jpegs = ["jpg", "jpeg"];
|
||||
if jpegs.iter().any(|&x| x == extension.to_ascii_lowercase()) {
|
||||
match fs::copy(path, &thumbnail) {
|
||||
Ok(_it) => return Ok(thumbnail),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
}
|
||||
let _output_thumb = Command::new("exiftool")
|
||||
.arg("-if")
|
||||
.arg("$jpgfromraw")
|
||||
.arg("-b")
|
||||
.arg("-jpgfromraw")
|
||||
.arg("-w")
|
||||
.arg(thumb_path.join("%f.jpg"))
|
||||
.arg("-execute")
|
||||
.arg("-if")
|
||||
.arg("$previewimage")
|
||||
.arg("-b")
|
||||
.arg("-previewimage")
|
||||
.arg("-w")
|
||||
.arg(thumb_path.join("%f.jpg"))
|
||||
.arg("-execute")
|
||||
.arg("-tagsfromfile")
|
||||
.arg("@")
|
||||
.arg("-srcfile")
|
||||
.arg(thumb_path.join("%f.jpg"))
|
||||
.arg("-overwrite_original")
|
||||
.arg("-common_args")
|
||||
.arg("--ext")
|
||||
.arg("jpg")
|
||||
.arg(path)
|
||||
.output()
|
||||
.expect("failed to execute exiftool to extract thumbnail");
|
||||
// println!("{:?}", _output_thumb);
|
||||
|
||||
if thumbnail.exists() {
|
||||
Ok(thumbnail)
|
||||
} else {
|
||||
Err("Could not create thumbnail".into())
|
||||
}
|
||||
// let pic = create_picture(&connection, new_picture);
|
||||
// println!("Created picture with filepath={} and id={}", filepath, pic);
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn extract_preview(path: &Path) -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||||
// let file_name = if let Some(p) = path.file_name() {
|
||||
// p
|
||||
// } else {
|
||||
// OsStr::new("")
|
||||
// };
|
||||
// let parent = if let Some(p) = path.parent() {
|
||||
// p
|
||||
// } else {
|
||||
// Path::new(LIBRARY_PATH)
|
||||
// };
|
||||
// let relative_parent = parent
|
||||
// .strip_prefix(LIBRARY_PATH)
|
||||
// .expect("Could not remove prefix");
|
||||
|
||||
// let thumb_path = Path::new(PICTURE_PATH).join(relative_parent);
|
||||
|
||||
// if !thumb_path.exists() {
|
||||
// fs::create_dir_all(&thumb_path).unwrap_or_else(|e| {
|
||||
// panic!("Could not create directory {}: {}", thumb_path.display(), e)
|
||||
// });
|
||||
// }
|
||||
|
||||
// let mut thumbnail = thumb_path.join(file_name);
|
||||
// thumbnail.set_extension("jpg");
|
||||
|
||||
// let extension = path.extension().unwrap();
|
||||
// let jpegs = ["jpg", "jpeg"];
|
||||
// if jpegs.iter().any(|&x| x == extension.to_ascii_lowercase()) {
|
||||
// match fs::copy(path, &thumbnail) {
|
||||
// Ok(_it) => return Ok(thumbnail),
|
||||
// Err(err) => return Err(err.into()),
|
||||
// };
|
||||
// }
|
||||
// let _output_thumb = Command::new("exiftool")
|
||||
// .arg("-if")
|
||||
// .arg("$jpgfromraw")
|
||||
// .arg("-b")
|
||||
// .arg("-jpgfromraw")
|
||||
// .arg("-w")
|
||||
// .arg(thumb_path.join("%f.jpg"))
|
||||
// .arg("-execute")
|
||||
// .arg("-if")
|
||||
// .arg("$previewimage")
|
||||
// .arg("-b")
|
||||
// .arg("-previewimage")
|
||||
// .arg("-w")
|
||||
// .arg(thumb_path.join("%f.jpg"))
|
||||
// .arg("-execute")
|
||||
// .arg("-tagsfromfile")
|
||||
// .arg("@")
|
||||
// .arg("-srcfile")
|
||||
// .arg(thumb_path.join("%f.jpg"))
|
||||
// .arg("-overwrite_original")
|
||||
// .arg("-common_args")
|
||||
// .arg("--ext")
|
||||
// .arg("jpg")
|
||||
// .arg(path)
|
||||
// .output()
|
||||
// .expect("failed to execute exiftool to extract thumbnail");
|
||||
// // println!("{:?}", _output_thumb);
|
||||
|
||||
// if thumbnail.exists() {
|
||||
// Ok(thumbnail)
|
||||
// } else {
|
||||
// Err("Could not create thumbnail".into())
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
extern crate diesel;
|
||||
// extern crate diesel;
|
||||
|
||||
use self::models::*;
|
||||
use backend::*;
|
||||
use diesel::prelude::*;
|
||||
// use self::models::*;
|
||||
// use backend::*;
|
||||
// use diesel::prelude::*;
|
||||
|
||||
fn main() {
|
||||
use self::schema::pictures::dsl::*;
|
||||
// use self::schema::pictures::dsl::*;
|
||||
|
||||
let connection = establish_connection();
|
||||
let results = pictures
|
||||
.limit(5)
|
||||
.load::<Picture>(&connection)
|
||||
.expect("Error loading pictures");
|
||||
// let connection = establish_connection();
|
||||
// let results = pictures
|
||||
// .limit(5)
|
||||
// .load::<Picture>(&connection)
|
||||
// .expect("Error loading pictures");
|
||||
|
||||
println!("Displaying {} pictures", results.len());
|
||||
for picture in results {
|
||||
println!("filepath: {}", picture.filepath);
|
||||
println!("\tid: {}", picture.id);
|
||||
}
|
||||
// println!("Displaying {} pictures", results.len());
|
||||
// for picture in results {
|
||||
// println!("filepath: {}", picture.filepath);
|
||||
// println!("\tid: {}", picture.id);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
extern crate dotenv;
|
||||
// #[macro_use]
|
||||
// extern crate diesel;
|
||||
// extern crate dotenv;
|
||||
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
// pub mod models;
|
||||
// pub mod schema;
|
||||
|
||||
use self::models::NewPicture;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
// use self::models::NewPicture;
|
||||
// use diesel::pg::PgConnection;
|
||||
// use diesel::prelude::*;
|
||||
// use dotenv::dotenv;
|
||||
// use std::env;
|
||||
|
||||
pub fn establish_connection() -> PgConnection {
|
||||
dotenv().ok();
|
||||
// pub fn establish_connection() -> PgConnection {
|
||||
// dotenv().ok();
|
||||
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
PgConnection::establish(&database_url)
|
||||
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||
}
|
||||
// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
// PgConnection::establish(&database_url)
|
||||
// .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||
// }
|
||||
|
||||
pub fn create_picture(conn: &PgConnection, new_picture: NewPicture) -> usize {
|
||||
use schema::pictures;
|
||||
// pub fn create_picture(conn: &PgConnection, new_picture: NewPicture) -> usize {
|
||||
// use schema::pictures;
|
||||
|
||||
diesel::insert_into(pictures::table)
|
||||
.values(&new_picture)
|
||||
.execute(conn)
|
||||
.expect("Error saving new picture")
|
||||
}
|
||||
// diesel::insert_into(pictures::table)
|
||||
// .values(&new_picture)
|
||||
// .execute(conn)
|
||||
// .expect("Error saving new picture")
|
||||
// }
|
||||
|
||||
@@ -1,59 +1,49 @@
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
mod api;
|
||||
mod models;
|
||||
mod repository;
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
use api::user_api::{create_user, delete_user, get_user, is_logged_in, login, update_user};
|
||||
use common::OutputPicture;
|
||||
use repository::mongodb_repo::MongoRepo;
|
||||
|
||||
use std::mem::swap;
|
||||
|
||||
use actix_files as fs;
|
||||
use actix_cors::Cors;
|
||||
use actix_session::{
|
||||
config::CookieContentSecurity, storage::CookieSessionStore, SessionMiddleware,
|
||||
};
|
||||
use actix_web::{
|
||||
cookie,
|
||||
// get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer,
|
||||
// get,
|
||||
// middleware,
|
||||
get,
|
||||
http,
|
||||
middleware,
|
||||
web,
|
||||
web::Data,
|
||||
App,
|
||||
HttpServer,
|
||||
};
|
||||
use actix_web::{Responder, Result};
|
||||
use actix_web_lab::web::spa;
|
||||
use backend::establish_connection;
|
||||
use diesel::prelude::*;
|
||||
use diesel::r2d2::{self, ConnectionManager};
|
||||
|
||||
// use uuid::Uuid;
|
||||
|
||||
mod actions;
|
||||
mod models;
|
||||
mod schema;
|
||||
|
||||
use common::OutputPicture;
|
||||
|
||||
type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||
|
||||
#[get("/api/pictures/")]
|
||||
async fn get_pictures(pool: web::Data<DbPool>) -> Result<impl Responder> {
|
||||
let conn = pool.get().unwrap();
|
||||
let pics = if let Ok(p) = actions::list_pictures(&conn) {
|
||||
p
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let pics: Vec<OutputPicture> = pics
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let mut w: u32 = x.width.try_into().unwrap();
|
||||
let mut h: u32 = x.height.try_into().unwrap();
|
||||
if let Some(o) = &x.orientation {
|
||||
if o == "Rotate 270 CW" {
|
||||
swap(&mut w, &mut h);
|
||||
}
|
||||
}
|
||||
async fn get_pictures() -> Result<impl Responder> {
|
||||
let pics: Vec<OutputPicture> = (1..15)
|
||||
.map(|_| {
|
||||
// let mut w: u32 = x.width.try_into().unwrap();
|
||||
// let mut h: u32 = x.height.try_into().unwrap();
|
||||
// if let Some(o) = &x.orientation {
|
||||
// if o == "Rotate 270 CW" {
|
||||
// swap(&mut w, &mut h);
|
||||
// }
|
||||
// }
|
||||
OutputPicture {
|
||||
thumbnail: x.thumbnail.clone(),
|
||||
width: w,
|
||||
height: h,
|
||||
thumbnail: Some(String::from("https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Mila_Kunis_2018.jpg/220px-Mila_Kunis_2018.jpg")),
|
||||
// thumbnail: x.thumbnail.clone(),
|
||||
// width: w,
|
||||
// height: h,
|
||||
width: 100,
|
||||
height: 100,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -61,40 +51,50 @@ async fn get_pictures(pool: web::Data<DbPool>) -> Result<impl Responder> {
|
||||
Ok(web::Json(pics))
|
||||
}
|
||||
|
||||
embed_migrations!("migrations");
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
let connection = establish_connection();
|
||||
|
||||
// // This will run the necessary migrations.
|
||||
// embedded_migrations::run(&connection).expect("Could not migrate database.");
|
||||
|
||||
// By default the output is thrown out. If you want to redirect it to stdout, you
|
||||
// should call embedded_migrations::run_with_output.
|
||||
embedded_migrations::run_with_output(&connection, &mut std::io::stdout())
|
||||
.expect("Could not migrate database.");
|
||||
|
||||
// set up database connection pool
|
||||
let conn_spec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
|
||||
let manager = ConnectionManager::<PgConnection>::new(conn_spec);
|
||||
let pool = r2d2::Pool::builder()
|
||||
.build(manager)
|
||||
.expect("Failed to create pool.");
|
||||
let secret_key = dotenv::var("SECRET").expect("SECRET not found");
|
||||
|
||||
let host = "0.0.0.0";
|
||||
let port = 8081;
|
||||
log::info!("starting HTTP server at http://{}:{}", host, port);
|
||||
|
||||
let db = MongoRepo::init().await;
|
||||
let db_data = Data::new(db);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allowed_origin("http://localhost:8080")
|
||||
.allowed_origin_fn(|origin, _req_head| origin.as_bytes().starts_with(b"localhost"))
|
||||
.allowed_methods(vec!["GET", "POST"])
|
||||
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||
.allowed_header(http::header::CONTENT_TYPE)
|
||||
.max_age(3600);
|
||||
|
||||
App::new()
|
||||
.app_data(web::Data::new(pool.clone()))
|
||||
// .app_data(web::Data::new(pool.clone()))
|
||||
.wrap(cors)
|
||||
.wrap(
|
||||
SessionMiddleware::builder(
|
||||
CookieSessionStore::default(),
|
||||
cookie::Key::from(&secret_key.clone().into_bytes()),
|
||||
)
|
||||
.cookie_content_security(CookieContentSecurity::Private)
|
||||
.build(),
|
||||
)
|
||||
.app_data(db_data.clone())
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(get_pictures)
|
||||
.service(fs::Files::new("/api/pictures/", "./pictures/"))
|
||||
// .service(fs::Files::new("/api/pictures/", "./pictures/"))
|
||||
.service(is_logged_in)
|
||||
.service(login)
|
||||
.service(create_user)
|
||||
.service(get_user)
|
||||
.service(update_user)
|
||||
.service(delete_user)
|
||||
.service(
|
||||
spa()
|
||||
.index_file("./dist/index.html")
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use super::schema::pictures;
|
||||
|
||||
#[derive(Queryable, Serialize)]
|
||||
pub struct Picture {
|
||||
pub id: i32,
|
||||
pub filepath: String,
|
||||
pub created_at: Option<i32>,
|
||||
pub focal_length: Option<String>,
|
||||
pub shutter_speed: Option<String>,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub make: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub lens: Option<String>,
|
||||
pub orientation: Option<String>,
|
||||
pub fnumber: Option<f64>,
|
||||
pub iso: Option<i32>,
|
||||
pub exposure_program: Option<String>,
|
||||
pub exposure_compensation: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "pictures"]
|
||||
pub struct NewPicture {
|
||||
pub filepath: String,
|
||||
pub created_at: Option<i32>,
|
||||
pub focal_length: Option<String>,
|
||||
pub shutter_speed: Option<String>,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub make: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub lens: Option<String>,
|
||||
pub orientation: Option<String>,
|
||||
pub fnumber: Option<f64>,
|
||||
pub iso: Option<i32>,
|
||||
pub exposure_program: Option<String>,
|
||||
pub exposure_compensation: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
}
|
||||
1
backend/src/models/mod.rs
Normal file
1
backend/src/models/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod user_model;
|
||||
11
backend/src/models/user_model.rs
Normal file
11
backend/src/models/user_model.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use mongodb::bson::oid::ObjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<ObjectId>,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
1
backend/src/repository/mod.rs
Normal file
1
backend/src/repository/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod mongodb_repo;
|
||||
100
backend/src/repository/mongodb_repo.rs
Normal file
100
backend/src/repository/mongodb_repo.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::env;
|
||||
extern crate dotenv;
|
||||
use dotenv::dotenv;
|
||||
|
||||
use crate::models::user_model::User;
|
||||
use mongodb::{
|
||||
bson::{doc, extjson::de::Error, oid::ObjectId},
|
||||
results::{DeleteResult, InsertOneResult, UpdateResult},
|
||||
Client, Collection,
|
||||
};
|
||||
|
||||
pub struct MongoRepo {
|
||||
col: Collection<User>,
|
||||
}
|
||||
|
||||
impl MongoRepo {
|
||||
pub async fn init() -> Self {
|
||||
dotenv().ok();
|
||||
let uri = match env::var("MONGOURI") {
|
||||
Ok(v) => v.to_string(),
|
||||
Err(_) => format!("Error loading env variable"),
|
||||
};
|
||||
let client = Client::with_uri_str(uri).await.unwrap();
|
||||
let db = client.database("photos");
|
||||
let col: Collection<User> = db.collection("User");
|
||||
MongoRepo { col }
|
||||
}
|
||||
|
||||
pub async fn create_user(&self, new_user: User) -> Result<InsertOneResult, Error> {
|
||||
let new_doc = User {
|
||||
id: None,
|
||||
username: new_user.username,
|
||||
email: new_user.email,
|
||||
password: new_user.password,
|
||||
};
|
||||
let user = self
|
||||
.col
|
||||
.insert_one(new_doc, None)
|
||||
.await
|
||||
.ok()
|
||||
.expect("Error creating user");
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, id: &String) -> Result<User, Error> {
|
||||
let obj_id = ObjectId::parse_str(id).unwrap();
|
||||
let filter = doc! {"_id": obj_id};
|
||||
let user_detail = self
|
||||
.col
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.ok()
|
||||
.expect("Error getting user's detail");
|
||||
Ok(user_detail.unwrap())
|
||||
}
|
||||
|
||||
pub async fn get_user_from_email(&self, email: &String) -> Result<User, Error> {
|
||||
let filter = doc! {"email": email};
|
||||
let user_detail = self
|
||||
.col
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.ok()
|
||||
.expect("Error getting user's detail");
|
||||
Ok(user_detail.unwrap())
|
||||
}
|
||||
|
||||
pub async fn update_user(&self, id: &String, new_user: User) -> Result<UpdateResult, Error> {
|
||||
let obj_id = ObjectId::parse_str(id).unwrap();
|
||||
let filter = doc! {"_id": obj_id};
|
||||
let new_doc = doc! {
|
||||
"$set":
|
||||
{
|
||||
"id": new_user.id,
|
||||
"username": new_user.username,
|
||||
"email": new_user.email,
|
||||
"password": new_user.password,
|
||||
},
|
||||
};
|
||||
let updated_doc = self
|
||||
.col
|
||||
.update_one(filter, new_doc, None)
|
||||
.await
|
||||
.ok()
|
||||
.expect("Error updating user");
|
||||
Ok(updated_doc)
|
||||
}
|
||||
|
||||
pub async fn delete_user(&self, id: &String) -> Result<DeleteResult, Error> {
|
||||
let obj_id = ObjectId::parse_str(id).unwrap();
|
||||
let filter = doc! {"_id": obj_id};
|
||||
let user_detail = self
|
||||
.col
|
||||
.delete_one(filter, None)
|
||||
.await
|
||||
.ok()
|
||||
.expect("Error deleting user");
|
||||
Ok(user_detail)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
table! {
|
||||
pictures (id) {
|
||||
id -> Int4,
|
||||
filepath -> Varchar,
|
||||
created_at -> Nullable<Int4>,
|
||||
focal_length -> Nullable<Varchar>,
|
||||
shutter_speed -> Nullable<Varchar>,
|
||||
width -> Int4,
|
||||
height -> Int4,
|
||||
make -> Nullable<Varchar>,
|
||||
model -> Nullable<Varchar>,
|
||||
lens -> Nullable<Varchar>,
|
||||
orientation -> Nullable<Varchar>,
|
||||
fnumber -> Nullable<Float8>,
|
||||
iso -> Nullable<Int4>,
|
||||
exposure_program -> Nullable<Varchar>,
|
||||
exposure_compensation -> Nullable<Varchar>,
|
||||
thumbnail -> Nullable<Varchar>,
|
||||
}
|
||||
}
|
||||
// table! {
|
||||
// pictures (id) {
|
||||
// id -> Int4,
|
||||
// filepath -> Varchar,
|
||||
// created_at -> Nullable<Int4>,
|
||||
// focal_length -> Nullable<Varchar>,
|
||||
// shutter_speed -> Nullable<Varchar>,
|
||||
// width -> Int4,
|
||||
// height -> Int4,
|
||||
// make -> Nullable<Varchar>,
|
||||
// model -> Nullable<Varchar>,
|
||||
// lens -> Nullable<Varchar>,
|
||||
// orientation -> Nullable<Varchar>,
|
||||
// fnumber -> Nullable<Float8>,
|
||||
// iso -> Nullable<Int4>,
|
||||
// exposure_program -> Nullable<Varchar>,
|
||||
// exposure_compensation -> Nullable<Varchar>,
|
||||
// thumbnail -> Nullable<Varchar>,
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -2,23 +2,23 @@ version: '3.1'
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
- POSTGRES_DB=diesel_demo
|
||||
- POSTGRES_USER=user
|
||||
- POSTGRES_PASSWORD=password
|
||||
# db:
|
||||
# image: postgres
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - ./data:/var/lib/postgresql/data
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# environment:
|
||||
# - POSTGRES_DB=diesel_demo
|
||||
# - POSTGRES_USER=user
|
||||
# - POSTGRES_PASSWORD=password
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 3000:8080
|
||||
# adminer:
|
||||
# image: adminer
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 3000:8080
|
||||
|
||||
# photos:
|
||||
# build: .
|
||||
@@ -31,3 +31,22 @@ services:
|
||||
# - db
|
||||
# links:
|
||||
# - db
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
restart: always
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: jheuel
|
||||
MONGO_INITDB_ROOT_PASSWORD: bla
|
||||
ports:
|
||||
- 27017:27017
|
||||
|
||||
mongo-express:
|
||||
image: mongo-express
|
||||
restart: always
|
||||
ports:
|
||||
- 8082:8081
|
||||
environment:
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME: jheuel
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: bla
|
||||
ME_CONFIG_MONGODB_URL: mongodb://jheuel:bla@mongo:27017/
|
||||
|
||||
@@ -7,20 +7,29 @@ license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.6"
|
||||
wasm-bindgen = "=0.2.82"
|
||||
wasm-bindgen = "=0.2.84"
|
||||
wasm-bindgen-futures = "0.4.32"
|
||||
gloo-net = "0.2.3"
|
||||
gloo-storage = "0.2"
|
||||
weblog = "0.3.0"
|
||||
web-sys = {version = "0.3.61", features = ["Window", "DataTransfer", "DataTransferItemList", "DataTransferItem", "FileSystemEntry", "FileSystemDirectoryEntry", "FileSystemDirectoryReader", "FileSystemDirectoryReader", "DragEvent"]}
|
||||
js-sys = "0.3.61"
|
||||
wee_alloc = "0.4.5"
|
||||
ybc = { git = "https://github.com/jheuel/ybc", branch = "yew-0-19-update" }
|
||||
yew = "0.19"
|
||||
yew-hooks = "0.1.56"
|
||||
ybc = { git = "https://github.com/jheuel/ybc", branch = "alpha-v0.4" }
|
||||
yew = "0.20"
|
||||
yew-router = "0.17"
|
||||
yew-hooks = "0.2.0"
|
||||
pathfinding = "3.0.13"
|
||||
common = { path = "../common" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0.58"
|
||||
yewtil = { version = "0.4.0", features = ["neq"] }
|
||||
thiserror = "1"
|
||||
lazy_static = "1.4"
|
||||
parking_lot = "0.12"
|
||||
dotenv = "0.15.0"
|
||||
dotenv_codegen = "0.15.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
97
frontend/src/components/base_page.rs
Normal file
97
frontend/src/components/base_page.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use crate::hooks::use_user_context;
|
||||
use crate::Route;
|
||||
use ybc::NavbarFixed::Top;
|
||||
use yew::prelude::*;
|
||||
use yew_hooks::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
pub struct Props {
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn BasePage(props: &Props) -> Html {
|
||||
let navigator = use_navigator().unwrap();
|
||||
let user_ctx = use_user_context();
|
||||
|
||||
let node = use_node_ref();
|
||||
let size = use_size(node.clone());
|
||||
|
||||
let authenticated = user_ctx.is_authenticated();
|
||||
let title = if authenticated {
|
||||
html! {"Photos"}
|
||||
} else {
|
||||
html! {"No photos"}
|
||||
};
|
||||
|
||||
let navbrand = html! {
|
||||
<ybc::NavbarItem>
|
||||
<ybc::Title
|
||||
classes={classes!("has-text-white")}
|
||||
size={ybc::HeaderSize::Is4}>
|
||||
{title}
|
||||
</ybc::Title>
|
||||
</ybc::NavbarItem>
|
||||
};
|
||||
|
||||
let account_button = if authenticated {
|
||||
let onclick = {
|
||||
Callback::from(move |_| {
|
||||
user_ctx.logout();
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<ybc::ButtonAnchor
|
||||
classes={classes!("is-outlined")}
|
||||
rel={String::from("noopener noreferrer")}
|
||||
target={String::from("_blank")}
|
||||
href=""
|
||||
{onclick}>
|
||||
{"logout"}
|
||||
</ybc::ButtonAnchor>
|
||||
}
|
||||
} else {
|
||||
let onclick = Callback::from(move |_| navigator.push(&Route::Login));
|
||||
html! {
|
||||
<ybc::Button
|
||||
// classes={classes!("is-outlined")}
|
||||
{onclick}>
|
||||
{"login"}
|
||||
</ybc::Button>
|
||||
}
|
||||
};
|
||||
|
||||
let navstart = html! {};
|
||||
let navend = html! {
|
||||
<>
|
||||
// <ybc::NavbarItem>
|
||||
// <ybc::Button
|
||||
// // classes={classes!("is-outlined")}
|
||||
// >
|
||||
// {"Photos"}
|
||||
// </ybc::Button>
|
||||
// </ybc::NavbarItem>
|
||||
<ybc::NavbarItem>
|
||||
{account_button}
|
||||
</ybc::NavbarItem>
|
||||
</>
|
||||
};
|
||||
|
||||
html! {
|
||||
<>
|
||||
<ybc::Navbar
|
||||
fixed={Top}
|
||||
classes={classes!("is-info")}
|
||||
padded={true}
|
||||
{navbrand}
|
||||
{navstart}
|
||||
{navend}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{ props.children.clone() }
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
41
frontend/src/components/home.js
Normal file
41
frontend/src/components/home.js
Normal file
@@ -0,0 +1,41 @@
|
||||
function getFilesDataTransferItems(dataTransferItems) {
|
||||
function traverseFileTreePromise(item, path = "", folder) {
|
||||
return new Promise(resolve => {
|
||||
if (item.isFile) {
|
||||
item.file(file => {
|
||||
file.filepath = path + "/" + file.name; //save full path
|
||||
folder.push(file);
|
||||
resolve(file);
|
||||
});
|
||||
} else if (item.isDirectory) {
|
||||
let dirReader = item.createReader();
|
||||
dirReader.readEntries(entries => {
|
||||
let entriesPromises = [];
|
||||
let subfolder = [];
|
||||
folder.push({ name: item.name, subfolder: subfolder });
|
||||
for (let entry of entries)
|
||||
entriesPromises.push(
|
||||
traverseFileTreePromise(entry, path + "/" + item.name, subfolder)
|
||||
);
|
||||
resolve(Promise.all(entriesPromises));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let files = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
let entriesPromises = [];
|
||||
for (let it of dataTransferItems)
|
||||
entriesPromises.push(
|
||||
traverseFileTreePromise(it.webkitGetAsEntry(), "", files)
|
||||
);
|
||||
Promise.all(entriesPromises).then(entries => {
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function get_files_data_transfer_items(data_transfer_items) {
|
||||
return getFilesDataTransferItems(data_transfer_items);
|
||||
}
|
||||
132
frontend/src/components/home.rs
Normal file
132
frontend/src/components/home.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use super::BasePage;
|
||||
use crate::gallery::Grid;
|
||||
use crate::hooks::use_user_context;
|
||||
use gloo_net::http::Request;
|
||||
|
||||
use js_sys::Array;
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
|
||||
use web_sys::{DataTransferItemList, File, FileSystemDirectoryEntry, FileSystemEntry};
|
||||
use weblog::console_log;
|
||||
use yew::prelude::*;
|
||||
use yew_hooks::prelude::*;
|
||||
|
||||
use common::OutputPicture;
|
||||
|
||||
#[function_component(Home)]
|
||||
pub fn home() -> Html {
|
||||
let user_ctx = use_user_context();
|
||||
|
||||
let node = use_node_ref();
|
||||
let size = use_size(node.clone());
|
||||
|
||||
let pictures = use_state(std::vec::Vec::new);
|
||||
|
||||
{
|
||||
let pictures = pictures.clone();
|
||||
use_effect_with_deps(
|
||||
move |_| {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let url = "/api/pictures/";
|
||||
let fetched_pictures: Vec<OutputPicture> = Request::get(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
pictures.set(fetched_pictures);
|
||||
});
|
||||
|| ()
|
||||
},
|
||||
(),
|
||||
);
|
||||
}
|
||||
|
||||
let ondrop = Callback::from(move |e: DragEvent| {
|
||||
e.prevent_default();
|
||||
|
||||
let items = e.data_transfer().unwrap().items();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let promise = get_files_data_transfer_items(items);
|
||||
let result = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap();
|
||||
|
||||
console_log!(&result);
|
||||
|
||||
let traverse = |d: JsValue| {
|
||||
let mut new_files: Vec<JsValue> = Vec::new();
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
|
||||
if let Ok(fse) = d.clone().dyn_into::<File>() {
|
||||
console_log!(&fse);
|
||||
// fse.filepath = path;
|
||||
files.push(fse);
|
||||
} else if let Ok(a) = d.clone().dyn_into::<js_sys::Array>() {
|
||||
for i in 0..a.length() {
|
||||
let f = a.get(i);
|
||||
new_files.push(f);
|
||||
}
|
||||
} else if let Ok(o) = d.clone().dyn_into::<js_sys::Object>() {
|
||||
if let Ok(subfolder) = js_sys::Reflect::get(&o, &JsValue::from_str("subfolder"))
|
||||
{
|
||||
new_files.push(subfolder);
|
||||
}
|
||||
}
|
||||
(new_files, files)
|
||||
};
|
||||
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
let mut new_files = vec![result];
|
||||
|
||||
loop {
|
||||
console_log!(files.len());
|
||||
let Some(current) = new_files.pop() else {
|
||||
console_log!("break");
|
||||
|
||||
break;
|
||||
};
|
||||
|
||||
let (news, fs) = traverse(current);
|
||||
news.iter().for_each(|x| new_files.push(x.clone()));
|
||||
fs.iter().for_each(|x| files.push(x.clone()));
|
||||
}
|
||||
let uiae: Vec<String> = files
|
||||
.iter()
|
||||
.map(|x| {
|
||||
if let Ok(filepath) = js_sys::Reflect::get(&x, &JsValue::from_str("filepath")) {
|
||||
filepath.as_string().unwrap_or("".to_string())
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
console_log!("end", uiae.join("\n"));
|
||||
});
|
||||
});
|
||||
|
||||
let ondragover = Callback::from(move |e: DragEvent| {
|
||||
e.prevent_default();
|
||||
});
|
||||
|
||||
let body = if user_ctx.is_authenticated() {
|
||||
html! {
|
||||
<Grid
|
||||
pictures={(*pictures).clone()}
|
||||
width={size.0}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
};
|
||||
html! {
|
||||
<BasePage>
|
||||
<div ref={node} {ondrop} {ondragover}>
|
||||
{body}
|
||||
</div>
|
||||
</BasePage>
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(module = "/src/components/home.js")]
|
||||
extern "C" {
|
||||
fn get_files_data_transfer_items(data_transfer_items: DataTransferItemList) -> js_sys::Promise;
|
||||
}
|
||||
48
frontend/src/components/list_errors.rs
Normal file
48
frontend/src/components/list_errors.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Properties, Clone, PartialEq, Eq)]
|
||||
pub struct Props {
|
||||
pub error: Option<Error>,
|
||||
}
|
||||
|
||||
#[function_component(ListErrors)]
|
||||
pub fn list_errors(props: &Props) -> Html {
|
||||
if let Some(error) = &props.error {
|
||||
html! {
|
||||
<ul class="error-messages">
|
||||
{
|
||||
match error {
|
||||
Error::UnprocessableEntity(error_info) => {
|
||||
html! {
|
||||
<>
|
||||
{for error_info.errors.iter().map(|(key, value)| {
|
||||
html! {
|
||||
<li>
|
||||
{ key }
|
||||
{for value.iter().map(|e| {
|
||||
html! {
|
||||
<>{" "} {e}</>
|
||||
}
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
})}
|
||||
</>
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
html! {
|
||||
<li>{error}</li>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
148
frontend/src/components/login.rs
Normal file
148
frontend/src/components/login.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
use ybc::TileCtx::{Ancestor, Child, Parent};
|
||||
use ybc::TileSize::*;
|
||||
use yew::prelude::*;
|
||||
use yew_hooks::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use super::BasePage;
|
||||
use crate::components::list_errors::ListErrors;
|
||||
use crate::hooks::use_user_context;
|
||||
use crate::services::auth::*;
|
||||
use crate::types::{LoginInfo, LoginInfoWrapper, UserInfoWrapper};
|
||||
use crate::Route;
|
||||
|
||||
/// Login page
|
||||
#[function_component(Login)]
|
||||
pub fn login_page() -> Html {
|
||||
let user_ctx = use_user_context();
|
||||
let login_info = use_state(LoginInfo::default);
|
||||
let user_login = {
|
||||
let login_info = login_info.clone();
|
||||
use_async(async move {
|
||||
let request = LoginInfoWrapper {
|
||||
user: (*login_info).clone(),
|
||||
};
|
||||
login(&request).send::<UserInfoWrapper>().await
|
||||
})
|
||||
};
|
||||
|
||||
use_effect_with_deps(
|
||||
move |user_login| {
|
||||
if let Some(user_info) = &user_login.data {
|
||||
user_ctx.login(user_info.user.clone());
|
||||
}
|
||||
|| ()
|
||||
},
|
||||
user_login.clone(),
|
||||
);
|
||||
|
||||
let onsubmit = {
|
||||
let user_login = user_login.clone();
|
||||
Callback::from(move |e: SubmitEvent| {
|
||||
e.prevent_default(); /* Prevent event propagation */
|
||||
user_login.run();
|
||||
})
|
||||
};
|
||||
let oninput_email = {
|
||||
let login_info = login_info.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let mut info = (*login_info).clone();
|
||||
info.email = input.value();
|
||||
login_info.set(info);
|
||||
})
|
||||
};
|
||||
let oninput_password = {
|
||||
let login_info = login_info.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let mut info = (*login_info).clone();
|
||||
info.password = input.value();
|
||||
login_info.set(info);
|
||||
})
|
||||
};
|
||||
|
||||
let form = html! {
|
||||
<div class="auth-page">
|
||||
<div class="container page">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3 col-xs-12">
|
||||
<h1 class="text-xs-center">{ "Sign In" }</h1>
|
||||
<p class="text-xs-center">
|
||||
<Link<Route> to={Route::Register}>
|
||||
{ "Need an account?" }
|
||||
</Link<Route>>
|
||||
</p>
|
||||
<ListErrors error={user_login.error.clone()} />
|
||||
<form {onsubmit}>
|
||||
<fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={login_info.email.clone()}
|
||||
oninput={oninput_email}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={login_info.password.clone()}
|
||||
oninput={oninput_password}
|
||||
/>
|
||||
</fieldset>
|
||||
<button
|
||||
class="btn btn-lg btn-primary pull-xs-right"
|
||||
type="submit"
|
||||
disabled=false>
|
||||
{ "Sign in" }
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
let hero_body = html! {
|
||||
<ybc::Container
|
||||
fluid=true
|
||||
|
||||
classes={
|
||||
classes!(
|
||||
"is-centered",
|
||||
"is-light",
|
||||
)
|
||||
}
|
||||
>
|
||||
<ybc::Tile ctx={Ancestor}>
|
||||
<ybc::Tile ctx={Parent} vertical=true size={Twelve}>
|
||||
<ybc::Tile ctx={Child} classes={classes!("box")}>
|
||||
{form}
|
||||
</ybc::Tile>
|
||||
</ybc::Tile>
|
||||
</ybc::Tile>
|
||||
</ybc::Container>
|
||||
};
|
||||
|
||||
html! {
|
||||
<BasePage>
|
||||
<ybc::Hero
|
||||
classes={
|
||||
classes!(
|
||||
"is-light",
|
||||
)
|
||||
}
|
||||
size={ybc::HeroSize::FullheightWithNavbar}
|
||||
body={hero_body}
|
||||
>
|
||||
</ybc::Hero>
|
||||
</BasePage>
|
||||
}
|
||||
}
|
||||
16
frontend/src/components/mod.rs
Normal file
16
frontend/src/components/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod home;
|
||||
pub use home::Home;
|
||||
|
||||
pub mod register;
|
||||
pub use register::Register;
|
||||
|
||||
pub mod login;
|
||||
pub use login::Login;
|
||||
|
||||
pub mod user_context_provider;
|
||||
pub use user_context_provider::*;
|
||||
|
||||
pub mod base_page;
|
||||
pub use base_page::*;
|
||||
|
||||
pub mod list_errors;
|
||||
168
frontend/src/components/register.rs
Normal file
168
frontend/src/components/register.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
use ybc::TileCtx::{Ancestor, Child, Parent};
|
||||
use ybc::TileSize::*;
|
||||
use yew::prelude::*;
|
||||
use yew_hooks::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use super::BasePage;
|
||||
use crate::components::list_errors::ListErrors;
|
||||
use crate::hooks::use_user_context;
|
||||
use crate::services::auth::*;
|
||||
use crate::types::{RegisterInfo, RegisterInfoWrapper, UserInfoWrapper};
|
||||
use crate::Route;
|
||||
|
||||
/// Register page
|
||||
#[function_component(Register)]
|
||||
pub fn register_page() -> Html {
|
||||
let user_ctx = use_user_context();
|
||||
let register_info = use_state(RegisterInfo::default);
|
||||
let user_register = {
|
||||
let register_info = register_info.clone();
|
||||
use_async(async move {
|
||||
let request = RegisterInfoWrapper {
|
||||
user: (*register_info).clone(),
|
||||
};
|
||||
register(&request).send::<UserInfoWrapper>().await
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
use_effect_with_deps(
|
||||
move |user_register| {
|
||||
if let Some(user_info) = &user_register.data {
|
||||
user_ctx.login(user_info.user.clone());
|
||||
}
|
||||
|| ()
|
||||
},
|
||||
user_register.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
let onsubmit = {
|
||||
let user_register = user_register.clone();
|
||||
Callback::from(move |e: SubmitEvent| {
|
||||
e.prevent_default(); /* Prevent event propagation */
|
||||
user_register.run();
|
||||
})
|
||||
};
|
||||
let oninput_username = {
|
||||
let register_info = register_info.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let mut info = (*register_info).clone();
|
||||
info.username = input.value();
|
||||
register_info.set(info);
|
||||
})
|
||||
};
|
||||
let oninput_email = {
|
||||
let register_info = register_info.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let mut info = (*register_info).clone();
|
||||
info.email = input.value();
|
||||
register_info.set(info);
|
||||
})
|
||||
};
|
||||
let oninput_password = {
|
||||
let register_info = register_info.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let mut info = (*register_info).clone();
|
||||
info.password = input.value();
|
||||
register_info.set(info);
|
||||
})
|
||||
};
|
||||
|
||||
let form = html! {
|
||||
<div class="auth-page">
|
||||
<div class="container page">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3 col-xs-12">
|
||||
<h1 class="text-xs-center">{ "Sign Up" }</h1>
|
||||
<p class="text-xs-center">
|
||||
<Link<Route> to={Route::Login}>
|
||||
{ "Have an account?" }
|
||||
</Link<Route>>
|
||||
</p>
|
||||
<ListErrors error={user_register.error.clone()} />
|
||||
<form {onsubmit}>
|
||||
<fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={register_info.username.clone()}
|
||||
oninput={oninput_username}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={register_info.email.clone()}
|
||||
oninput={oninput_email}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={register_info.password.clone()}
|
||||
oninput={oninput_password}
|
||||
/>
|
||||
</fieldset>
|
||||
<button
|
||||
class="btn btn-lg btn-primary pull-xs-right"
|
||||
type="submit"
|
||||
disabled=false>
|
||||
{ "Sign up" }
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
let hero_body = html! {
|
||||
<ybc::Container
|
||||
fluid=true
|
||||
|
||||
classes={
|
||||
classes!(
|
||||
"is-centered",
|
||||
"is-light",
|
||||
)
|
||||
}
|
||||
>
|
||||
<ybc::Tile ctx={Ancestor}>
|
||||
<ybc::Tile ctx={Parent} vertical=true size={Twelve}>
|
||||
<ybc::Tile ctx={Child} classes={classes!("box")}>
|
||||
{form}
|
||||
</ybc::Tile>
|
||||
</ybc::Tile>
|
||||
</ybc::Tile>
|
||||
</ybc::Container>
|
||||
};
|
||||
|
||||
html! {
|
||||
<BasePage>
|
||||
<ybc::Hero
|
||||
classes={
|
||||
classes!(
|
||||
"is-light",
|
||||
)
|
||||
}
|
||||
size={ybc::HeroSize::FullheightWithNavbar}
|
||||
body={hero_body}
|
||||
>
|
||||
</ybc::Hero>
|
||||
</BasePage>
|
||||
}
|
||||
}
|
||||
56
frontend/src/components/user_context_provider.rs
Normal file
56
frontend/src/components/user_context_provider.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
//! User context provider.
|
||||
|
||||
use weblog::console_log;
|
||||
use yew::prelude::*;
|
||||
use yew_hooks::prelude::*;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::services::{auth::*, get_token, set_token};
|
||||
use crate::types::{UserInfo, UserInfoWrapper};
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct Props {
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
/// User context provider.
|
||||
#[function_component(UserContextProvider)]
|
||||
pub fn user_context_provider(props: &Props) -> Html {
|
||||
let user_ctx = use_state(UserInfo::default);
|
||||
let current_user = use_async(async move { current().send::<UserInfoWrapper>().await });
|
||||
|
||||
{
|
||||
let current_user = current_user.clone();
|
||||
use_mount(move || {
|
||||
if get_token().is_some() {
|
||||
current_user.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let user_ctx = user_ctx.clone();
|
||||
use_effect_with_deps(
|
||||
move |current_user| {
|
||||
if let Some(user_info) = ¤t_user.data {
|
||||
user_ctx.set(user_info.user.clone());
|
||||
}
|
||||
|
||||
if let Some(error) = ¤t_user.error {
|
||||
match error {
|
||||
Error::Unauthorized | Error::Forbidden => set_token(None),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|| ()
|
||||
},
|
||||
current_user,
|
||||
)
|
||||
}
|
||||
|
||||
html! {
|
||||
<ContextProvider<UseStateHandle<UserInfo>> context={user_ctx}>
|
||||
{ for props.children.iter() }
|
||||
</ContextProvider<UseStateHandle<UserInfo>>>
|
||||
}
|
||||
}
|
||||
49
frontend/src/error.rs
Normal file
49
frontend/src/error.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Error type for error handling
|
||||
|
||||
use crate::types::ErrorInfo;
|
||||
use thiserror::Error as ThisError;
|
||||
|
||||
/// Define all possible errors
|
||||
#[derive(ThisError, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// 401
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
|
||||
/// 403
|
||||
#[error("Forbidden")]
|
||||
Forbidden,
|
||||
|
||||
/// 404
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
|
||||
/// 422
|
||||
#[error("Unprocessable Entity: {0:?}")]
|
||||
UnprocessableEntity(ErrorInfo),
|
||||
|
||||
/// 500
|
||||
#[error("Internal Server Error")]
|
||||
InternalServerError,
|
||||
|
||||
/// serde deserialize error
|
||||
#[error("Deserialize Error")]
|
||||
DeserializeError,
|
||||
|
||||
/// request error
|
||||
#[error("Http Request Error")]
|
||||
RequestError,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn from_status_code(status: u16, reported_error: Error) -> Self {
|
||||
match status {
|
||||
401 => Error::Unauthorized,
|
||||
403 => Error::Forbidden,
|
||||
404 => Error::NotFound,
|
||||
500 => Error::InternalServerError,
|
||||
422 => reported_error,
|
||||
_ => Error::RequestError,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ pub struct GridProps {
|
||||
|
||||
#[function_component(Grid)]
|
||||
pub fn grid(props: &GridProps) -> Html {
|
||||
let target_height = 300;
|
||||
let target_height = 100;
|
||||
let container_width = if props.width == 0 { 0 } else { props.width - 4 };
|
||||
let margin = 2;
|
||||
let dimensions = compute_row_layout(
|
||||
@@ -44,6 +44,7 @@ pub fn grid(props: &GridProps) -> Html {
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
html! {
|
||||
<div style={
|
||||
concat!(
|
||||
@@ -52,7 +53,14 @@ pub fn grid(props: &GridProps) -> Html {
|
||||
"flex-direction: row;",
|
||||
)}>
|
||||
{ props.pictures.iter().zip(dimensions).map(|(p, d)|
|
||||
html!{<Picture margin={margin} picture={p.clone()} width={d.width} height={d.height} />}
|
||||
html!{
|
||||
<Picture
|
||||
margin={margin}
|
||||
picture={p.clone()}
|
||||
width={d.width}
|
||||
height={d.height}
|
||||
/>
|
||||
}
|
||||
).collect::<Html>()}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use pathfinding::prelude::dijkstra;
|
||||
use weblog::console_log;
|
||||
|
||||
pub struct Rect {
|
||||
pub width: u32,
|
||||
@@ -6,7 +7,8 @@ pub struct Rect {
|
||||
}
|
||||
|
||||
pub fn get_common_height(row: &[Rect], container_width: u32, margin: u32) -> f32 {
|
||||
let row_width: u32 = container_width - row.len() as u32 * (margin * 2);
|
||||
debug_assert!(container_width > (row.len() as u32) * (margin * 2));
|
||||
let row_width: u32 = container_width - (row.len() as u32) * (margin * 2);
|
||||
let total_aspect_ratio: f32 = row
|
||||
.iter()
|
||||
.map(|p| (p.width as f32) / (p.height as f32))
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct PictureProps {
|
||||
#[function_component(Picture)]
|
||||
pub fn picture(props: &PictureProps) -> Html {
|
||||
let thumb = if let Some(thumb) = &props.picture.thumbnail {
|
||||
format!("/api/pictures/{}", thumb)
|
||||
format!("{}", thumb)
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
3
frontend/src/hooks/mod.rs
Normal file
3
frontend/src/hooks/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod use_user_context;
|
||||
|
||||
pub use use_user_context::*;
|
||||
73
frontend/src/hooks/use_user_context.rs
Normal file
73
frontend/src/hooks/use_user_context.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::services::set_token;
|
||||
use crate::types::UserInfo;
|
||||
use crate::Route;
|
||||
|
||||
/// State handle for the [`use_user_context`] hook.
|
||||
pub struct UseUserContextHandle {
|
||||
inner: UseStateHandle<UserInfo>,
|
||||
navigator: Navigator,
|
||||
}
|
||||
|
||||
impl UseUserContextHandle {
|
||||
pub fn login(&self, value: UserInfo) {
|
||||
// Set global token after logged in
|
||||
set_token(Some(value.token.clone()));
|
||||
self.inner.set(value);
|
||||
// Redirect to home page
|
||||
self.navigator.push(&Route::Home);
|
||||
}
|
||||
|
||||
pub fn logout(&self) {
|
||||
// Clear global token after logged out
|
||||
set_token(None);
|
||||
self.inner.set(UserInfo::default());
|
||||
// Redirect to home page
|
||||
self.navigator.push(&Route::Home);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for UseUserContextHandle {
|
||||
type Target = UserInfo;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for UseUserContextHandle {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
navigator: self.navigator.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UseUserContextHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
*self.inner == *other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for UseUserContextHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("UseUserContextHandle")
|
||||
.field("value", &format!("{:?}", *self.inner))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// This hook is used to manage user context.
|
||||
#[hook]
|
||||
pub fn use_user_context() -> UseUserContextHandle {
|
||||
let inner = use_context::<UseStateHandle<UserInfo>>().unwrap();
|
||||
let navigator = use_navigator().unwrap();
|
||||
|
||||
UseUserContextHandle { inner, navigator }
|
||||
}
|
||||
@@ -3,111 +3,52 @@
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
use console_error_panic_hook::set_once as set_panic_hook;
|
||||
use gloo_net::http::Request;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use yew_hooks::prelude::*;
|
||||
// use weblog::console_log;
|
||||
|
||||
use ybc::NavbarFixed::Top;
|
||||
use ybc::TileCtx::{Ancestor, Child, Parent};
|
||||
use ybc::TileSize::*;
|
||||
|
||||
mod components;
|
||||
mod error;
|
||||
mod gallery;
|
||||
use common::OutputPicture;
|
||||
use gallery::Grid;
|
||||
mod hooks;
|
||||
mod services;
|
||||
mod types;
|
||||
|
||||
use components::{Home, Login, Register, UserContextProvider};
|
||||
use console_error_panic_hook::set_once as set_panic_hook;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let node = use_node_ref();
|
||||
let size = use_size(node.clone());
|
||||
#[derive(Clone, Routable, PartialEq)]
|
||||
enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/register")]
|
||||
Register,
|
||||
#[at("/login")]
|
||||
Login,
|
||||
#[not_found]
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
let pictures = use_state(std::vec::Vec::new);
|
||||
{
|
||||
let pictures = pictures.clone();
|
||||
use_effect_with_deps(
|
||||
move |_| {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let url = "/api/pictures/";
|
||||
let fetched_pictures: Vec<OutputPicture> = Request::get(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
pictures.set(fetched_pictures);
|
||||
});
|
||||
|| ()
|
||||
},
|
||||
(),
|
||||
);
|
||||
}
|
||||
let navbrand = html! {
|
||||
<ybc::NavbarItem>
|
||||
<ybc::Title
|
||||
classes={classes!("has-text-white")}
|
||||
size={ybc::HeaderSize::Is4}>
|
||||
{"Photos"}
|
||||
</ybc::Title>
|
||||
</ybc::NavbarItem>
|
||||
};
|
||||
let navstart = html! {};
|
||||
let navend = html! {
|
||||
<>
|
||||
<ybc::NavbarItem>
|
||||
<ybc::ButtonAnchor
|
||||
classes={classes!("is-outlined")}
|
||||
rel={String::from("noopener noreferrer")}
|
||||
target={String::from("_blank")}
|
||||
href="">
|
||||
{"Photos"}
|
||||
</ybc::ButtonAnchor>
|
||||
</ybc::NavbarItem>
|
||||
</>
|
||||
};
|
||||
|
||||
html! {
|
||||
<>
|
||||
<ybc::Navbar
|
||||
fixed={Top}
|
||||
classes={classes!("is-info")}
|
||||
padded={true}
|
||||
{navbrand}
|
||||
{navstart}
|
||||
{navend}
|
||||
/>
|
||||
<ybc::Container fluid=true classes={classes!("is-centered")}>
|
||||
<ybc::Tile ctx={Ancestor}>
|
||||
<ybc::Tile ctx={Parent} vertical=true size={Twelve}>
|
||||
<ybc::Tile ctx={Child} classes={classes!("box")}>
|
||||
<div ref={node} style={
|
||||
concat!(
|
||||
"position: 'relative';",
|
||||
)}>
|
||||
<Grid
|
||||
pictures={(*pictures).clone()}
|
||||
width={size.0}
|
||||
/>
|
||||
</div>
|
||||
</ybc::Tile>
|
||||
</ybc::Tile>
|
||||
</ybc::Tile>
|
||||
</ybc::Container>
|
||||
</>
|
||||
fn switch(routes: Route) -> Html {
|
||||
match routes {
|
||||
Route::Home => html! { <Home /> },
|
||||
Route::Register => html! { <Register /> },
|
||||
Route::Login => html! { <Login /> },
|
||||
Route::NotFound => html! { <h1>{ "404" }</h1> },
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(inline_js = "export function snippetTest() { console.log('Hello from JS FFI!'); }")]
|
||||
extern "C" {
|
||||
fn snippetTest();
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
html! {
|
||||
<BrowserRouter>
|
||||
<UserContextProvider>
|
||||
<Switch<Route> render={switch} />
|
||||
</UserContextProvider>
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
set_panic_hook();
|
||||
snippetTest();
|
||||
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
22
frontend/src/services/auth.rs
Normal file
22
frontend/src/services/auth.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
pub use super::requests::{request_delete, request_get, request_post, request_put, Request};
|
||||
use crate::types;
|
||||
|
||||
/// Get current user info
|
||||
pub fn current() -> Request {
|
||||
request_get("/api/user")
|
||||
}
|
||||
|
||||
/// Login a user
|
||||
pub fn login(login_info: &types::LoginInfoWrapper) -> Request {
|
||||
request_post("/api/users/login", login_info)
|
||||
}
|
||||
|
||||
/// Register a new user
|
||||
pub fn register(register_info: &types::RegisterInfoWrapper) -> Request {
|
||||
request_post("/api/user", register_info)
|
||||
}
|
||||
|
||||
/// Save info of current user
|
||||
pub fn save(user_update_info: &types::UserUpdateInfoWrapper) -> Request {
|
||||
request_put("/api/user", user_update_info)
|
||||
}
|
||||
41
frontend/src/services/mod.rs
Normal file
41
frontend/src/services/mod.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
pub mod auth;
|
||||
pub mod requests;
|
||||
|
||||
use dotenv_codegen::dotenv;
|
||||
|
||||
pub use requests::{request_delete, request_get, request_post, request_put};
|
||||
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
const API_ROOT: &str = dotenv!("API_ROOT");
|
||||
const TOKEN_KEY: &str = "jheuel-token";
|
||||
|
||||
lazy_static! {
|
||||
/// Jwt token read from local storage.
|
||||
pub static ref TOKEN: RwLock<Option<String>> = {
|
||||
if let Ok(token) = LocalStorage::get(TOKEN_KEY) {
|
||||
RwLock::new(Some(token))
|
||||
} else {
|
||||
RwLock::new(None)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Set jwt token to local storage.
|
||||
pub fn set_token(token: Option<String>) {
|
||||
if let Some(t) = token.clone() {
|
||||
LocalStorage::set(TOKEN_KEY, t).expect("failed to set");
|
||||
} else {
|
||||
LocalStorage::delete(TOKEN_KEY);
|
||||
}
|
||||
let mut token_lock = TOKEN.write();
|
||||
*token_lock = token;
|
||||
}
|
||||
|
||||
/// Get jwt token from lazy static.
|
||||
pub fn get_token() -> Option<String> {
|
||||
let token_lock = TOKEN.read();
|
||||
token_lock.clone()
|
||||
}
|
||||
80
frontend/src/services/requests.rs
Normal file
80
frontend/src/services/requests.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use super::API_ROOT;
|
||||
use crate::error::Error;
|
||||
use crate::types;
|
||||
use gloo_net::http::{Method, Request as GlooRequest};
|
||||
use weblog::console_log;
|
||||
|
||||
pub struct Request(Option<GlooRequest>);
|
||||
|
||||
fn request<B: serde::Serialize>(method: gloo_net::http::Method, url: &str, body: &B) -> Request {
|
||||
let url = format!("{}{}", API_ROOT, url);
|
||||
let builder = GlooRequest::new(&url)
|
||||
.method(method)
|
||||
.header("Content-Type", "application/json");
|
||||
let req = Request(Some(builder)).set_token();
|
||||
Request(req.0.and_then(|r| r.json(body).ok()))
|
||||
}
|
||||
|
||||
impl Request {
|
||||
fn set_token(self) -> Self {
|
||||
let Some(r) = self.0 else {
|
||||
return Self(None);
|
||||
};
|
||||
let builder = match super::get_token() {
|
||||
Some(token) => r.header("Authorization", &format!("Token {}", token)),
|
||||
None => r,
|
||||
};
|
||||
Self(Some(builder))
|
||||
}
|
||||
|
||||
pub async fn send<T: serde::de::DeserializeOwned>(self) -> Result<T, Error> {
|
||||
let Some(r) = self.0 else {
|
||||
return Err(Error::RequestError);
|
||||
};
|
||||
let response = r.send().await;
|
||||
|
||||
if let Ok(data) = response {
|
||||
if data.status() == 200 {
|
||||
let data: Result<T, _> = data.json::<T>().await;
|
||||
if let Ok(data) = data {
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(Error::DeserializeError)
|
||||
}
|
||||
} else {
|
||||
let status = data.status();
|
||||
let error = match data.json::<types::ErrorInfo>().await {
|
||||
Ok(data) => Error::UnprocessableEntity(data),
|
||||
Err(_) => Error::DeserializeError,
|
||||
};
|
||||
Err(Error::from_status_code(status, error))
|
||||
}
|
||||
} else {
|
||||
Err(Error::RequestError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_delete(url: &str) -> Request {
|
||||
let url = format!("{}{}", API_ROOT, url);
|
||||
Request(Some(GlooRequest::new(&url).method(Method::DELETE))).set_token()
|
||||
}
|
||||
|
||||
pub fn request_get(url: &str) -> Request {
|
||||
let url = format!("{}{}", API_ROOT, url);
|
||||
Request(Some(GlooRequest::new(&url).method(Method::GET))).set_token()
|
||||
}
|
||||
|
||||
pub fn request_post<B>(url: &str, body: &B) -> Request
|
||||
where
|
||||
B: serde::Serialize,
|
||||
{
|
||||
request(Method::POST, url, body)
|
||||
}
|
||||
|
||||
pub fn request_put<B>(url: &str, body: &B) -> Request
|
||||
where
|
||||
B: serde::Serialize,
|
||||
{
|
||||
request(Method::PUT, url, body)
|
||||
}
|
||||
64
frontend/src/types/auth.rs
Normal file
64
frontend/src/types/auth.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginInfo {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginInfoWrapper {
|
||||
pub user: LoginInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterInfo {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterInfoWrapper {
|
||||
pub user: RegisterInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserInfo {
|
||||
pub email: String,
|
||||
pub token: String,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl UserInfo {
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
!self.token.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserInfoWrapper {
|
||||
pub user: UserInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserUpdateInfo {
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
pub password: Option<String>,
|
||||
pub image: String,
|
||||
pub bio: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserUpdateInfoWrapper {
|
||||
pub user: UserUpdateInfo,
|
||||
}
|
||||
16
frontend/src/types/mod.rs
Normal file
16
frontend/src/types/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod auth;
|
||||
pub use auth::{
|
||||
LoginInfo, LoginInfoWrapper, RegisterInfo, RegisterInfoWrapper, UserInfo, UserInfoWrapper,
|
||||
UserUpdateInfo, UserUpdateInfoWrapper,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErrorInfo {
|
||||
pub errors: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
pub type DeleteWrapper = HashMap<(), ()>;
|
||||
Reference in New Issue
Block a user