Browse code
Tweaking login form
- Rate limiting login attempts
- Showing feedback to attempts to create new users/passwords
Showing 7 changed files
- db/schema.sql
- requirements.txt
- src/main.py
- src/marrow/user.py
- static/css/main.css
- static/js/new/controller.js
- static/partials/login.html
... | ... |
@@ -4,7 +4,7 @@ DROP TABLE IF EXISTS user_links; |
4 | 4 |
DROP TABLE IF EXISTS users; |
5 | 5 |
CREATE TABLE users ( |
6 | 6 |
id SERIAL UNIQUE NOT NULL, |
7 |
- name TEXT UNIQUE NOT NULL, |
|
7 |
+ name TEXT UNIQUE NOT NULL CHECK (name <> ''), |
|
8 | 8 |
password TEXT NOT NULL, |
9 | 9 |
email TEXT NOT NULL, |
10 | 10 |
PRIMARY KEY (id) |
... | ... |
@@ -1,4 +1,5 @@ |
1 |
-from flask import Flask, g |
|
1 |
+from flask import Flask, g, request |
|
2 |
+from flask_limiter import Limiter |
|
2 | 3 |
import os |
3 | 4 |
import base64 |
4 | 5 |
|
... | ... |
@@ -20,6 +21,10 @@ except ImportError: |
20 | 21 |
app.secret_key = config.secret_key |
21 | 22 |
app.debug = config.debug |
22 | 23 |
|
24 |
+limiter = Limiter(app) |
|
25 |
+limiter.limit("60/hour 3/second", key_func=lambda: request.host)(user.user_blueprint) |
|
26 |
+limiter.limit("dd")(user.user_blueprint) |
|
27 |
+ |
|
23 | 28 |
# Blueprints # |
24 | 29 |
user.get_users(app) |
25 | 30 |
app.register_blueprint(user.user_blueprint, url_prefix='/user') |
... | ... |
@@ -1,8 +1,12 @@ |
1 |
+import json |
|
2 |
+ |
|
1 | 3 |
import flask |
2 |
-from flask.ext.cors import cross_origin |
|
3 | 4 |
from flask import Blueprint, session, redirect, url_for, escape, request, abort, g |
5 |
+from flask_limiter import Limiter |
|
6 |
+from flask.ext.cors import cross_origin |
|
7 |
+import psycopg2 |
|
8 |
+ |
|
4 | 9 |
from . import database |
5 |
-import json |
|
6 | 10 |
|
7 | 11 |
user_blueprint = Blueprint('user', __name__) |
8 | 12 |
|
... | ... |
@@ -58,17 +62,32 @@ def subscribe(username): |
58 | 62 |
@user_blueprint.route('/add', methods=['POST']) |
59 | 63 |
def adduser(): |
60 | 64 |
db = database.get_db() |
65 |
+ result = {'status': False, 'message': ''} |
|
61 | 66 |
with db.cursor() as cur: |
62 | 67 |
obj = request.get_json(); |
63 |
- username, password = obj['username'], obj['password'] |
|
64 |
- username = username.strip().lower() |
|
65 |
- password = password.strip() |
|
66 |
- cur.execute('INSERT INTO users (name,password,email) VALUES (%s,%s,%s)', |
|
67 |
- (username, password, 'abc@def.com')) |
|
68 |
- _get_users() |
|
69 |
- db.commit() |
|
70 |
- session['username'] = username |
|
71 |
- return json.dumps(True) |
|
68 |
+ try: |
|
69 |
+ username, password = obj['username'], obj['password'] |
|
70 |
+ except KeyError: |
|
71 |
+ print obj |
|
72 |
+ result['message'] = 'Username required' if 'username' not in obj else 'Password required' |
|
73 |
+ else: |
|
74 |
+ username = username.strip().lower() |
|
75 |
+ password = password.strip() |
|
76 |
+ try: |
|
77 |
+ cur.execute('INSERT INTO users (name,password,email) VALUES (%s,%s,%s)', |
|
78 |
+ (username, password, 'abc@def.com')) |
|
79 |
+ session['username'] = username |
|
80 |
+ result['status'] = True |
|
81 |
+ _get_users() |
|
82 |
+ except psycopg2.IntegrityError as e: |
|
83 |
+ db.rollback() |
|
84 |
+ if e.pgcode == '23505': #username not unique |
|
85 |
+ result['message'] = 'Username in use' |
|
86 |
+ elif e.pgcode == '23502': #username empty |
|
87 |
+ result['message'] = 'Username required' |
|
88 |
+ else: raise |
|
89 |
+ else: db.commit() |
|
90 |
+ return json.dumps(result) |
|
72 | 91 |
|
73 | 92 |
@user_blueprint.route('/check') |
74 | 93 |
def checkuser(): |
... | ... |
@@ -82,7 +101,7 @@ def gen_ak(db): |
82 | 101 |
@cross_origin(allow_headers='Content-Type') |
83 | 102 |
def login(): |
84 | 103 |
obj = request.get_json(); |
85 |
- result = False |
|
104 |
+ result = {'status': False, 'message': ''} |
|
86 | 105 |
username, password = obj['username'], obj['password'] |
87 | 106 |
username = username.strip().lower() |
88 | 107 |
password = password.strip() |
... | ... |
@@ -97,7 +116,9 @@ def login(): |
97 | 116 |
result = {'success': True, 'ak': ak} |
98 | 117 |
else: |
99 | 118 |
session['username'] = username |
100 |
- result = True |
|
119 |
+ result['status'] = True |
|
120 |
+ else: |
|
121 |
+ result['message'] = 'Wrong Username or Password' |
|
101 | 122 |
return json.dumps(result) |
102 | 123 |
|
103 | 124 |
@user_blueprint.route('/logout') |
... | ... |
@@ -132,6 +132,13 @@ main form input[type="text"] { |
132 | 132 |
|
133 | 133 |
/* @group Login Module */ |
134 | 134 |
|
135 |
+#login_form div.message{ |
|
136 |
+ position: absolute; |
|
137 |
+ background: red; |
|
138 |
+ width: 100%; |
|
139 |
+ text-align: center; |
|
140 |
+} |
|
141 |
+ |
|
135 | 142 |
#login_form { |
136 | 143 |
position: absolute; |
137 | 144 |
z-index: 100; |
... | ... |
@@ -222,6 +229,9 @@ body.is-logged-on #nav-main { |
222 | 229 |
left: -25%; |
223 | 230 |
top: 66px; |
224 | 231 |
} |
232 |
+ #login_form div.message{ |
|
233 |
+ top: -33px; |
|
234 |
+ } |
|
225 | 235 |
#login_form form { |
226 | 236 |
margin-top: 100px; |
227 | 237 |
} |
... | ... |
@@ -311,6 +321,9 @@ body.is-logged-on #nav-main { |
311 | 321 |
#login_form form { |
312 | 322 |
margin-top: 20px; |
313 | 323 |
} |
324 |
+ #login_form div.message{ |
|
325 |
+ margin-top 10px; |
|
326 |
+ } |
|
314 | 327 |
form input[type="password"] { |
315 | 328 |
} |
316 | 329 |
|
... | ... |
@@ -64,6 +64,8 @@ marrowApp.config(['$locationProvider', |
64 | 64 |
]); |
65 | 65 |
|
66 | 66 |
marrowApp.controller('LoginCtrl', function ($scope,$http,$route,$location) { |
67 |
+ $scope.message = ''; |
|
68 |
+ |
|
67 | 69 |
check_login().success( |
68 | 70 |
function(is_loggedon) { |
69 | 71 |
is_loggedon = JSON.parse(is_loggedon); |
... | ... |
@@ -77,8 +79,8 @@ marrowApp.controller('LoginCtrl', function ($scope,$http,$route,$location) { |
77 | 79 |
console.log(postObj); |
78 | 80 |
$http.post("/api/user/add", postObj) |
79 | 81 |
.success(function(added_user) { |
80 |
- added_user = JSON.parse(added_user); |
|
81 |
- if(added_user === true) {$location.url('/');} |
|
82 |
+ if (added_user.status === true) {$location.url('/');} |
|
83 |
+ else {$scope.message = added_user.message;} |
|
82 | 84 |
}); |
83 | 85 |
}; |
84 | 86 |
|
... | ... |
@@ -89,10 +91,9 @@ marrowApp.controller('LoginCtrl', function ($scope,$http,$route,$location) { |
89 | 91 |
$http.post("/api/user/login", {"username":username, "password":password}) |
90 | 92 |
.success( |
91 | 93 |
function (login_succeeded) { |
92 |
- login_succeeded = JSON.parse(login_succeeded); |
|
93 | 94 |
var el = angular.element(document.querySelector('#login_form')); |
94 |
- if (login_succeeded === true) {el.removeClass('is-hidden');} |
|
95 |
- $location.url('/'); |
|
95 |
+ if (login_succeeded.status === true) {$location.url('/');} |
|
96 |
+ else {$scope.message = login_succeeded.message;} |
|
96 | 97 |
}); |
97 | 98 |
}; |
98 | 99 |
}); |
... | ... |
@@ -231,7 +232,9 @@ marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route) { |
231 | 232 |
}; |
232 | 233 |
|
233 | 234 |
$scope.logout = function() { |
234 |
- $http.get('/api/user/logout').finally(function() { $location.url('/login'); }); |
|
235 |
+ $http.get('/api/user/logout').success(function() { |
|
236 |
+ $location.url('/login'); |
|
237 |
+ }); |
|
235 | 238 |
}; |
236 | 239 |
}); |
237 | 240 |
|
... | ... |
@@ -1,6 +1,6 @@ |
1 | 1 |
<div id="login_form"> |
2 |
- <div class="message"></div> |
|
3 | 2 |
<form> |
3 |
+ <div class="message">{{message}}</div> |
|
4 | 4 |
<input type="text" ng-model="username" placeholder="Username" /> |
5 | 5 |
<input type="password" ng-model="password" placeholder="Password" /> |
6 | 6 |
<button ng-click="login()">Log In</button> |