jmw327 11 жил өмнө
parent
commit
201e0267b6

+ 36 - 0
web/.gitignore

@@ -0,0 +1,36 @@
+# Project specific
+config.yaml
+venv/
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+bin/
+build/
+develop-eggs/
+dist/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+.tox/
+.coverage
+.cache
+nosetests.xml
+coverage.xml

+ 3 - 0
web/.gitmodules

@@ -0,0 +1,3 @@
+[submodule "cleancss.git"]
+	path = cleancss.git
+	url = git://github.com/raylu/py-cleancss.git

+ 1 - 0
web/cleancss

@@ -0,0 +1 @@
+cleancss.git/cleancss

+ 28 - 0
web/config.py

@@ -0,0 +1,28 @@
+import yaml
+
+class Config:
+	def __init__(self, cdict):
+		attrs = set(self.attrs) # copy and "unfreeze"
+		for k, v in cdict.items():
+			attrs.remove(k) # check if the key is allowed, mark it as present
+			setattr(self, k, v)
+		if len(attrs) != 0:
+			raise KeyError('missing required bot config keys: %s' % attrs)
+
+class WebConfig(Config):
+	attrs = frozenset([
+		'port',
+		'host',
+		'cookie_secret',
+		'debug',
+	])
+
+class DBConfig(Config):
+	attrs = frozenset([
+		'user',
+		'database',
+	])
+
+__doc = yaml.load(open('config.yaml', 'r'))
+web = WebConfig(__doc['web'])
+db = DBConfig(__doc['db'])

+ 11 - 0
web/config.yaml.example

@@ -0,0 +1,11 @@
+web:
+    port: 8888
+    host: 'http://127.0.0.1:8888'
+    cookie_secret: 'dis is super sekrit'
+    debug: true
+
+db:
+    user: 'sysvitals'
+    database: 'sysvitals'
+
+# vim: set et ft=yaml:

+ 46 - 0
web/db.py

@@ -0,0 +1,46 @@
+import hashlib
+import hmac
+import os
+
+import tornado.gen
+import psycopg2
+import momoko
+
+import config
+
+def hash_pw(password, key=None):
+	if key is None:
+		key = os.urandom(16)
+		digest = hmac.new(key, password, hashlib.sha256)
+		key = key.encode("hex")
+		hashed = digest.hexdigest()
+		return key, hashed
+
+class MomokoDB:
+	db = momoko.Pool(dsn='dbname=%s user=%s' % (config.db.database, config.db.user), size=2)
+
+	@tornado.gen.coroutine
+	def execute(self, query, *args):
+		result = yield momoko.Op(self.db.execute, query, args, cursor_factory=psycopg2.extras.DictCursor)
+		return result
+
+	@tornado.gen.coroutine
+	def create_user(self, username, password):
+		salt, hashed_password = hash_pw(password)
+		query = 'INSERT INTO users (username, password, salt) VALUES (%s, %s, %s);'
+		yield self.execute(query, username, hashed_password, salt)
+
+	@tornado.gen.coroutine
+	def get_user(self, username):
+		query = 'SELECT * FROM users WHERE username=%s;'
+		cursor = yield self.execute(query, username)
+		return cursor.fetchone()
+
+	@tornado.gen.coroutine
+	def check_user(self, username, password):
+		user = yield self.get_user(username)
+		if not user:
+			return
+		_, hashed = hash_pw(password, user['salt'].decode("hex"))
+		if hashed == user['password']:
+			return user

+ 3 - 0
web/requirements.txt

@@ -0,0 +1,3 @@
+PyYAML==3.10
+tornado==3.2
+momoko==1.0.0

+ 9 - 0
web/schema.sql

@@ -0,0 +1,9 @@
+DROP TABLE IF EXISTS users;
+
+CREATE TABLE users (
+	id serial PRIMARY KEY,
+	username varchar(32) NOT NULL,
+	password char(64) NOT NULL,
+	salt char(32) NOT NULL,
+	UNIQUE (username)
+);

+ 100 - 0
web/server.py

