setting.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import json
  2. from io import BytesIO
  3. import arrow
  4. from flask import render_template, request, redirect, url_for, flash, Response
  5. from flask_login import login_required, current_user, logout_user
  6. from flask_wtf import FlaskForm
  7. from flask_wtf.file import FileField
  8. from wtforms import StringField, validators
  9. from wtforms.fields.html5 import EmailField
  10. from app import s3, email_utils
  11. from app.config import URL
  12. from app.dashboard.base import dashboard_bp
  13. from app.email_utils import can_be_used_as_personal_email, email_already_used
  14. from app.extensions import db
  15. from app.log import LOG
  16. from app.models import (
  17. PlanEnum,
  18. File,
  19. ResetPasswordCode,
  20. EmailChange,
  21. User,
  22. GenEmail,
  23. DeletedAlias,
  24. CustomDomain,
  25. Client,
  26. AliasGeneratorEnum,
  27. )
  28. from app.utils import random_string
  29. class SettingForm(FlaskForm):
  30. name = StringField("Name")
  31. profile_picture = FileField("Profile Picture")
  32. class ChangeEmailForm(FlaskForm):
  33. email = EmailField(
  34. "email", validators=[validators.DataRequired(), validators.Email()]
  35. )
  36. class PromoCodeForm(FlaskForm):
  37. code = StringField("Name", validators=[validators.DataRequired()])
  38. @dashboard_bp.route("/setting", methods=["GET", "POST"])
  39. @login_required
  40. def setting():
  41. form = SettingForm()
  42. promo_form = PromoCodeForm()
  43. change_email_form = ChangeEmailForm()
  44. email_change = EmailChange.get_by(user_id=current_user.id)
  45. if email_change:
  46. pending_email = email_change.new_email
  47. else:
  48. pending_email = None
  49. if request.method == "POST":
  50. if request.form.get("form-name") == "update-email":
  51. if change_email_form.validate():
  52. if (
  53. change_email_form.email.data != current_user.email
  54. and not pending_email
  55. ):
  56. new_email = change_email_form.email.data
  57. # check if this email is not already used
  58. if (
  59. email_already_used(new_email)
  60. or GenEmail.get_by(email=new_email)
  61. or DeletedAlias.get_by(email=new_email)
  62. ):
  63. flash(f"Email {new_email} already used", "error")
  64. elif not can_be_used_as_personal_email(new_email):
  65. flash(
  66. "You cannot use this email address as your personal inbox.",
  67. "error",
  68. )
  69. else:
  70. email_change = EmailChange.create(
  71. user_id=current_user.id,
  72. code=random_string(
  73. 60
  74. ), # todo: make sure the code is unique
  75. new_email=new_email,
  76. )
  77. db.session.commit()
  78. send_change_email_confirmation(current_user, email_change)
  79. flash(
  80. "A confirmation email is on the way, please check your inbox",
  81. "success",
  82. )
  83. return redirect(url_for("dashboard.setting"))
  84. if request.form.get("form-name") == "update-profile":
  85. if form.validate():
  86. profile_updated = False
  87. # update user info
  88. if form.name.data != current_user.name:
  89. current_user.name = form.name.data
  90. db.session.commit()
  91. profile_updated = True
  92. if form.profile_picture.data:
  93. file_path = random_string(30)
  94. file = File.create(path=file_path)
  95. s3.upload_from_bytesio(
  96. file_path, BytesIO(form.profile_picture.data.read())
  97. )
  98. db.session.flush()
  99. LOG.d("upload file %s to s3", file)
  100. current_user.profile_picture_id = file.id
  101. db.session.commit()
  102. profile_updated = True
  103. if profile_updated:
  104. flash(f"Your profile has been updated", "success")
  105. return redirect(url_for("dashboard.setting"))
  106. elif request.form.get("form-name") == "change-password":
  107. flash(
  108. "You are going to receive an email containing instructions to change your password",
  109. "success",
  110. )
  111. send_reset_password_email(current_user)
  112. return redirect(url_for("dashboard.setting"))
  113. elif request.form.get("form-name") == "notification-preference":
  114. choose = request.form.get("notification")
  115. if choose == "on":
  116. current_user.notification = True
  117. else:
  118. current_user.notification = False
  119. db.session.commit()
  120. flash("Your notification preference has been updated", "success")
  121. return redirect(url_for("dashboard.setting"))
  122. elif request.form.get("form-name") == "delete-account":
  123. User.delete(current_user.id)
  124. db.session.commit()
  125. flash("Your account has been deleted", "success")
  126. logout_user()
  127. return redirect(url_for("auth.register"))
  128. elif request.form.get("form-name") == "change-alias-generator":
  129. scheme = int(request.form.get("alias-generator-scheme"))
  130. if AliasGeneratorEnum.has_value(scheme):
  131. current_user.alias_generator = scheme
  132. db.session.commit()
  133. flash("Your preference has been updated", "success")
  134. return redirect(url_for("dashboard.setting"))
  135. elif request.form.get("form-name") == "export-data":
  136. data = {
  137. "email": current_user.email,
  138. "name": current_user.name,
  139. "aliases": [],
  140. "apps": [],
  141. "custom_domains": [],
  142. }
  143. for alias in GenEmail.filter_by(
  144. user_id=current_user.id
  145. ).all(): # type: GenEmail
  146. data["aliases"].append(dict(email=alias.email, enabled=alias.enabled))
  147. for custom_domain in CustomDomain.filter_by(user_id=current_user.id).all():
  148. data["custom_domains"].append(custom_domain.domain)
  149. for app in Client.filter_by(user_id=current_user.id): # type: Client
  150. data["apps"].append(
  151. dict(name=app.name, home_url=app.home_url, published=app.published)
  152. )
  153. return Response(
  154. json.dumps(data),
  155. mimetype="text/json",
  156. headers={"Content-Disposition": "attachment;filename=data.json"},
  157. )
  158. return render_template(
  159. "dashboard/setting.html",
  160. form=form,
  161. PlanEnum=PlanEnum,
  162. promo_form=promo_form,
  163. change_email_form=change_email_form,
  164. pending_email=pending_email,
  165. AliasGeneratorEnum=AliasGeneratorEnum,
  166. )
  167. def send_reset_password_email(user):
  168. """
  169. generate a new ResetPasswordCode and send it over email to user
  170. """
  171. # the activation code is valid for 1h
  172. reset_password_code = ResetPasswordCode.create(
  173. user_id=user.id, code=random_string(60)
  174. )
  175. db.session.commit()
  176. reset_password_link = f"{URL}/auth/reset_password?code={reset_password_code.code}"
  177. email_utils.send_reset_password_email(user.email, user.name, reset_password_link)
  178. def send_change_email_confirmation(user: User, email_change: EmailChange):
  179. """
  180. send confirmation email to the new email address
  181. """
  182. link = f"{URL}/auth/change_email?code={email_change.code}"
  183. email_utils.send_change_email(email_change.new_email, user.email, user.name, link)
  184. @dashboard_bp.route("/resend_email_change", methods=["GET", "POST"])
  185. @login_required
  186. def resend_email_change():
  187. email_change = EmailChange.get_by(user_id=current_user.id)
  188. if email_change:
  189. # extend email change expiration
  190. email_change.expired = arrow.now().shift(hours=12)
  191. db.session.commit()
  192. send_change_email_confirmation(current_user, email_change)
  193. flash("A confirmation email is on the way, please check your inbox", "success")
  194. return redirect(url_for("dashboard.setting"))
  195. else:
  196. flash(
  197. "You have no pending email change. Redirect back to Setting page", "warning"
  198. )
  199. return redirect(url_for("dashboard.setting"))
  200. @dashboard_bp.route("/cancel_email_change", methods=["GET", "POST"])
  201. @login_required
  202. def cancel_email_change():
  203. email_change = EmailChange.get_by(user_id=current_user.id)
  204. if email_change:
  205. EmailChange.delete(email_change.id)
  206. db.session.commit()
  207. flash("Your email change is cancelled", "success")
  208. return redirect(url_for("dashboard.setting"))
  209. else:
  210. flash(
  211. "You have no pending email change. Redirect back to Setting page", "warning"
  212. )
  213. return redirect(url_for("dashboard.setting"))