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.

486 lines
15 KiB

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