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:
parent
6d69f5001e
commit
40fb4ebbb9
2 changed files with 54 additions and 57 deletions
105
src/handlers.rs
105
src/handlers.rs
|
@ -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")
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue