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.

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