]>
Commit | Line | Data |
---|---|---|
08df6527 MT |
1 | #!/usr/bin/python |
2 | ||
3 | import logging | |
4 | import tornado.web | |
2fe1d960 | 5 | import urllib.parse |
08df6527 | 6 | |
124a8404 | 7 | from . import base |
08df6527 | 8 | |
da24ac0a | 9 | class AuthenticationMixin(object): |
d8a15b2e | 10 | def login(self, account): |
08df6527 | 11 | # User has logged in, create a session |
906e1e6a MT |
12 | session_id, session_expires = self.backend.accounts.create_session( |
13 | account, self.request.host) | |
08df6527 MT |
14 | |
15 | # Check if a new session was created | |
16 | if not session_id: | |
17 | raise tornado.web.HTTPError(500, "Could not create session") | |
18 | ||
19 | # Send session cookie to the client | |
20 | self.set_cookie("session_id", session_id, | |
abd1d7aa | 21 | domain=self.request.host, expires=session_expires, secure=True) |
08df6527 MT |
22 | |
23 | def logout(self): | |
24 | session_id = self.get_cookie("session_id") | |
25 | if not session_id: | |
26 | return | |
27 | ||
906e1e6a | 28 | success = self.backend.accounts.destroy_session(session_id, self.request.host) |
08df6527 MT |
29 | if success: |
30 | self.clear_cookie("session_id") | |
31 | ||
32 | ||
08df6527 MT |
33 | class LoginHandler(AuthenticationMixin, base.BaseHandler): |
34 | def get(self): | |
35 | next = self.get_argument("next", None) | |
36 | ||
a76ac21e MT |
37 | self.render("auth/login.html", next=next, |
38 | incorrect=False, username=None) | |
08df6527 | 39 | |
53a15fe0 | 40 | @base.ratelimit(minutes=15, requests=10) |
08df6527 MT |
41 | def post(self): |
42 | username = self.get_argument("username") | |
43 | password = self.get_argument("password") | |
a76ac21e | 44 | next = self.get_argument("next", "/") |
08df6527 | 45 | |
328a7710 MT |
46 | # Find user |
47 | account = self.backend.accounts.auth(username, password) | |
48 | if not account: | |
a76ac21e MT |
49 | logging.error("Unknown user or invalid password: %s" % username) |
50 | ||
51 | # Set status to 401 | |
52 | self.set_status(401) | |
53 | ||
54 | # Render login page again | |
55 | return self.render("auth/login.html", | |
56 | incorrect=True, username=username, next=next, | |
57 | ) | |
328a7710 MT |
58 | |
59 | # Create session | |
08df6527 | 60 | with self.db.transaction(): |
328a7710 | 61 | self.login(account) |
08df6527 | 62 | |
a76ac21e MT |
63 | # Redirect the user |
64 | return self.redirect(next) | |
08df6527 MT |
65 | |
66 | ||
67 | class LogoutHandler(AuthenticationMixin, base.BaseHandler): | |
68 | def get(self): | |
69 | with self.db.transaction(): | |
70 | self.logout() | |
71 | ||
72 | # Get back to the start page | |
73 | self.redirect("/") | |
9b8ff27d MT |
74 | |
75 | ||
da24ac0a | 76 | class RegisterHandler(base.BaseHandler): |
f32dd17f | 77 | def get(self): |
d521c9df MT |
78 | # Redirect logged in users away |
79 | if self.current_user: | |
80 | self.redirect("/") | |
73f2f29a | 81 | return |
d521c9df | 82 | |
f32dd17f MT |
83 | self.render("auth/register.html") |
84 | ||
53a15fe0 | 85 | @base.ratelimit(minutes=15, requests=5) |
9fdf4fb7 | 86 | async def post(self): |
f32dd17f MT |
87 | uid = self.get_argument("uid") |
88 | email = self.get_argument("email") | |
89 | ||
90 | first_name = self.get_argument("first_name") | |
91 | last_name = self.get_argument("last_name") | |
92 | ||
93 | # Register account | |
f2ba8a1f MT |
94 | try: |
95 | with self.db.transaction(): | |
96 | self.backend.accounts.register(uid, email, | |
757372cd MT |
97 | first_name=first_name, last_name=last_name, |
98 | country_code=self.current_country_code) | |
f2ba8a1f | 99 | except ValueError as e: |
09c67399 | 100 | raise tornado.web.HTTPError(400, "%s" % e) from e |
f32dd17f MT |
101 | |
102 | self.render("auth/register-success.html") | |
103 | ||
104 | ||
d8a15b2e MT |
105 | class ActivateHandler(AuthenticationMixin, base.BaseHandler): |
106 | def get(self, uid, activation_code): | |
b4d72c76 | 107 | self.render("auth/activate.html") |
d8a15b2e MT |
108 | |
109 | def post(self, uid, activation_code): | |
b4d72c76 MT |
110 | password1 = self.get_argument("password1") |
111 | password2 = self.get_argument("password2") | |
d8a15b2e | 112 | |
b4d72c76 MT |
113 | if not password1 == password2: |
114 | raise tornado.web.HTTPError(400, "Passwords do not match") | |
d8a15b2e | 115 | |
b4d72c76 MT |
116 | with self.db.transaction(): |
117 | account = self.backend.accounts.activate(uid, activation_code) | |
118 | if not account: | |
119 | raise tornado.web.HTTPError(400, "Account not found: %s" % uid) | |
d8a15b2e | 120 | |
b4d72c76 MT |
121 | # Set the new password |
122 | account.passwd(password1) | |
d8a15b2e | 123 | |
b4d72c76 MT |
124 | # Create session |
125 | self.login(account) | |
d8a15b2e | 126 | |
b00cc400 MT |
127 | # Redirect to success page |
128 | self.render("auth/activated.html", account=account) | |
689effd0 MT |
129 | |
130 | ||
da24ac0a | 131 | class PasswordResetInitiationHandler(base.BaseHandler): |
c7594d58 MT |
132 | def get(self): |
133 | username = self.get_argument("username", None) | |
134 | ||
135 | self.render("auth/password-reset-initiation.html", username=username) | |
136 | ||
53a15fe0 | 137 | @base.ratelimit(minutes=15, requests=10) |
c7594d58 MT |
138 | def post(self): |
139 | username = self.get_argument("username") | |
140 | ||
141 | # Fetch account and submit password reset | |
142 | account = self.backend.accounts.get_by_uid(username) | |
143 | if account: | |
144 | with self.db.transaction(): | |
391ede9e | 145 | account.request_password_reset() |
c7594d58 MT |
146 | |
147 | self.render("auth/password-reset-successful.html") | |
148 | ||
149 | ||
391ede9e MT |
150 | class PasswordResetHandler(AuthenticationMixin, base.BaseHandler): |
151 | def get(self, uid, reset_code): | |
152 | account = self.backend.accounts.get_by_uid(uid) | |
153 | if not account: | |
154 | raise tornado.web.HTTPError(404, "Could not find account: %s" % uid) | |
155 | ||
156 | self.render("auth/password-reset.html", account=account) | |
157 | ||
158 | def post(self, uid, reset_code): | |
159 | account = self.backend.accounts.get_by_uid(uid) | |
160 | if not account: | |
161 | raise tornado.web.HTTPError(404, "Could not find account: %s" % uid) | |
162 | ||
163 | password1 = self.get_argument("password1") | |
164 | password2 = self.get_argument("password2") | |
165 | ||
166 | if not password1 == password2: | |
167 | raise tornado.web.HTTPError(400, "Passwords do not match") | |
168 | ||
169 | # Try to perform password reset | |
170 | with self.db.transaction(): | |
171 | account.reset_password(reset_code, password1) | |
172 | ||
173 | # Login the user straight away after reset was successful | |
174 | self.login(account) | |
175 | ||
176 | # Redirect back to / | |
177 | self.redirect("/") | |
178 | ||
179 | ||
da24ac0a | 180 | class SSODiscourse(base.BaseHandler): |
2fe1d960 MT |
181 | @base.ratelimit(minutes=24*60, requests=100) |
182 | @tornado.web.authenticated | |
183 | def get(self): | |
184 | # Fetch Discourse's parameters | |
185 | sso = self.get_argument("sso") | |
186 | sig = self.get_argument("sig") | |
187 | ||
188 | # Decode payload | |
189 | try: | |
190 | params = self.accounts.decode_discourse_payload(sso, sig) | |
191 | ||
192 | # Raise bad request if the signature is invalid | |
193 | except ValueError: | |
194 | raise tornado.web.HTTPError(400) | |
195 | ||
196 | # Redirect back if user is already logged in | |
197 | args = { | |
198 | "nonce" : params.get("nonce"), | |
199 | "external_id" : self.current_user.uid, | |
200 | ||
201 | # Pass email address | |
202 | "email" : self.current_user.email, | |
203 | "require_activation" : "false", | |
204 | ||
205 | # More details about the user | |
206 | "username" : self.current_user.uid, | |
207 | "name" : "%s" % self.current_user, | |
208 | "bio" : self.current_user.description or "", | |
209 | ||
210 | # Avatar | |
211 | "avatar_url" : self.current_user.avatar_url(absolute=True), | |
212 | "avatar_force_update" : "true", | |
213 | ||
214 | # Send a welcome message | |
215 | "suppress_welcome_message" : "false", | |
216 | ||
217 | # Group memberships | |
218 | "admin" : "true" if self.current_user.is_admin() else "false", | |
219 | "moderator" : "true" if self.current_user.is_moderator() else "false", | |
220 | } | |
221 | ||
222 | # Format payload and sign it | |
223 | payload = self.accounts.encode_discourse_payload(**args) | |
224 | signature = self.accounts.sign_discourse_payload(payload) | |
225 | ||
226 | qs = urllib.parse.urlencode({ | |
227 | "sso" : payload, | |
228 | "sig" : signature, | |
229 | }) | |
230 | ||
231 | # Redirect user | |
232 | self.redirect("%s?%s" % (params.get("return_sso_url"), qs)) | |
233 | ||
234 | ||
689effd0 | 235 | class APICheckUID(base.APIHandler): |
66181c96 | 236 | @base.ratelimit(minutes=1, requests=100) |
689effd0 MT |
237 | def get(self): |
238 | uid = self.get_argument("uid") | |
239 | result = None | |
240 | ||
241 | if not uid: | |
242 | result = "empty" | |
243 | ||
244 | # Check if the username is syntactically valid | |
245 | elif not self.backend.accounts.uid_is_valid(uid): | |
246 | result = "invalid" | |
247 | ||
248 | # Check if the username is already taken | |
249 | elif self.backend.accounts.uid_exists(uid): | |
250 | result = "taken" | |
251 | ||
252 | # Username seems to be okay | |
253 | self.finish({ "result" : result or "ok" }) | |
66181c96 MT |
254 | |
255 | ||
256 | class APICheckEmail(base.APIHandler): | |
257 | @base.ratelimit(minutes=1, requests=100) | |
258 | def get(self): | |
259 | email = self.get_argument("email") | |
260 | result = None | |
261 | ||
262 | if not email: | |
263 | result = "empty" | |
264 | ||
3095c017 MT |
265 | elif not self.backend.accounts.mail_is_valid(email): |
266 | result = "invalid" | |
267 | ||
66181c96 MT |
268 | # Check if this email address is blacklisted |
269 | elif self.backend.accounts.mail_is_blacklisted(email): | |
3095c017 | 270 | result = "blacklisted" |
66181c96 MT |
271 | |
272 | # Check if this email address is already useed | |
273 | elif self.backend.accounts.get_by_mail(email): | |
274 | result = "taken" | |
275 | ||
276 | self.finish({ "result" : result or "ok" }) |