@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+
+import os
+import urllib.parse
+
+import cleancss
+import tornado.gen
+import tornado.escape
+import tornado.httpclient
+import tornado.ioloop
+import tornado.web
+
+from config import web as config
+import db
+
+class BaseHandler(tornado.web.RequestHandler):
+	def render(self, *args, **kwargs):
+		kwargs['host'] = config.host
+		return super(BaseHandler, self).render(*args, **kwargs)
+
+	def render_string(self, *args, **kwargs):
+		s = super(BaseHandler, self).render_string(*args, **kwargs)
+		return s.replace(b'\n', b'') # this is like Django's {% spaceless %}
+
+	def get_current_user(self):
+		return self.get_secure_cookie('username')
+
+	@tornado.gen.coroutine
+	def create_session(self, username):
+		self.set_secure_cookie('username', username)
+
+	@property
+	def db(self):
+		return self.application.db
+
+class MainHandler(BaseHandler):
+	@tornado.gen.coroutine
+	def get(self):
+		self.render('home.html')
+
+class RegisterHandler(BaseHandler):
+	@tornado.gen.coroutine
+	def get(self):
+		self.render('register.html')
+
+	@tornado.gen.coroutine
+	def post(self):
+		username = self.get_argument("username", "")
+		password = self.get_argument("password", "")
+		yield db.create_user(username, password)
+		self.create_session(user['username'])
+		self.redirect("/")
+
+class LoginHandler(BaseHandler):
+	@tornado.gen.coroutine
+	def get(self):
+		self.render('login.html')
+
+	@tornado.gen.coroutine
+	def post(self):
+		username = self.get_argument("username", "")
+		password = self.get_argument("password", "")
+		user = yield db.check_user(username, password)
+		if not user:
+			self.render("login.html")
+		else:
+			self.create_session(user['username'])
+			self.redirect("/")
+
+class LogoutHandler(BaseHandler):
+	def get(self):
+		self.clear_all_cookies()
+		self.redirect('/')
+
+class CSSHandler(tornado.web.RequestHandler):
+	def get(self, css_path):
+		css_path = os.path.join(os.path.dirname(__file__), 'static', css_path) + '.ccss'
+		with open(css_path, 'r') as f:
+			self.set_header('Content-Type', 'text/css')
+			self.write(cleancss.convert(f))
+
+if __name__ == '__main__':
+	app = tornado.web.Application(
+		handlers=[
+			(r'/', MainHandler),
+			(r'/register', RegisterHandler),
+			(r'/login', LoginHandler),
+			(r'/logout', LogoutHandler),
+			(r'/(css/.+)\.css', CSSHandler),
+		],
+		template_path=os.path.join(os.path.dirname(__file__), 'templates'),
+		static_path=os.path.join(os.path.dirname(__file__), 'static'),
+		login_url='/login',
+		cookie_secret=config.cookie_secret,
+		debug=config.debug,
+	)
+	app.listen(config.port)
+	app.db = db.MomokoDB()
+	print('listening on :%d' % config.port)
+	tornado.ioloop.IOLoop.instance().start()

+ 6 - 0
web/static/css/account.ccss

@@ -0,0 +1,6 @@
+textarea:
+	display: block
+	margin-top: 5px
+	width: 50%
+	height: 100px
+	font: inherit

+ 113 - 0
web/static/css/base.ccss

@@ -0,0 +1,113 @@
+*:
+	box-sizing: border-box
+
+body:
+	padding: 0
+	margin: 0
+	height: 100%
+	background: #fff
+	color: #777
+	font-family: sans-serif
+
+a:
+	text-decoration: none
+	color: #00BFA8
+	border-bottom: 2px solid #00BFA8
+
+	&:hover:
+		color: #CB5847
+		border-color: #CB5847
+
+img:
+	border-radius: 4px
+	overflow: hidden
+
+form:
+	input, textarea:
+		border: 1px solid #eaeaea
+		padding: 15px
+		margin-bottom: 15px
+		border-radius: 4px
+		width: 100%
+
+	[type="button"], [type="submit"]:
+		width: auto
+		padding: 15px 30px
+		border-radius: 4px
+		cursor: pointer
+		background: #DC825F
+		color: #fff
+
+ul:
+	list-style: none
+	padding: 0
+	margin: 0
+
+	li:
+		padding: 15px
+		border-top: 1px solid #eaeaea
+
+.clear:
+	clear: both
+
+.wrapper:
+	width: 900px
+	margin: 0px auto
+
+#content:
+	padding: 15px 0px
+
+#topbar:
+	position: relative
+	padding: 30px 0px
+	background: #fff
+	border-bottom: 1px solid #eaeaea
+
+	a:
+		color: #777
+
+		&:hover:
+			color: #333
+
+	#title:
+		display: inline-block
+		
+		a:
+			border: none
+
+	#nav:
+		display: inline-block
+		vertical-align: middle
+		float: right
+
+		img:
+			width: 32px
+			height: 32px
+			margin-right: 15px
+			vertical-align: middle
+			display: inline-block
+			border-radius: 50%
+
+		a:
+			padding-right: 15px
+			vertical-align: middle
+			border: none
+
+			&:last-child:
+				padding-right: 0px
+
+		#user-nav:
+			margin-top: -7px
+
+#profile-avatar:
+	width: 200px
+	height: 200px
+
+.question:
+	margin-bottom: 15px
+	color: #333
+
+.answer:
+	margin-bottom: 30px
+	padding-left: 15px
+	border-left: 6px solid #ddd

