front/auth+users: implement talking to backend

This commit is contained in:
rr- 2016-03-28 22:33:20 +02:00
parent 2e4e77791d
commit 5a0ce0b49d
11 changed files with 177 additions and 31 deletions

View file

@ -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": {

View file

@ -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);
}

View file

@ -5,11 +5,11 @@
<ul>
<li>
<label for='user-name'>User name</label>
<input id='user-name' name='user-name' type='text' required/>
<input id='user-name' name='name' type='text' required/>
</li>
<li>
<label for='user-password'>Password</label>
<input id='user-password' name='user-password' type='password' required/>
<input id='user-password' name='password' type='password' required/>
</li>
<li>
<input id='remember-user' name='remember-user' type='checkbox'/>
@ -17,6 +17,7 @@
</li>
</ul>
</fieldset>
<fieldset class='messages'></fieldset>
<fieldset class='buttons'>
<input type='submit' value='Log in'/>
<a>Forgot the password?</a>

View file

@ -18,6 +18,7 @@
</li>
</ul>
</fieldset>
<fieldset class='messages'></fieldset>
<fieldset class='buttons'>
<input type='submit' value='Create an account'/>
</fieldset>

39
static/js/api.js Normal file
View file

@ -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;

View file

@ -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); });
});
}});
}

View file

@ -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);
});
});
}});
}

View file

@ -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(); });

View file

@ -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) {

View file

@ -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);
});
});
}
}

View file

@ -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);
});
});
}
}