First commit; prototype state.

This commit is contained in:
jeff 2020-12-01 21:32:16 +00:00
commit 09b6ddc36a
27 changed files with 2067 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL="./tenebrous.sqlite"

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
todo.org
*.sqlite

1580
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

20
Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "tenebrous-sheets"
version = "0.1.0"
authors = ["jeff <jeff@agnos.is>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
diesel = "1.4"
thiserror = "1.0"
rocket = { version= "0.4.6", features = ["private-cookies"] }
[dependencies.rocket_contrib]
version = "0.4.6"
default-features = false
features = [ "tera_templates", "diesel_sqlite_pool" ]

12
Rocket.toml Normal file
View File

@ -0,0 +1,12 @@
[development]
address = "localhost"
port = 8000
keep_alive = 5
read_timeout = 5
write_timeout = 5
log = "normal"
secret_key = "MzFno9TrtoYF6xrinMMpyUWyzPGkVzF7/GrujylmFdw="
limits = { forms = 32768 }
[development.databases]
tenebrous_db = { url = "./tenebrous.sqlite" }

5
diesel.toml Normal file
View File

@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

0
migrations/.gitkeep Normal file
View File

View File

@ -0,0 +1 @@
DROP TABLE characters;

View File

@ -0,0 +1,7 @@
CREATE TABLE characters(
id INTEGER NOT NULL PRIMARY KEY,
user_id INTEGER NOT NULL,
viewable BOOLEAN NOT NULL,
character_name TEXT NOT NULL,
character_data BLOB NUT NULL
);

1
rust-toolchain Normal file
View File

@ -0,0 +1 @@
nightly

1
src/.#main.rs Symbolic link
View File

@ -0,0 +1 @@
jeff@seraph.8565:1606079828

14
src/catchers.rs Normal file
View File

@ -0,0 +1,14 @@
use rocket::request::Request;
use rocket_contrib::templates::Template;
use std::collections::HashMap;
pub fn catchers() -> Vec<rocket::Catcher> {
catchers![forbidden]
}
#[catch(403)]
fn forbidden(_: &Request) -> Template {
let mut context = HashMap::new();
context.insert("message", "homie aint allowed");
Template::render("error", context)
}

31
src/db.rs Normal file
View File

@ -0,0 +1,31 @@
use crate::models::characters::{CharacterEntry, NewCharacter};
use diesel::prelude::*;
#[database("tenebrous_db")]
pub(crate) struct TenebrousDbConn(diesel::SqliteConnection);
pub(crate) fn load_character(
conn: TenebrousDbConn,
character_id: i32,
) -> QueryResult<Option<CharacterEntry>> {
use crate::schema::characters::dsl::*;
characters
.filter(id.eq(character_id))
.limit(1)
.first::<CharacterEntry>(&*conn)
.optional()
}
pub(crate) fn insert_character(
conn: TenebrousDbConn,
new_character: &NewCharacter,
) -> QueryResult<()> {
use crate::schema::characters;
diesel::insert_into(characters::table)
.values(new_character)
.execute(&*conn)?;
Ok(())
}

55
src/errors.rs Normal file
View File

@ -0,0 +1,55 @@
use rocket::http::Status;
use rocket::request::Request;
use rocket::response::status;
use rocket::response::{self, Responder};
use rocket_contrib::templates::Template;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("resource not found")]
NotFound,
#[error("you must be logged in")]
NotLoggedIn,
#[error("you do not have permission to access this")]
NoPermission,
#[error("query error: {0}")]
QueryError(#[from] diesel::result::Error),
}
impl Error {
fn is_sensitive(&self) -> bool {
use Error::*;
match self {
QueryError(_) => true,
_ => false,
}
}
}
impl<'r> Responder<'r> for Error {
fn respond_to(self, req: &Request) -> response::Result<'r> {
//Hide sensitive error information
let message: String = if self.is_sensitive() {
"internal error".into()
} else {
self.to_string()
};
let mut context = HashMap::new();
context.insert("message", message);
let resp = Template::render("error", context).respond_to(req)?;
use Error::*;
match self {
NotFound => status::NotFound(resp).respond_to(req),
NotLoggedIn => status::Forbidden(Some(resp)).respond_to(req),
NoPermission => status::Forbidden(Some(resp)).respond_to(req),
_ => status::Custom(Status::InternalServerError, resp).respond_to(req),
}
}
}