+ 14 - 0
web/static/css/users.ccss

@@ -0,0 +1,14 @@
+.user_info:
+	padding: 15px 0px
+	border-top: 1px solid #eaeaea
+
+	img:
+		top: 5px
+		margin-right: 30px
+		width: 64px
+		height: 64px
+		display: inline-block
+		vertical-align: middle
+	
+	a:
+		vertical-align: middle

BIN
web/static/img/favicon.png


BIN
web/static/img/lpmc-logo.png


+ 30 - 0
web/static/js/account.js

@@ -0,0 +1,30 @@
+window.addEvent('domready', function() {
+	'use strict';
+
+	$('update_emails').addEvent('click', function() {
+		new Request.JSON({
+			'url': '/github_emails',
+			'onSuccess': function(response) {
+				var emails = $('emails');
+				response.each(function(email) {
+					var div = new Element('div');
+					div.appendText(email['email']);
+					div.addEvent('click', function() {
+						set_email(email['email']);
+					});
+					emails.grab(div, 'top');
+				});
+			},
+		}).get();
+	});
+
+	function set_email(email) {
+		new Request({
+			'url': '/account/contact_info',
+			'onSuccess': function(response) {
+				if (response)
+					location.reload();
+			},
+		}).post({'info_type': 0, 'info': email});
+	}
+});

+ 30 - 0
web/templates/base.html

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>LPMC</title>
+	<link rel="stylesheet" type="text/css" href="/css/base.css" />
+	<script src="//ajax.googleapis.com/ajax/libs/mootools/1.4.5/mootools.js"></script>
+	{% block js %}{% end %}
+	{% block css %}{% end %}
+</head>
+<body>
+    <div id="topbar">
+        <div class="wrapper">
+            <div id="title"><a href="/">website</a></div>
+            <div id="nav">
+                {% if current_user %}
+                    <div id="user-nav">
+                        <a href="/logout">logout</a>
+                    </div>
+                {% else %}
+                    <a href="/login">login</a>
+                    <a href="/register">sign up</a>
+                {% end %}
+            </div>
+        </div>
+    </div>
+	<div class="wrapper">
+		{% block main %}{% end %}
+	</div>
+</body>
+</html>

+ 5 - 0
web/templates/home.html

@@ -0,0 +1,5 @@
+{% extends "base.html" %}
+
+{% block main %}
+	Hello.
+{% end %}

+ 15 - 0
web/templates/login.html

@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block main %}
+
+<h1> Sign in </h1>
+<form id="auth-form" action="" method="post">
+	<label for="username">Username</label>
+	<input type="text" name="username" id="username">
+	<label for="password">Password</label>
+	<input type="password" name="password" id="password">
+	<input type="submit" class="button" name="signin" value="Sign in">
+	{% module xsrf_form_html() %}
+</form>
+
+{% end %}

+ 15 - 0
web/templates/register.html

@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block main %}
+
+<h1> Sign up </h1>
+<form id="auth-form" action="" method="post">
+	<label for="username">Username</label>
+	<input type="text" name="username" id="username">
+	<label for="password">Password</label>
+	<input type="password" name="password" id="password">
+	<input type="submit" class="button" name="signup" value="Sign up">
+	{% module xsrf_form_html() %}
+</form>
+
+{% end %}