feature/admin
overlisted 2 years ago
commit cec57a4364
Signed by: me
GPG Key ID: 1ACCDCC0429C9737

@ -0,0 +1,2 @@
# ADMIN_KEY=poop
# DATABASE_URL=postgres://localhost/mango

2
.gitignore vendored

@ -0,0 +1,2 @@
/target
/.env

2203
Cargo.lock generated

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,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…
Cancel
Save