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.

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