diff --git a/package.json b/package.json
index 050a0a18..6ed32b0f 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"ini": "^1.3.4",
"merge": "^1.2.0",
"page": "^1.7.1",
+ "superagent": "^1.8.3",
"uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony"
},
"devDependencies": {
diff --git a/static/css/main.css b/static/css/main.css
index f176df1d..611405ec 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -4,6 +4,8 @@
--text-color: #111;
--inactive-link-color: #888;
--line-color: #DDD;
+ --message-error-border-color: #FCC;
+ --message-error-background-color: #FFF5F5;
--input-bad-border-color: #FCC;
--input-bad-background-color: #FFF5F5;
--input-good-border-color: #D3E3D3;
@@ -91,3 +93,11 @@ nav.text-nav ul li.active a {
#top-nav ul li[data-name=help] {
float: none;
}
+
+.messages .message {
+ padding: 0.5em;
+}
+.message.error {
+ border: 1px solid var(--message-error-border-color);
+ background: var(--message-error-background-color);
+}
diff --git a/static/html/login.tpl b/static/html/login.tpl
index 98a5980a..2fa511e4 100644
--- a/static/html/login.tpl
+++ b/static/html/login.tpl
@@ -5,11 +5,11 @@
+
+
diff --git a/static/js/api.js b/static/js/api.js
new file mode 100644
index 00000000..4c3e3bdd
--- /dev/null
+++ b/static/js/api.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const request = require('superagent');
+const config = require('./config.js');
+
+class Api {
+ get(url) {
+ const fullUrl = this.getFullUrl(url);
+ return this.process(fullUrl, () => request.get(fullUrl));
+ }
+
+ post(url, data) {
+ const fullUrl = this.getFullUrl(url);
+ return this.process(fullUrl, () => request.post(fullUrl).send(data));
+ }
+
+ process(url, requestFactory) {
+ return new Promise((resolve, reject) => {
+ let req = requestFactory();
+ if (this.userName && this.userPassword) {
+ req.auth(this.userName, this.userPassword);
+ }
+ req.set('Accept', 'application/json')
+ .end((error, response) => {
+ if (error) {
+ reject(response.body);
+ } else {
+ resolve(response.body);
+ }
+ });
+ });
+ }
+
+ getFullUrl(url) {
+ return (config.basic.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
+ }
+}
+
+module.exports = Api;
diff --git a/static/js/controllers/auth_controller.js b/static/js/controllers/auth_controller.js
index 9372af0e..5490f456 100644
--- a/static/js/controllers/auth_controller.js
+++ b/static/js/controllers/auth_controller.js
@@ -1,10 +1,15 @@
'use strict';
+const page = require('page');
+const config = require('../config.js');
+
class AuthController {
- constructor(topNavigationController, loginView) {
+ constructor(api, topNavigationController, loginView) {
+ this.api = api;
this.topNavigationController = topNavigationController;
this.loginView = loginView;
this.currentUser = null;
+ /* TODO: load from cookies */
}
isLoggedIn() {
@@ -15,20 +20,40 @@ class AuthController {
return true;
}
- login(user) {
- this.currentUser = user;
+ login(userName, userPassword) {
+ return new Promise((resolve, reject) => {
+ this.api.userName = userName;
+ this.api.userPassword = userPassword;
+ this.api.get('/user/' + userName)
+ .then(resolve)
+ .catch(reject);
+ });
}
logout(user) {
this.currentUser = null;
+ this.api.userName = null;
+ this.api.userPassword = null;
+ /* TODO: clear cookie */
}
loginRoute() {
this.topNavigationController.activate('login');
this.loginView.render({
- login: (user, password) => {
- alert(user, password);
- //self.authController.login(user);
+ login: (userName, userPassword, doRemember) => {
+ return new Promise((resolve, reject) => {
+ this
+ .login(userName, userPassword)
+ .then(response => {
+ if (doRemember) {
+ /* TODO: set cookie */
+ }
+ resolve();
+ page('/');
+ /* TODO: update top navigation */
+ })
+ .catch(response => { reject(response.description); });
+ });
}});
}
diff --git a/static/js/controllers/users_controller.js b/static/js/controllers/users_controller.js
index 24494928..b0f81238 100644
--- a/static/js/controllers/users_controller.js
+++ b/static/js/controllers/users_controller.js
@@ -1,7 +1,11 @@
'use strict';
+const page = require('page');
+
class UsersController {
- constructor(topNavigationController, authController, registrationView) {
+ constructor(
+ api, topNavigationController, authController, registrationView) {
+ this.api = api;
this.topNavigationController = topNavigationController;
this.authController = authController;
this.registrationView = registrationView;
@@ -12,12 +16,31 @@ class UsersController {
}
createUserRoute() {
- const self = this;
this.topNavigationController.activate('register');
this.registrationView.render({
- register: (user) => {
- alert(user);
- self.authController.login(user);
+ register: (userName, userPassword, userEmail) => {
+ const data = {
+ 'name': userName,
+ 'password': userPassword,
+ 'email': userEmail
+ };
+ // TODO: reduce callback hell
+ return new Promise((resolve, reject) => {
+ this.api.post('/users/', data)
+ .then(() => {
+ this.authController.login(userName, userPassword)
+ .then(() => {
+ resolve();
+ page('/');
+ })
+ .catch(response => {
+ reject(response.description);
+ });
+ })
+ .catch(response => {
+ reject(response.description);
+ });
+ });
}});
}
diff --git a/static/js/main.js b/static/js/main.js
index d1d16bf6..ce1e73c2 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -6,6 +6,7 @@
const page = require('page');
const handlebars = require('handlebars');
+const Api = require('./api.js');
const LoginView = require('./views/login_view.js');
const RegistrationView = require('./views/registration_view.js');
const TopNavigationView = require('./views/top_navigation_view.js');
@@ -24,11 +25,13 @@ const TagsController = require('./controllers/tags_controller.js');
// -------------------
// - resolve objects -
// -------------------
+const api = new Api();
+
const topNavigationView = new TopNavigationView(handlebars);
const loginView = new LoginView(handlebars);
const registrationView = new RegistrationView(handlebars);
-const authController = new AuthController(null, loginView);
+const authController = new AuthController(api, null, loginView);
const topNavigationController
= new TopNavigationController(topNavigationView, authController);
// break cyclic dependency topNavigationView<->authController
@@ -37,6 +40,7 @@ authController.topNavigationController = topNavigationController;
const homeController = new HomeController(topNavigationController);
const postsController = new PostsController(topNavigationController);
const usersController = new UsersController(
+ api,
topNavigationController,
authController,
registrationView);
@@ -52,13 +56,13 @@ page('/', () => { homeController.indexRoute(); });
page('/upload', () => { postsController.uploadPostsRoute(); });
page('/posts', () => { postsController.listPostsRoute(); });
-page('/post/:id', (id) => { postsController.showPostRoute(id); });
-page('/post/:id/edit', (id) => { postsController.editPostRoute(id); });
+page('/post/:id', id => { postsController.showPostRoute(id); });
+page('/post/:id/edit', id => { postsController.editPostRoute(id); });
page('/register', () => { usersController.createUserRoute(); });
page('/users', () => { usersController.listUsersRoute(); });
-page('/user/:user', (user) => { usersController.showUserRoute(user); });
-page('/user/:user/edit', (user) => { usersController.editUserRoute(user); });
+page('/user/:user', user => { usersController.showUserRoute(user); });
+page('/user/:user/edit', user => { usersController.editUserRoute(user); });
page('/history', () => { historyController.showHistoryRoute(); });
page('/tags', () => { tagsController.listTagsRoute(); });
diff --git a/static/js/views/base_view.js b/static/js/views/base_view.js
index 06baabd7..694e44f2 100644
--- a/static/js/views/base_view.js
+++ b/static/js/views/base_view.js
@@ -19,14 +19,32 @@ class BaseView {
return this.handlebars.compile(templateText);
}
+ showError(messagesHolder, errorMessage) {
+ /* TODO: animate this */
+ const node = document.createElement('div');
+ node.innerHTML = errorMessage;
+ node.classList.add('message');
+ node.classList.add('error');
+ messagesHolder.appendChild(node);
+ }
+
+ clearMessages(messagesHolder) {
+ /* TODO: animate that */
+ while (messagesHolder.lastChild) {
+ messagesHolder.removeChild(messagesHolder.lastChild);
+ }
+ }
+
decorateValidator(form) {
// postpone showing form fields validity until user actually tries
// to submit it (seeing red/green form w/o doing anything breaks POLA)
- const submitButton
- = document.querySelector('#content-holder .buttons input');
- submitButton.addEventListener('click', (e) => {
+ const submitButton = form.querySelector('.buttons input');
+ submitButton.addEventListener('click', e => {
form.classList.add('show-validation');
});
+ form.addEventListener('submit', e => {
+ form.classList.remove('show-validation');
+ });
}
showView(html) {
diff --git a/static/js/views/login_view.js b/static/js/views/login_view.js
index 5d13b3b9..e2b08c0d 100644
--- a/static/js/views/login_view.js
+++ b/static/js/views/login_view.js
@@ -11,17 +11,32 @@ class LoginView extends BaseView {
render(options) {
this.showView(this.template());
- const form = document.querySelector('#content-holder form');
+ const messagesHolder = this.contentHolder.querySelector('.messages');
+ const form = this.contentHolder.querySelector('form');
this.decorateValidator(form);
const userNameField = document.getElementById('user-name');
const passwordField = document.getElementById('user-password');
+ const rememberUserField = document.getElementById('remember-user');
userNameField.setAttribute('pattern', config.service.userNameRegex);
passwordField.setAttribute('pattern', config.service.passwordRegex);
- form.addEventListener('submit', (e) => {
+ form.addEventListener('submit', e => {
e.preventDefault();
- options.login(userNameField.value, passwordField.value);
+ this.clearMessages(messagesHolder);
+ form.setAttribute('disabled', true);
+ options
+ .login(
+ userNameField.value,
+ passwordField.value,
+ rememberUserField.checked)
+ .then(() => {
+ form.setAttribute('disabled', false);
+ })
+ .catch(errorMessage => {
+ form.setAttribute('disabled', false);
+ this.showError(messagesHolder, errorMessage);
+ });
});
}
}
diff --git a/static/js/views/registration_view.js b/static/js/views/registration_view.js
index a414be29..fb5841c3 100644
--- a/static/js/views/registration_view.js
+++ b/static/js/views/registration_view.js
@@ -11,6 +11,7 @@ class RegistrationView extends BaseView {
render(options) {
this.showView(this.template());
+ const messagesHolder = this.contentHolder.querySelector('.messages');
const form = document.querySelector('#content-holder form');
this.decorateValidator(form);
@@ -20,14 +21,22 @@ class RegistrationView extends BaseView {
userNameField.setAttribute('pattern', config.service.userNameRegex);
passwordField.setAttribute('pattern', config.service.passwordRegex);
- form.addEventListener('submit', (e) => {
+ form.addEventListener('submit', e => {
e.preventDefault();
- const user = {
- name: userNameField.value,
- password: passwordField.value,
- email: emailField.value,
- };
- options.register(user);
+ this.clearMessages(messagesHolder);
+ form.setAttribute('disabled', true);
+ options
+ .register(
+ userNameField.value,
+ passwordField.value,
+ emailField.value)
+ .then(() => {
+ form.setAttribute('disabled', false);
+ })
+ .catch(errorMessage => {
+ form.setAttribute('disabled', false);
+ this.showError(messagesHolder, errorMessage);
+ });
});
}
}