client_max_body_size 50M;
location / {
- rewrite (\/(user|u\/|inbox|post|community|c\/|create_post|create_community|login|search|setup|sponsors|communities|modlog|home)+) /static/index.html break;
+ rewrite (\/(user|u\/|inbox|post|community|c\/|create_post|create_community|login|search|setup|sponsors|communities|modlog|home|password_change)+) /static/index.html break;
proxy_pass http://0.0.0.0:8536;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
DATABASE_PASSWORD=password
DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
JWT_SECRET=changeme
+
RATE_LIMIT_MESSAGE=30
RATE_LIMIT_MESSAGE_PER_SECOND=60
RATE_LIMIT_POST=3
RATE_LIMIT_POST_PER_SECOND=600
RATE_LIMIT_REGISTER=1
RATE_LIMIT_REGISTER_PER_SECOND=3600
+
+# Optional email fields
+SMTP_SERVER=
+SMTP_LOGIN=
+SMTP_PASSWORD=
+SMTP_FROM_ADDRESS=Domain.com Lemmy Admin <notifications@domain.com>
COPY ui /app/ui
RUN yarn build
-FROM rust:1.38 as rust
-
-# Install musl
-RUN apt-get update
-RUN apt-get install musl-tools -y
-RUN rustup target add x86_64-unknown-linux-musl
+FROM ekidd/rust-musl-builder:1.38.0-openssl11 as rust
# Cache deps
WORKDIR /app
+RUN sudo chown -R rust:rust .
RUN USER=root cargo new server
WORKDIR /app/server
COPY server/Cargo.toml server/Cargo.lock ./
-RUN mkdir -p ./src/bin \
+RUN sudo chown -R rust:rust .
+RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
-RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl
+RUN cargo build --release
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server*
COPY server/src ./src/
COPY server/migrations ./migrations/
-# build for release
-RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --frozen --release --target=x86_64-unknown-linux-musl
+# Build for release
+RUN cargo build --frozen --release
# Get diesel-cli on there just in case
# RUN cargo install diesel_cli --no-default-features --features postgres
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
+ - SMTP_SERVER=${SMTP_SERVER}
+ - SMTP_LOGIN=${SMTP_LOGIN}
+ - SMTP_PASSWORD=${SMTP_PASSWORD}
+ - SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
restart: always
depends_on:
- lemmy_db
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "gcc"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "generic-array"
version = "0.12.3"
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre_email 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
"strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "rand"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "rand"
version = "0.4.6"
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "rust-crypto"
+version = "0.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "rustc-serialize"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "rustc_version"
version = "0.2.3"
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
"checksum futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "45dc39533a6cae6da2b56da48edae506bb767ec07370f86f70fc062e9d435869"
+"checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
"checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55"
"checksum h2 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "a539b63339fbbb00e081e84b6e11bd1d9634a82d91da2984a18ac74a8823f392"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+"checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c"
"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
"checksum resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b263b4aa1b5de9ffc0054a2386f96992058bb6870aab516f8cdeb8a667d56dcb"
"checksum ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)" = "426bc186e3e95cac1e4a4be125a4aca7e84c2d616ffc02244eef36e2a60a093c"
+"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
"checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af"
+"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997"
"checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
jsonwebtoken = "6.0.1"
regex = "1.1.9"
lazy_static = "1.3.0"
+lettre = "0.9.2"
+lettre_email = "0.9.2"
+rust-crypto = "^0.2"
let user_email = &user.email.expect("email");
let subject = &format!("Password reset for {}", user.name);
let hostname = Settings::get().hostname;
- let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/{}>Click here to reset your password</a>", user.name, hostname, &token);
+ let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
match send_email(subject, user_email, &user.name, html) {
Ok(_o) => _o,
Err(_e) => {
use super::*;
use crate::schema::password_reset_request;
use crate::schema::password_reset_request::dsl::*;
-
-use bcrypt::{hash, DEFAULT_COST};
+use crypto::sha2::Sha256;
+use crypto::digest::Digest;
#[derive(Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "password_reset_request"]
impl PasswordResetRequest {
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
- let token_hash =
- hash(token, DEFAULT_COST).expect("Couldn't hash token");
+ let mut hasher = Sha256::new();
+ hasher.input_str(token);
+ let token_hash = hasher.result_str();
let form = PasswordResetRequestForm {
user_id: from_user_id,
Self::create(&conn, &form)
}
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
- let token_hash =
- hash(token, DEFAULT_COST).expect("Couldn't hash token");
-
- password_reset_request.filter(token_encrypted.eq(token_hash)).first::<Self>(conn)
+ let mut hasher = Sha256::new();
+ hasher.input_str(token);
+ let token_hash = hasher.result_str();
+ password_reset_request
+ .filter(token_encrypted.eq(token_hash))
+ .filter(published.gt(now - 1.days()))
+ .first::<Self>(conn)
}
}
pub extern crate strum;
pub extern crate lettre;
pub extern crate lettre_email;
+pub extern crate crypto;
pub mod api;
pub mod apub;
fn get() -> Self {
dotenv().ok();
- let email_config = if env::var("SMTP_SERVER").is_ok() {
+ let email_config = if env::var("SMTP_SERVER").is_ok() &&
+ !env::var("SMTP_SERVER").unwrap().eq("") {
Some(EmailConfig {
smtp_server: env::var("SMTP_SERVER").expect("SMTP_SERVER must be set"),
smtp_login: env::var("SMTP_LOGIN").expect("SMTP_LOGIN must be set"),
let email_config = Settings::get().email_config.ok_or("no_email_setup")?;
let email = Email::builder()
- // .to((to_email, username))
.to((to_email, to_username))
.from((email_config.smtp_login.to_owned(), email_config.smtp_from_address))
.subject(subject)
.build()
.unwrap();
- let mut mailer = SmtpClient::new_simple(&email_config.smtp_server).unwrap()
- .hello_name(ClientId::Domain("localhost".to_string()))
- .credentials(Credentials::new(
- email_config.smtp_login.to_owned(),
- email_config.smtp_password.to_owned()))
- .smtp_utf8(true)
- .authentication_mechanism(Mechanism::Plain)
- .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
- .transport();
+ let mut mailer = SmtpClient::new_simple(&email_config.smtp_server).unwrap()
+ .hello_name(ClientId::Domain("localhost".to_string()))
+ .credentials(Credentials::new(
+ email_config.smtp_login.to_owned(),
+ email_config.smtp_password.to_owned()))
+ .smtp_utf8(true)
+ .authentication_mechanism(Mechanism::Plain)
+ .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
+ .transport();
let result = mailer.send(email.into());
PasswordResetForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { msgOp } from '../utils';
+import { msgOp, validEmail } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
class="form-control"
required
/>
- <div
+ <button
+ disabled={!validEmail(this.state.loginForm.username_or_email)}
onClick={linkEvent(this, this.handlePasswordReset)}
- class="pointer d-inline-block float-right text-muted small font-weight-bold"
+ className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
>
<T i18nKey="forgot_password">#</T>
- </div>
+ </button>
</div>
</div>
<div class="form-group row">
}
handlePasswordReset(i: Login) {
+ event.preventDefault();
let resetForm: PasswordResetForm = {
email: i.state.loginForm.username_or_email,
};
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ LoginResponse,
+ PasswordChangeForm,
+} from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { msgOp, capitalizeFirstLetter } from '../utils';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface State {
+ passwordChangeForm: PasswordChangeForm;
+ loading: boolean;
+}
+
+export class PasswordChange extends Component<any, State> {
+ private subscription: Subscription;
+
+ emptyState: State = {
+ passwordChangeForm: {
+ token: this.props.match.params.token,
+ password: undefined,
+ password_verify: undefined,
+ },
+ loading: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(
+ retryWhen(errors =>
+ errors.pipe(
+ delay(3000),
+ take(10)
+ )
+ )
+ )
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ componentDidMount() {
+ document.title = `${i18n.t('password_change')} - ${
+ WebSocketService.Instance.site.name
+ }`;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>
+ <T i18nKey="password_change">#</T>
+ </h5>
+ {this.passwordChangeForm()}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ passwordChangeForm() {
+ return (
+ <form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ <T i18nKey="new_password">#</T>
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ value={this.state.passwordChangeForm.password}
+ onInput={linkEvent(this, this.handlePasswordChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ <T i18nKey="verify_password">#</T>
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ value={this.state.passwordChangeForm.password_verify}
+ onInput={linkEvent(this, this.handleVerifyPasswordChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary">
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('save'))
+ )}
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+
+ handlePasswordChange(i: PasswordChange, event: any) {
+ i.state.passwordChangeForm.password = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleVerifyPasswordChange(i: PasswordChange, event: any) {
+ i.state.passwordChangeForm.password_verify = event.target.value;
+ i.setState(i.state);
+ }
+
+ handlePasswordChangeSubmit(i: PasswordChange, event: any) {
+ event.preventDefault();
+ i.state.loading = true;
+ i.setState(i.state);
+
+ WebSocketService.Instance.passwordChange(i.state.passwordChangeForm);
+ }
+
+ parseMessage(msg: any) {
+ let op: UserOperation = msgOp(msg);
+ if (msg.error) {
+ alert(i18n.t(msg.error));
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else {
+ if (op == UserOperation.PasswordChange) {
+ this.state = this.emptyState;
+ this.setState(this.state);
+ let res: LoginResponse = msg;
+ UserService.Instance.login(res);
+ this.props.history.push('/');
+ }
+ }
+ }
+}
import { Login } from './components/login';
import { CreatePost } from './components/create-post';
import { CreateCommunity } from './components/create-community';
+import { PasswordChange } from './components/password_change';
import { Post } from './components/post';
import { Community } from './components/community';
import { Communities } from './components/communities';
/>
<Route path={`/search`} component={Search} />
<Route path={`/sponsors`} component={Sponsors} />
+ <Route
+ path={`/password_change/:token`}
+ component={PasswordChange}
+ />
</Switch>
<Symbols />
</div>
UserSettingsForm,
DeleteAccountForm,
PasswordResetForm,
+ PasswordChangeForm,
} from '../interfaces';
import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs';
this.subject.next(this.wsSendWrapper(UserOperation.PasswordReset, form));
}
+ public passwordChange(form: PasswordChangeForm) {
+ this.subject.next(this.wsSendWrapper(UserOperation.PasswordChange, form));
+ }
+
private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data };
console.log(send);
verify_password: 'Verify Password',
forgot_password: 'forgot password',
reset_password_mail_sent: 'Sent an Email to reset your password.',
+ password_change: 'Password Change',
+ new_password: 'New Password',
no_email_setup: "This server hasn't correctly set up email.",
email: 'Email',
optional: 'Optional',
}
}
+export function validEmail(email: string) {
+ let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ return re.test(String(email).toLowerCase());
+}
+
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}