refactor(handlers/render): Use users from database for all functions

Converse now sets the user ID as the session identity parameter in
actix_web's identity middleware after a successful login and uses the
ID to determine identity when creating threads & posts and when
validating edit permissions.
This commit is contained in:
Vincent Ambo 2018-05-01 23:00:15 +02:00 committed by Vincent Ambo
parent 6d69f5001e
commit 40fb4ebbb9
2 changed files with 54 additions and 57 deletions

View file

@ -26,7 +26,8 @@
use actix::prelude::*; use actix::prelude::*;
use actix_web; use actix_web;
use actix_web::*; use actix_web::*;
use actix_web::middleware::{Started, Middleware, RequestSession}; use actix_web::middleware::identity::RequestIdentity;
use actix_web::middleware::{Started, Middleware};
use db::*; use db::*;
use errors::ConverseError; use errors::ConverseError;
use futures::Future; use futures::Future;
@ -37,6 +38,8 @@ use render::*;
type ConverseResponse = Box<Future<Item=HttpResponse, Error=ConverseError>>; type ConverseResponse = Box<Future<Item=HttpResponse, Error=ConverseError>>;
const HTML: &'static str = "text/html"; const HTML: &'static str = "text/html";
const ANONYMOUS: i32 = 1;
const NEW_THREAD_LENGTH_ERR: &'static str = "Title and body can not be empty!";
/// Represents the state carried by the web server actors. /// Represents the state carried by the web server actors.
pub struct AppState { pub struct AppState {
@ -61,14 +64,25 @@ pub fn forum_index(state: State<AppState>) -> ConverseResponse {
.responder() .responder()
} }
/// Returns the ID of the currently logged in user. If there is no ID
/// present, the ID of the anonymous user will be returned.
pub fn get_user_id(req: &HttpRequest<AppState>) -> i32 {
if let Some(id) = req.identity() {
// If this .expect() call is triggered, someone is likely
// attempting to mess with their cookies. These requests can
// be allowed to fail without further ado.
id.parse().expect("Session cookie contained invalid data!")
} else {
ANONYMOUS
}
}
/// This handler retrieves and displays a single forum thread. /// This handler retrieves and displays a single forum thread.
pub fn forum_thread(state: State<AppState>, pub fn forum_thread(state: State<AppState>,
mut req: HttpRequest<AppState>, req: HttpRequest<AppState>,
thread_id: Path<i32>) -> ConverseResponse { thread_id: Path<i32>) -> ConverseResponse {
let id = thread_id.into_inner(); let id = thread_id.into_inner();
let user = req.session().get(AUTHOR) let user = get_user_id(&req);
.unwrap_or_else(|_| None)
.map(|a: Author| a.email);
state.db.send(GetThread(id)) state.db.send(GetThread(id))
.flatten() .flatten()
@ -89,28 +103,17 @@ pub fn new_thread(state: State<AppState>) -> ConverseResponse {
.responder() .responder()
} }
/// This function provides an anonymous "default" author if logins are
/// not required.
fn anonymous() -> Author {
Author {
name: "Anonymous".into(),
email: "anonymous@nothing.org".into(),
}
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct NewThreadForm { pub struct NewThreadForm {
pub title: String, pub title: String,
pub post: String, pub post: String,
} }
const NEW_THREAD_LENGTH_ERR: &'static str = "Title and body can not be empty!";
/// This handler receives a "New thread"-form and redirects the user /// This handler receives a "New thread"-form and redirects the user
/// to the new thread after creation. /// to the new thread after creation.
pub fn submit_thread(state: State<AppState>, pub fn submit_thread(state: State<AppState>,
input: Form<NewThreadForm>, input: Form<NewThreadForm>,
mut req: HttpRequest<AppState>) -> ConverseResponse { req: HttpRequest<AppState>) -> ConverseResponse {
// Trim whitespace out of inputs: // Trim whitespace out of inputs:
let input = NewThreadForm { let input = NewThreadForm {
title: input.title.trim().into(), title: input.title.trim().into(),
@ -130,14 +133,11 @@ pub fn submit_thread(state: State<AppState>,
.responder(); .responder();
} }
let author: Author = req.session().get(AUTHOR) let user_id = get_user_id(&req);
.unwrap_or_else(|_| Some(anonymous()))
.unwrap_or_else(anonymous);
let new_thread = NewThread { let new_thread = NewThread {
user_id,
title: input.title, title: input.title,
author_name: author.name,
author_email: author.email,
}; };
let msg = CreateThread { let msg = CreateThread {
@ -167,16 +167,13 @@ pub struct NewPostForm {
/// new post after creation. /// new post after creation.
pub fn reply_thread(state: State<AppState>, pub fn reply_thread(state: State<AppState>,
input: Form<NewPostForm>, input: Form<NewPostForm>,
mut req: HttpRequest<AppState>) -> ConverseResponse { req: HttpRequest<AppState>) -> ConverseResponse {
let author: Author = req.session().get(AUTHOR) let user_id = get_user_id(&req);
.unwrap_or_else(|_| Some(anonymous()))
.unwrap_or_else(anonymous);
let new_post = NewPost { let new_post = NewPost {
user_id,
thread_id: input.thread_id, thread_id: input.thread_id,
body: input.post.trim().into(), body: input.post.trim().into(),
author_name: author.name,
author_email: author.email,
}; };
state.db.send(CreatePost(new_post)) state.db.send(CreatePost(new_post))
@ -196,19 +193,16 @@ pub fn reply_thread(state: State<AppState>,
/// they are currently ungracefully redirected back to the post /// they are currently ungracefully redirected back to the post
/// itself. /// itself.
pub fn edit_form(state: State<AppState>, pub fn edit_form(state: State<AppState>,
mut req: HttpRequest<AppState>, req: HttpRequest<AppState>,
query: Path<GetPost>) -> ConverseResponse { query: Path<GetPost>) -> ConverseResponse {
let author: Option<Author> = req.session().get(AUTHOR) let user_id = get_user_id(&req);
.unwrap_or_else(|_| None);
state.db.send(query.into_inner()) state.db.send(query.into_inner())
.flatten() .flatten()
.from_err() .from_err()
.and_then(move |post| { .and_then(move |post| {
if let Some(author) = author { if user_id != 1 && post.user_id == user_id {
if author.email.eq(&post.author_email) { return Ok(post);
return Ok(post);
}
} }
Err(ConverseError::PostEditForbidden { id: post.id }) Err(ConverseError::PostEditForbidden { id: post.id })
@ -229,21 +223,19 @@ pub fn edit_form(state: State<AppState>,
/// This handler "executes" an edit to a post if the current user owns /// This handler "executes" an edit to a post if the current user owns
/// the edited post. /// the edited post.
pub fn edit_post(state: State<AppState>, pub fn edit_post(state: State<AppState>,
mut req: HttpRequest<AppState>, req: HttpRequest<AppState>,
update: Form<UpdatePost>) -> ConverseResponse { update: Form<UpdatePost>) -> ConverseResponse {
let author: Option<Author> = req.session().get(AUTHOR) let user_id = get_user_id(&req);
.unwrap_or_else(|_| None);
state.db.send(GetPost { id: update.post_id }) state.db.send(GetPost { id: update.post_id })
.flatten() .flatten()
.from_err() .from_err()
.and_then(move |post| { .and_then(move |post| {
if let Some(author) = author { if user_id != 1 && post.user_id == user_id {
if author.email.eq(&post.author_email) { Ok(())
return Ok(()); } else {
} Err(ConverseError::PostEditForbidden { id: post.id })
} }
Err(ConverseError::PostEditForbidden { id: post.id })
}) })
.and_then(move |_| state.db.send(update.0).from_err()) .and_then(move |_| state.db.send(update.0).from_err())
.flatten() .flatten()
@ -280,17 +272,24 @@ pub fn login(state: State<AppState>) -> ConverseResponse {
.responder() .responder()
} }
const AUTHOR: &'static str = "author"; /// This handler handles an OIDC callback (i.e. completed login).
///
/// Upon receiving the callback, a token is retrieved from the OIDC
/// provider and a user lookup is performed. If a user with a matching
/// email-address is found in the database, it is logged in -
/// otherwise a new user is created.
pub fn callback(state: State<AppState>, pub fn callback(state: State<AppState>,
data: Form<CodeResponse>, data: Form<CodeResponse>,
mut req: HttpRequest<AppState>) -> ConverseResponse { mut req: HttpRequest<AppState>) -> ConverseResponse {
state.oidc.send(RetrieveToken(data.0)) state.oidc.send(RetrieveToken(data.0)).flatten()
.from_err() .map(|author| LookupOrCreateUser {
.and_then(move |result| { email: author.email,
let author = result?; name: author.name,
info!("Setting cookie for {} after callback", author.name); })
req.session().set(AUTHOR, author)?; .and_then(move |msg| state.db.send(msg).from_err()).flatten()
.and_then(move |user| {
info!("Completed login for user {} ({})", user.email, user.id);
req.remember(user.id.to_string());
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.header("Location", "/") .header("Location", "/")
.finish())}) .finish())})
@ -303,10 +302,10 @@ pub struct RequireLogin;
impl <S> Middleware<S> for RequireLogin { impl <S> Middleware<S> for RequireLogin {
fn start(&self, req: &mut HttpRequest<S>) -> actix_web::Result<Started> { fn start(&self, req: &mut HttpRequest<S>) -> actix_web::Result<Started> {
let has_author = req.session().get::<Author>(AUTHOR)?.is_some(); let logged_in = req.identity().is_some();
let is_oidc_req = req.path().starts_with("/oidc"); let is_oidc_req = req.path().starts_with("/oidc");
if !is_oidc_req && !has_author { if !is_oidc_req && !logged_in {
Ok(Started::Response( Ok(Started::Response(
HttpResponse::SeeOther() HttpResponse::SeeOther()
.header("Location", "/oidc/login") .header("Location", "/oidc/login")

View file

@ -87,7 +87,7 @@ impl Handler<IndexPage> for Renderer {
/// Message used to render a thread. /// Message used to render a thread.
pub struct ThreadPage { pub struct ThreadPage {
pub current_user: Option<String>, pub current_user: i32,
pub thread: Thread, pub thread: Thread,
pub posts: Vec<SimplePost>, pub posts: Vec<SimplePost>,
} }
@ -122,9 +122,7 @@ fn prepare_thread(comrak: &ComrakOptions, page: ThreadPage) -> RenderableThreadP
let user = page.current_user; let user = page.current_user;
let posts = page.posts.into_iter().map(|post| { let posts = page.posts.into_iter().map(|post| {
let editable = user.clone() let editable = user != 1 && post.user_id == user;
.map(|c| post.author_email.eq(&c))
.unwrap_or_else(|| false);
RenderablePost { RenderablePost {
id: post.id, id: post.id,