front/auth+users: implement talking to backend
This commit is contained in:
parent
2e4e77791d
commit
5a0ce0b49d
11 changed files with 177 additions and 31 deletions
|
@ -15,6 +15,7 @@
|
||||||
"ini": "^1.3.4",
|
"ini": "^1.3.4",
|
||||||
"merge": "^1.2.0",
|
"merge": "^1.2.0",
|
||||||
"page": "^1.7.1",
|
"page": "^1.7.1",
|
||||||
|
"superagent": "^1.8.3",
|
||||||
"uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony"
|
"uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
--text-color: #111;
|
--text-color: #111;
|
||||||
--inactive-link-color: #888;
|
--inactive-link-color: #888;
|
||||||
--line-color: #DDD;
|
--line-color: #DDD;
|
||||||
|
--message-error-border-color: #FCC;
|
||||||
|
--message-error-background-color: #FFF5F5;
|
||||||
--input-bad-border-color: #FCC;
|
--input-bad-border-color: #FCC;
|
||||||
--input-bad-background-color: #FFF5F5;
|
--input-bad-background-color: #FFF5F5;
|
||||||
--input-good-border-color: #D3E3D3;
|
--input-good-border-color: #D3E3D3;
|
||||||
|
@ -91,3 +93,11 @@ nav.text-nav ul li.active a {
|
||||||
#top-nav ul li[data-name=help] {
|
#top-nav ul li[data-name=help] {
|
||||||
float: none;
|
float: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.messages .message {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
.message.error {
|
||||||
|
border: 1px solid var(--message-error-border-color);
|
||||||
|
background: var(--message-error-background-color);
|
||||||
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<label for='user-name'>User name</label>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<label for='user-password'>Password</label>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<input id='remember-user' name='remember-user' type='checkbox'/>
|
<input id='remember-user' name='remember-user' type='checkbox'/>
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset class='messages'></fieldset>
|
||||||
<fieldset class='buttons'>
|
<fieldset class='buttons'>
|
||||||
<input type='submit' value='Log in'/>
|
<input type='submit' value='Log in'/>
|
||||||
<a>Forgot the password?</a>
|
<a>Forgot the password?</a>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset class='messages'></fieldset>
|
||||||
<fieldset class='buttons'>
|
<fieldset class='buttons'>
|
||||||
<input type='submit' value='Create an account'/>
|
<input type='submit' value='Create an account'/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
39
static/js/api.js
Normal file
39
static/js/api.js
Normal 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;
|
|
@ -1,10 +1,15 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const page = require('page');
|
||||||
|
const config = require('../config.js');
|
||||||
|
|
||||||
class AuthController {
|
class AuthController {
|
||||||
constructor(topNavigationController, loginView) {
|
constructor(api, topNavigationController, loginView) {
|
||||||
|
this.api = api;
|
||||||
this.topNavigationController = topNavigationController;
|
this.topNavigationController = topNavigationController;
|
||||||
this.loginView = loginView;
|
this.loginView = loginView;
|
||||||
this.currentUser = null;
|
this.currentUser = null;
|
||||||
|
/* TODO: load from cookies */
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
|
@ -15,20 +20,40 @@ class AuthController {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
login(user) {
|
login(userName, userPassword) {
|
||||||
this.currentUser = user;
|
return new Promise((resolve, reject) => {
|
||||||
|
this.api.userName = userName;
|
||||||
|
this.api.userPassword = userPassword;
|
||||||
|
this.api.get('/user/' + userName)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(user) {
|
logout(user) {
|
||||||
this.currentUser = null;
|
this.currentUser = null;
|
||||||
|
this.api.userName = null;
|
||||||
|
this.api.userPassword = null;
|
||||||
|
/* TODO: clear cookie */
|
||||||
}
|
}
|
||||||
|
|
||||||
loginRoute() {
|
loginRoute() {
|
||||||
this.topNavigationController.activate('login');
|
this.topNavigationController.activate('login');
|
||||||
this.loginView.render({
|
this.loginView.render({
|
||||||
login: (user, password) => {
|
login: (userName, userPassword, doRemember) => {
|
||||||
alert(user, password);
|
return new Promise((resolve, reject) => {
|
||||||
//self.authController.login(user);
|
this
|
||||||
|
.login(userName, userPassword)
|
||||||
|
.then(response => {
|
||||||
|
if (doRemember) {
|
||||||
|
/* TODO: set cookie */
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
page('/');
|
||||||
|
/* TODO: update top navigation */
|
||||||
|
})
|
||||||
|
.catch(response => { reject(response.description); });
|
||||||
|
});
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const page = require('page');
|
||||||
|
|
||||||
class UsersController {
|
class UsersController {
|
||||||
constructor(topNavigationController, authController, registrationView) {
|
constructor(
|
||||||
|
api, topNavigationController, authController, registrationView) {
|
||||||
|
this.api = api;
|
||||||
this.topNavigationController = topNavigationController;
|
this.topNavigationController = topNavigationController;
|
||||||
this.authController = authController;
|
this.authController = authController;
|
||||||
this.registrationView = registrationView;
|
this.registrationView = registrationView;
|
||||||
|
@ -12,12 +16,31 @@ class UsersController {
|
||||||
}
|
}
|
||||||
|
|
||||||
createUserRoute() {
|
createUserRoute() {
|
||||||
const self = this;
|
|
||||||
this.topNavigationController.activate('register');
|
this.topNavigationController.activate('register');
|
||||||
this.registrationView.render({
|
this.registrationView.render({
|
||||||
register: (user) => {
|
register: (userName, userPassword, userEmail) => {
|
||||||
alert(user);
|
const data = {
|
||||||
self.authController.login(user);
|
'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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
const page = require('page');
|
const page = require('page');
|
||||||
const handlebars = require('handlebars');
|
const handlebars = require('handlebars');
|
||||||
|
|
||||||
|
const Api = require('./api.js');
|
||||||
const LoginView = require('./views/login_view.js');
|
const LoginView = require('./views/login_view.js');
|
||||||
const RegistrationView = require('./views/registration_view.js');
|
const RegistrationView = require('./views/registration_view.js');
|
||||||
const TopNavigationView = require('./views/top_navigation_view.js');
|
const TopNavigationView = require('./views/top_navigation_view.js');
|
||||||
|
@ -24,11 +25,13 @@ const TagsController = require('./controllers/tags_controller.js');
|
||||||
// -------------------
|
// -------------------
|
||||||
// - resolve objects -
|
// - resolve objects -
|
||||||
// -------------------
|
// -------------------
|
||||||
|
const api = new Api();
|
||||||
|
|
||||||
const topNavigationView = new TopNavigationView(handlebars);
|
const topNavigationView = new TopNavigationView(handlebars);
|
||||||
const loginView = new LoginView(handlebars);
|
const loginView = new LoginView(handlebars);
|
||||||
const registrationView = new RegistrationView(handlebars);
|
const registrationView = new RegistrationView(handlebars);
|
||||||
|
|
||||||
const authController = new AuthController(null, loginView);
|
const authController = new AuthController(api, null, loginView);
|
||||||
const topNavigationController
|
const topNavigationController
|
||||||
= new TopNavigationController(topNavigationView, authController);
|
= new TopNavigationController(topNavigationView, authController);
|
||||||
// break cyclic dependency topNavigationView<->authController
|
// break cyclic dependency topNavigationView<->authController
|
||||||
|
@ -37,6 +40,7 @@ authController.topNavigationController = topNavigationController;
|
||||||
const homeController = new HomeController(topNavigationController);
|
const homeController = new HomeController(topNavigationController);
|
||||||
const postsController = new PostsController(topNavigationController);
|
const postsController = new PostsController(topNavigationController);
|
||||||
const usersController = new UsersController(
|
const usersController = new UsersController(
|
||||||
|
api,
|
||||||
topNavigationController,
|
topNavigationController,
|
||||||
authController,
|
authController,
|
||||||
registrationView);
|
registrationView);
|
||||||
|
@ -52,13 +56,13 @@ page('/', () => { homeController.indexRoute(); });
|
||||||
|
|
||||||
page('/upload', () => { postsController.uploadPostsRoute(); });
|
page('/upload', () => { postsController.uploadPostsRoute(); });
|
||||||
page('/posts', () => { postsController.listPostsRoute(); });
|
page('/posts', () => { postsController.listPostsRoute(); });
|
||||||
page('/post/:id', (id) => { postsController.showPostRoute(id); });
|
page('/post/:id', id => { postsController.showPostRoute(id); });
|
||||||
page('/post/:id/edit', (id) => { postsController.editPostRoute(id); });
|
page('/post/:id/edit', id => { postsController.editPostRoute(id); });
|
||||||
|
|
||||||
page('/register', () => { usersController.createUserRoute(); });
|
page('/register', () => { usersController.createUserRoute(); });
|
||||||
page('/users', () => { usersController.listUsersRoute(); });
|
page('/users', () => { usersController.listUsersRoute(); });
|
||||||
page('/user/:user', (user) => { usersController.showUserRoute(user); });
|
page('/user/:user', user => { usersController.showUserRoute(user); });
|
||||||
page('/user/:user/edit', (user) => { usersController.editUserRoute(user); });
|
page('/user/:user/edit', user => { usersController.editUserRoute(user); });
|
||||||
|
|
||||||
page('/history', () => { historyController.showHistoryRoute(); });
|
page('/history', () => { historyController.showHistoryRoute(); });
|
||||||
page('/tags', () => { tagsController.listTagsRoute(); });
|
page('/tags', () => { tagsController.listTagsRoute(); });
|
||||||
|
|
|
@ -19,14 +19,32 @@ class BaseView {
|
||||||
return this.handlebars.compile(templateText);
|
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) {
|
decorateValidator(form) {
|
||||||
// postpone showing form fields validity until user actually tries
|
// postpone showing form fields validity until user actually tries
|
||||||
// to submit it (seeing red/green form w/o doing anything breaks POLA)
|
// to submit it (seeing red/green form w/o doing anything breaks POLA)
|
||||||
const submitButton
|
const submitButton = form.querySelector('.buttons input');
|
||||||
= document.querySelector('#content-holder .buttons input');
|
submitButton.addEventListener('click', e => {
|
||||||
submitButton.addEventListener('click', (e) => {
|
|
||||||
form.classList.add('show-validation');
|
form.classList.add('show-validation');
|
||||||
});
|
});
|
||||||
|
form.addEventListener('submit', e => {
|
||||||
|
form.classList.remove('show-validation');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showView(html) {
|
showView(html) {
|
||||||
|
|
|
@ -11,17 +11,32 @@ class LoginView extends BaseView {
|
||||||
|
|
||||||
render(options) {
|
render(options) {
|
||||||
this.showView(this.template());
|
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);
|
this.decorateValidator(form);
|
||||||
|
|
||||||
const userNameField = document.getElementById('user-name');
|
const userNameField = document.getElementById('user-name');
|
||||||
const passwordField = document.getElementById('user-password');
|
const passwordField = document.getElementById('user-password');
|
||||||
|
const rememberUserField = document.getElementById('remember-user');
|
||||||
userNameField.setAttribute('pattern', config.service.userNameRegex);
|
userNameField.setAttribute('pattern', config.service.userNameRegex);
|
||||||
passwordField.setAttribute('pattern', config.service.passwordRegex);
|
passwordField.setAttribute('pattern', config.service.passwordRegex);
|
||||||
|
|
||||||
form.addEventListener('submit', (e) => {
|
form.addEventListener('submit', e => {
|
||||||
e.preventDefault();
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ class RegistrationView extends BaseView {
|
||||||
|
|
||||||
render(options) {
|
render(options) {
|
||||||
this.showView(this.template());
|
this.showView(this.template());
|
||||||
|
const messagesHolder = this.contentHolder.querySelector('.messages');
|
||||||
const form = document.querySelector('#content-holder form');
|
const form = document.querySelector('#content-holder form');
|
||||||
this.decorateValidator(form);
|
this.decorateValidator(form);
|
||||||
|
|
||||||
|
@ -20,14 +21,22 @@ class RegistrationView extends BaseView {
|
||||||
userNameField.setAttribute('pattern', config.service.userNameRegex);
|
userNameField.setAttribute('pattern', config.service.userNameRegex);
|
||||||
passwordField.setAttribute('pattern', config.service.passwordRegex);
|
passwordField.setAttribute('pattern', config.service.passwordRegex);
|
||||||
|
|
||||||
form.addEventListener('submit', (e) => {
|
form.addEventListener('submit', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const user = {
|
this.clearMessages(messagesHolder);
|
||||||
name: userNameField.value,
|
form.setAttribute('disabled', true);
|
||||||
password: passwordField.value,
|
options
|
||||||
email: emailField.value,
|
.register(
|
||||||
};
|
userNameField.value,
|
||||||
options.register(user);
|
passwordField.value,
|
||||||
|
emailField.value)
|
||||||
|
.then(() => {
|
||||||
|
form.setAttribute('disabled', false);
|
||||||
|
})
|
||||||
|
.catch(errorMessage => {
|
||||||
|
form.setAttribute('disabled', false);
|
||||||
|
this.showError(messagesHolder, errorMessage);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue