]>
Commit | Line | Data |
---|---|---|
2cd9af74 MT |
1 | #!/usr/bin/python |
2 | ||
bdaf6b46 | 3 | import datetime |
e96e445b | 4 | import ldap |
2cd9af74 | 5 | import logging |
020de883 | 6 | import imghdr |
2cd9af74 | 7 | import tornado.web |
2dac7110 | 8 | import urllib.parse |
2cd9af74 | 9 | |
0099c2a7 MT |
10 | from .. import countries |
11 | ||
9b8ff27d | 12 | from . import auth |
124a8404 | 13 | from . import base |
786e9ca8 MT |
14 | from . import ui_modules |
15 | ||
9b8ff27d | 16 | class IndexHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
17 | @tornado.web.authenticated |
18 | def get(self): | |
5d42f49b MT |
19 | hints = [] |
20 | ||
21 | # Suggest uploading an avatar if this user does not have one | |
22 | if not self.current_user.has_avatar(): | |
23 | hints.append("avatar") | |
24 | ||
67fc00bc MT |
25 | # Suggest adding a description |
26 | if not self.current_user.description: | |
27 | hints.append("description") | |
28 | ||
5d42f49b | 29 | self.render("people/index.html", hints=hints) |
786e9ca8 | 30 | |
2cd9af74 | 31 | |
c4f1a618 | 32 | class AvatarHandler(base.BaseHandler): |
0cddf220 | 33 | def get(self, uid): |
2cd9af74 | 34 | # Get the desired size of the avatar file |
a0f767a7 | 35 | size = self.get_argument("size", None) |
2cd9af74 MT |
36 | |
37 | try: | |
38 | size = int(size) | |
39 | except (TypeError, ValueError): | |
40 | size = None | |
41 | ||
0cddf220 | 42 | logging.debug("Querying for avatar of %s" % uid) |
2cd9af74 | 43 | |
f6ed3d4d | 44 | # Fetch user account |
0cddf220 | 45 | account = self.backend.accounts.get_by_uid(uid) |
f6ed3d4d | 46 | if not account: |
0cddf220 | 47 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) |
2cd9af74 | 48 | |
f6ed3d4d MT |
49 | # Allow downstream to cache this for 60 minutes |
50 | self.set_expires(3600) | |
2cd9af74 | 51 | |
f6ed3d4d MT |
52 | # Resize avatar |
53 | avatar = account.get_avatar(size) | |
2cd9af74 | 54 | |
f6ed3d4d MT |
55 | # If there is no avatar, we serve a default image |
56 | if not avatar: | |
0cddf220 MT |
57 | logging.debug("No avatar uploaded for %s" % account) |
58 | ||
3f9f12f0 | 59 | return self.redirect(self.static_url("img/default-avatar.jpg")) |
2cd9af74 | 60 | |
020de883 MT |
61 | # Guess content type |
62 | type = imghdr.what(None, avatar) | |
63 | ||
f6ed3d4d | 64 | # Set headers about content |
020de883 MT |
65 | self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type)) |
66 | self.set_header("Content-Type", "image/%s" % type) | |
2cd9af74 | 67 | |
f6ed3d4d | 68 | # Deliver payload |
2cd9af74 | 69 | self.finish(avatar) |
786e9ca8 MT |
70 | |
71 | ||
9b8ff27d | 72 | class CallsHandler(auth.CacheMixin, base.BaseHandler): |
bdaf6b46 MT |
73 | @tornado.web.authenticated |
74 | def get(self, uid, date=None): | |
75 | account = self.backend.accounts.get_by_uid(uid) | |
76 | if not account: | |
77 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
78 | ||
d561d931 MT |
79 | # Check for permissions |
80 | if not account.can_be_managed_by(self.current_user): | |
81 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
82 | ||
bdaf6b46 | 83 | if date: |
44b4640b MT |
84 | try: |
85 | date = datetime.datetime.strptime(date, "%Y-%m-%d").date() | |
86 | except ValueError: | |
87 | raise tornado.web.HTTPError(400, "Invalid date: %s" % date) | |
bdaf6b46 MT |
88 | else: |
89 | date = datetime.date.today() | |
90 | ||
91 | self.render("people/calls.html", account=account, date=date) | |
92 | ||
93 | ||
9b8ff27d | 94 | class CallHandler(auth.CacheMixin, base.BaseHandler): |
68ece434 MT |
95 | @tornado.web.authenticated |
96 | def get(self, uid, uuid): | |
d09d554b MT |
97 | account = self.backend.accounts.get_by_uid(uid) |
98 | if not account: | |
99 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
100 | ||
d561d931 MT |
101 | # Check for permissions |
102 | if not account.can_be_managed_by(self.current_user): | |
103 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
104 | ||
68ece434 MT |
105 | call = self.backend.talk.freeswitch.get_call_by_uuid(uuid) |
106 | if not call: | |
107 | raise tornado.web.HTTPError(404, "Could not find call %s" % uuid) | |
108 | ||
109 | # XXX limit | |
110 | ||
d09d554b | 111 | self.render("people/call.html", account=account, call=call) |
68ece434 MT |
112 | |
113 | ||
9b8ff27d | 114 | class ConferencesHandler(auth.CacheMixin, base.BaseHandler): |
30aeccdb MT |
115 | @tornado.web.authenticated |
116 | def get(self): | |
117 | self.render("people/conferences.html", conferences=self.backend.talk.conferences) | |
118 | ||
119 | ||
18b13823 MT |
120 | class GroupsHandler(auth.CacheMixin, base.BaseHandler): |
121 | @tornado.web.authenticated | |
122 | def get(self): | |
123 | # Only staff can see other groups | |
124 | if not self.current_user.is_staff(): | |
125 | raise tornado.web.HTTPError(403) | |
126 | ||
127 | self.render("people/groups.html") | |
128 | ||
129 | ||
bef47ee8 MT |
130 | class GroupHandler(auth.CacheMixin, base.BaseHandler): |
131 | @tornado.web.authenticated | |
132 | def get(self, gid): | |
133 | # Only staff can see other groups | |
134 | if not self.current_user.is_staff(): | |
135 | raise tornado.web.HTTPError(403) | |
136 | ||
137 | # Fetch group | |
138 | group = self.backend.groups.get_by_gid(gid) | |
139 | if not group: | |
140 | raise tornado.web.HTTPError(404, "Could not find group %s" % gid) | |
141 | ||
142 | self.render("people/group.html", group=group) | |
143 | ||
144 | ||
9b8ff27d | 145 | class SearchHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
146 | @tornado.web.authenticated |
147 | def get(self): | |
148 | q = self.get_argument("q") | |
149 | ||
150 | # Perform the search | |
51907e45 | 151 | accounts = self.backend.accounts.search(q) |
786e9ca8 MT |
152 | |
153 | # Redirect when only one result was found | |
154 | if len(accounts) == 1: | |
155 | self.redirect("/users/%s" % accounts[0].uid) | |
156 | return | |
157 | ||
158 | self.render("people/search.html", q=q, accounts=accounts) | |
159 | ||
f4672785 | 160 | |
9b8ff27d | 161 | class SIPHandler(auth.CacheMixin, base.BaseHandler): |
e0daee8f MT |
162 | @tornado.web.authenticated |
163 | def get(self, uid): | |
164 | account = self.backend.accounts.get_by_uid(uid) | |
165 | if not account: | |
166 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
167 | ||
168 | # Check for permissions | |
169 | if not account.can_be_managed_by(self.current_user): | |
170 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
171 | ||
172 | self.render("people/sip.html", account=account) | |
173 | ||
174 | ||
9b8ff27d | 175 | class UsersHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
176 | @tornado.web.authenticated |
177 | def get(self): | |
71a3109c MT |
178 | # Only staff can see other users |
179 | if not self.current_user.is_staff(): | |
180 | raise tornado.web.HTTPError(403) | |
181 | ||
786e9ca8 MT |
182 | self.render("people/users.html") |
183 | ||
184 | ||
9b8ff27d | 185 | class UserHandler(auth.CacheMixin, base.BaseHandler): |
786e9ca8 MT |
186 | @tornado.web.authenticated |
187 | def get(self, uid): | |
188 | account = self.backend.accounts.get_by_uid(uid) | |
189 | if not account: | |
190 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
191 | ||
192 | self.render("people/user.html", account=account) | |
193 | ||
194 | ||
9b8ff27d | 195 | class UserEditHandler(auth.CacheMixin, base.BaseHandler): |
e96e445b MT |
196 | @tornado.web.authenticated |
197 | def get(self, uid): | |
198 | account = self.backend.accounts.get_by_uid(uid) | |
199 | if not account: | |
200 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
201 | ||
202 | # Check for permissions | |
203 | if not account.can_be_managed_by(self.current_user): | |
204 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
205 | ||
0099c2a7 | 206 | self.render("people/user-edit.html", account=account, countries=countries.get_all()) |
e96e445b MT |
207 | |
208 | @tornado.web.authenticated | |
209 | def post(self, uid): | |
210 | account = self.backend.accounts.get_by_uid(uid) | |
211 | if not account: | |
212 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
213 | ||
214 | # Check for permissions | |
215 | if not account.can_be_managed_by(self.current_user): | |
216 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
217 | ||
218 | # Unfortunately this cannot be wrapped into a transaction | |
219 | try: | |
0099c2a7 MT |
220 | account.first_name = self.get_argument("first_name") |
221 | account.last_name = self.get_argument("last_name") | |
d6e57f73 | 222 | account.nickname = self.get_argument("nickname", None) |
0099c2a7 MT |
223 | account.street = self.get_argument("street", None) |
224 | account.city = self.get_argument("city", None) | |
225 | account.postal_code = self.get_argument("postal_code", None) | |
226 | account.country_code = self.get_argument("country_code", None) | |
1c4522dc | 227 | account.description = self.get_argument("description", None) |
e96e445b | 228 | |
5cc10421 MT |
229 | # Avatar |
230 | try: | |
231 | filename, data, mimetype = self.get_file("avatar") | |
232 | ||
233 | if not mimetype.startswith("image/"): | |
234 | raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype) | |
235 | ||
236 | account.upload_avatar(data) | |
9bbf48b8 | 237 | except TypeError: |
5cc10421 MT |
238 | pass |
239 | ||
e96e445b MT |
240 | |
241 | account.mail_routing_address = self.get_argument("mail_routing_address", None) | |
242 | ||
243 | # Telephone | |
244 | account.phone_numbers = self.get_argument("phone_numbers", "").splitlines() | |
245 | account.sip_routing_address = self.get_argument("sip_routing_address", None) | |
246 | except ldap.STRONG_AUTH_REQUIRED as e: | |
247 | raise tornado.web.HTTPError(403, "%s" % e) from e | |
248 | ||
249 | # Redirect back to user page | |
250 | self.redirect("/users/%s" % account.uid) | |
251 | ||
252 | ||
9b8ff27d | 253 | class UserPasswdHandler(auth.CacheMixin, base.BaseHandler): |
3ea97943 MT |
254 | @tornado.web.authenticated |
255 | def get(self, uid): | |
256 | account = self.backend.accounts.get_by_uid(uid) | |
257 | if not account: | |
258 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
259 | ||
260 | # Check for permissions | |
261 | if not account.can_be_managed_by(self.current_user): | |
262 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
263 | ||
264 | self.render("people/passwd.html", account=account) | |
265 | ||
266 | @tornado.web.authenticated | |
267 | def post(self, uid): | |
268 | account = self.backend.accounts.get_by_uid(uid) | |
269 | if not account: | |
270 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
271 | ||
272 | # Check for permissions | |
273 | if not account.can_be_managed_by(self.current_user): | |
274 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
275 | ||
276 | # Get current password | |
277 | password = self.get_argument("password") | |
278 | ||
279 | # Get new password | |
280 | password1 = self.get_argument("password1") | |
281 | password2 = self.get_argument("password2") | |
282 | ||
283 | # Passwords must match | |
284 | if not password1 == password2: | |
285 | raise tornado.web.HTTPError(400, "Passwords do not match") | |
286 | ||
287 | # XXX Check password complexity | |
288 | ||
289 | # Check if old password matches | |
290 | if not account.check_password(password): | |
291 | raise tornado.web.HTTPError(403, "Incorrect password for %s" % account) | |
292 | ||
293 | # Save new password | |
294 | account.passwd(password1) | |
295 | ||
296 | # Redirect back to user's page | |
297 | self.redirect("/users/%s" % account.uid) | |
298 | ||
299 | ||
2dac7110 MT |
300 | class SSODiscourse(auth.CacheMixin, base.BaseHandler): |
301 | def _get_discourse_params(self): | |
302 | # Fetch Discourse's parameters | |
303 | sso = self.get_argument("sso") | |
304 | sig = self.get_argument("sig") | |
305 | ||
306 | # Decode payload | |
307 | try: | |
308 | return self.accounts.decode_discourse_payload(sso, sig) | |
309 | ||
310 | # Raise bad request if the signature is invalid | |
311 | except ValueError: | |
312 | raise tornado.web.HTTPError(400) | |
313 | ||
314 | def _redirect_user_to_discourse(self, account, nonce, return_sso_url): | |
315 | """ | |
316 | Redirects the user back to Discourse passing some | |
317 | attributes of the user account to Discourse | |
318 | """ | |
319 | args = { | |
320 | "nonce" : nonce, | |
321 | "external_id" : account.uid, | |
322 | ||
323 | # Pass email address | |
324 | "email" : account.email, | |
325 | "require_activation" : "false", | |
326 | ||
327 | # More details about the user | |
328 | "username" : account.uid, | |
329 | "name" : "%s" % account, | |
9bb4915d | 330 | "bio" : account.description or "", |
2dac7110 MT |
331 | |
332 | # Avatar | |
333 | "avatar_url" : account.avatar_url(), | |
334 | "avatar_force_update" : "true", | |
335 | ||
336 | # Send a welcome message | |
337 | "suppress_welcome_message" : "false", | |
338 | ||
339 | # Group memberships | |
340 | "admin" : "true" if account.is_admin() else "false", | |
eae206f4 | 341 | "moderator" : "true" if account.is_moderator() else "false", |
2dac7110 MT |
342 | } |
343 | ||
344 | # Format payload and sign it | |
345 | payload = self.accounts.encode_discourse_payload(**args) | |
346 | signature = self.accounts.sign_discourse_payload(payload) | |
347 | ||
348 | qs = urllib.parse.urlencode({ | |
349 | "sso" : payload, | |
350 | "sig" : signature, | |
351 | }) | |
352 | ||
353 | # Redirect user | |
354 | self.redirect("%s?%s" % (return_sso_url, qs)) | |
355 | ||
356 | @base.ratelimit(minutes=24*60, requests=100) | |
357 | def get(self): | |
358 | params = self._get_discourse_params() | |
359 | ||
360 | # Redirect back if user is already logged in | |
a2de2e1f MT |
361 | if self.current_user: |
362 | return self._redirect_user_to_discourse(self.current_user, **params) | |
2dac7110 MT |
363 | |
364 | # Otherwise the user needs to authenticate | |
328a7710 MT |
365 | self.render("auth/login.html", next=None) |
366 | ||
367 | @base.ratelimit(minutes=24*60, requests=100) | |
368 | def post(self): | |
369 | params = self._get_discourse_params() | |
370 | ||
371 | # Get credentials | |
372 | username = self.get_argument("username") | |
373 | password = self.get_argument("password") | |
374 | ||
375 | # Check credentials | |
376 | account = self.accounts.auth(username, password) | |
377 | if not account: | |
378 | raise tornado.web.HTTPError(401, "Unknown user or invalid password: %s" % username) | |
379 | ||
380 | # If the user has been authenticated, we will redirect to Discourse | |
381 | self._redirect_user_to_discourse(account, **params) | |
2dac7110 MT |
382 | |
383 | ||
9150881e MT |
384 | class NewAccountsModule(ui_modules.UIModule): |
385 | def render(self, days=14): | |
386 | t = datetime.datetime.utcnow() - datetime.timedelta(days=days) | |
387 | ||
388 | # Fetch all accounts created after t | |
389 | accounts = self.backend.accounts.get_created_after(t) | |
390 | ||
391 | accounts.sort(key=lambda a: a.created_at, reverse=True) | |
392 | ||
393 | return self.render_string("people/modules/accounts-new.html", | |
394 | accounts=accounts, t=t) | |
395 | ||
396 | ||
786e9ca8 MT |
397 | class AccountsListModule(ui_modules.UIModule): |
398 | def render(self, accounts=None): | |
399 | if accounts is None: | |
51907e45 | 400 | accounts = self.backend.accounts |
786e9ca8 MT |
401 | |
402 | return self.render_string("people/modules/accounts-list.html", accounts=accounts) | |
403 | ||
404 | ||
405 | class CDRModule(ui_modules.UIModule): | |
bdaf6b46 MT |
406 | def render(self, account, date=None, limit=None): |
407 | cdr = account.get_cdr(date=date, limit=limit) | |
786e9ca8 | 408 | |
89e47299 MT |
409 | return self.render_string("people/modules/cdr.html", |
410 | account=account, cdr=list(cdr)) | |
786e9ca8 MT |
411 | |
412 | ||
413 | class ChannelsModule(ui_modules.UIModule): | |
414 | def render(self, account): | |
1f38be5a MT |
415 | return self.render_string("people/modules/channels.html", |
416 | account=account, channels=account.sip_channels) | |
786e9ca8 MT |
417 | |
418 | ||
68ece434 MT |
419 | class MOSModule(ui_modules.UIModule): |
420 | def render(self, call): | |
421 | return self.render_string("people/modules/mos.html", call=call) | |
422 | ||
423 | ||
b5e2077f | 424 | class PasswordModule(ui_modules.UIModule): |
56894a8b | 425 | def render(self, account=None): |
b5e2077f MT |
426 | return self.render_string("people/modules/password.html", account=account) |
427 | ||
428 | def javascript_files(self): | |
429 | return "js/zxcvbn.js" | |
430 | ||
431 | def embedded_javascript(self): | |
432 | return self.render_string("people/modules/password.js") | |
433 | ||
434 | ||
786e9ca8 MT |
435 | class RegistrationsModule(ui_modules.UIModule): |
436 | def render(self, account): | |
437 | return self.render_string("people/modules/registrations.html", account=account) | |
7afd64bb MT |
438 | |
439 | ||
440 | class SIPStatusModule(ui_modules.UIModule): | |
441 | def render(self, account): | |
442 | return self.render_string("people/modules/sip-status.html", account=account) |