summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@blos.sm>2023-10-04 19:55:17 +0100
committerLibravatar cel 🌸 <cel@blos.sm>2023-10-04 19:59:53 +0100
commit5d395d4ed73061b247c32dc63db6ddfa2dd62d39 (patch)
tree084df0c83294123c6e884fa46d022f120ba5de86 /src
downloadpinussy-5d395d4ed73061b247c32dc63db6ddfa2dd62d39.tar.gz
pinussy-5d395d4ed73061b247c32dc63db6ddfa2dd62d39.tar.bz2
pinussy-5d395d4ed73061b247c32dc63db6ddfa2dd62d39.zip
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/actix_ructe.rs10
-rw-r--r--src/build.rs10
-rw-r--r--src/main.rs221
3 files changed, 241 insertions, 0 deletions
diff --git a/src/actix_ructe.rs b/src/actix_ructe.rs
new file mode 100644
index 0000000..e6ba0db
--- /dev/null
+++ b/src/actix_ructe.rs
@@ -0,0 +1,10 @@
+macro_rules! render {
+ ($template:path) => {{
+ let mut buf = Vec::new();
+ $template(&mut buf).map(|()| buf)
+ }};
+ ($template:path, $($arg:expr),* $(,)*) => {{
+ let mut buf = Vec::new();
+ $template(&mut buf, $($arg),*).map(|()| buf)
+ }};
+}
diff --git a/src/build.rs b/src/build.rs
new file mode 100644
index 0000000..46d590c
--- /dev/null
+++ b/src/build.rs
@@ -0,0 +1,10 @@
+use ructe::{Result, Ructe};
+
+fn main() -> Result<()> {
+ let mut ructe = Ructe::from_env()?;
+ ructe
+ .statics()?
+ .add_files("./static")?
+ .add_sass_file("./style.scss")?;
+ ructe.compile_templates("templates")
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..eb7f0e8
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,221 @@
+#[macro_use]
+mod actix_ructe;
+
+use std::time::{Duration, SystemTime};
+
+use actix_web::body::{BoxBody, EitherBody, MessageBody};
+use actix_web::dev::ServiceResponse;
+use actix_web::http::{header, StatusCode};
+use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
+use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, ResponseError};
+use bcrypt::{hash, verify, DEFAULT_COST};
+use serde::Deserialize;
+use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
+use templates::statics::StaticFile;
+
+static FAR: Duration = Duration::from_secs(180 * 24 * 60 * 60);
+
+type Result<T> = std::result::Result<T, PinussyError>;
+
+#[derive(Clone)]
+struct Pinussy {
+ db: Pool<Postgres>,
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ let pool = PgPoolOptions::new()
+ .max_connections(5)
+ .connect("postgres://pinussy:pinussy@localhost/pinussy")
+ .await
+ .unwrap();
+
+ sqlx::migrate!("./migrations").run(&pool).await.unwrap();
+
+ let pinussy = Pinussy { db: pool };
+
+ HttpServer::new(move || {
+ App::new()
+ .wrap(
+ ErrorHandlers::new()
+ .handler(StatusCode::NOT_FOUND, render_404)
+ .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
+ )
+ .app_data(web::Data::new(pinussy.clone()))
+ .service(web::resource("/static/{filename}").to(static_file))
+ .service(home)
+ .service(get_login)
+ .service(post_login)
+ .service(get_signup)
+ .service(post_signup)
+ .service(get_users)
+ // .service(get_pins)
+ // .service(post_pin)
+ // .service(get_pin)
+ // .service(post_board)
+ // .service(get_board)
+ })
+ .bind(("127.0.0.1", 8080))?
+ .run()
+ .await
+}
+
+#[get("/")]
+async fn home() -> HttpResponse {
+ HttpResponse::Ok().body("Hello world!")
+}
+
+#[get("/signup")]
+async fn get_signup() -> HttpResponse {
+ HttpResponse::Ok().body(render!(templates::signup_html).unwrap())
+}
+
+#[derive(Deserialize)]
+struct SignupForm {
+ username: String,
+ password: String,
+}
+
+#[post("/signup")]
+async fn post_signup(
+ state: web::Data<Pinussy>,
+ form: web::Form<SignupForm>,
+) -> Result<HttpResponse> {
+ let password_hash = hash(&form.password, DEFAULT_COST)?;
+ sqlx::query!(
+ "insert into users(username, password) values ($1, $2)",
+ &form.username,
+ password_hash
+ )
+ .execute(&state.db)
+ .await?;
+ Ok(HttpResponse::Ok().body(render!(templates::signup_html).unwrap()))
+}
+
+#[get("/login")]
+async fn get_login() -> HttpResponse {
+ HttpResponse::Ok().body(render!(templates::login_html).unwrap())
+}
+
+#[derive(Deserialize)]
+struct LoginForm {
+ username: String,
+ password: String,
+ rememberme: bool,
+}
+
+#[post("/login")]
+async fn post_login(form: web::Form<LoginForm>) -> Result<HttpResponse> {
+ Ok(HttpResponse::Ok().body(render!(templates::login_html).unwrap()))
+}
+
+#[derive(sqlx::Type)]
+#[sqlx(type_name = "privacy", rename_all = "lowercase")]
+enum Privacy {
+ Private,
+ Unlisted,
+ Public,
+}
+
+#[derive(sqlx::FromRow)]
+pub struct User {
+ id: i32,
+ username: String,
+ password: String,
+ email: Option<String>,
+ bio: Option<String>,
+ site: Option<String>,
+ privacy: Privacy,
+ admin: bool,
+}
+
+#[get("/users")]
+async fn get_users(state: web::Data<Pinussy>) -> Result<HttpResponse> {
+ let users: Vec<User> = sqlx::query_as("select * from users")
+ .fetch_all(&state.db)
+ .await
+ .unwrap();
+ println!("lol");
+ Ok(HttpResponse::Ok().body(render!(templates::users_html, users).unwrap()))
+}
+
+async fn static_file(path: web::Path<String>) -> HttpResponse {
+ let name = &path.into_inner();
+ if let Some(data) = StaticFile::get(name) {
+ let far_expires = SystemTime::now() + FAR;
+ HttpResponse::Ok()
+ .insert_header(header::Expires(far_expires.into()))
+ .insert_header(header::ContentType(data.mime.clone()))
+ .body(data.content)
+ } else {
+ HttpResponse::NotFound()
+ .reason("No such static file.")
+ .finish()
+ }
+}
+
+fn render_404(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> {
+ Ok(error_response(
+ res,
+ StatusCode::NOT_FOUND,
+ "The resource you requested can't be found.",
+ ))
+}
+
+fn render_500(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> {
+ Ok(error_response(
+ res,
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "Sorry, Something went wrong. This is probably not your fault.",
+ ))
+}
+
+fn error_response(
+ mut res: ServiceResponse,
+ status_code: StatusCode,
+ message: &str,
+) -> ErrorHandlerResponse<BoxBody> {
+ res.headers_mut().insert(
+ header::CONTENT_TYPE,
+ header::HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
+ );
+ ErrorHandlerResponse::Response(res.map_body(|_head, _body| {
+ EitherBody::right(MessageBody::boxed(
+ render!(templates::error_html, status_code, message).unwrap(),
+ ))
+ }))
+}
+
+#[derive(Debug)]
+enum PinussyError {
+ Database(sqlx::Error),
+ Bcrypt,
+}
+
+impl From<sqlx::Error> for PinussyError {
+ fn from(e: sqlx::Error) -> Self {
+ Self::Database(e)
+ }
+}
+
+impl From<bcrypt::BcryptError> for PinussyError {
+ fn from(e: bcrypt::BcryptError) -> Self {
+ Self::Bcrypt
+ }
+}
+
+impl std::fmt::Display for PinussyError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl std::error::Error for PinussyError {}
+
+impl ResponseError for PinussyError {
+ fn error_response(&self) -> HttpResponse<BoxBody> {
+ HttpResponse::new(self.status_code())
+ }
+}
+
+include!(concat!(env!("OUT_DIR"), "/templates.rs"));