initial commit of slink

This commit is contained in:
2021-02-04 15:07:55 +01:00
commit d64f205162
23 changed files with 3989 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub(crate) struct LinkForm {
pub title: String,
pub target: String,
pub code: String,
}
+139
View File
@@ -0,0 +1,139 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate log;
mod forms;
pub mod models;
pub mod schema;
mod views;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::middleware::Logger;
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_static_files;
use dotenv::dotenv;
use tera::Tera;
#[derive(Debug)]
pub enum ServerError {
Argonautic,
Diesel,
Environment,
Template(tera::Error),
User(String),
}
impl std::fmt::Display for ServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Test")
}
}
impl actix_web::error::ResponseError for ServerError {
fn error_response(&self) -> HttpResponse {
match self {
ServerError::Argonautic => {
HttpResponse::InternalServerError().json("Argonautica Error.")
}
ServerError::Diesel => HttpResponse::InternalServerError().json("Diesel Error."),
ServerError::Environment => {
HttpResponse::InternalServerError().json("Environment Error.")
}
ServerError::Template(e) => {
HttpResponse::InternalServerError().json(format!("Template Error. {:?}", e))
}
ServerError::User(data) => HttpResponse::InternalServerError().json(data),
}
}
}
impl From<std::env::VarError> for ServerError {
fn from(e: std::env::VarError) -> ServerError {
error!("Environment error {:?}", e);
ServerError::Environment
}
}
/* impl From<r2d2::Error> for ServerError {
fn from(_: r2d2::Error) -> ServerError {
ServerError::R2D2Error
}
} */
impl From<diesel::result::Error> for ServerError {
fn from(err: diesel::result::Error) -> ServerError {
error!("Database error {:?}", err);
match err {
diesel::result::Error::NotFound => ServerError::User("Username not found.".to_string()),
_ => ServerError::Diesel,
}
}
}
impl From<argonautica::Error> for ServerError {
fn from(e: argonautica::Error) -> ServerError {
error!("Authentication error {:?}", e);
ServerError::Argonautic
}
}
impl From<tera::Error> for ServerError {
fn from(e: tera::Error) -> ServerError {
error!("Template error {:?}", e);
ServerError::Template(e)
}
}
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
env_logger::init();
println!("Running on: http://127.0.0.1:8156");
HttpServer::new(|| {
let tera = Tera::new("templates/**/*").expect("failed to initialize the templates");
let generated = generate();
App::new()
.wrap(Logger::default())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&[0; 32])
.name("auth-cookie")
.secure(false),
))
.data(tera)
.service(actix_web_static_files::ResourceFiles::new(
"/static", generated,
))
// directly go to the main page of Freie-Hochschule-Stuttgart
.route("/", web::get().to(views::redirect_fhs))
// admin block
.service(
web::scope("/admin")
// list all links
.route("/index/", web::get().to(views::index))
// invite users
.route("/signup/", web::get().to(views::signup))
.route("/signup/", web::post().to(views::process_signup))
// logout
.route("/logout/", web::to(views::logout))
// submit a new url for shortening
.route("/submit/", web::get().to(views::submission))
.route("/submit/", web::post().to(views::process_submission))
// view an existing url
.service(
web::scope("/view")
.route("/{redirect_id}", web::get().to(views::view_link)),
)
// login to the admin area
.route("/login/", web::get().to(views::login))
.route("/login/", web::post().to(views::process_login)),
)
// redirect to the url hidden behind the code
.route("/{redirect_id}", web::get().to(views::redirect))
})
.bind("127.0.0.1:8156")?
.run()
.await
}
+85
View File
@@ -0,0 +1,85 @@
use crate::{forms::LinkForm, ServerError};
use super::schema::{links, users};
use argonautica::Hasher;
use diesel::{Insertable, Queryable};
use dotenv::dotenv;
use serde::{Deserialize, Serialize};
#[derive(Queryable, Serialize)]
pub struct User {
pub id: i32,
pub username: String,
pub email: String,
pub password: String,
}
#[derive(Debug, Deserialize, Insertable)]
#[table_name = "users"]
pub struct NewUser {
pub username: String,
pub email: String,
pub password: String,
}
impl NewUser {
pub(crate) fn new(
username: String,
email: String,
password: String,
) -> Result<Self, ServerError> {
dotenv().ok();
let secret = std::env::var("SECRET_KEY")?;
let hash = Hasher::default()
.with_password(password)
.with_secret_key(secret)
.hash()
.unwrap();
Ok(NewUser {
username,
email,
password: hash,
})
}
}
#[derive(Debug, Deserialize)]
pub struct LoginUser {
pub username: String,
pub password: String,
}
#[derive(Serialize, Debug, Queryable)]
pub struct Link {
pub id: i32,
pub title: String,
pub target: String,
pub code: String,
pub author: i32,
pub created_at: chrono::NaiveDateTime,
}
#[derive(Serialize, Insertable)]
#[table_name = "links"]
pub struct NewLink {
pub title: String,
pub target: String,
pub code: String,
pub author: i32,
pub created_at: chrono::NaiveDateTime,
}
impl NewLink {
pub(crate) fn from_link_form(form: LinkForm, uid: i32) -> Self {
Self {
title: form.title,
target: form.target,
code: form.code,
author: uid,
created_at: chrono::Local::now().naive_utc(),
}
}
}
+23
View File
@@ -0,0 +1,23 @@
table! {
links (id) {
id -> Integer,
title -> Text,
target -> Text,
code -> Text,
author -> Integer,
created_at -> Timestamp,
}
}
table! {
users (id) {
id -> Integer,
username -> Text,
email -> Text,
password -> Text,
}
}
joinable!(links -> users (author));
allow_tables_to_appear_in_same_query!(links, users,);
+281
View File
@@ -0,0 +1,281 @@
use actix_identity::Identity;
use actix_web::{web, HttpResponse};
use qrcodegen::{QrCode, QrCodeEcc};
use crate::ServerError;
use super::forms::LinkForm;
use super::models::{Link, LoginUser, NewLink, NewUser, User};
use argonautica::Verifier;
use diesel::sqlite::SqliteConnection;
use diesel::{prelude::*, result::Error::NotFound};
use dotenv::dotenv;
use tera::{Context, Tera};
fn establish_connection() -> Result<SqliteConnection, ServerError> {
dotenv().ok();
let database_url = std::env::var("DATABASE_URL")?;
match SqliteConnection::establish(&database_url) {
Ok(c) => Ok(c),
Err(e) => {
info!("Error connecting to database: {}, {}", database_url, e);
Err(ServerError::User(
"Error connecting to Database".to_string(),
))
}
}
}
/// Show the list of all available links if a user is authenticated
pub(crate) async fn index(
tera: web::Data<Tera>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
use super::schema::links::dsl::links;
use super::schema::users::dsl::users;
if let Some(id) = id.identity() {
let connection = establish_connection()?;
let all_links: Vec<(Link, User)> = links.inner_join(users).load(&connection)?;
let mut data = Context::new();
data.insert("name", &id);
data.insert("title", "Links der Freien Hochschule Stuttgart");
data.insert("links_per_users", &all_links);
let rendered = tera.render("index.html", &data)?;
Ok(HttpResponse::Ok().body(rendered))
} else {
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
}
pub(crate) async fn view_link(
tera: web::Data<Tera>,
id: Identity,
link_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
println!("Viewing link!");
use super::schema::links::dsl::{code, links};
if let Some(id) = id.identity() {
let connection = establish_connection()?;
let link: Link = links
.filter(code.eq(&link_id.0))
.first::<Link>(&connection)?;
let qr =
QrCode::encode_text(&format!("http://fhs.li/{}", &link_id.0), QrCodeEcc::Low).unwrap();
let svg = qr.to_svg_string(4);
let mut data = Context::new();
data.insert("name", &id);
data.insert(
"title",
&format!("Links {} der Freien Hochschule Stuttgart", link_id.0),
);
data.insert("link", &link);
data.insert("qr", &svg);
let rendered = tera.render("view_link.html", &data)?;
Ok(HttpResponse::Ok().body(rendered))
} else {
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
}
pub(crate) async fn signup(
tera: web::Data<Tera>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
if let Some(id) = id.identity() {
let mut data = Context::new();
data.insert("title", "Sign Up");
data.insert("name", &id);
let rendered = tera.render("signup.html", &data)?;
Ok(HttpResponse::Ok().body(rendered))
} else {
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
}
pub(crate) async fn process_signup(
data: web::Form<NewUser>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
if let Some(_id) = id.identity() {
use super::schema::users;
let connection = establish_connection()?;
let new_user = NewUser::new(
data.username.clone(),
data.email.clone(),
data.password.clone(),
)?;
diesel::insert_into(users::table)
.values(&new_user)
.execute(&connection)?;
println!("{:?}", data);
Ok(HttpResponse::Ok().body(format!("Successfully saved user: {}", data.username)))
} else {
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
}
pub(crate) async fn login(
tera: web::Data<Tera>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let mut data = Context::new();
data.insert("title", "Login");
if let Some(_id) = id.identity() {
return Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/index/")
.body("Redirect to /index/"));
}
let rendered = tera.render("login.html", &data)?;
Ok(HttpResponse::Ok().body(rendered))
}
pub(crate) async fn process_login(
data: web::Form<LoginUser>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
use super::schema::users::dsl::{username, users};
let connection = establish_connection()?;
let user = users
.filter(username.eq(&data.username))
.first::<User>(&connection);
match user {
Ok(u) => {
dotenv().ok();
let secret = std::env::var("SECRET_KEY")?;
let valid = Verifier::default()
.with_hash(u.password)
.with_password(data.password.clone())
.with_secret_key(secret)
.verify()?;
if valid {
let session_token = u.username;
id.remember(session_token);
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/index/")
.body("Redirect to /index/"))
} else {
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
}
Err(_e) => Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/")),
}
}
pub(crate) async fn logout(id: Identity) -> Result<HttpResponse, ServerError> {
id.forget();
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
pub(crate) async fn redirect(
tera: web::Data<Tera>,
data: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
use super::schema::links::dsl::{code, links};
let connection = establish_connection()?;
let link = links.filter(code.eq(&data.0)).first::<Link>(&connection);
match link {
Ok(link) => Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, link.target.clone())
.body(format!("Redirect to {}", link.target))),
Err(NotFound) => {
let mut data = Context::new();
data.insert("title", "Wurde gelöscht");
let rendered = tera.render("not_found.html", &data)?;
Ok(HttpResponse::NotFound().body(rendered))
}
Err(e) => Err(e.into()),
}
}
pub(crate) async fn redirect_fhs() -> Result<HttpResponse, ServerError> {
Ok(HttpResponse::TemporaryRedirect().set_header(
actix_web::http::header::LOCATION,
"https://www.freie-hochschule-stuttgart.de",
).body("If you are not redirected automatically go to https://www.freie-hochschule-stuttgart.de"))
}
pub(crate) async fn submission(
tera: web::Data<Tera>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
if let Some(id) = id.identity() {
let mut data = Context::new();
data.insert("title", "Submit a Post");
data.insert("name", &id);
let rendered = tera.render("submission.html", &data)?;
return Ok(HttpResponse::Ok().body(rendered));
}
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
pub(crate) async fn process_submission(
data: web::Form<LinkForm>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
if let Some(id) = id.identity() {
use super::schema::users::dsl::{username, users};
let connection = establish_connection()?;
let user: Result<User, diesel::result::Error> =
users.filter(username.eq(id)).first(&connection);
match user {
Ok(u) => {
use super::schema::links;
let new_post = NewLink::from_link_form(data.into_inner(), u.id);
diesel::insert_into(links::table)
.values(&new_post)
.execute(&connection)?;
return Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/index/")
.body("Redirect to /index/"));
}
Err(_e) => Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/")),
}
} else {
Ok(HttpResponse::TemporaryRedirect()
.set_header(actix_web::http::header::LOCATION, "/login/")
.body("Redirect to /login/"))
}
}