First commit; prototype state.
This commit is contained in:
commit
092da7490f
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
todo.org
|
||||||
|
*.sqlite
|
File diff suppressed because it is too large
Load Diff
|
@ -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" ]
|
|
@ -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" }
|
|
@ -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,0 +1 @@
|
||||||
|
DROP TABLE characters;
|
|
@ -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
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
nightly
|
|
@ -0,0 +1 @@
|
||||||
|
jeff@seraph.8565:1606079828
|
|
@ -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)
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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],
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod auth;
|
||||||
|
pub mod characters;
|
||||||
|
pub mod root;
|
|
@ -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)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
table! {
|
||||||
|
characters (id) {
|
||||||
|
id -> Integer,
|
||||||
|
user_id -> Integer,
|
||||||
|
viewable -> Bool,
|
||||||
|
character_name -> Text,
|
||||||
|
character_data -> Nullable<Binary>,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
You have encountered an error: {{ message }}
|
||||||
|
{% endblock content %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div>
|
||||||
|
<h1>Character {{name}}</h1>
|
||||||
|
<h3>User: {{username}}</h3>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
Loading…
Reference in New Issue