commit
cec57a4364
@ -0,0 +1,2 @@
|
||||
# ADMIN_KEY=poop
|
||||
# DATABASE_URL=postgres://localhost/mango
|
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/.env
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "mango"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = "1"
|
||||
dotenv = "0"
|
||||
|
||||
rocket = { version = "0.5.0-rc.1", features = ["secrets"] }
|
||||
|
||||
diesel = { version = "1", features = ["postgres", "r2d2"] }
|
||||
diesel_migrations = "1"
|
||||
serde = "1"
|
||||
rocket_sync_db_pools = { version = "0.1.0-rc.1", features = ["diesel_postgres_pool"] }
|
||||
|
||||
handlebars = "4"
|
||||
serde_json = "1"
|
||||
rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["handlebars"] }
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Ilya Maximov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,3 @@
|
||||
[global.databases.api]
|
||||
# overwrite this in .env if you need to
|
||||
url = "postgres://localhost/mango"
|
@ -0,0 +1,5 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/db/schema.rs"
|
@ -0,0 +1,6 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
@ -0,0 +1,36 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
@ -0,0 +1 @@
|
||||
DROP TABLE projects;
|
@ -0,0 +1,5 @@
|
||||
CREATE TABLE projects (
|
||||
id VARCHAR(32) PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
description VARCHAR NOT NULL
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
pub mod model;
|
||||
pub mod schema;
|
@ -0,0 +1,10 @@
|
||||
use super::schema::*;
|
||||
use rocket::FromForm;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Queryable, Insertable, AsChangeset, Serialize, FromForm)]
|
||||
pub struct Project {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
table! {
|
||||
projects (id) {
|
||||
id -> Varchar,
|
||||
name -> Varchar,
|
||||
description -> Varchar,
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
use super::prelude::*;
|
||||
|
||||
#[rocket::catch(401)]
|
||||
fn teapot() -> (Status, &'static str) {
|
||||
(Status { code: 418 }, "🫖")
|
||||
}
|
||||
|
||||
#[rocket::post("/login", data = "<key>")]
|
||||
fn login(jar: &CookieJar<'_>, key: String) {
|
||||
jar.add_private(Cookie::new("nothing", key));
|
||||
}
|
||||
|
||||
#[rocket::get("/")]
|
||||
async fn index(_access: AdminAccess, db: Db) -> Template {
|
||||
let projects = db
|
||||
.run(|conn| schema::projects::table.load::<model::Project>(conn))
|
||||
.await
|
||||
.expect("failed to load projects");
|
||||
|
||||
Template::render("admin/index", json!({ "projects": projects }))
|
||||
}
|
||||
|
||||
#[rocket::post("/projects", data = "<data>")]
|
||||
async fn upsert_project(_access: AdminAccess, db: Db, data: Form<Strict<model::Project>>) {
|
||||
db.run(move |conn| {
|
||||
diesel::insert_into(schema::projects::table)
|
||||
.values(&**data)
|
||||
.on_conflict(schema::projects::id)
|
||||
.do_update()
|
||||
.set(&**data)
|
||||
.execute(conn)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn fairing() -> impl Fairing {
|
||||
AdHoc::on_ignite("Admin Frontend", |rocket| async {
|
||||
rocket
|
||||
.register("/admin", rocket::catchers![teapot])
|
||||
.mount("/admin", rocket::routes![login, index])
|
||||
.mount("/admin/api", rocket::routes![upsert_project])
|
||||
})
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
use super::prelude::*;
|
||||
|
||||
embed_migrations!();
|
||||
|
||||
async fn run_migrations(rocket: rocket::Rocket<rocket::Build>) -> rocket::Rocket<rocket::Build> {
|
||||
let conn = Db::get_one(&rocket).await.expect("database connection");
|
||||
conn.run(|c| embedded_migrations::run(c))
|
||||
.await
|
||||
.expect("diesel migrations");
|
||||
|
||||
rocket
|
||||
}
|
||||
|
||||
pub fn fairing() -> impl Fairing {
|
||||
AdHoc::on_ignite("Database", |rocket| async {
|
||||
rocket
|
||||
.attach(Db::fairing())
|
||||
.attach(AdHoc::on_ignite("Diesel Migrations", run_migrations))
|
||||
})
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
use super::prelude::*;
|
||||
|
||||
#[rocket::get("/")]
|
||||
async fn index(db: Db) -> Template {
|
||||
let projects = db
|
||||
.run(|conn| schema::projects::table.load::<model::Project>(conn))
|
||||
.await
|
||||
.expect("failed to load projects");
|
||||
|
||||
Template::render("index", json!({ "projects": projects }))
|
||||
}
|
||||
|
||||
pub fn fairing() -> impl Fairing {
|
||||
AdHoc::on_ignite("Frontend", |rocket| async {
|
||||
rocket
|
||||
.attach(Template::fairing())
|
||||
.mount("/", rocket::routes![index])
|
||||
})
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
mod prelude;
|
||||
|
||||
mod admin;
|
||||
mod db;
|
||||
mod frontend;
|
||||
|
||||
pub use admin::fairing as admin;
|
||||
pub use db::fairing as db;
|
||||
pub use frontend::fairing as frontend;
|
@ -0,0 +1,10 @@
|
||||
pub use crate::guards::*;
|
||||
|
||||
pub use crate::db::{model, schema};
|
||||
pub use diesel::{ExpressionMethods, RunQueryDsl};
|
||||
|
||||
pub use rocket::fairing::{AdHoc, Fairing};
|
||||
pub use rocket::form::{Form, Strict};
|
||||
pub use rocket::http::{Cookie, CookieJar, Status};
|
||||
pub use rocket_dyn_templates::Template;
|
||||
pub use serde_json::json;
|
@ -0,0 +1,23 @@
|
||||
use super::prelude::*;
|
||||
|
||||
pub struct AdminAccess;
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminAccess {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let cookie = request
|
||||
.cookies()
|
||||
.get_private("nothing")
|
||||
.map(|x| String::from(x.value()));
|
||||
|
||||
// no key = everyone has access. useful for development because rocket resets the encryption
|
||||
// thing after every restart
|
||||
if cookie == std::env::var("ADMIN_KEY").ok() {
|
||||
Outcome::Success(AdminAccess)
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, ()))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
use diesel::PgConnection;
|
||||
|
||||
#[rocket_sync_db_pools::database("api")]
|
||||
pub struct Db(PgConnection);
|
@ -0,0 +1,7 @@
|
||||
mod prelude;
|
||||
|
||||
mod admin;
|
||||
mod db;
|
||||
|
||||
pub use admin::*;
|
||||
pub use db::*;
|
@ -0,0 +1,3 @@
|
||||
pub use rocket::async_trait;
|
||||
pub use rocket::http::Status;
|
||||
pub use rocket::request::{FromRequest, Outcome, Request};
|
@ -0,0 +1,18 @@
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
mod db;
|
||||
mod fairings;
|
||||
mod guards;
|
||||
|
||||
#[rocket::launch]
|
||||
fn rocket() -> _ {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
rocket::build()
|
||||
.attach(fairings::db())
|
||||
.attach(fairings::admin())
|
||||
.attach(fairings::frontend())
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<html>
|
||||
good morning
|
||||
|
||||
<script>
|
||||
window.onload = () => {
|
||||
document.getElementById("upsert-project").onsubmit = async e => {
|
||||
e.preventDefault();
|
||||
await fetch("admin/api/projects", { method: "post", body: new FormData(e.target) });
|
||||
window.location = window.location;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<h1>upsert project</h1>
|
||||
<form id="upsert-project">
|
||||
<label>id</label>
|
||||
<input name="id">
|
||||
<label>display name</label>
|
||||
<input name="name">
|
||||
<label>desc</label>
|
||||
<input name="description">
|
||||
<input type="submit" value="send">
|
||||
</form>
|
||||
|
||||
<h1>projects</h1>
|
||||
{{#each projects}}
|
||||
<article>
|
||||
<h2>{{name}} (id {{id}})</h2>
|
||||
<span>{{description}}</span>
|
||||
</article>
|
||||
{{/each}}
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,10 @@
|
||||
<html>
|
||||
<body>
|
||||
{{#each projects}}
|
||||
<article>
|
||||
<h1>{{name}}</h1>
|
||||
<span>{{description}}</span>
|
||||
</article>
|
||||
{{/each}}
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in new issue