4.28.2
All checks were successful
continuous-integration/drone/tag Build is passing

This commit is contained in:
MrMeeb 2023-05-16 11:00:09 +00:00
parent fd988d6ef0
commit 651f3f1e9c
7 changed files with 132 additions and 52 deletions

View File

@ -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)

View File

@ -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())

View File

@ -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)

View File

@ -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:
if partner_sub and partner_sub.is_active(): partner_sub: PartnerSubscription = PartnerSubscription.find_by_user_id(
return partner_sub self.id
)
if partner_sub and partner_sub.is_active():
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:

View File

@ -181,10 +181,10 @@
<!-- END change name & profile picture --> <!-- END change name & profile picture -->
<!-- Change email --> <!-- Change email -->
<div class="card"> <div class="card">
<form method="post" enctype="multipart/form-data"> <div class="card-body">
<input type="hidden" name="form-name" value="update-email"> <form method="post" enctype="multipart/form-data">
{{ change_email_form.csrf_token }} <input type="hidden" name="form-name" value="update-email">
<div class="card-body"> {{ change_email_form.csrf_token }}
<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) }}
{% if pending_email %}
<div class="mt-2">
<span class="text-danger">Pending email change: {{ pending_email }}</span>
<a href="{{ url_for('dashboard.resend_email_change') }}"
class="btn btn-secondary btn-sm">
Resend
confirmation email
</a>
<a href="{{ url_for('dashboard.cancel_email_change') }}"
class="btn btn-secondary btn-sm">
Cancel email
change
</a>
</div>
{% endif %}
</div> </div>
<button class="btn btn-outline-primary">Change Email</button> <button class="btn btn-outline-primary">Change Email</button>
</div> </form>
</form> {% if pending_email %}
<div class="mt-2">
<span class="text-danger float-left">Pending email change: {{ pending_email }}</span>
<form method="POST"
action="{{ url_for('dashboard.resend_email_change') }}"
class="float-left ml-2">
{{ change_email_form.csrf_token }}
<a onclick="this.closest('form').submit()"
class="btn btn-secondary btn-sm">Resend confirmation email</a>
</form>
<form method="POST"
action="{{ url_for('dashboard.cancel_email_change') }}"
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>
{% endif %}
</div>
</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>

View 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

View File

@ -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)