]> git.ipfire.org Git - ipfire.org.git/blame - src/web/people.py
people: Encourage people to add a description
[ipfire.org.git] / src / web / people.py
CommitLineData
2cd9af74
MT
1#!/usr/bin/python
2
bdaf6b46 3import datetime
e96e445b 4import ldap
2cd9af74 5import logging
020de883 6import imghdr
2cd9af74 7import tornado.web
2dac7110 8import urllib.parse
2cd9af74 9
0099c2a7
MT
10from .. import countries
11
9b8ff27d 12from . import auth
124a8404 13from . import base
786e9ca8
MT
14from . import ui_modules
15
9b8ff27d 16class 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 32class 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 72class 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 94class 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 114class 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
120class 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
130class 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 145class 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 161class 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 175class 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 185class 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 195class 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 # Email
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 253class 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
300class 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
384class 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
397class 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
405class 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
413class 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
419class MOSModule(ui_modules.UIModule):
420 def render(self, call):
421 return self.render_string("people/modules/mos.html", call=call)
422
423
b5e2077f 424class 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
435class RegistrationsModule(ui_modules.UIModule):
436 def render(self, account):
437 return self.render_string("people/modules/registrations.html", account=account)
7afd64bb
MT
438
439
440class SIPStatusModule(ui_modules.UIModule):
441 def render(self, account):
442 return self.render_string("people/modules/sip-status.html", account=account)