]> git.ipfire.org Git - ipfire.org.git/blame - src/web/users.py
users: Connect to Asterisk and show any ongoing calls
[ipfire.org.git] / src / web / users.py
CommitLineData
b01a1ee3
MT
1#!/usr/bin/python
2
53b2117f
MT
3import PIL
4import imghdr
5import io
3c986f14 6import ldap
53b2117f
MT
7import logging
8import os.path
b01a1ee3
MT
9import tornado.web
10
3c986f14
MT
11from .. import countries
12
b01a1ee3 13from . import base
beb13102
MT
14from . import ui_modules
15
53b2117f
MT
16COLOUR_LIGHT = (237,232,232)
17COLOUR_DARK = (49,53,60)
18
da24ac0a 19class IndexHandler(base.BaseHandler):
beb13102
MT
20 @tornado.web.authenticated
21 def get(self):
22 results = None
23
24 # Query Term
25 q = self.get_argument("q", None)
26
27 # Peform search
28 if q:
29 results = self.backend.accounts.search(q)
30
31 self.render("users/index.html", q=q, results=results)
32
b01a1ee3 33
da24ac0a 34class ShowHandler(base.BaseHandler):
b01a1ee3 35 @tornado.web.authenticated
d6c41da2 36 async def get(self, uid):
b01a1ee3
MT
37 account = self.backend.accounts.get_by_uid(uid)
38 if not account:
39 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
40
d6c41da2
MT
41 # Fetch SIP channels
42 sip_channels = await account.get_sip_channels()
43
44 self.render("users/show.html", account=account, sip_channels=sip_channels)
beb13102
MT
45
46
53b2117f
MT
47class AvatarHandler(base.BaseHandler):
48 def get(self, uid):
49 # Get the desired size of the avatar file
50 size = self.get_argument("size", None)
51
52 try:
53 size = int(size)
54 except (TypeError, ValueError):
55 size = None
56
57 logging.debug("Querying for avatar of %s" % uid)
58
59 # Fetch user account
60 account = self.backend.accounts.get_by_uid(uid)
61 if not account:
62 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
63
64 # Allow downstream to cache this for a year
65 self.set_expires(31536000)
66
67 # Resize avatar
68 avatar = account.get_avatar(size)
69
70 # If there is no avatar, we serve a default image
71 if not avatar:
72 logging.debug("No avatar uploaded for %s" % account)
73
74 # Generate a random avatar with only one letter
75 avatar = self._get_avatar(account, size=size)
76
77 # Guess content type
78 type = imghdr.what(None, avatar)
79
80 # If we could not guess the type, we will try something else
81 if not type:
82 # Could this be an SVG file?
83 if avatar.startswith(b"<"):
84 type = "svg+xml"
85
86 # Set headers about content
87 self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type))
88 if type:
89 self.set_header("Content-Type", "image/%s" % type)
90
91 # Deliver payload
92 self.finish(avatar)
93
94 def _get_avatar(self, account, size=None, **args):
95 letter = ("%s" % account)[0].upper()
96
97 if size is None:
98 size = 256
99
100 # The generated avatar cannot be larger than 1024px
101 if size >= 2048:
102 size = 2048
103
104 # Cache key
105 key = "avatar:letter:%s:%s" % (letter, size)
106
107 # Fetch avatar from the cache
108 avatar = self.memcached.get(key)
109 if not avatar:
110 avatar = self._make_avatar(letter, size=size, **args)
111
112 # Cache for forever
113 self.memcached.set(key, avatar)
114
115 return avatar
116
117 def _make_avatar(self, letter, format="PNG", size=None, **args):
118 # Generate an image of the correct size
119 image = PIL.Image.new("RGBA", (size, size), COLOUR_LIGHT)
120
121 # Have a canvas
122 draw = PIL.ImageDraw.Draw(image)
123
124 # Load font
125 font = PIL.ImageFont.truetype(os.path.join(
126 self.application.settings.get("static_path", ""),
127 "fonts/Prompt-Bold.ttf"
128 ), size, encoding="unic")
129
130 # Determine size of the printed letter
131 w, h = font.getsize(letter)
132
133 # Mukta seems to be very broken and the height needs to be corrected
134 h //= 0.7
135
136 # Draw the letter in the center
137 draw.text(((size - w) / 2, (size - h) / 2), letter,
138 font=font, fill=COLOUR_DARK)
139
140 with io.BytesIO() as f:
141 # If writing out the image does not work with optimization,
142 # we try to write it out without any optimization.
143 try:
144 image.save(f, format, optimize=True, **args)
145 except:
146 image.save(f, format, **args)
147
148 return f.getvalue()
149
150
3c986f14
MT
151class EditHandler(base.BaseHandler):
152 @tornado.web.authenticated
153 def get(self, uid):
154 account = self.backend.accounts.get_by_uid(uid)
155 if not account:
156 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
157
158 # Check for permissions
159 if not account.can_be_managed_by(self.current_user):
160 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
161
162 self.render("users/edit.html", account=account, countries=countries.get_all())
163
164 @tornado.web.authenticated
165 def post(self, uid):
166 account = self.backend.accounts.get_by_uid(uid)
167 if not account:
168 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
169
170 # Check for permissions
171 if not account.can_be_managed_by(self.current_user):
172 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
173
174 # Unfortunately this cannot be wrapped into a transaction
175 try:
176 account.first_name = self.get_argument("first_name")
177 account.last_name = self.get_argument("last_name")
178 account.nickname = self.get_argument("nickname", None)
179 account.street = self.get_argument("street", None)
180 account.city = self.get_argument("city", None)
181 account.postal_code = self.get_argument("postal_code", None)
182 account.country_code = self.get_argument("country_code", None)
183 account.description = self.get_argument("description", None)
184
185 # Avatar
186 try:
187 filename, data, mimetype = self.get_file("avatar")
188
189 if not mimetype.startswith("image/"):
190 raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype)
191
192 account.upload_avatar(data)
193 except TypeError:
194 pass
195
196 # Email
197 account.mail_routing_address = self.get_argument("mail_routing_address", None)
198
199 # Telephone
200 account.phone_numbers = self.get_argument("phone_numbers", "").splitlines()
201 account.sip_routing_address = self.get_argument("sip_routing_address", None)
202 except ldap.STRONG_AUTH_REQUIRED as e:
203 raise tornado.web.HTTPError(403, "%s" % e) from e
204
205 # Redirect back to user page
206 self.redirect("/users/%s" % account.uid)
207
208
1cd4d7d3
MT
209class DeleteHandler(base.BaseHandler):
210 @tornado.web.authenticated
211 def get(self, uid):
212 account = self.backend.accounts.get_by_uid(uid)
213 if not account:
214 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
215
216 # Check for permissions
217 if not account.can_be_deleted_by(self.current_user):
218 raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
219
220 self.render("users/delete.html", account=account)
221
222 @tornado.web.authenticated
223 async def post(self, uid):
224 account = self.backend.accounts.get_by_uid(uid)
225 if not account:
226 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
227
228 # Check for permissions
229 if not account.can_be_deleted_by(self.current_user):
230 raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
231
232 # Delete!
233 with self.db.transaction():
234 await account.delete(self.current_user)
235
236 self.render("users/deleted.html", account=account)
237
238
e4d2f51f
MT
239class PasswdHandler(base.BaseHandler):
240 @tornado.web.authenticated
241 def get(self, uid):
242 account = self.backend.accounts.get_by_uid(uid)
243 if not account:
244 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
245
246 # Check for permissions
247 if not account.can_be_managed_by(self.current_user):
248 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
249
250 self.render("users/passwd.html", account=account)
251
252 @tornado.web.authenticated
253 def post(self, uid):
254 account = self.backend.accounts.get_by_uid(uid)
255 if not account:
256 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
257
258 # Check for permissions
259 if not account.can_be_managed_by(self.current_user):
260 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
261
262 # Get current password
263 password = self.get_argument("password")
264
265 # Get new password
266 password1 = self.get_argument("password1")
267 password2 = self.get_argument("password2")
268
269 # Passwords must match
270 if not password1 == password2:
271 raise tornado.web.HTTPError(400, "Passwords do not match")
272
273 # XXX Check password complexity
274
275 # Check if old password matches
276 if not account.check_password(password):
277 raise tornado.web.HTTPError(403, "Incorrect password for %s" % account)
278
279 # Save new password
280 account.passwd(password1)
281
282 # Redirect back to user's page
283 self.redirect("/users/%s" % account.uid)
284
285
da24ac0a 286class GroupIndexHandler(base.BaseHandler):
f39251eb
MT
287 @tornado.web.authenticated
288 def get(self):
289 # Only staff can see other groups
290 if not self.current_user.is_staff():
291 raise tornado.web.HTTPError(403)
292
293 self.render("users/groups/index.html")
294
295
da24ac0a 296class GroupShowHandler(base.BaseHandler):
f39251eb
MT
297 @tornado.web.authenticated
298 def get(self, gid):
299 # Only staff can see other groups
300 if not self.current_user.is_staff():
301 raise tornado.web.HTTPError(403)
302
303 # Fetch group
304 group = self.backend.groups.get_by_gid(gid)
305 if not group:
306 raise tornado.web.HTTPError(404, "Could not find group %s" % gid)
307
308 self.render("users/groups/show.html", group=group)
309
310
beb13102
MT
311class ListModule(ui_modules.UIModule):
312 def render(self, accounts, show_created_at=False):
313 return self.render_string("users/modules/list.html", accounts=accounts,
314 show_created_at=show_created_at)