39
src/main.rs Normal file
View File

@ -0,0 +1,39 @@
#![feature(proc_macro_hygiene, decl_macro, never_type, unsized_locals)]
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;
#[macro_use]
extern crate diesel;
use rocket_contrib::templates::Template;
pub mod catchers;
pub mod db;
pub mod errors;
pub mod models;
pub mod routes;
pub mod schema;
fn main() {
let root_routes: Vec<rocket::Route> = {
routes::root::routes()
.into_iter()
.chain(routes::auth::routes().into_iter())
.collect()
};
let character_routes = routes::characters::routes();
let catchers = catchers::catchers();
rocket::ignite()
.attach(Template::fairing())
.attach(db::TenebrousDbConn::fairing())
.mount("/", root_routes)
.mount("/characters", character_routes)
.register(catchers)
.launch();
}

44
src/models.rs Normal file
View File

@ -0,0 +1,44 @@
use rocket::http::RawStr;
use rocket::outcome::IntoOutcome;
use rocket::request::{self, FromParam, FromRequest, Request};
use serde_derive::Serialize;
pub mod characters;
#[derive(Eq, PartialEq, Serialize, Debug)]
pub struct User {
pub id: usize,
pub username: String,
}
impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = !;
fn from_request(request: &'a Request<'r>) -> request::Outcome<User, !> {
request
.cookies()
.get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok())
.map(|id|
//TODO load from db
User {
id: id,
username: "somebody".to_string(),
})
.or_forward(())
}
}
impl<'r> FromParam<'r> for User {
type Error = &'r str;
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
let username: String = param.url_decode().or(Err("Invalid character ID"))?;
//TODO load from DB
Ok(User {
id: 1,
username: username,
})
}
}

22
src/models/characters.rs Normal file
View File

@ -0,0 +1,22 @@
use crate::schema::characters;
use rocket::http::RawStr;
use rocket::request::{self, FromParam, FromRequest, Request};
use serde_derive::Serialize;
#[derive(Serialize, Debug, Queryable)]
pub struct CharacterEntry {
pub id: i32,
pub user_id: i32,
pub viewable: bool,
pub name: String,
pub data: Option<Vec<u8>>,
}
#[derive(Insertable)]
#[table_name = "characters"]
pub struct NewCharacter<'a> {
pub user_id: i32,
pub viewable: bool,
pub character_name: &'a str,
pub character_data: &'a [u8],
}

3
src/routes.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod auth;
pub mod characters;
pub mod root;

50
src/routes/auth.rs Normal file
View File

@ -0,0 +1,50 @@
use crate::models::User;
use rocket::http::{Cookie, Cookies};
use rocket::request::{FlashMessage, Form};
use rocket::response::{Flash, Redirect};
use rocket_contrib::templates::Template;
use std::collections::HashMap;
pub(crate) fn routes() -> Vec<rocket::Route> {
routes![login, logout, login_user, login_page]
}
#[derive(FromForm)]
struct Login {
username: String,
password: String,
}
#[post("/login", data = "<login>")]
fn login(mut cookies: Cookies, login: Form<Login>) -> Result<Redirect, Flash<Redirect>> {
if login.username == "Sergio" && login.password == "password" {
cookies.add_private(Cookie::new("user_id", 1.to_string()));
Ok(Redirect::to(uri!(super::root::index)))
} else {
Err(Flash::error(
Redirect::to(uri!(login_page)),
"Invalid username/password.",
))
}
}
#[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> {
cookies.remove_private(Cookie::named("user_id"));
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
}
#[get("/login")]
fn login_user(_user: User) -> Redirect {
Redirect::to(uri!(super::root::index))
}
#[get("/login", rank = 2)]
fn login_page(flash: Option<FlashMessage>) -> Template {
let mut context = HashMap::new();
if let Some(ref msg) = flash {
context.insert("flash", msg.msg());
}
Template::render("login", &context)
}

59
src/routes/characters.rs Normal file
View File

