feature: custom domains using Fairings
#596
igalic/feat/custom-fairing-domains
into master
@ -0,0 +1,2 @@
|
||||
-- undo the adding of custom_domain column to blogs table.
|
||||
ALTER TABLE blogs DROP COLUMN custom_domain;
|
@ -0,0 +1,2 @@
|
||||
--- Adding custom domain to Blog as an optional field
|
||||
ALTER TABLE blogs ADD COLUMN custom_domain VARCHAR DEFAULT NULL UNIQUE;
|
@ -0,0 +1,56 @@
|
||||
-- undo the adding of "custom_domain" to blogs
|
||||
CREATE TABLE IF NOT EXISTS "blogs_drop_custom_domain" (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
actor_id VARCHAR NOT NULL,
|
||||
title VARCHAR NOT NULL,
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ap_url text not null default '' UNIQUE,
|
||||
private_key TEXT,
|
||||
public_key TEXT NOT NULL DEFAULT '',
|
||||
fqn TEXT NOT NULL DEFAULT '',
|
||||
summary_html TEXT NOT NULL DEFAULT '',
|
||||
icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
||||
banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
||||
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
|
||||
);
|
||||
|
||||
INSERT INTO blogs_drop_custom_domain (
|
||||
id,
|
||||
actor_id,
|
||||
title,
|
||||
summary,
|
||||
outbox_url,
|
||||
inbox_url,
|
||||
instance_id,
|
||||
creation_date,
|
||||
ap_url,
|
||||
private_key,
|
||||
public_key,
|
||||
fqn,
|
||||
summary_html,
|
||||
icon_id,
|
||||
banner_id
|
||||
) SELECT
|
||||
id,
|
||||
actor_id,
|
||||
title,
|
||||
summary,
|
||||
outbox_url,
|
||||
inbox_url,
|
||||
instance_id,
|
||||
creation_date,
|
||||
ap_url,
|
||||
private_key,
|
||||
public_key,
|
||||
fqn,
|
||||
summary_html,
|
||||
icon_id,
|
||||
banner_id
|
||||
FROM blogs;
|
||||
|
||||
DROP TABLE blogs;
|
||||
ALTER TABLE "blogs_drop_custom_domain" RENAME to blogs;
|
@ -0,0 +1,57 @@
|
||||
-- add custom_domain to blogs
|
||||
CREATE TABLE IF NOT EXISTS "blogs_add_custom_domain" (
|
||||
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
actor_id VARCHAR NOT NULL,
|
||||
title VARCHAR NOT NULL,
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
outbox_url VARCHAR NOT NULL UNIQUE,
|
||||
inbox_url VARCHAR NOT NULL UNIQUE,
|
||||
instance_id INTEGER REFERENCES instances(id) ON DELETE CASCADE NOT NULL,
|
||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ap_url text not null default '' UNIQUE,
|
||||
private_key TEXT,
|
||||
public_key TEXT NOT NULL DEFAULT '',
|
||||
fqn TEXT NOT NULL DEFAULT '',
|
||||
summary_html TEXT NOT NULL DEFAULT '',
|
||||
icon_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
||||
banner_id INTEGER REFERENCES medias(id) ON DELETE SET NULL DEFAULT NULL,
|
||||
custom_domain text default NULL UNIQUE,
|
||||
CONSTRAINT blog_unique UNIQUE (actor_id, instance_id)
|
||||
);
|
||||
|
||||
INSERT INTO blogs_add_custom_domain (
|
||||
id,
|
||||
actor_id,
|
||||
title,
|
||||
summary,
|
||||
outbox_url,
|
||||
inbox_url,
|
||||
instance_id,
|
||||
creation_date,
|
||||
ap_url,
|
||||
private_key,
|
||||
public_key,
|
||||
fqn,
|
||||
summary_html,
|
||||
icon_id,
|
||||
banner_id
|
||||
) SELECT
|
||||
id,
|
||||
actor_id,
|
||||
title,
|
||||
summary,
|
||||
outbox_url,
|
||||
inbox_url,
|
||||
instance_id,
|
||||
creation_date,
|
||||
ap_url,
|
||||
private_key,
|
||||
public_key,
|
||||
fqn,
|
||||
summary_html,
|
||||
icon_id,
|
||||
banner_id
|
||||
FROM blogs;
|
||||
|
||||
DROP TABLE blogs;
|
||||
ALTER TABLE "blogs_add_custom_domain" RENAME to blogs;
|
@ -22,6 +22,7 @@ extern crate num_cpus;
|
||||
extern crate plume_api;
|
||||
extern crate plume_common;
|
||||
extern crate plume_models;
|
||||
extern crate reqwest;
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
@ -42,17 +43,21 @@ extern crate webfinger;
|
||||
use clap::App;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
blogs::Host,
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
instance::Instance,
|
||||
migrations::IMPORTED_MIGRATIONS,
|
||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||
Connection, Error, CONFIG,
|
||||
};
|
||||
use rocket::{fairing::AdHoc, http::ext::IntoOwned, http::uri::Origin};
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use std::collections::HashMap;
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
init_i18n!(
|
||||
"plume", ar, bg, ca, cs, de, en, eo, es, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr, sk, sv
|
||||
@ -87,6 +92,7 @@ fn init_pool() -> Option<DbPool> {
|
||||
.build(manager)
|
||||
.ok()?;
|
||||
Instance::cache_local(&pool.get().unwrap());
|
||||
Blog::cache_custom_domains(&pool.get().unwrap());
|
||||
Some(pool)
|
||||
}
|
||||
|
||||
@ -175,7 +181,42 @@ Then try to restart Plume
|
||||
println!("Please refer to the documentation to see how to configure it.");
|
||||
}
|
||||
|
||||
let custom_domain_fairing = AdHoc::on_request("Custom Blog Domains", |req, _data| {
|
||||
let host = req.guard::<Host>();
|
||||
if host.is_success()
|
||||
&& req
|
||||
.uri()
|
||||
.segments()
|
||||
.next()
|
||||
.map(|path| path != "static" && path != "api")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let rewrite_uri = format!("/custom_domains/{}/{}", host.unwrap(), req.uri());
|
||||
let uri = Origin::parse_owned(rewrite_uri).unwrap();
|
||||
let uri = uri.to_normalized().into_owned();
|
||||
trinity-1686a
commented 5 years ago
Review
I'd be in favor of using the newtype design pattern for this, so that it make more sense what it is, even at the type level. I'd be in favor of using the newtype design pattern for this, so that it make more sense what it is, even at the type level.
I don't know where are the &str coming from, but you might want to use String instead, or you'll have some difficulties with lifetime
Review
aye. i haven't gotten that far yet, because i haven't implemented the other side of this aye. i haven't gotten that far yet, because i haven't implemented the other side of this
|
||||
req.set_uri(uri);
|
||||
}
|
||||
});
|
||||
|
||||
let valid_domains: HashMap<String, Instant> = HashMap::new();
|
||||
let rocket = rocket::custom(CONFIG.rocket.clone().unwrap())
|
||||
.mount(
|
||||
"/custom_domains/domain_validation/",
|
||||
routes![routes::blogs::custom::domain_validation,],
|
||||
)
|
||||
.mount(
|
||||
"/domain_validation/",
|
||||
routes![routes::blogs::domain_validation,],
|
||||
)
|
||||
.mount(
|
||||
"/custom_domains/",
|
||||
routes![
|
||||
routes::blogs::custom::details,
|
||||
routes::posts::custom::details,
|
||||
routes::blogs::custom::activity_details,
|
||||
routes::search::custom::search,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
@ -288,6 +329,7 @@ Then try to restart Plume
|
||||
.manage(dbpool)
|
||||
.manage(Arc::new(workpool))
|
||||
.manage(searcher)
|
||||
.manage(Mutex::new(valid_domains))
|
||||
.manage(include_i18n!())
|
||||
.attach(
|
||||
CsrfFairingBuilder::new()
|
||||
@ -314,7 +356,8 @@ Then try to restart Plume
|
||||
])
|
||||
.finalize()
|
||||
.expect("main: csrf fairing creation error"),
|
||||
);
|
||||
)
|
||||
.attach(custom_domain_fairing);
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||
|
@ -2,11 +2,14 @@ use activitypub::collection::OrderedCollection;
|
||||
use atom_syndication::{Entry, FeedBuilder};
|
||||
use diesel::SaveChangesDsl;
|
||||
use rocket::{
|
||||
http::ContentType,
|
||||
http::{ContentType, Status},
|
||||
request::LenientForm,
|
||||
response::{content::Content, Flash, Redirect},
|
||||
State,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
@ -16,14 +19,17 @@ use plume_models::{
|
||||
blog_authors::*, blogs::*, instance::Instance, medias::*, posts::Post, safe_string::SafeString,
|
||||
users::User, Connection, PlumeRocket,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
||||
use template_utils::{IntoContext, Ructe};
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
fn detail_guts(
|
||||
blog: Blog,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
let conn = &*rockets.conn;
|
||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||
let posts = Post::blog_page(conn, &blog, page.limits())?;
|
||||
let articles_count = Post::count_for_blog(conn, &blog)?;
|
||||
let authors = &blog.list_authors(conn)?;
|
||||
@ -35,7 +41,43 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
|
||||
page.0,
|
||||
Page::total(articles_count as i32),
|
||||
posts
|
||||
)))
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
pub fn details(
|
||||
name: String,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let blog = Blog::find_by_fqn(&rockets, &name)?;
|
||||
|
||||
// check this first, and return early
|
||||
// doing this prevents partially moving `blog` into the `match (tuple)`,
|
||||
// which makes it impossible to reuse then.
|
||||
if blog.custom_domain == None {
|
||||
return detail_guts(blog, page, rockets);
|
||||
}
|
||||
|
||||
match (blog.custom_domain, page) {
|
||||
(Some(ref custom_domain), Some(ref page)) => {
|
||||
Ok(Redirect::to(format!("https://{}/?page={}", custom_domain, page)).into())
|
||||
}
|
||||
(Some(ref custom_domain), _) => {
|
||||
Ok(Redirect::to(format!("https://{}/", custom_domain)).into())
|
||||
}
|
||||
// we need this match arm, or the match won't compile
|
||||
(None, _) => unreachable!("This code path should have already been handled!"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activity_detail_guts(
|
||||
blog: Blog,
|
||||
rockets: PlumeRocket,
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomGroup>> {
|
||||
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
|
||||
}
|
||||
|
||||
#[get("/~/<name>", rank = 1)]
|
||||
@ -45,7 +87,7 @@ pub fn activity_details(
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomGroup>> {
|
||||
let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
|
||||
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
|
||||
activity_detail_guts(blog, rockets, _ap)
|
||||
}
|
||||
|
||||
#[get("/blogs/new")]
|
||||
@ -57,6 +99,76 @@ pub fn new(rockets: PlumeRocket, _user: User) -> Ructe {
|
||||
))
|
||||
}
|
||||
trinity-1686a
commented 5 years ago
Review
I believe there are consts that already contains standards http status in the module I believe there are consts that already contains standards http status in the module
trinity-1686a
commented 5 years ago
Review
Same as above Same as above
Review
```
error[E0308]: mismatched types
--> src/routes/blogs.rs:113:28
|
113 | return Status::new(Status::NotFound, "validation id not found");
| ^^^^^^^^^^^^^^^^ expected u16, found struct `rocket::http::Status`
|
= note: expected type `u16`
found type `rocket::http::Status`
error[E0308]: mismatched types
--> src/routes/blogs.rs:123:28
|
123 | return Status::new(Status::Gone, "validation expired");
| ^^^^^^^^^^^^ expected u16, found struct `rocket::http::Status`
|
= note: expected type `u16`
found type `rocket::http::Status`
```
trinity-1686a
commented 5 years ago
Review
Have you tried simply
? Have you tried simply
```suggestion
return Status::Gone;
```
?
Review
No, cuz I wanted to give a specific response. No, cuz I wanted to give a specific response.
trinity-1686a
commented 5 years ago
Review
imported from rocket::response::status::Custom ```suggestion
return Custom(Status::Gone, "validation expired");
```
imported from rocket::response::status::Custom
Your version is sending a standard error code with a non standard text, some implementations don't handle this well. This return the corresponding "error" text, and set the body of the result to the text you provide
|
||||
|
||||
// mounted as /domain_validation/
|
||||
#[get("/<validation_id>")]
|
||||
pub fn domain_validation(
|
||||
validation_id: String,
|
||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
||||
) -> Status {
|
||||
let mutex = valid_domains.inner().lock();
|
||||
let mut validation_map = mutex.unwrap();
|
||||
let validation_getter = validation_map.clone();
|
||||
|
||||
let value = validation_getter.get(&validation_id);
|
||||
if value.is_none() {
|
||||
// validation id not found
|
||||
return Status::NotFound;
|
||||
}
|
||||
|
||||
// we have valid id, now check the time
|
||||
let valid_until = value.unwrap();
|
||||
let now = Instant::now();
|
||||
|
||||
// nope, expired (410: gone)
|
||||
if now.duration_since(*valid_until).as_secs() > 0 {
|
||||
validation_map.remove(&validation_id);
|
||||
// validation expired
|
||||
return Status::Gone;
|
||||
}
|
||||
|
||||
validation_map.remove(&validation_id);
|
||||
Status::Ok
|
||||
}
|
||||
|
||||
pub mod custom {
|
||||
use plume_common::activity_pub::{ActivityStream, ApRequest};
|
||||
use plume_models::{blogs::Blog, blogs::CustomGroup, blogs::Host, PlumeRocket};
|
||||
use rocket::{http::Status, State};
|
||||
use routes::{errors::ErrorPage, Page, RespondOrRedirect};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
#[get("/<custom_domain>?<page>", rank = 2)]
|
||||
pub fn details(
|
||||
custom_domain: String,
|
||||
page: Option<Page>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<RespondOrRedirect, ErrorPage> {
|
||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain))?;
|
||||
super::detail_guts(blog, page, rockets)
|
||||
}
|
||||
|
||||
#[get("/<custom_domain>", rank = 1)]
|
||||
pub fn activity_details(
|
||||
custom_domain: String,
|
||||
trinity-1686a
commented 5 years ago
Review
clippy says
clippy says
```suggestion
let custom_domain = if form.custom_domain.is_empty() {
```
Review
I can't accept your proposal via Github (apparently I don't have the right to push to this repository 🙃), but I made it manually, thanks. I can't accept your proposal via Github (apparently I don't have the right to push to this repository :upside_down_face:), but I made it manually, thanks.
|
||||
rockets: PlumeRocket,
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomGroup>> {
|
||||
let blog = Blog::find_by_host(&rockets, Host::new(custom_domain)).ok()?;
|
||||
super::activity_detail_guts(blog, rockets, _ap)
|
||||
}
|
||||
|
||||
// mounted as /custom_domains/domain_validation/
|
||||
#[get("/<validation_id>")]
|
||||
pub fn domain_validation(
|
||||
validation_id: String,
|
||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
||||
) -> Status {
|
||||
super::domain_validation(validation_id, valid_domains)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/blogs/new", rank = 2)]
|
||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
@ -72,6 +184,7 @@ pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
pub struct NewBlogForm {
|
||||
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
|
||||
pub title: String,
|
||||
pub custom_domain: String,
|
||||
}
|
||||
|
||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
@ -83,13 +196,43 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_domain(domain: &str, valid_domains: State<Mutex<HashMap<String, Instant>>>) -> bool {
|
||||
let mutex = valid_domains.inner().lock();
|
||||
let mut validation_map = mutex.unwrap();
|
||||
|
||||
let random_id = utils::random_hex();
|
||||
validation_map.insert(
|
||||
random_id.clone(),
|
||||
Instant::now().checked_add(Duration::new(60, 0)).unwrap(),
|
||||
);
|
||||
|
||||
trinity-1686a
commented 5 years ago
Review
```suggestion
Flash::warning(
```
|
||||
let client = Client::new();
|
||||
let validation_uri = format!("https://{}/domain_validation/{}", domain, random_id);
|
||||
|
||||
match client.get(&validation_uri).send() {
|
||||
Ok(resp) => resp.status().is_success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
trinity-1686a
commented 5 years ago
Review
You should remove the feature, as it's no longer used You should remove the feature, as it's no longer used
|
||||
|
||||
#[post("/blogs/new", data = "<form>")]
|
||||
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect {
|
||||
pub fn create(
|
||||
form: LenientForm<NewBlogForm>,
|
||||
rockets: PlumeRocket,
|
||||
valid_domains: State<Mutex<HashMap<String, Instant>>>,
|
||||
) -> RespondOrRedirect {
|
||||
let slug = utils::make_actor_id(&form.title);
|
||||
let conn = &*rockets.conn;
|
||||
let intl = &rockets.intl.catalog;
|
||||
let user = rockets.user.clone().unwrap();
|
||||
|
||||
let (custom_domain, dns_ok) = if form.custom_domain.is_empty() {
|
||||
(None, true)
|
||||
} else {
|
||||
let dns_check = valid_domain(&form.custom_domain.clone(), valid_domains);
|
||||
(Some(Host::new(form.custom_domain.clone())), dns_check)
|
||||
};
|
||||
|
||||
let mut errors = match form.validate() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e,
|
||||
@ -121,6 +264,7 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOr
|
||||
Instance::get_local()
|
||||
.expect("blog::create: instance error")
|
||||
.id,
|
||||
custom_domain,
|
||||
)
|
||||
.expect("blog::create: new local error"),
|
||||
)
|
||||
@ -136,11 +280,19 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOr
|
||||
)
|
||||
.expect("blog::create: author error");
|
||||
|
||||
Flash::success(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created!"),
|
||||
)
|
||||
.into()
|
||||
if dns_ok {
|
||||
Flash::success(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created!"),
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
Flash::warning(
|
||||
Redirect::to(uri!(details: name = slug.clone(), page = _)),
|
||||
&i18n!(intl, "Your blog was successfully created, but the custom domain seems invalid. Please check it is correct from your blog's settings."),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/~/<name>/delete")]
|
||||
@ -181,6 +333,7 @@ pub struct EditForm {
|
||||
pub summary: String,
|
||||
pub icon: Option<i32>,
|
||||
pub banner: Option<i32>,
|
||||
pub custom_domain: String,
|
||||
}
|
||||
|
||||
#[get("/~/<name>/edit")]
|
||||
@ -198,6 +351,10 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
.clone()
|
||||
.expect("blogs::edit: User was None while it shouldn't");
|
||||
let medias = Media::for_user(conn, user.id).expect("Couldn't list media");
|
||||
let custom_domain = match blog.custom_domain {
|
||||
Some(ref c) => c.to_string(),
|
||||
_ => String::from(""),
|
||||
};
|
||||
Ok(render!(blogs::edit(
|
||||
&rockets.to_context(),
|
||||
&blog,
|
||||
@ -207,6 +364,7 @@ pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
summary: blog.summary.clone(),
|
||||
icon: blog.icon_id,
|
||||
banner: blog.banner_id,
|
||||
custom_domain: custom_domain,
|
||||
},
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
@ -318,6 +476,10 @@ pub fn update(
|
||||
);
|
||||
blog.icon_id = form.icon;
|
||||
blog.banner_id = form.banner;
|
||||
if !form.custom_domain.is_empty() {
|
||||
blog.custom_domain = Some(Host::new(form.custom_domain.clone()))
|
||||
}
|
||||
|
||||
blog.save_changes::<Blog>(&*conn)
|
||||
.expect("Couldn't save blog changes");
|
||||
Ok(Flash::success(
|
||||
|
Just wondering why copying the table is required, instead of just adding the column?
because SQLite supports a limited subset of ALTER TABLE
Damn, I missed it was SQLite. Thanks.