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