718 lines
20 KiB
Rust
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)
|
|
})
|
|
}
|