You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

476 lines
15 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
8 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. from datetime import timedelta as td
  2. import uuid
  3. from django.conf import settings
  4. from django.contrib import messages
  5. from django.contrib.auth import login as auth_login
  6. from django.contrib.auth import logout as auth_logout
  7. from django.contrib.auth import authenticate
  8. from django.contrib.auth.decorators import login_required
  9. from django.contrib.auth.models import User
  10. from django.core import signing
  11. from django.http import (
  12. HttpResponseForbidden,
  13. HttpResponseBadRequest,
  14. HttpResponseNotFound,
  15. )
  16. from django.shortcuts import get_object_or_404, redirect, render
  17. from django.utils.timezone import now
  18. from django.urls import resolve, Resolver404
  19. from django.views.decorators.csrf import csrf_exempt
  20. from django.views.decorators.http import require_POST
  21. from hc.accounts.forms import (
  22. ChangeEmailForm,
  23. PasswordLoginForm,
  24. InviteTeamMemberForm,
  25. RemoveTeamMemberForm,
  26. ReportSettingsForm,
  27. SetPasswordForm,
  28. ProjectNameForm,
  29. AvailableEmailForm,
  30. EmailLoginForm,
  31. )
  32. from hc.accounts.models import Profile, Project, Member
  33. from hc.api.models import Channel, Check, TokenBucket
  34. from hc.payments.models import Subscription
  35. NEXT_WHITELIST = (
  36. "hc-checks",
  37. "hc-details",
  38. "hc-log",
  39. "hc-channels",
  40. "hc-add-slack",
  41. "hc-add-pushover",
  42. )
  43. def _is_whitelisted(path):
  44. try:
  45. match = resolve(path)
  46. except Resolver404:
  47. return False
  48. return match.url_name in NEXT_WHITELIST
  49. def _make_user(email, with_project=True):
  50. username = str(uuid.uuid4())[:30]
  51. user = User(username=username, email=email)
  52. user.set_unusable_password()
  53. user.save()
  54. project = None
  55. if with_project:
  56. project = Project(owner=user)
  57. project.badge_key = user.username
  58. project.save()
  59. check = Check(project=project)
  60. check.name = "My First Check"
  61. check.save()
  62. channel = Channel(project=project)
  63. channel.kind = "email"
  64. channel.value = email
  65. channel.email_verified = True
  66. channel.save()
  67. channel.checks.add(check)
  68. # Ensure a profile gets created
  69. profile = Profile.objects.for_user(user)
  70. profile.current_project = project
  71. profile.save()
  72. return user
  73. def _redirect_after_login(request):
  74. """ Redirect to the URL indicated in ?next= query parameter. """
  75. redirect_url = request.GET.get("next")
  76. if _is_whitelisted(redirect_url):
  77. return redirect(redirect_url)
  78. if request.user.project_set.count() == 1:
  79. project = request.user.project_set.first()
  80. return redirect("hc-checks", project.code)
  81. return redirect("hc-index")
  82. def login(request):
  83. form = PasswordLoginForm()
  84. magic_form = EmailLoginForm()
  85. if request.method == "POST":
  86. if request.POST.get("action") == "login":
  87. form = PasswordLoginForm(request.POST)
  88. if form.is_valid():
  89. auth_login(request, form.user)
  90. return _redirect_after_login(request)
  91. else:
  92. magic_form = EmailLoginForm(request.POST)
  93. if magic_form.is_valid():
  94. redirect_url = request.GET.get("next")
  95. if not _is_whitelisted(redirect_url):
  96. redirect_url = None
  97. profile = Profile.objects.for_user(magic_form.user)
  98. profile.send_instant_login_link(redirect_url=redirect_url)
  99. response = redirect("hc-login-link-sent")
  100. # check_token_submit looks for this cookie to decide if
  101. # it needs to do the extra POST step.
  102. response.set_cookie("auto-login", "1", max_age=300, httponly=True)
  103. return response
  104. bad_link = request.session.pop("bad_link", None)
  105. ctx = {
  106. "page": "login",
  107. "form": form,
  108. "magic_form": magic_form,
  109. "bad_link": bad_link,
  110. "registration_open": settings.REGISTRATION_OPEN,
  111. }
  112. return render(request, "accounts/login.html", ctx)
  113. def logout(request):
  114. auth_logout(request)
  115. return redirect("hc-index")
  116. @require_POST
  117. def signup(request):
  118. if not settings.REGISTRATION_OPEN:
  119. return HttpResponseForbidden()
  120. ctx = {}
  121. form = AvailableEmailForm(request.POST)
  122. if form.is_valid():
  123. email = form.cleaned_data["identity"]
  124. user = _make_user(email)
  125. profile = Profile.objects.for_user(user)
  126. profile.send_instant_login_link()
  127. ctx["created"] = True
  128. else:
  129. ctx = {"form": form}
  130. return render(request, "accounts/signup_result.html", ctx)
  131. def login_link_sent(request):
  132. return render(request, "accounts/login_link_sent.html")
  133. def link_sent(request):
  134. return render(request, "accounts/link_sent.html")
  135. def check_token(request, username, token):
  136. if request.user.is_authenticated and request.user.username == username:
  137. # User is already logged in
  138. return _redirect_after_login(request)
  139. # Some email servers open links in emails to check for malicious content.
  140. # To work around this, we sign user in if the method is POST
  141. # *or* if the browser presents a cookie we had set when sending the login link.
  142. #
  143. # If the method is GET, we instead serve a HTML form and a piece
  144. # of Javascript to automatically submit it.
  145. if request.method == "POST" or "auto-login" in request.COOKIES:
  146. user = authenticate(username=username, token=token)
  147. if user is not None and user.is_active:
  148. user.profile.token = ""
  149. user.profile.save()
  150. auth_login(request, user)
  151. return _redirect_after_login(request)
  152. request.session["bad_link"] = True
  153. return redirect("hc-login")
  154. return render(request, "accounts/check_token_submit.html")
  155. @login_required
  156. def profile(request):
  157. profile = request.profile
  158. ctx = {"page": "profile", "profile": profile, "my_projects_status": "default"}
  159. if request.method == "POST":
  160. if "change_email" in request.POST:
  161. profile.send_change_email_link()
  162. return redirect("hc-link-sent")
  163. elif "set_password" in request.POST:
  164. profile.send_set_password_link()
  165. return redirect("hc-link-sent")
  166. elif "leave_project" in request.POST:
  167. code = request.POST["code"]
  168. try:
  169. project = Project.objects.get(code=code, member__user=request.user)
  170. except Project.DoesNotExist:
  171. return HttpResponseBadRequest()
  172. if profile.current_project == project:
  173. profile.current_project = None
  174. profile.save()
  175. Member.objects.filter(project=project, user=request.user).delete()
  176. ctx["left_project"] = project
  177. ctx["my_projects_status"] = "info"
  178. # Retrieve projects right before rendering the template--
  179. # The list of the projects might have *just* changed
  180. ctx["projects"] = list(profile.projects())
  181. return render(request, "accounts/profile.html", ctx)
  182. @login_required
  183. @require_POST
  184. def add_project(request):
  185. form = ProjectNameForm(request.POST)
  186. if not form.is_valid():
  187. return HttpResponseBadRequest()
  188. project = Project(owner=request.user)
  189. project.code = project.badge_key = str(uuid.uuid4())
  190. project.name = form.cleaned_data["name"]
  191. project.save()
  192. return redirect("hc-checks", project.code)
  193. @login_required
  194. def project(request, code):
  195. if request.user.is_superuser:
  196. q = Project.objects
  197. else:
  198. q = request.profile.projects()
  199. try:
  200. project = q.get(code=code)
  201. except Project.DoesNotExist:
  202. return HttpResponseNotFound()
  203. is_owner = project.owner_id == request.user.id
  204. ctx = {
  205. "page": "project",
  206. "project": project,
  207. "is_owner": is_owner,
  208. "show_api_keys": "show_api_keys" in request.GET,
  209. "project_name_status": "default",
  210. "api_status": "default",
  211. "team_status": "default",
  212. }
  213. if request.method == "POST":
  214. if "create_api_keys" in request.POST:
  215. project.set_api_keys()
  216. project.save()
  217. ctx["show_api_keys"] = True
  218. ctx["api_keys_created"] = True
  219. ctx["api_status"] = "success"
  220. elif "revoke_api_keys" in request.POST:
  221. project.api_key = ""
  222. project.api_key_readonly = ""
  223. project.save()
  224. ctx["api_keys_revoked"] = True
  225. ctx["api_status"] = "info"
  226. elif "show_api_keys" in request.POST:
  227. ctx["show_api_keys"] = True
  228. elif "invite_team_member" in request.POST:
  229. if not is_owner or not project.can_invite():
  230. return HttpResponseForbidden()
  231. form = InviteTeamMemberForm(request.POST)
  232. if form.is_valid():
  233. if not TokenBucket.authorize_invite(request.user):
  234. return render(request, "try_later.html")
  235. email = form.cleaned_data["email"]
  236. try:
  237. user = User.objects.get(email=email)
  238. except User.DoesNotExist:
  239. user = _make_user(email, with_project=False)
  240. project.invite(user)
  241. ctx["team_member_invited"] = email
  242. ctx["team_status"] = "success"
  243. elif "remove_team_member" in request.POST:
  244. if not is_owner:
  245. return HttpResponseForbidden()
  246. form = RemoveTeamMemberForm(request.POST)
  247. if form.is_valid():
  248. q = User.objects
  249. q = q.filter(email=form.cleaned_data["email"])
  250. q = q.filter(memberships__project=project)
  251. farewell_user = q.first()
  252. if farewell_user is None:
  253. return HttpResponseBadRequest()
  254. farewell_user.profile.current_project = None
  255. farewell_user.profile.save()
  256. Member.objects.filter(project=project, user=farewell_user).delete()
  257. ctx["team_member_removed"] = form.cleaned_data["email"]
  258. ctx["team_status"] = "info"
  259. elif "set_project_name" in request.POST:
  260. form = ProjectNameForm(request.POST)
  261. if form.is_valid():
  262. project.name = form.cleaned_data["name"]
  263. project.save()
  264. if request.profile.current_project == project:
  265. request.profile.current_project.name = project.name
  266. ctx["project_name_updated"] = True
  267. ctx["project_name_status"] = "success"
  268. # Count members right before rendering the template, in case
  269. # we just invited or removed someone
  270. ctx["num_members"] = project.member_set.count()
  271. return render(request, "accounts/project.html", ctx)
  272. @login_required
  273. def notifications(request):
  274. profile = request.profile
  275. ctx = {"status": "default", "page": "profile", "profile": profile}
  276. if request.method == "POST":
  277. form = ReportSettingsForm(request.POST)
  278. if form.is_valid():
  279. if profile.reports_allowed != form.cleaned_data["reports_allowed"]:
  280. profile.reports_allowed = form.cleaned_data["reports_allowed"]
  281. if profile.reports_allowed:
  282. profile.next_report_date = now() + td(days=30)
  283. else:
  284. profile.next_report_date = None
  285. if profile.nag_period != form.cleaned_data["nag_period"]:
  286. # Set the new nag period
  287. profile.nag_period = form.cleaned_data["nag_period"]
  288. # and schedule next_nag_date:
  289. if profile.nag_period:
  290. profile.next_nag_date = now() + profile.nag_period
  291. else:
  292. profile.next_nag_date = None
  293. profile.save()
  294. ctx["status"] = "info"
  295. return render(request, "accounts/notifications.html", ctx)
  296. @login_required
  297. def set_password(request, token):
  298. if not request.profile.check_token(token, "set-password"):
  299. return HttpResponseBadRequest()
  300. if request.method == "POST":
  301. form = SetPasswordForm(request.POST)
  302. if form.is_valid():
  303. password = form.cleaned_data["password"]
  304. request.user.set_password(password)
  305. request.user.save()
  306. request.profile.token = ""
  307. request.profile.save()
  308. # Setting a password logs the user out, so here we
  309. # log them back in.
  310. u = authenticate(username=request.user.email, password=password)
  311. auth_login(request, u)
  312. messages.success(request, "Your password has been set!")
  313. return redirect("hc-profile")
  314. return render(request, "accounts/set_password.html", {})
  315. @login_required
  316. def change_email(request, token):
  317. if not request.profile.check_token(token, "change-email"):
  318. return HttpResponseBadRequest()
  319. if request.method == "POST":
  320. form = ChangeEmailForm(request.POST)
  321. if form.is_valid():
  322. request.user.email = form.cleaned_data["email"]
  323. request.user.set_unusable_password()
  324. request.user.save()
  325. request.profile.token = ""
  326. request.profile.save()
  327. return redirect("hc-change-email-done")
  328. else:
  329. form = ChangeEmailForm()
  330. return render(request, "accounts/change_email.html", {"form": form})
  331. def change_email_done(request):
  332. return render(request, "accounts/change_email_done.html")
  333. @csrf_exempt
  334. def unsubscribe_reports(request, username):
  335. signer = signing.TimestampSigner(salt="reports")
  336. try:
  337. username = signer.unsign(username)
  338. except signing.BadSignature:
  339. return render(request, "bad_link.html")
  340. # Some email servers open links in emails to check for malicious content.
  341. # To work around this, we serve a form that auto-submits with JS.
  342. if "ask" in request.GET and request.method != "POST":
  343. return render(request, "accounts/unsubscribe_submit.html")
  344. user = User.objects.get(username=username)
  345. profile = Profile.objects.for_user(user)
  346. profile.reports_allowed = False
  347. profile.next_report_date = None
  348. profile.nag_period = td()
  349. profile.next_nag_date = None
  350. profile.save()
  351. return render(request, "accounts/unsubscribed.html")
  352. @require_POST
  353. @login_required
  354. def close(request):
  355. user = request.user
  356. # Subscription needs to be canceled before it is deleted:
  357. sub = Subscription.objects.filter(user=user).first()
  358. if sub:
  359. sub.cancel(delete_customer=True)
  360. user.delete()
  361. # Deleting user also deletes its profile, checks, channels etc.
  362. request.session.flush()
  363. return redirect("hc-index")
  364. @require_POST
  365. @login_required
  366. def remove_project(request, code):
  367. project = get_object_or_404(Project, code=code, owner=request.user)
  368. project.delete()
  369. return redirect("hc-index")