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