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