hyaenidae/src/profiles/update.rs
asonix 010dd2952f Server: Expose NSFW toggle, Dark Mode toggle
Ensure all submission view permission logic is the same
2021-02-03 21:09:25 -06:00

718 lines
20 KiB
Rust

use crate::{
error::{Error, OptionExt},
middleware::{ProfileData, UserProfile},
nav::NavState,
profiles::{settings::Settings, state::EditProfileState, to_current_profile},
ActixLoader, State,
};
use actix_session::Session;
use actix_web::{client::Client, web, HttpRequest, HttpResponse, Scope};
use hyaenidae_accounts::User;
use hyaenidae_profiles::store::Profile;
use hyaenidae_toolkit::TextInput;
use i18n_embed_fl::fl;
use uuid::Uuid;
pub(super) fn create_scope() -> Scope {
web::scope("/create")
.service(web::resource("").route(web::get().to(to_create)))
.service(
web::resource("/handle")
.route(web::get().to(new_handle))
.route(web::post().to(create_handle)),
)
.service(
web::resource("/bio")
.route(web::get().to(new_bio))
.route(web::post().to(create_bio)),
)
.service(
web::resource("/icon")
.route(web::get().to(new_icon))
.route(web::post().to(create_icon)),
)
.service(
web::resource("/banner")
.route(web::get().to(new_banner))
.route(web::post().to(create_banner)),
)
.service(
web::resource("/require-login")
.route(web::get().to(new_require_login))
.route(web::post().to(create_require_login)),
)
.service(web::resource("/done").route(web::get().to(done)))
}
pub(super) fn update_scope() -> Scope {
web::scope("/update")
.service(
web::resource("/bio")
.route(web::post().to(update_bio))
.route(web::get().to(to_current_profile)),
)
.service(
web::resource("/icon")
.route(web::post().to(update_icon))
.route(web::get().to(to_current_profile)),
)
.service(
web::resource("/banner")
.route(web::post().to(update_banner))
.route(web::get().to(to_current_profile)),
)
.service(
web::resource("/require-login")
.route(web::post().to(update_require_login))
.route(web::get().to(to_current_profile)),
)
.service(
web::resource("/settings")
.route(web::post().to(update_settings))
.route(web::get().to(to_current_profile)),
)
}
pub(super) fn to_create() -> HttpResponse {
crate::redirect("/profiles/create/handle")
}
fn to_bio() -> HttpResponse {
crate::redirect("/profiles/create/bio")
}
fn to_icon() -> HttpResponse {
crate::redirect("/profiles/create/icon")
}
fn to_banner() -> HttpResponse {
crate::redirect("/profiles/create/banner")
}
fn to_require_login() -> HttpResponse {
crate::redirect("/profiles/create/require-login")
}
fn to_done() -> HttpResponse {
crate::redirect("/profiles/create/done")
}
async fn refresh(profile: Profile, state: &State) -> Result<Profile, Error> {
let id = profile.id();
super::profile_from_id(id, state).await
}
const MAX_HANDLE_LEN: usize = 20;
const MAX_DISPLAY_NAME_LEN: usize = 20;
const MAX_DESCRIPTION_LEN: usize = 500;
fn validate_handle(handle: &str) -> Option<String> {
if handle.chars().any(|c| !c.is_alphanumeric()) {
return Some("Must contain only alphanumeric characters".to_owned());
}
if handle.len() > MAX_HANDLE_LEN {
return Some(format!(
"Must be shorter than {} characters",
MAX_HANDLE_LEN
));
}
None
}
fn validate_display_name(display_name: &str) -> Option<String> {
if display_name.len() > MAX_DISPLAY_NAME_LEN {
return Some(format!(
"Must be shorter than {} characters",
MAX_DISPLAY_NAME_LEN
));
}
if display_name.len() == 0 {
return Some(format!("Must be present"));
}
None
}
fn validate_description(description: &str) -> Option<String> {
if description.len() > MAX_DESCRIPTION_LEN {
return Some(format!(
"Must be shorter than {} characters",
MAX_DESCRIPTION_LEN
));
}
None
}
async fn update_bio(
loader: ActixLoader,
form: web::Form<BioForm>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
use hyaenidae_profiles::apub::actions::UpdateProfile;
let profile = profile.into_inner();
let display_name = form.display_name.clone();
let description = form.description.clone();
let display_name_error = validate_display_name(&display_name);
let description_error = validate_description(&description);
let res = if display_name_error.is_none() && description_error.is_none() {
let res = state
.profiles
.run(UpdateProfile::from_text(
profile.id(),
display_name.clone(),
description.clone(),
))
.await;
Some(res)
} else {
None
};
let profile = refresh(profile, &state).await?;
let mut profile_state = EditProfileState::for_profile(profile, &state).await?;
profile_state
.display_name_value(display_name)
.description_value(description);
match res {
Some(Ok(_)) => return Ok(to_current_profile()),
Some(Err(e)) => {
profile_state.display_name_error(e.to_string());
}
None => {
if let Some(e) = display_name_error {
profile_state.display_name_error(e);
}
if let Some(e) = description_error {
profile_state.description_error(e);
}
}
};
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state)
})
}
async fn do_update_image<F, A>(
request: HttpRequest,
payload: web::Payload,
profile: UserProfile,
client: web::Data<Client>,
state: &State,
f: F,
) -> Result<Result<(), (EditProfileState, String)>, Error>
where
F: FnOnce(Uuid) -> A,
A: hyaenidae_profiles::Action + Send + 'static,
{
let profile = profile.into_inner();
let res = state
.profiles
.upload_image(request, payload.into_inner(), &client)
.await;
let error = match res {
Ok(file_ids) if file_ids.len() == 1 => {
let res = state.profiles.run((f)(file_ids[0])).await;
match res {
Ok(_) => return Ok(Ok(())),
Err(e) => e.to_string(),
}
}
Ok(_) => "Incorrect number of files".to_owned(),
Err(e) => e.to_string(),
};
let profile = refresh(profile, state).await?;
let profile_state = EditProfileState::for_profile(profile, state).await?;
Ok(Err((profile_state, error)))
}
async fn update_icon(
loader: ActixLoader,
request: HttpRequest,
payload: web::Payload,
profile: UserProfile,
nav_state: NavState,
client: web::Data<Client>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
use hyaenidae_profiles::apub::actions::UpdateProfile;
let profile_id = profile.0.id();
let cb = move |file_id| UpdateProfile::from_icon(profile_id, file_id);
let (mut profile_state, error) =
match do_update_image(request, payload, profile, client, &state, cb).await? {
Ok(_) => return Ok(to_current_profile()),
Err(profile_state) => profile_state,
};
profile_state.icon_error(&error);
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state)
})
}
async fn update_banner(
loader: ActixLoader,
request: HttpRequest,
payload: web::Payload,
profile: UserProfile,
nav_state: NavState,
client: web::Data<Client>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
use hyaenidae_profiles::apub::actions::UpdateProfile;
let profile_id = profile.0.id();
let cb = move |file_id| UpdateProfile::from_banner(profile_id, file_id);
let (mut profile_state, error) =
match do_update_image(request, payload, profile, client, &state, cb).await? {
Ok(()) => return Ok(to_current_profile()),
Err(profile_state) => profile_state,
};
profile_state.banner_error(&error);
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state)
})
}
async fn update_require_login(
loader: ActixLoader,
form: web::Form<RequireLoginForm>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.into_inner();
let login_required = form.require_login.is_some();
use hyaenidae_profiles::apub::actions::UpdateProfile;
let res = state
.profiles
.run(UpdateProfile::from_login_required(
profile.id(),
login_required,
))
.await;
let profile = refresh(profile, &state).await?;
let mut profile_state = EditProfileState::for_profile(profile, &state).await?;
profile_state.login_required_value(login_required);
match res {
Ok(_) => return Ok(to_current_profile()),
Err(e) => {
profile_state.login_required_error(&e.to_string());
}
};
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state)
})
}
#[derive(serde::Deserialize)]
struct SettingsForm {
sensitive: Option<String>,
dark: Option<String>,
}
async fn update_settings(
loader: ActixLoader,
form: web::Form<SettingsForm>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.0;
let settings = Settings {
sensitive: form.sensitive.is_some(),
dark: form.dark.is_some(),
};
let error = match state.settings.update(profile.id(), settings).await {
Ok(_) => return Ok(to_current_profile()),
Err(e) => e.to_string(),
};
let mut profile_state = EditProfileState::for_profile(profile, &state).await?;
profile_state.settings_error(error);
crate::rendered(HttpResponse::InternalServerError(), |cursor| {
crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct HandleForm {
pub(crate) handle: String,
}
#[derive(Default)]
pub struct HandleState {
handle_value: Option<String>,
handle_error: Option<String>,
}
impl HandleState {
pub(crate) fn handle(&self, loader: &ActixLoader) -> TextInput {
let input = TextInput::new("handle")
.title(&fl!(loader, "create-handle-input"))
.placeholder(&fl!(loader, "create-handle-placeholder"))
.error_opt(self.handle_error.clone());
if let Some(text) = &self.handle_value {
input.value(text)
} else {
input
}
}
fn new() -> Self {
Self::default()
}
fn handle_value(&mut self, text: String) -> &mut Self {
self.handle_value = Some(text);
self
}
fn handle_error(&mut self, text: String) -> &mut Self {
self.handle_error = Some(text);
self
}
}
async fn new_handle(
loader: ActixLoader,
_: User,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let state = HandleState::new();
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::handle(cursor, &loader, &state, &nav_state)
})
}
async fn create_handle(
loader: ActixLoader,
user: User,
form: web::Form<HandleForm>,
session: Session,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
use hyaenidae_profiles::apub::actions::CreateProfile;
let form = form.into_inner();
let domain = state.domain.clone();
let handle = form.handle.clone();
let error = match validate_handle(&handle) {
Some(error) => error,
None => {
let fallible = || async move {
let exists = state
.profiles
.store
.profiles
.by_handle(&handle, &domain)?
.is_some();
if !exists {
let id = state
.profiles
.run(CreateProfile::from_local(user.id(), handle, domain))
.await?
.ok_or_else(|| anyhow::anyhow!("Failed to create profile"))?;
Ok(id)
} else {
Err(anyhow::anyhow!("Handle already in use"))
}
};
match (fallible)().await {
Ok(id) => {
ProfileData::set_data(id, &session).ok().req()?;
return Ok(to_bio());
}
Err(e) => e.to_string(),
}
}
};
let mut state = HandleState::new();
state.handle_error(error).handle_value(form.handle);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::profiles::create::handle(cursor, &loader, &state, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct BioForm {
pub(crate) display_name: String,
pub(crate) description: String,
}
async fn new_bio(
loader: ActixLoader,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.into_inner();
let profile_state = EditProfileState::for_profile(profile, &state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::bio(cursor, &loader, &profile_state, &nav_state)
})
}
async fn create_bio(
loader: ActixLoader,
form: web::Form<BioForm>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
use hyaenidae_profiles::apub::actions::UpdateProfile;
let profile = profile.into_inner();
let display_name = form.display_name.clone();
let description = form.description.clone();
let display_name_error = validate_display_name(&display_name);
let description_error = validate_description(&description);
let res = if display_name_error.is_none() && description_error.is_none() {
let res = state
.profiles
.run(UpdateProfile::from_text(
profile.id(),
display_name.clone(),
description.clone(),
))
.await;
Some(res)
} else {
None
};
let profile = refresh(profile, &state).await?;
let mut profile_state = EditProfileState::for_profile(profile, &state).await?;
profile_state
.display_name_value(display_name)
.description_value(description);
match res {
Some(Ok(_)) => return Ok(to_icon()),
Some(Err(e)) => {
profile_state.display_name_error(e.to_string());
}
None => {
if let Some(e) = display_name_error {
profile_state.display_name_error(e);
}
if let Some(e) = description_error {
profile_state.description_error(e);
}
}
};
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::profiles::create::bio(cursor, &loader, &profile_state, &nav_state)
})
}
async fn new_icon(
loader: ActixLoader,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.into_inner();
let profile_state = EditProfileState::for_profile(profile, &state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::icon(cursor, &loader, &profile_state, &nav_state)
})
}
async fn create_icon(
loader: ActixLoader,
request: HttpRequest,
payload: web::Payload,
profile: UserProfile,
nav_state: NavState,
client: web::Data<Client>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
use hyaenidae_profiles::apub::actions::UpdateProfile;
let profile_id = profile.0.id();
let cb = move |file_id| UpdateProfile::from_icon(profile_id, file_id);
let (mut profile_state, error) =
match do_update_image(request, payload, profile, client, &state, cb).await? {
Ok(_) => return Ok(to_banner()),
Err(profile_state) => profile_state,
};
profile_state.icon_error(&error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::profiles::create::icon(cursor, &loader, &profile_state, &nav_state)
})
}
async fn new_banner(
loader: ActixLoader,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.into_inner();
let profile_state = EditProfileState::for_profile(profile, &state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::banner(cursor, &loader, &profile_state, &nav_state)
})
}
async fn create_banner(
loader: ActixLoader,
request: HttpRequest,
payload: web::Payload,
profile: UserProfile,
nav_state: NavState,
client: web::Data<Client>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
use hyaenidae_profiles::apub::actions::UpdateProfile;
let profile_id = profile.0.id();
let cb = move |file_id| UpdateProfile::from_banner(profile_id, file_id);
let (mut profile_state, error) =
match do_update_image(request, payload, profile, client, &state, cb).await? {
Ok(()) => return Ok(to_require_login()),
Err(profile_state) => profile_state,
};
profile_state.banner_error(&error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::profiles::create::banner(cursor, &loader, &profile_state, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RequireLoginForm {
require_login: Option<String>,
}
async fn new_require_login(
loader: ActixLoader,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.into_inner();
let profile_state = EditProfileState::for_profile(profile, &state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::require_login(
cursor,
&loader,
&profile_state,
&nav_state,
)
})
}
async fn create_require_login(
loader: ActixLoader,
form: web::Form<RequireLoginForm>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.into_inner();
use hyaenidae_profiles::apub::actions::UpdateProfile;
let res = state
.profiles
.run(UpdateProfile::from_login_required(
profile.id(),
form.require_login.is_some(),
))
.await;
let error = match res {
Ok(_) => return Ok(to_done()),
Err(e) => e.to_string(),
};
let profile = refresh(profile, &state).await?;
let mut profile_state = EditProfileState::for_profile(profile, &state).await?;
profile_state
.login_required_error(&error)
.login_required_value(form.require_login.is_some());
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::require_login(
cursor,
&loader,
&profile_state,
&nav_state,
)
})
}
async fn done(
loader: ActixLoader,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.into_inner();
let state = EditProfileState::for_profile(profile, &state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::done(cursor, &loader, &state, &nav_state)
})
}