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