# License for the specific language governing permissions and limitations
# under the License.
+import bcrypt
+import concurrent.futures
import MySQLdb
import markdown
import os.path
import re
import subprocess
import torndb
-import tornado.auth
+import tornado.escape
+from tornado import gen
import tornado.httpserver
import tornado.ioloop
import tornado.options
define("mysql_password", default="blog", help="blog database password")
+# A thread pool to be used for password hashing with bcrypt.
+executor = concurrent.futures.ThreadPoolExecutor(2)
+
+
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r"/feed", FeedHandler),
(r"/entry/([^/]+)", EntryHandler),
(r"/compose", ComposeHandler),
+ (r"/auth/create", AuthCreateHandler),
(r"/auth/login", AuthLoginHandler),
(r"/auth/logout", AuthLogoutHandler),
]
if not user_id: return None
return self.db.get("SELECT * FROM authors WHERE id = %s", int(user_id))
+ def any_author_exists(self):
+ return bool(self.db.get("SELECT * FROM authors LIMIT 1"))
+
class HomeHandler(BaseHandler):
def get(self):
self.redirect("/entry/" + slug)
-class AuthLoginHandler(BaseHandler, tornado.auth.GoogleMixin):
- @tornado.web.asynchronous
+class AuthCreateHandler(BaseHandler):
def get(self):
- if self.get_argument("openid.mode", None):
- self.get_authenticated_user(self._on_auth)
- return
- self.authenticate_redirect()
+ self.render("create_author.html")
+
+ @gen.coroutine
+ def post(self):
+ if self.any_author_exists():
+ raise tornado.web.HTTPError(400, "author already created")
+ hashed_password = yield executor.submit(
+ bcrypt.hashpw, tornado.escape.utf8(self.get_argument("password")),
+ bcrypt.gensalt())
+ author_id = self.db.execute(
+ "INSERT INTO authors (email, name, hashed_password) "
+ "VALUES (%s, %s, %s)",
+ self.get_argument("email"), self.get_argument("name"),
+ hashed_password)
+ self.set_secure_cookie("blogdemo_user", str(author_id))
+ self.redirect(self.get_argument("next", "/"))
- def _on_auth(self, user):
- if not user:
- raise tornado.web.HTTPError(500, "Google auth failed")
+
+class AuthLoginHandler(BaseHandler):
+ def get(self):
+ # If there are no authors, redirect to the account creation page.
+ if not self.any_author_exists():
+ self.redirect("/auth/create")
+ else:
+ self.render("login.html", error=None)
+
+ @gen.coroutine
+ def post(self):
author = self.db.get("SELECT * FROM authors WHERE email = %s",
- user["email"])
+ self.get_argument("email"))
if not author:
- # Auto-create first author
- any_author = self.db.get("SELECT * FROM authors LIMIT 1")
- if not any_author:
- author_id = self.db.execute(
- "INSERT INTO authors (email,name) VALUES (%s,%s)",
- user["email"], user["name"])
- else:
- self.redirect("/")
- return
+ self.render("login.html", error="email not found")
+ return
+ hashed_password = yield executor.submit(
+ bcrypt.hashpw, tornado.escape.utf8(self.get_argument("password")),
+ tornado.escape.utf8(author.hashed_password))
+ if hashed_password == author.hashed_password:
+ self.set_secure_cookie("blogdemo_user", str(author.id))
+ self.redirect(self.get_argument("next", "/"))
else:
- author_id = author["id"]
- self.set_secure_cookie("blogdemo_user", str(author_id))
- self.redirect(self.get_argument("next", "/"))
+ self.render("login.html", error="incorrect password")
class AuthLogoutHandler(BaseHandler):