diff --git a/.gitignore b/.gitignore index c9bbeaa..ca6690a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ - +**/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 2b065b2..4914270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,22 +6,23 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -actix-web = "4.0.0-beta.12" -actix-rt = "2.5.0" -actix-session = "0.5.0-beta.4" +actix-web = "4.0.1" +actix-rt = "2.7.0" +actix-session = "0.5.0" tera = "1.15.0" oauth2 = "4.1" base64 = "0.13.0" - -rand = "0.8.4" +async-trait = "0.1.53" +rand = "0.8.5" url = "2.2.2" -http = "0.2.5" +http = "0.2.6" dotenv = "0.15" -futures = "0.3.18" -serde = "1.0.130" -serde_json = "1.0.71" -serde_derive = "1.0.130" - -sea-orm = { version = "0.5.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false } +futures = "0.3.21" +serde = "1.0.136" +serde_json = "1.0.79" +serde_derive = "1.0.136" +quote = "1.0" +sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false } +actix_admin = { path = "actix_admin" } \ No newline at end of file diff --git a/actix_admin/Cargo.toml b/actix_admin/Cargo.toml new file mode 100644 index 0000000..f051e25 --- /dev/null +++ b/actix_admin/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "actix_admin" +version = "0.1.0" +edition = "2021" + +[dependencies] +actix-web = "4.0.1" +actix-rt = "2.7.0" +actix-session = "0.5.0" +tera = "1.15.0" +actix_admin_macros = { path = "actix_admin_macros" } +oauth2 = "4.1" +base64 = "0.13.0" +async-trait = "0.1.53" +rand = "0.8.5" +url = "2.2.2" +http = "0.2.6" +dotenv = "0.15" +lazy_static = "1.4.0" +futures = "0.3.21" +serde = "1.0.136" +serde_json = "1.0.79" +serde_derive = "1.0.136" +sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false } \ No newline at end of file diff --git a/actix_admin/actix_admin_macros/Cargo.toml b/actix_admin/actix_admin_macros/Cargo.toml new file mode 100644 index 0000000..d4aaa9d --- /dev/null +++ b/actix_admin/actix_admin_macros/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "actix_admin_macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +actix-web = "4.0.1" +actix-rt = "2.7.0" +actix-session = "0.5.0" +tera = "1.15.0" + +oauth2 = "4.1" +base64 = "0.13.0" + +quote = "1.0" +syn = { version = "1.0", features = ["full", "extra-traits"] } +proc-macro2 = { version = "1.0.36", default-features = false } + +rand = "0.8.5" +url = "2.2.2" +http = "0.2.6" +dotenv = "0.15" +futures = "0.3.21" +serde = "1.0.136" +serde_json = "1.0.79" +serde_derive = "1.0.136" +sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false } \ No newline at end of file diff --git a/actix_admin/actix_admin_macros/src/lib.rs b/actix_admin/actix_admin_macros/src/lib.rs new file mode 100644 index 0000000..117122b --- /dev/null +++ b/actix_admin/actix_admin_macros/src/lib.rs @@ -0,0 +1,40 @@ +use proc_macro; +use quote::quote; + +#[proc_macro_derive(DeriveActixAdminModel)] +pub fn derive_crud_fns(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let expanded = quote! { + use std::convert::From; + use async_trait::async_trait; + use actix_admin::{ ActixAdminModelTrait, ActixAdminModel }; + + impl From for ActixAdminModel { + fn from(entity: Entity) -> Self { + ActixAdminModel { + fields: Vec::new() + } + } + } + #[async_trait] + impl ActixAdminModelTrait for Entity { + async fn list(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec { + use sea_orm::{ query::* }; + let paginator = Entity::find() + .order_by_asc(Column::Id) + .paginate(db, posts_per_page); + let entities = paginator + .fetch_page(page - 1) + .await + .expect("could not retrieve entities"); + //entities to ActixAdminModel + vec![ + ActixAdminModel { + fields: Vec::new() + } + ] + } + } + }; + + proc_macro::TokenStream::from(expanded) +} diff --git a/actix_admin/actix_admin_macros/tests/macros_sqlx.rs b/actix_admin/actix_admin_macros/tests/macros_sqlx.rs new file mode 100644 index 0000000..90b5d8d --- /dev/null +++ b/actix_admin/actix_admin_macros/tests/macros_sqlx.rs @@ -0,0 +1,24 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; +use actix_admin_macros::ActixAdmin; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, ActixAdmin)] +#[sea_orm(table_name = "test")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + pub id: i32, + pub title: String, + #[sea_orm(column_type = "Text")] + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +#[test] +fn test_macro() { + assert_eq!(4, 2); +} \ No newline at end of file diff --git a/actix_admin/src/lib.rs b/actix_admin/src/lib.rs new file mode 100644 index 0000000..02b7e79 --- /dev/null +++ b/actix_admin/src/lib.rs @@ -0,0 +1,153 @@ +use actix_web::{error, guard, web, Error, HttpRequest, HttpResponse}; +use actix_web::{ dev, App, FromRequest}; +use actix_web::error::ErrorBadRequest; +use serde_derive::Deserialize; +use std::collections::HashMap; +use tera::{Context, Tera}; +use futures::future::{ok, err, Ready}; +use lazy_static::lazy_static; +use sea_orm::DatabaseConnection; +use sea_orm::EntityTrait; +use sea_orm::ModelTrait; + +use async_trait::async_trait; + +pub use actix_admin_macros::DeriveActixAdminModel; + +const DEFAULT_POSTS_PER_PAGE: usize = 5; + +// templates +lazy_static! { + static ref TERA: Tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); +} + +// Paging +#[derive(Debug, Deserialize)] +pub struct Params { + page: Option, + posts_per_page: Option, +} + +// Fields +#[derive(Clone, Debug)] +pub enum Field { + Text +} + +// AppDataTrait +pub trait AppDataTrait { + fn get_db(&self) -> &DatabaseConnection; + fn get_view_model_map(&self) -> &HashMap<&'static str, ActixAdminViewModel>; +} + +// ActixAdminModel +#[async_trait] +pub trait ActixAdminModelTrait { + async fn list(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec; +} + +#[derive(Clone, Debug)] +pub struct ActixAdminModel { + pub fields: Vec<(&'static str, Field)> +} + +// ActixAdminViewModel +pub trait ActixAdminViewModelTrait : Clone { + fn get_model_name(&self) -> &str; + //fn get_entities() -> Vec; +} + +impl ActixAdminViewModelTrait for ActixAdminViewModel { + fn get_model_name(&self) -> &str { + &self.entity_name + } +} + +#[derive(Clone, Debug)] +pub struct ActixAdminViewModel { + pub entity_name: &'static str, + pub admin_model: ActixAdminModel +} + +// ActixAdminController +#[derive(Clone, Debug)] +pub struct ActixAdmin { + view_models: HashMap<&'static str, ActixAdminViewModel>, +} + +impl ActixAdmin { + pub fn new() -> Self { + let actix_admin = ActixAdmin { + view_models: HashMap::new(), + }; + actix_admin + } + + pub fn create_scope(self, _app_state: &T) -> actix_web::Scope { + let mut scope = web::scope("/admin").route("/", web::get().to(index::)); + + for view_model in self.view_models { + scope = scope.service( + web::scope(&format!("/{}", view_model.0)).route("/list", web::get().to(list::)) + ); + } + + scope + } + + pub fn add_entity(mut self, view_model: ActixAdminViewModel) -> Self { + self.view_models.insert(view_model.entity_name, view_model); + self + } + + pub fn get_view_model_map(&self) -> HashMap<&'static str, ActixAdminViewModel> { + self.view_models.clone() + } +} + +async fn index(data: web::Data) -> Result { + let keys = Vec::from_iter(data.get_view_model_map().keys()); + + let mut ctx = Context::new(); + ctx.insert("view_models", &keys); + + let body = TERA + .render("index.html", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +async fn list(req: HttpRequest, data: web::Data) -> Result { + let db = &data.get_db(); + let params = web::Query::::from_query(req.query_string()).unwrap(); + + let page = params.page.unwrap_or(1); + let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let columns: Vec = Vec::new(); + + // let paginator = post::Entity::find() + // .order_by_asc(post::Column::Id) + // .paginate(db, posts_per_page); + //let num_pages = paginator.num_pages().await.ok().unwrap(); + + let posts: Vec<&str> = Vec::new(); + //let posts = paginator + // .fetch_page(page - 1) + // .await + // .expect("could not retrieve posts"); + let mut ctx = Context::new(); + ctx.insert("posts", &posts); + ctx.insert("page", &page); + ctx.insert("posts_per_page", &posts_per_page); + ctx.insert("num_pages", "5" /*&num_pages*/); + ctx.insert("columns", &columns); + + // let body = data.tmpl + // .render("list.html", &ctx) + // .map_err(|_| error::ErrorInternalServerError("Template error"))?; + //Ok(HttpResponse::Ok().content_type("text/html").body(body)) + Ok(HttpResponse::Ok() + .content_type("text/html") + .body("")) +} \ No newline at end of file diff --git a/actix_admin/templates/base.html b/actix_admin/templates/base.html new file mode 100644 index 0000000..f54fac2 --- /dev/null +++ b/actix_admin/templates/base.html @@ -0,0 +1,14 @@ + + + + + Actix Admin + + + + + + {% block content %} + {% endblock content %} + + \ No newline at end of file diff --git a/actix_admin/templates/index.html b/actix_admin/templates/index.html new file mode 100644 index 0000000..0348967 --- /dev/null +++ b/actix_admin/templates/index.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +

