This commit is contained in:
parent
fd988d6ef0
commit
651f3f1e9c
@ -1,4 +1,4 @@
|
|||||||
from flask import request, render_template, redirect, url_for, flash, g
|
from flask import request, render_template, flash, g
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, validators
|
from wtforms import StringField, validators
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ class ForgotPasswordForm(FlaskForm):
|
|||||||
|
|
||||||
@auth_bp.route("/forgot_password", methods=["GET", "POST"])
|
@auth_bp.route("/forgot_password", methods=["GET", "POST"])
|
||||||
@limiter.limit(
|
@limiter.limit(
|
||||||
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
|
"10/hour", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
|
||||||
)
|
)
|
||||||
def forgot_password():
|
def forgot_password():
|
||||||
form = ForgotPasswordForm(request.form)
|
form = ForgotPasswordForm(request.form)
|
||||||
@ -37,6 +37,5 @@ def forgot_password():
|
|||||||
if user:
|
if user:
|
||||||
LOG.d("Send forgot password email to %s", user)
|
LOG.d("Send forgot password email to %s", user)
|
||||||
send_reset_password_email(user)
|
send_reset_password_email(user)
|
||||||
return redirect(url_for("auth.forgot_password"))
|
|
||||||
|
|
||||||
return render_template("auth/forgot_password.html", form=form)
|
return render_template("auth/forgot_password.html", form=form)
|
||||||
|
@ -60,8 +60,8 @@ def reset_password():
|
|||||||
# this can be served to activate user too
|
# this can be served to activate user too
|
||||||
user.activated = True
|
user.activated = True
|
||||||
|
|
||||||
# remove the reset password code
|
# remove all reset password codes
|
||||||
ResetPasswordCode.delete(reset_password_code.id)
|
ResetPasswordCode.filter_by(user_id=user.id).delete()
|
||||||
|
|
||||||
# change the alternative_id to log user out on other browsers
|
# change the alternative_id to log user out on other browsers
|
||||||
user.alternative_id = str(uuid.uuid4())
|
user.alternative_id = str(uuid.uuid4())
|
||||||
|
@ -198,6 +198,16 @@ def setting():
|
|||||||
)
|
)
|
||||||
return redirect(url_for("dashboard.setting"))
|
return redirect(url_for("dashboard.setting"))
|
||||||
|
|
||||||
|
if current_user.profile_picture_id is not None:
|
||||||
|
current_profile_file = File.get_by(
|
||||||
|
id=current_user.profile_picture_id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
current_profile_file is not None
|
||||||
|
and current_profile_file.user_id == current_user.id
|
||||||
|
):
|
||||||
|
s3.delete(current_user.path)
|
||||||
|
|
||||||
file_path = random_string(30)
|
file_path = random_string(30)
|
||||||
file = File.create(user_id=current_user.id, path=file_path)
|
file = File.create(user_id=current_user.id, path=file_path)
|
||||||
|
|
||||||
@ -451,8 +461,13 @@ def send_change_email_confirmation(user: User, email_change: EmailChange):
|
|||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/resend_email_change", methods=["GET", "POST"])
|
@dashboard_bp.route("/resend_email_change", methods=["GET", "POST"])
|
||||||
|
@limiter.limit("5/hour")
|
||||||
@login_required
|
@login_required
|
||||||
def resend_email_change():
|
def resend_email_change():
|
||||||
|
form = CSRFValidationForm()
|
||||||
|
if not form.validate():
|
||||||
|
flash("Invalid request. Please try again", "warning")
|
||||||
|
return redirect(url_for("dashboard.setting"))
|
||||||
email_change = EmailChange.get_by(user_id=current_user.id)
|
email_change = EmailChange.get_by(user_id=current_user.id)
|
||||||
if email_change:
|
if email_change:
|
||||||
# extend email change expiration
|
# extend email change expiration
|
||||||
@ -472,6 +487,10 @@ def resend_email_change():
|
|||||||
@dashboard_bp.route("/cancel_email_change", methods=["GET", "POST"])
|
@dashboard_bp.route("/cancel_email_change", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def cancel_email_change():
|
def cancel_email_change():
|
||||||
|
form = CSRFValidationForm()
|
||||||
|
if not form.validate():
|
||||||
|
flash("Invalid request. Please try again", "warning")
|
||||||
|
return redirect(url_for("dashboard.setting"))
|
||||||
email_change = EmailChange.get_by(user_id=current_user.id)
|
email_change = EmailChange.get_by(user_id=current_user.id)
|
||||||
if email_change:
|
if email_change:
|
||||||
EmailChange.delete(email_change.id)
|
EmailChange.delete(email_change.id)
|
||||||
|
@ -580,19 +580,6 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
Session.flush()
|
Session.flush()
|
||||||
user.default_mailbox_id = mb.id
|
user.default_mailbox_id = mb.id
|
||||||
|
|
||||||
# create a first alias mail to show user how to use when they login
|
|
||||||
alias = Alias.create_new(
|
|
||||||
user,
|
|
||||||
prefix="simplelogin-newsletter",
|
|
||||||
mailbox_id=mb.id,
|
|
||||||
note="This is your first alias. It's used to receive SimpleLogin communications "
|
|
||||||
"like new features announcements, newsletters.",
|
|
||||||
)
|
|
||||||
Session.flush()
|
|
||||||
|
|
||||||
user.newsletter_alias_id = alias.id
|
|
||||||
Session.flush()
|
|
||||||
|
|
||||||
# generate an alternative_id if needed
|
# generate an alternative_id if needed
|
||||||
if "alternative_id" not in kwargs:
|
if "alternative_id" not in kwargs:
|
||||||
user.alternative_id = str(uuid.uuid4())
|
user.alternative_id = str(uuid.uuid4())
|
||||||
@ -611,6 +598,19 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
Session.flush()
|
Session.flush()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
# create a first alias mail to show user how to use when they login
|
||||||
|
alias = Alias.create_new(
|
||||||
|
user,
|
||||||
|
prefix="simplelogin-newsletter",
|
||||||
|
mailbox_id=mb.id,
|
||||||
|
note="This is your first alias. It's used to receive SimpleLogin communications "
|
||||||
|
"like new features announcements, newsletters.",
|
||||||
|
)
|
||||||
|
Session.flush()
|
||||||
|
|
||||||
|
user.newsletter_alias_id = alias.id
|
||||||
|
Session.flush()
|
||||||
|
|
||||||
if config.DISABLE_ONBOARDING:
|
if config.DISABLE_ONBOARDING:
|
||||||
LOG.d("Disable onboarding emails")
|
LOG.d("Disable onboarding emails")
|
||||||
return user
|
return user
|
||||||
@ -636,7 +636,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
def get_active_subscription(
|
def get_active_subscription(
|
||||||
self,
|
self, include_partner_subscription: bool = True
|
||||||
) -> Optional[
|
) -> Optional[
|
||||||
Union[
|
Union[
|
||||||
Subscription
|
Subscription
|
||||||
@ -664,19 +664,24 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
if coinbase_subscription and coinbase_subscription.is_active():
|
if coinbase_subscription and coinbase_subscription.is_active():
|
||||||
return coinbase_subscription
|
return coinbase_subscription
|
||||||
|
|
||||||
partner_sub: PartnerSubscription = PartnerSubscription.find_by_user_id(self.id)
|
if include_partner_subscription:
|
||||||
|
partner_sub: PartnerSubscription = PartnerSubscription.find_by_user_id(
|
||||||
|
self.id
|
||||||
|
)
|
||||||
if partner_sub and partner_sub.is_active():
|
if partner_sub and partner_sub.is_active():
|
||||||
return partner_sub
|
return partner_sub
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# region Billing
|
# region Billing
|
||||||
def lifetime_or_active_subscription(self) -> bool:
|
def lifetime_or_active_subscription(
|
||||||
|
self, include_partner_subscription: bool = True
|
||||||
|
) -> bool:
|
||||||
"""True if user has lifetime licence or active subscription"""
|
"""True if user has lifetime licence or active subscription"""
|
||||||
if self.lifetime:
|
if self.lifetime:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return self.get_active_subscription() is not None
|
return self.get_active_subscription(include_partner_subscription) is not None
|
||||||
|
|
||||||
def is_paid(self) -> bool:
|
def is_paid(self) -> bool:
|
||||||
"""same as _lifetime_or_active_subscription but not include free manual subscription"""
|
"""same as _lifetime_or_active_subscription but not include free manual subscription"""
|
||||||
@ -705,14 +710,14 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_premium(self) -> bool:
|
def is_premium(self, include_partner_subscription: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
user is premium if they:
|
user is premium if they:
|
||||||
- have a lifetime deal or
|
- have a lifetime deal or
|
||||||
- in trial period or
|
- in trial period or
|
||||||
- active subscription
|
- active subscription
|
||||||
"""
|
"""
|
||||||
if self.lifetime_or_active_subscription():
|
if self.lifetime_or_active_subscription(include_partner_subscription):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.trial_end and arrow.now() < self.trial_end:
|
if self.trial_end and arrow.now() < self.trial_end:
|
||||||
|
@ -181,10 +181,10 @@
|
|||||||
<!-- END change name & profile picture -->
|
<!-- END change name & profile picture -->
|
||||||
<!-- Change email -->
|
<!-- Change email -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="form-name" value="update-email">
|
<input type="hidden" name="form-name" value="update-email">
|
||||||
{{ change_email_form.csrf_token }}
|
{{ change_email_form.csrf_token }}
|
||||||
<div class="card-body">
|
|
||||||
<div class="card-title">Account Email</div>
|
<div class="card-title">Account Email</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
This email address is used to log in to SimpleLogin.
|
This email address is used to log in to SimpleLogin.
|
||||||
@ -199,26 +199,30 @@
|
|||||||
<!-- Not allow user to change email if there's a pending change -->
|
<!-- Not allow user to change email if there's a pending change -->
|
||||||
{{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }}
|
{{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }}
|
||||||
{{ render_field_errors(change_email_form.email) }}
|
{{ render_field_errors(change_email_form.email) }}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-primary">Change Email</button>
|
||||||
|
</form>
|
||||||
{% if pending_email %}
|
{% if pending_email %}
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-danger">Pending email change: {{ pending_email }}</span>
|
<span class="text-danger float-left">Pending email change: {{ pending_email }}</span>
|
||||||
<a href="{{ url_for('dashboard.resend_email_change') }}"
|
<form method="POST"
|
||||||
class="btn btn-secondary btn-sm">
|
action="{{ url_for('dashboard.resend_email_change') }}"
|
||||||
Resend
|
class="float-left ml-2">
|
||||||
confirmation email
|
{{ change_email_form.csrf_token }}
|
||||||
</a>
|
<a onclick="this.closest('form').submit()"
|
||||||
<a href="{{ url_for('dashboard.cancel_email_change') }}"
|
class="btn btn-secondary btn-sm">Resend confirmation email</a>
|
||||||
class="btn btn-secondary btn-sm">
|
</form>
|
||||||
Cancel email
|
<form method="POST"
|
||||||
change
|
action="{{ url_for('dashboard.cancel_email_change') }}"
|
||||||
</a>
|
class="float-left ml-2">
|
||||||
|
{{ change_email_form.csrf_token }}
|
||||||
|
<a onclick="this.closest('form').submit()"
|
||||||
|
class="btn btn-secondary btn-sm">Cancel email change</a>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-outline-primary">Change Email</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- END Change email -->
|
<!-- END Change email -->
|
||||||
<!-- Connect with Proton -->
|
<!-- Connect with Proton -->
|
||||||
@ -265,11 +269,15 @@
|
|||||||
<div class="card" id="change_password">
|
<div class="card" id="change_password">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title">Password</div>
|
<div class="card-title">Password</div>
|
||||||
<div class="mb-3">You will receive an email containing instructions on how to change your password.</div>
|
<div class="mb-3">
|
||||||
|
You will receive an email containing instructions on how to change your password.
|
||||||
|
</div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ csrf_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="change-password">
|
<input type="hidden" name="form-name" value="change-password">
|
||||||
<button class="btn btn-outline-primary">Change password</button>
|
<button class="btn btn-outline-primary">
|
||||||
|
Change password
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
26
app/tests/auth/test_reset_password.py
Normal file
26
app/tests/auth/test_reset_password.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from app.db import Session
|
||||||
|
from app.models import User, ResetPasswordCode
|
||||||
|
from tests.utils import create_new_user, random_token
|
||||||
|
|
||||||
|
|
||||||
|
def test_successful_reset_password(flask_client):
|
||||||
|
user = create_new_user()
|
||||||
|
original_pass_hash = user.password
|
||||||
|
user_id = user.id
|
||||||
|
reset_code = random_token()
|
||||||
|
ResetPasswordCode.create(user_id=user.id, code=reset_code)
|
||||||
|
ResetPasswordCode.create(user_id=user.id, code=random_token())
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
|
r = flask_client.post(
|
||||||
|
url_for("auth.reset_password", code=reset_code),
|
||||||
|
data={"password": "1231idsfjaads"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 302
|
||||||
|
|
||||||
|
assert ResetPasswordCode.get_by(user_id=user_id) is None
|
||||||
|
user = User.get(user_id)
|
||||||
|
assert user.password != original_pass_hash
|
@ -1,7 +1,9 @@
|
|||||||
|
import arrow
|
||||||
from app import config
|
from app import config
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.models import User, Job
|
from app.models import User, Job, PartnerSubscription, PartnerUser, ManualSubscription
|
||||||
from tests.utils import random_email
|
from app.proton.utils import get_proton_partner
|
||||||
|
from tests.utils import random_email, random_token
|
||||||
|
|
||||||
|
|
||||||
def test_create_from_partner(flask_client):
|
def test_create_from_partner(flask_client):
|
||||||
@ -11,6 +13,7 @@ def test_create_from_partner(flask_client):
|
|||||||
)
|
)
|
||||||
assert user.notification is False
|
assert user.notification is False
|
||||||
assert user.trial_end is None
|
assert user.trial_end is None
|
||||||
|
assert user.newsletter_alias_id is None
|
||||||
job = Session.query(Job).order_by(Job.id.desc()).first()
|
job = Session.query(Job).order_by(Job.id.desc()).first()
|
||||||
assert job is not None
|
assert job is not None
|
||||||
assert job.name == config.JOB_SEND_PROTON_WELCOME_1
|
assert job.name == config.JOB_SEND_PROTON_WELCOME_1
|
||||||
@ -23,3 +26,23 @@ def test_user_created_by_partner(flask_client):
|
|||||||
|
|
||||||
regular_user = User.create(email=random_email())
|
regular_user = User.create(email=random_email())
|
||||||
assert regular_user.created_by_partner is False
|
assert regular_user.created_by_partner is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_is_premium(flask_client):
|
||||||
|
user = User.create(email=random_email(), from_partner=True)
|
||||||
|
assert not user.is_premium()
|
||||||
|
partner_user = PartnerUser.create(
|
||||||
|
user_id=user.id,
|
||||||
|
partner_id=get_proton_partner().id,
|
||||||
|
partner_email=user.email,
|
||||||
|
external_user_id=random_token(),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
ps = PartnerSubscription.create(
|
||||||
|
partner_user_id=partner_user.id, end_at=arrow.now().shift(years=1), flush=True
|
||||||
|
)
|
||||||
|
assert user.is_premium()
|
||||||
|
assert not user.is_premium(include_partner_subscription=False)
|
||||||
|
ManualSubscription.create(user_id=user.id, end_at=ps.end_at)
|
||||||
|
assert user.is_premium()
|
||||||
|
assert user.is_premium(include_partner_subscription=False)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user