Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/990
This commit is contained in:
commit
1d06a8f1ad
11
CHANGELOG.md
11
CHANGELOG.md
@ -6,11 +6,18 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Introduce environment variable `MAIL_PORT`
|
- Introduce environment variable `MAIL_PORT` (#980)
|
||||||
|
- Introduce email sign-up feature (#636)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Some styling improvements (#976, #977, #978)
|
||||||
|
|
||||||
### Fiexed
|
### Fiexed
|
||||||
|
|
||||||
- Fix a bug that notification page doesn't show
|
- Fix comment link (#974)
|
||||||
|
- Fix a bug that prevents posting articles (#975)
|
||||||
|
- Fix a bug that notification page doesn't show (#981)
|
||||||
|
|
||||||
## [[0.7.0]] - 2022-01-02
|
## [[0.7.0]] - 2022-01-02
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE email_signups;
|
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE email_signups (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
email VARCHAR NOT NULL,
|
||||||
|
token VARCHAR NOT NULL,
|
||||||
|
expiration_date TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX email_signups_token ON email_signups (token);
|
||||||
|
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
|
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE email_signups;
|
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE email_signups (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email VARCHAR NOT NULL,
|
||||||
|
token VARCHAR NOT NULL,
|
||||||
|
expiration_date TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX email_signups_token ON email_signups (token);
|
||||||
|
CREATE UNIQUE INDEX email_signups_token_requests_email ON email_signups (email);
|
@ -1,4 +1,5 @@
|
|||||||
use crate::search::TokenizerKind as SearchTokenizer;
|
use crate::search::TokenizerKind as SearchTokenizer;
|
||||||
|
use crate::signups::Strategy as SignupStrategy;
|
||||||
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||||
use rocket::config::Limits;
|
use rocket::config::Limits;
|
||||||
use rocket::Config as RocketConfig;
|
use rocket::Config as RocketConfig;
|
||||||
@ -16,6 +17,7 @@ pub struct Config {
|
|||||||
pub db_name: &'static str,
|
pub db_name: &'static str,
|
||||||
pub db_max_size: Option<u32>,
|
pub db_max_size: Option<u32>,
|
||||||
pub db_min_idle: Option<u32>,
|
pub db_min_idle: Option<u32>,
|
||||||
|
pub signup: SignupStrategy,
|
||||||
pub search_index: String,
|
pub search_index: String,
|
||||||
pub search_tokenizers: SearchTokenizerConfig,
|
pub search_tokenizers: SearchTokenizerConfig,
|
||||||
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
|
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
|
||||||
@ -362,6 +364,7 @@ lazy_static! {
|
|||||||
s.parse::<u32>()
|
s.parse::<u32>()
|
||||||
.expect("Couldn't parse DB_MIN_IDLE into u32")
|
.expect("Couldn't parse DB_MIN_IDLE into u32")
|
||||||
)),
|
)),
|
||||||
|
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
database_url: var("DATABASE_URL")
|
database_url: var("DATABASE_URL")
|
||||||
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
||||||
|
143
plume-models/src/email_signups.rs
Normal file
143
plume-models/src/email_signups.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
use crate::{
|
||||||
|
db_conn::DbConn,
|
||||||
|
schema::email_signups,
|
||||||
|
users::{NewUser, Role, User},
|
||||||
|
Error, Result,
|
||||||
|
};
|
||||||
|
use chrono::{offset::Utc, Duration, NaiveDateTime};
|
||||||
|
use diesel::{
|
||||||
|
Connection as _, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl,
|
||||||
|
};
|
||||||
|
use plume_common::utils::random_hex;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
const TOKEN_VALIDITY_HOURS: i64 = 2;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Token(String);
|
||||||
|
|
||||||
|
impl From<String> for Token {
|
||||||
|
fn from(string: String) -> Self {
|
||||||
|
Token(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Token> for String {
|
||||||
|
fn from(token: Token) -> Self {
|
||||||
|
token.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Token {
|
||||||
|
type Target = String;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Token {
|
||||||
|
fn generate() -> Self {
|
||||||
|
Self(random_hex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable)]
|
||||||
|
pub struct EmailSignup {
|
||||||
|
pub id: i32,
|
||||||
|
pub email: String,
|
||||||
|
pub token: String,
|
||||||
|
pub expiration_date: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[table_name = "email_signups"]
|
||||||
|
pub struct NewEmailSignup<'a> {
|
||||||
|
pub email: &'a str,
|
||||||
|
pub token: &'a str,
|
||||||
|
pub expiration_date: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailSignup {
|
||||||
|
pub fn start(conn: &DbConn, email: &str) -> Result<Token> {
|
||||||
|
conn.transaction(|| {
|
||||||
|
Self::ensure_user_not_exist_by_email(conn, email)?;
|
||||||
|
let _rows = Self::delete_existings_by_email(conn, email)?;
|
||||||
|
let token = Token::generate();
|
||||||
|
let expiration_date = Utc::now()
|
||||||
|
.naive_utc()
|
||||||
|
.checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
|
||||||
|
.expect("could not calculate expiration date");
|
||||||
|
let new_signup = NewEmailSignup {
|
||||||
|
email,
|
||||||
|
token: &token,
|
||||||
|
expiration_date,
|
||||||
|
};
|
||||||
|
let _rows = diesel::insert_into(email_signups::table)
|
||||||
|
.values(new_signup)
|
||||||
|
.execute(&**conn)?;
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_token(conn: &DbConn, token: Token) -> Result<Self> {
|
||||||
|
let signup = email_signups::table
|
||||||
|
.filter(email_signups::token.eq(token.as_str()))
|
||||||
|
.first::<Self>(&**conn)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
Ok(signup)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn confirm(&self, conn: &DbConn) -> Result<()> {
|
||||||
|
conn.transaction(|| {
|
||||||
|
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
||||||
|
if self.expired() {
|
||||||
|
Self::delete_existings_by_email(conn, &self.email)?;
|
||||||
|
return Err(Error::Expired);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete(&self, conn: &DbConn, username: String, password: String) -> Result<User> {
|
||||||
|
conn.transaction(|| {
|
||||||
|
Self::ensure_user_not_exist_by_email(conn, &self.email)?;
|
||||||
|
let user = NewUser::new_local(
|
||||||
|
conn,
|
||||||
|
username,
|
||||||
|
"".to_string(),
|
||||||
|
Role::Normal,
|
||||||
|
"",
|
||||||
|
self.email.clone(),
|
||||||
|
Some(User::hash_pass(&password)?),
|
||||||
|
)?;
|
||||||
|
self.delete(conn)?;
|
||||||
|
Ok(user)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&self, conn: &DbConn) -> Result<()> {
|
||||||
|
let _rows = diesel::delete(self).execute(&**conn).map_err(Error::from)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_user_not_exist_by_email(conn: &DbConn, email: &str) -> Result<()> {
|
||||||
|
if User::email_used(conn, email)? {
|
||||||
|
let _rows = Self::delete_existings_by_email(conn, email)?;
|
||||||
|
return Err(Error::UserAlreadyExists);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_existings_by_email(conn: &DbConn, email: &str) -> Result<usize> {
|
||||||
|
let existing_signups = email_signups::table.filter(email_signups::email.eq(email));
|
||||||
|
diesel::delete(existing_signups)
|
||||||
|
.execute(&**conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expired(&self) -> bool {
|
||||||
|
self.expiration_date < Utc::now().naive_utc()
|
||||||
|
}
|
||||||
|
}
|
@ -67,6 +67,7 @@ pub enum Error {
|
|||||||
Url,
|
Url,
|
||||||
Webfinger,
|
Webfinger,
|
||||||
Expired,
|
Expired,
|
||||||
|
UserAlreadyExists,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bcrypt::BcryptError> for Error {
|
impl From<bcrypt::BcryptError> for Error {
|
||||||
@ -376,6 +377,7 @@ pub mod blogs;
|
|||||||
pub mod comment_seers;
|
pub mod comment_seers;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod db_conn;
|
pub mod db_conn;
|
||||||
|
pub mod email_signups;
|
||||||
pub mod follows;
|
pub mod follows;
|
||||||
pub mod headers;
|
pub mod headers;
|
||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
@ -396,6 +398,7 @@ pub mod safe_string;
|
|||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
pub mod signups;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
@ -73,6 +73,7 @@ table! {
|
|||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
email_blocklist(id){
|
email_blocklist(id){
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@ -83,6 +84,15 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
email_signups (id) {
|
||||||
|
id -> Int4,
|
||||||
|
email -> Varchar,
|
||||||
|
token -> Varchar,
|
||||||
|
expiration_date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
follows (id) {
|
follows (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@ -306,6 +316,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
blogs,
|
blogs,
|
||||||
comments,
|
comments,
|
||||||
comment_seers,
|
comment_seers,
|
||||||
|
email_signups,
|
||||||
follows,
|
follows,
|
||||||
instances,
|
instances,
|
||||||
likes,
|
likes,
|
||||||
|
45
plume-models/src/signups.rs
Normal file
45
plume-models/src/signups.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub enum Strategy {
|
||||||
|
Password,
|
||||||
|
Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Strategy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Strategy {
|
||||||
|
type Err = StrategyError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
use self::Strategy::*;
|
||||||
|
|
||||||
|
match s {
|
||||||
|
"password" => Ok(Password),
|
||||||
|
"email" => Ok(Email),
|
||||||
|
s => Err(StrategyError::Unsupported(s.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StrategyError {
|
||||||
|
Unsupported(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for StrategyError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
use self::StrategyError::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
// FIXME: Calc option strings from enum
|
||||||
|
Unsupported(s) => write!(f, "Unsupported strategy: {}. Choose password or email", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for StrategyError {}
|
@ -202,6 +202,22 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Should create user record with normalized(lowercased) email
|
||||||
|
*/
|
||||||
|
pub fn email_used(conn: &DbConn, email: &str) -> Result<bool> {
|
||||||
|
use diesel::dsl::{exists, select};
|
||||||
|
|
||||||
|
select(exists(
|
||||||
|
users::table
|
||||||
|
.filter(users::instance_id.eq(Instance::get_local()?.id))
|
||||||
|
.filter(users::email.eq(email))
|
||||||
|
.or_filter(users::email.eq(email.to_ascii_lowercase())),
|
||||||
|
))
|
||||||
|
.get_result(&**conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
|
fn fetch_from_webfinger(conn: &DbConn, acct: &str) -> Result<User> {
|
||||||
let link = resolve(acct.to_owned(), true)?
|
let link = resolve(acct.to_owned(), true)?
|
||||||
.links
|
.links
|
||||||
|
@ -149,6 +149,10 @@ Then try to restart Plume.
|
|||||||
routes::comments::create,
|
routes::comments::create,
|
||||||
routes::comments::delete,
|
routes::comments::delete,
|
||||||
routes::comments::activity_pub,
|
routes::comments::activity_pub,
|
||||||
|
routes::email_signups::create,
|
||||||
|
routes::email_signups::created,
|
||||||
|
routes::email_signups::show,
|
||||||
|
routes::email_signups::signup,
|
||||||
routes::instance::index,
|
routes::instance::index,
|
||||||
routes::instance::admin,
|
routes::instance::admin,
|
||||||
routes::instance::admin_mod,
|
routes::instance::admin_mod,
|
||||||
|
235
src/routes/email_signups.rs
Normal file
235
src/routes/email_signups.rs
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
use crate::{
|
||||||
|
mail::{build_mail, Mailer},
|
||||||
|
routes::{errors::ErrorPage, RespondOrRedirect},
|
||||||
|
template_utils::{IntoContext, Ructe},
|
||||||
|
};
|
||||||
|
use plume_models::{
|
||||||
|
db_conn::DbConn, email_signups::EmailSignup, instance::Instance, lettre::Transport,
|
||||||
|
signups::Strategy as SignupStrategy, Error, PlumeRocket, CONFIG,
|
||||||
|
};
|
||||||
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
request::LenientForm,
|
||||||
|
response::{Flash, Redirect},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tracing::warn;
|
||||||
|
use validator::{Validate, ValidationError, ValidationErrors};
|
||||||
|
|
||||||
|
#[derive(Default, FromForm, Validate)]
|
||||||
|
#[validate(schema(
|
||||||
|
function = "emails_match",
|
||||||
|
skip_on_field_errors = "false",
|
||||||
|
message = "Emails are not matching"
|
||||||
|
))]
|
||||||
|
pub struct EmailSignupForm {
|
||||||
|
#[validate(email(message = "Invalid email"))]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(email(message = "Invalid email"))]
|
||||||
|
pub email_confirmation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emails_match(form: &EmailSignupForm) -> Result<(), ValidationError> {
|
||||||
|
if form.email_confirmation == form.email {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("emails_match"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, FromForm, Validate)]
|
||||||
|
#[validate(schema(
|
||||||
|
function = "passwords_match",
|
||||||
|
skip_on_field_errors = "false",
|
||||||
|
message = "Passwords are not matching"
|
||||||
|
))]
|
||||||
|
pub struct NewUserForm {
|
||||||
|
#[validate(length(min = "1", message = "Username should be at least 1 characters long"))]
|
||||||
|
pub username: String,
|
||||||
|
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
||||||
|
pub password: String,
|
||||||
|
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
||||||
|
pub password_confirmation: String,
|
||||||
|
pub email: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
||||||
|
if form.password != form.password_confirmation {
|
||||||
|
Err(ValidationError::new("password_match"))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/email_signups/new", data = "<form>")]
|
||||||
|
pub fn create(
|
||||||
|
mail: State<'_, Arc<Mutex<Mailer>>>,
|
||||||
|
form: LenientForm<EmailSignupForm>,
|
||||||
|
conn: DbConn,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<RespondOrRedirect, Ructe> {
|
||||||
|
use RespondOrRedirect::{FlashRedirect, Response};
|
||||||
|
|
||||||
|
if !matches!(CONFIG.signup, SignupStrategy::Email) {
|
||||||
|
return Ok(FlashRedirect(Flash::error(
|
||||||
|
Redirect::to(uri!(super::user::new)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"Email registrations are not enabled. Please restart."
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let registration_open = !Instance::get_local()
|
||||||
|
.map(|i| i.open_registrations)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if registration_open {
|
||||||
|
return Ok(FlashRedirect(Flash::error(
|
||||||
|
Redirect::to(uri!(super::user::new)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"Registrations are closed on this instance."
|
||||||
|
),
|
||||||
|
))); // Actually, it is an error
|
||||||
|
}
|
||||||
|
let mut form = form.into_inner();
|
||||||
|
form.email = form.email.trim().to_owned();
|
||||||
|
form.validate().map_err(|err| {
|
||||||
|
render!(email_signups::new(
|
||||||
|
&(&conn, &rockets).to_context(),
|
||||||
|
registration_open,
|
||||||
|
&form,
|
||||||
|
err
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let res = EmailSignup::start(&conn, &form.email);
|
||||||
|
if let Some(err) = res.as_ref().err() {
|
||||||
|
return Ok(match err {
|
||||||
|
Error::UserAlreadyExists => {
|
||||||
|
// TODO: Notify to admin (and the user?)
|
||||||
|
warn!("Registration attempted for existing user: {}. Registraion halted and email sending skipped.", &form.email);
|
||||||
|
Response(render!(email_signups::create(
|
||||||
|
&(&conn, &rockets).to_context()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Error::NotFound => {
|
||||||
|
Response(render!(errors::not_found(&(&conn, &rockets).to_context())))
|
||||||
|
}
|
||||||
|
_ => Response(render!(errors::not_found(&(&conn, &rockets).to_context()))), // FIXME
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let token = res.unwrap();
|
||||||
|
let url = format!(
|
||||||
|
"https://{}{}",
|
||||||
|
CONFIG.base_url,
|
||||||
|
uri!(show: token = token.to_string())
|
||||||
|
);
|
||||||
|
let message = build_mail(
|
||||||
|
form.email,
|
||||||
|
i18n!(rockets.intl.catalog, "User registration"),
|
||||||
|
i18n!(rockets.intl.catalog, "Here is the link for registration: {0}"; url),
|
||||||
|
)
|
||||||
|
.expect("Mail configuration has already been done at ignition process");
|
||||||
|
// TODO: Render error page
|
||||||
|
if let Some(ref mut mailer) = *mail.lock().unwrap() {
|
||||||
|
mailer.send(message.into()).ok(); // TODO: Render error page
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response(render!(email_signups::create(
|
||||||
|
&(&conn, &rockets).to_context()
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/email_signups/new")]
|
||||||
|
pub fn created(conn: DbConn, rockets: PlumeRocket) -> Ructe {
|
||||||
|
render!(email_signups::create(&(&conn, &rockets).to_context()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/email_signups/<token>")]
|
||||||
|
pub fn show(token: String, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
|
let signup = EmailSignup::find_by_token(&conn, token.into())?;
|
||||||
|
let confirmation = signup.confirm(&conn);
|
||||||
|
if let Some(err) = confirmation.err() {
|
||||||
|
match err {
|
||||||
|
Error::Expired => {
|
||||||
|
return Ok(render!(email_signups::new(
|
||||||
|
&(&conn, &rockets).to_context(),
|
||||||
|
Instance::get_local()?.open_registrations,
|
||||||
|
&EmailSignupForm::default(),
|
||||||
|
ValidationErrors::default()
|
||||||
|
)))
|
||||||
|
} // TODO: Flash and redirect
|
||||||
|
Error::NotFound => return Err(Error::NotFound.into()),
|
||||||
|
_ => return Err(Error::NotFound.into()), // FIXME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let form = NewUserForm {
|
||||||
|
email: signup.email,
|
||||||
|
token: signup.token,
|
||||||
|
..NewUserForm::default()
|
||||||
|
};
|
||||||
|
Ok(render!(email_signups::edit(
|
||||||
|
&(&conn, &rockets).to_context(),
|
||||||
|
Instance::get_local()?.open_registrations,
|
||||||
|
&form,
|
||||||
|
ValidationErrors::default()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/email_signups/signup", data = "<form>")]
|
||||||
|
pub fn signup(
|
||||||
|
form: LenientForm<NewUserForm>,
|
||||||
|
conn: DbConn,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<RespondOrRedirect, Status> {
|
||||||
|
use RespondOrRedirect::{FlashRedirect, Response};
|
||||||
|
|
||||||
|
let instance = Instance::get_local().map_err(|e| {
|
||||||
|
warn!("{:?}", e);
|
||||||
|
Status::InternalServerError
|
||||||
|
})?;
|
||||||
|
if let Some(err) = form.validate().err() {
|
||||||
|
return Ok(Response(render!(email_signups::edit(
|
||||||
|
&(&conn, &rockets).to_context(),
|
||||||
|
instance.open_registrations,
|
||||||
|
&form,
|
||||||
|
err
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
let signup = EmailSignup::find_by_token(&conn, form.token.clone().into())
|
||||||
|
.map_err(|_| Status::NotFound)?;
|
||||||
|
if form.email != signup.email {
|
||||||
|
let mut err = ValidationErrors::default();
|
||||||
|
err.add("email", ValidationError::new("Email couldn't changed"));
|
||||||
|
let form = NewUserForm {
|
||||||
|
username: form.username.clone(),
|
||||||
|
password: form.password.clone(),
|
||||||
|
password_confirmation: form.password_confirmation.clone(),
|
||||||
|
email: signup.email,
|
||||||
|
token: form.token.clone(),
|
||||||
|
};
|
||||||
|
return Ok(Response(render!(email_signups::edit(
|
||||||
|
&(&conn, &rockets).to_context(),
|
||||||
|
instance.open_registrations,
|
||||||
|
&form,
|
||||||
|
err
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
let _user = signup
|
||||||
|
.complete(&conn, form.username.clone(), form.password.clone())
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("{:?}", e);
|
||||||
|
Status::UnprocessableEntity
|
||||||
|
})?;
|
||||||
|
Ok(FlashRedirect(Flash::success(
|
||||||
|
Redirect::to(uri!(super::session::new: m = _)),
|
||||||
|
i18n!(
|
||||||
|
rockets.intl.catalog,
|
||||||
|
"Your account has been created. Now you just need to log in, before you can use it."
|
||||||
|
),
|
||||||
|
)))
|
||||||
|
}
|
@ -190,6 +190,7 @@ fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
|||||||
|
|
||||||
pub mod blogs;
|
pub mod blogs;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
|
pub mod email_signups;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod likes;
|
pub mod likes;
|
||||||
|
@ -10,14 +10,16 @@ use std::{borrow::Cow, collections::HashMap};
|
|||||||
use validator::{Validate, ValidationError, ValidationErrors};
|
use validator::{Validate, ValidationError, ValidationErrors};
|
||||||
|
|
||||||
use crate::inbox;
|
use crate::inbox;
|
||||||
use crate::routes::{errors::ErrorPage, Page, RemoteForm, RespondOrRedirect};
|
use crate::routes::{
|
||||||
|
email_signups::EmailSignupForm, errors::ErrorPage, Page, RemoteForm, RespondOrRedirect,
|
||||||
|
};
|
||||||
use crate::template_utils::{IntoContext, Ructe};
|
use crate::template_utils::{IntoContext, Ructe};
|
||||||
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, Id};
|
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, Id};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
blogs::Blog, db_conn::DbConn, follows, headers::Headers, inbox::inbox as local_inbox,
|
blogs::Blog, db_conn::DbConn, follows, headers::Headers, inbox::inbox as local_inbox,
|
||||||
instance::Instance, medias::Media, posts::Post, reshares::Reshare, safe_string::SafeString,
|
instance::Instance, medias::Media, posts::Post, reshares::Reshare, safe_string::SafeString,
|
||||||
users::*, Error, PlumeRocket, CONFIG,
|
signups::Strategy as SignupStrategy, users::*, Error, PlumeRocket, CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/me")]
|
#[get("/me")]
|
||||||
@ -260,12 +262,23 @@ pub fn activity_details(
|
|||||||
|
|
||||||
#[get("/users/new")]
|
#[get("/users/new")]
|
||||||
pub fn new(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
pub fn new(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||||
Ok(render!(users::new(
|
use SignupStrategy::*;
|
||||||
&(&conn, &rockets).to_context(),
|
|
||||||
Instance::get_local()?.open_registrations,
|
let rendered = match CONFIG.signup {
|
||||||
&NewUserForm::default(),
|
Password => render!(users::new(
|
||||||
ValidationErrors::default()
|
&(&conn, &rockets).to_context(),
|
||||||
)))
|
Instance::get_local()?.open_registrations,
|
||||||
|
&NewUserForm::default(),
|
||||||
|
ValidationErrors::default()
|
||||||
|
)),
|
||||||
|
Email => render!(email_signups::new(
|
||||||
|
&(&conn, &rockets).to_context(),
|
||||||
|
Instance::get_local()?.open_registrations,
|
||||||
|
&EmailSignupForm::default(),
|
||||||
|
ValidationErrors::default()
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
Ok(rendered)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/edit")]
|
#[get("/@/<name>/edit")]
|
||||||
|
9
templates/email_signups/create.rs.html
Normal file
9
templates/email_signups/create.rs.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@use crate::template_utils::*;
|
||||||
|
@use crate::templates::base;
|
||||||
|
|
||||||
|
@(ctx: BaseContext)
|
||||||
|
|
||||||
|
@:base(ctx, i18n!(ctx.1, "Registration"), {}, {}, {
|
||||||
|
<h1>@i18n!(ctx.1, "Check your inbox!")</h1>
|
||||||
|
<p>@i18n!(ctx.1, "We sent a mail to the address you gave us, with a link for registration.")</p>
|
||||||
|
})
|
43
templates/email_signups/edit.rs.html
Normal file
43
templates/email_signups/edit.rs.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
@use std::borrow::Cow;
|
||||||
|
@use validator::{ValidationErrors, ValidationErrorsKind};
|
||||||
|
@use crate::templates::base;
|
||||||
|
@use crate::template_utils::*;
|
||||||
|
@use crate::routes::email_signups::NewUserForm;
|
||||||
|
@use crate::routes::*;
|
||||||
|
|
||||||
|
@(ctx: BaseContext, enabled: bool, form: &NewUserForm, errors: ValidationErrors)
|
||||||
|
|
||||||
|
@:base(ctx, i18n!(ctx.1, "Create your account"), {}, {}, {
|
||||||
|
@if enabled {
|
||||||
|
<h1>@i18n!(ctx.1, "Create an account")</h1>
|
||||||
|
<form method="post" action="@uri!(email_signups::signup)">
|
||||||
|
@if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("__all__") {
|
||||||
|
<p class="error">@errs[0].message.as_ref().unwrap_or(&Cow::from("Unknown error"))</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@(Input::new("username", i18n!(ctx.1, "Username"))
|
||||||
|
.default(&form.username)
|
||||||
|
.error(&errors)
|
||||||
|
.set_prop("required", "")
|
||||||
|
.html(ctx.1))
|
||||||
|
@(Input::new("password", i18n!(ctx.1, "Password"))
|
||||||
|
.default(&form.password)
|
||||||
|
.error(&errors)
|
||||||
|
.set_prop("minlength", 8)
|
||||||
|
.input_type("password")
|
||||||
|
.html(ctx.1))
|
||||||
|
@(Input::new("password_confirmation", i18n!(ctx.1, "Password confirmation"))
|
||||||
|
.default(&form.password_confirmation)
|
||||||
|
.error(&errors)
|
||||||
|
.set_prop("minlength", 8)
|
||||||
|
.input_type("password")
|
||||||
|
.html(ctx.1))
|
||||||
|
<input type="hidden" name="email" value="@form.email">
|
||||||
|
<input type="hidden" name="token" value="@form.token">
|
||||||
|
|
||||||
|
<input type="submit" value="@i18n!(ctx.1, "Create your account")" />
|
||||||
|
</form>
|
||||||
|
} else {
|
||||||
|
<p class="center">@i18n!(ctx.1, "Apologies, but registrations are closed on this particular instance. You can, however, find a different one.")</p>
|
||||||
|
}
|
||||||
|
})
|
37
templates/email_signups/new.rs.html
Normal file
37
templates/email_signups/new.rs.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@use std::borrow::Cow;
|
||||||
|
@use validator::{ValidationErrors, ValidationErrorsKind};
|
||||||
|
@use crate::templates::base;
|
||||||
|
@use crate::template_utils::*;
|
||||||
|
@use crate::routes::email_signups::EmailSignupForm;
|
||||||
|
@use crate::routes::*;
|
||||||
|
|
||||||
|
@(ctx: BaseContext, enabled: bool, form: &EmailSignupForm, errors: ValidationErrors)
|
||||||
|
|
||||||
|
@:base(ctx, i18n!(ctx.1, "Create your account"), {}, {}, {
|
||||||
|
@if enabled {
|
||||||
|
<h1>@i18n!(ctx.1, "Create an account")</h1>
|
||||||
|
<form method="post" action="@uri!(email_signups::create)">
|
||||||
|
@if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("__all__") {
|
||||||
|
<p class="error">@errs[0].message.as_ref().unwrap_or(&Cow::from("Unknown error"))</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@(Input::new("email", i18n!(ctx.1, "Email"))
|
||||||
|
.input_type("email")
|
||||||
|
.default(&form.email)
|
||||||
|
.error(&errors)
|
||||||
|
.set_prop("required", "")
|
||||||
|
.html(ctx.1))
|
||||||
|
|
||||||
|
@(Input::new("email_confirmation", i18n!(ctx.1, "Email confirmation"))
|
||||||
|
.input_type("email")
|
||||||
|
.default(&form.email_confirmation)
|
||||||
|
.error(&errors)
|
||||||
|
.set_prop("required", "")
|
||||||
|
.html(ctx.1))
|
||||||
|
|
||||||
|
<input type="submit" value="@i18n!(ctx.1, "Create your account")" />
|
||||||
|
</form>
|
||||||
|
} else {
|
||||||
|
<p class="center">@i18n!(ctx.1, "Apologies, but registrations are closed on this particular instance. You can, however, find a different one.")</p>
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user