Index

+{% endblock content %} diff --git a/actix_admin/templates/list.html b/actix_admin/templates/list.html new file mode 100644 index 0000000..6db02d1 --- /dev/null +++ b/actix_admin/templates/list.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +Hello +

posts: {{ posts }}

+

page: {{ page }}

+

posts_per_page: {{ posts_per_page }}

+

num_pages: {{ num_pages }}

+{% endblock content %} \ No newline at end of file diff --git a/src/actix_admin/mod.rs b/src/actix_admin/mod.rs deleted file mode 100644 index 564b993..0000000 --- a/src/actix_admin/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -use actix_web::{web, guard, HttpRequest, HttpResponse, Error, error}; -use tera::{ Tera, Context}; - -use crate::entity::Post; -use crate::entity::post; - -use sea_orm::{ entity::*, query::*, SelectorTrait, ModelTrait, ConnectionTrait, ColumnTrait, PaginatorTrait, EntityTrait }; -use sea_orm::{{ DatabaseConnection, ConnectOptions }}; - -const DEFAULT_POSTS_PER_PAGE: usize = 5; - -#[derive(Debug, Deserialize)] -pub struct Params { - page: Option, - posts_per_page: Option, -} - -async fn index(data: web::Data) -> &'static str { - "Welcome!" -} - -async fn list( - req: HttpRequest, - data: web::Data) -> Result - { - let db = &data.db; - let params = web::Query::::from_query(req.query_string()).unwrap(); - - let page = params.page.unwrap_or(1); - let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - let paginator = Post::find() - .order_by_asc(post::Column::Id) - .paginate(db, posts_per_page); - let num_pages = paginator.num_pages().await.ok().unwrap(); - - let posts = paginator - .fetch_page(page - 1) - .await - .expect("could not retrieve posts"); - - let mut ctx = Context::new(); - ctx.insert("posts", &posts); - ctx.insert("page", &page); - ctx.insert("posts_per_page", &posts_per_page); - ctx.insert("num_pages", &num_pages); - - let body = data.tmpl - .render("list.html", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -fn entity_scope(entity: T) -> actix_web::Scope { - let entity_name = entity.table_name(); - let scope = web::scope(&format!("/{}",entity_name)) - .route("/list", web::get().to(list::)); - scope -} - -pub fn admin_scope() -> actix_web::Scope { - let scope = web::scope("/admin") - .route("/", web::get().to(index)) - .service(entity_scope(Post)); - scope -} \ No newline at end of file diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 14cf6d7..f511698 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,7 +1,9 @@ // setup use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; use sea_orm::{error::*, sea_query, ConnectionTrait, DbConn, ExecResult}; - +use sea_orm::{{ DatabaseConnection, ConnectOptions }}; +use sea_orm::{ entity::*, query::*, SelectorTrait, ModelTrait, ColumnTrait, PaginatorTrait, EntityTrait }; +use async_trait::async_trait; pub mod post; pub use post::Entity as Post; diff --git a/src/entity/post.rs b/src/entity/post.rs index b131d2a..cd86078 100644 --- a/src/entity/post.rs +++ b/src/entity/post.rs @@ -1,7 +1,9 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] +use actix_admin::{ DeriveActixAdminModel }; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdminModel)] #[sea_orm(table_name = "post")] pub struct Model { #[sea_orm(primary_key)] diff --git a/src/main.rs b/src/main.rs index 8bd551d..5cf5152 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,21 +11,35 @@ use oauth2::{ }; use std::time::{Duration}; use std::env; -use sea_orm::{{ DatabaseConnection, ConnectOptions }}; +use sea_orm::{{ DatabaseConnection, ConnectOptions, EntityName }}; +use actix_admin::{ActixAdminViewModelTrait, AppDataTrait, ActixAdminViewModel, ActixAdminModel}; +use std::collections::HashMap; mod web_auth; mod entity; -mod actix_admin; + +use entity::{ Post }; #[derive(Debug, Clone)] pub struct AppState { pub oauth: BasicClient, pub api_base_url: String, pub tmpl: Tera, - pub db: DatabaseConnection + pub db: DatabaseConnection, + pub view_model_map: HashMap<&'static str, ActixAdminViewModel> } -fn index(session: Session, data: web::Data) -> HttpResponse { +impl AppDataTrait for AppState { + fn get_db(&self) -> &DatabaseConnection { + &self.db + } + + fn get_view_model_map(&self) -> &HashMap<&'static str, ActixAdminViewModel> { + &self.view_model_map + } +} + +async fn index(session: Session, data: web::Data) -> HttpResponse { let login = session.get::("user_info").unwrap(); let web_auth_link = if login.is_some() { "logout" } else { "login" }; @@ -85,18 +99,27 @@ async fn main() { let conn = sea_orm::Database::connect(opt).await.unwrap(); let _ = entity::create_post_table(&conn).await; + let viewmodel_entity = ActixAdminViewModel { + entity_name: "posts", + admin_model: ActixAdminModel::from(Post) + }; + + let actix_admin = actix_admin::ActixAdmin::new() + .add_entity(viewmodel_entity.clone()); + let app_state = AppState { oauth: client, api_base_url, tmpl: tera, - db: conn + db: conn, + view_model_map: actix_admin.get_view_model_map() }; HttpServer::new(move || { App::new() .app_data(web::Data::new(app_state.clone())) .wrap(CookieSession::signed(&[0; 32]).secure(false)) - .service(actix_admin::admin_scope()) + .service(actix_admin.clone().create_scope(&app_state)) .route("/", web::get().to(index)) .route("/login", web::get().to(web_auth::login)) .route("/logout", web::get().to(web_auth::logout)) diff --git a/src/web_auth/mod.rs b/src/web_auth/mod.rs index 04224c5..f3ac1d2 100644 --- a/src/web_auth/mod.rs +++ b/src/web_auth/mod.rs @@ -10,7 +10,7 @@ use oauth2::{ use std::str; use url::Url; -pub fn login(data: web::Data) -> HttpResponse { +pub async fn login(data: web::Data) -> HttpResponse { // Create a PKCE code verifier and SHA-256 encode it as a code challenge. // let (_pkce_code_challenge, _pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); // Generate the authorization URL to which we'll redirect the user. @@ -28,7 +28,7 @@ pub fn login(data: web::Data) -> HttpResponse { .finish() } -pub fn logout(session: Session) -> HttpResponse { +pub async fn logout(session: Session) -> HttpResponse { session.remove("user_info"); HttpResponse::Found() .append_header((header::LOCATION, "/".to_string()))