@ -0,0 +1,59 @@
use crate::db::{self, TenebrousDbConn};
use crate::errors::Error;
use crate::models::{
characters::{CharacterEntry, NewCharacter},
User,
};
use diesel::prelude::*;
use rocket_contrib::templates::Template;
use serde_derive::Serialize;
use std::collections::HashMap;
pub(crate) fn routes() -> Vec<rocket::Route> {
routes![view_character, edit_character]
}
//TODO make private -- currently is referenced in homepage route.
//or move to common place.
#[derive(Serialize)]
pub struct TemplateContext {
pub characters: Vec<CharacterEntry>,
pub user: User,
}
//TODO should return result based on whether or not character is publicly viewable.
#[get("/<user>/<character_id>")]
fn view_character(
character_id: i32,
user: Option<User>,
conn: TenebrousDbConn,
) -> Result<Template, Error> {
let user = user.ok_or(Error::NotFound)?;
let character = db::load_character(conn, character_id)?.ok_or(Error::NotFound)?;
let mut context = HashMap::new();
context.insert("name", character.name);
context.insert("username", user.username);
Ok(Template::render("view_character", context))
}
#[get("/<owner>/<character_id>/edit")]
fn edit_character(
character_id: i32,
owner: Option<User>,
logged_in_user: Option<User>,
conn: TenebrousDbConn,
) -> Result<Template, Error> {
let owner = owner.ok_or(Error::NotFound)?;
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
let character = db::load_character(conn, character_id)?.ok_or(Error::NotFound)?;
if logged_in_user != owner {
return Err(Error::NoPermission);
}
let mut context = HashMap::new();
context.insert("name", character.name);
context.insert("username", owner.username);
Ok(Template::render("view_character", context))
}

41
src/routes/root.rs Normal file
View File

@ -0,0 +1,41 @@
use crate::db::TenebrousDbConn;
use crate::models::{characters::CharacterEntry, User};
use rocket::response::Redirect;
use rocket_contrib::templates::Template;
pub fn routes() -> Vec<rocket::Route> {
routes![index, user_index]
}
#[get("/")]
fn user_index(user: User, conn: TenebrousDbConn) -> Template {
use crate::routes::characters::TemplateContext;
let characters = vec![
CharacterEntry {
id: 1,
user_id: 1,
name: "Bob".to_string(),
viewable: true,
data: Some(vec![]),
},
CharacterEntry {
id: 2,
user_id: 1,
name: "Alice".to_string(),
viewable: true,
data: Some(vec![]),
},
];
let context = TemplateContext {
characters: characters,
user: user,
};
Template::render("index", &context)
}
#[get("/", rank = 2)]
fn index() -> Redirect {
Redirect::to(uri!(super::auth::login_page))
}

9
src/schema.rs Normal file
View File

@ -0,0 +1,9 @@
table! {
characters (id) {
id -> Integer,
user_id -> Integer,
viewable -> Bool,
character_name -> Text,
character_data -> Nullable<Binary>,
}
}

16
templates/base.html.tera Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Tera Demo</title>
</head>
<body>
{% if blocking_error %}
<div>
{{ blocking_error }}
</div>
{% else %}
{% block content %}{% endblock content %}
{% endif %}
</body>
</html>

View File

@ -0,0 +1,5 @@
{% extends "base" %}
{% block content %}
You have encountered an error: {{ message }}
{% endblock content %}

19
templates/index.html.tera Normal file
View File

@ -0,0 +1,19 @@
{% extends "base" %}
{% block content %}
<div>
<h1>Hi {{user.username}}</h1>
<h3>Here are your characters:</h3>
<ul>
{% for char in characters %}
<li>
<a href="characters/{{ user.username }}/{{char.id}}">
{{ char.name }}
</a>
</li>
{% endfor %}
</ul>
<p>Try going to <a href="/hello/YourName">/hello/YourName</a></p>
</div>
{% endblock content %}

21
templates/login.html.tera Normal file
View File

@ -0,0 +1,21 @@
{% extends "base" %}
{% block content %}
<div>
<h1>Rocket Session: Please Login</h1>
<p>Please login to continue.</p>
{% if flash %}
<p>Error: {{ flash }}</p>
{% endif %}
<form action="/login" method="post" accept-charset="utf-8">
<label for="username">username</label>
<input type="text" name="username" id="username" value="" />
<label for="password">password</label>
<input type="password" name="password" id="password" value="" />
<p><input type="submit" value="login"></p>
</form>
</div>
{% endblock content %}

View File

@ -0,0 +1,8 @@
{% extends "base" %}
{% block content %}
<div>
<h1>Character {{name}}</h1>
<h3>User: {{username}}</h3>
</div>
{% endblock content %}