This page shows tutorials available.
This is the multi-page printable view of this section. Click here to print.
Tutorials
- 1: Configuration
- 2: Authentication
- 3: The Console
- 4: Implementing Events
- 5: The Model
- 6: Remote Resources
- 7: The URL Router
1 - Configuration
alt-seven configuration options tutorial
You configure your alt-seven application by passing an options structure to the a7.init method. There are a host of options that you can tailor to your needs. Below is an example call to a7.init() with all possible options specified. The default value for a given option is bracketed like so [templateLiterals],
var remoteModules = {}; // format specified in Remote section
var options = {
model: ["altseven"], // defaults to the internal model. Detail in the Model tutorial
auth: {
sessionTimeout: 15 * 60 * 1000 // 15 minutes. The built-in remote function calls the refresh URL at this interval
},
console: {
enabled: true,
wsServer: "ws://example.com/ws"|"", // server-side websocket logger, specified in Console tutorial
container: ["gadgetui.display.FloatingPane"|""],//if enabled = false, defaults to ""
top: [100],
left: [500],
width: [500],
height: [300]
},
logging: {
logLevel: ["ERROR,FATAL,INFO"], // all option: "DEBUG,INFO,WARN,FATAL,TRACE"
toBrowserConsole: true|[false] // passes the log message to the console
},
remote: {
loginURL: "/login", // default: "", remote method should return a json object { success: true|false, user: Object }.
logoutURL: "/logout", // default: "", remote method should return a json object { success: true|false }
refreshURL: "/refresh", // default: "", remote method should return a json object { success: true|false }
useTokens: [true]|false // useTokens looks for token, format specified in the authentication section
modules: {} // format specified in Remote section
},
router: {
useEvents: [true]|false, // option calls events when a route is matched. Specified in the Routing tutorial
routes: [
['/auth/showlogin', 'auth.showLogin'] // routes is an array of paths to match. More details in the Routing tutorial
]
},
ui: {
renderer: "Handlebars|Mustache|[templateLiterals]", // templating engine. templateLiterals is the internal engine
debounceTime: 20, // time in ms before a render request of an object can be queued
timeout: 3600000 // 1 hour , time before a cached view expires
}
};
a7.init(options);
See the app.js file in the LacesIDE project for a complete way to implement the a7.init(options) method in the context of an application.
2 - Authentication
alt-seven authentication tutorial
Configuration
In order to enable built-in authentication, you must configure your application with the correct options. Here is a sample of the authentication options to be passed to the framework:
remote: {
modules: app.remote,
loginURL: "/api/auth/login",
logoutURL: "/api/auth/logout",
refreshURL: "api/auth/refresh"
},
In addition to the authentication methods, the a7 model must be enabled for authentication to work. See the page on configuring your application to understand all the options available to you.
Built-in Events and Remote Methods
The event system contains a set of pre-defined events and corresponding remote methods for the built-in authentication system. For login, you need to pass a params object to the event in this format { username: "", password: "", success: ""|func, failure: ""|func }
.
a7.events.subscribe("auth.login", function(params) {
a7.remote.invoke("auth.login", params);
});
a7.events.subscribe("auth.logout", function(params) {
a7.remote.invoke("auth.logout", params);
});
a7.events.subscribe("auth.refresh", function(params) {
a7.remote.invoke("auth.refresh", params);
});
a7.events.subscribe("auth.sessionTimeout", function() {
a7.security.invalidateSession();
});
a7.events.subscribe("auth.invalidateSession", function() {
a7.security.invalidateSession();
});
Calling the login event
In the login form, the programmer can then specify success and failure parameters, which can be methods, event names, or router destinations. In the below example, you send the username and password to the auth.login event and specify success and failure methods to run after the authentication method returns from the server.
a7.events.publish('auth.login', {
username: loginform.state.username,
password: loginform.state.password,
success: function( json ){
main.runApp();
},
failure: function( json ){
state.message = json.message;
state.username = "";
state.password = "";
loginform.setState( state );
loginform.fireEvent("mustRender");
}
});
Options for success and failure params
Success and failure could also be specified as events, such as:
success: "auth.loginSuccess",
failure: "auth.loginFailure"
or as router destinations:
success: "auth/loginSuccess",
failure: "auth/loginFailure"
The programmer must then create the events or router destinations named:
a7.events.subscribe( "auth.loginSuccess", function( obj ){
// handle login success tasks
});
You can see more details on working with the router in the routing tutorial.
Creating remote authentication methods
You can build remote methods for authentication on the platform of your choice. The remote methods must satisfy these contracts:
The User object
Alt-seven has a built-in User object that it employs for its authentication system. The User object by default is an empty object that is populated with the keys returned in the login response from the remote method. In order to facilitate authentication checking, you can specify a set of default keys, such as userID, to populate the anonymous User object when a system user is not authenticated. When the user is authenticated, the login response sets the user into the a7 model and into sessionStorage.user. The logout response clears the existing user and sets an anonymous user into the model and sessionStorage.user.
// the User object may be extended in the future with events you can listen to, so this would be the preferred method for creating a user object
let user = a7.components.Constructor(
a7.components.User,
[{ userID: '' }],
true // add events
)
// or simply
let user = a7.components.User( { userID: '' } ); // no events will be added to the user object
login
Input arguments: String username String password
Return:
Note that the login remote method uses Basic Authentication, sending the username and password as Base64-encoded strings in the method header. The server authentication method must check the Authorization header to get the username and password. See the code from a7.remote below:
login: function( params ){
a7.log.trace( "remote call: auth.login" );
var request,
args = { method: 'POST',
headers: {
"Authorization": "Basic " + a7.util.base64.encode64( params.username + ":" + params.password )
}
};
request = new Request( _options.loginURL , args );
var promise = fetch( request );
// continues ...
The returned user object, in JSON format, will be parsed by the response in the remote method, the keys from the user object will be added to the built-in alt-seven User object, and the user object will be set in the model as so a7.model.set( "user", user )
.
If you are using tokens for the remote methods, the response will look for the ‘X-Token’ response header and set the returned token into sessionStorage.token. This token will be used to keep the user’s session alive using the refresh() remote method.
Let’s look at the remote authentication method in the LacesIDE open source application. It provides a good example of how to work with the auth meachanism in NodeJS. Other languages and platforms will vary. The controller methods for login, logout, and refresh are listed below. They contain the heart of the functionality you need to understand. You can look through the LacesIDE source code if you want to see the full details of the implementation. Start with routes/api.auth.js
and go from there.
The remote method extracts the username and password from the Authorization header, Base64 decodes the authorization string, then splits it into the username and password components. In this example, userservice.getByUsername(username) retrieves the user record from the database, the password is hashed with crypto.pbkdf2Sync, and the hash is compared to the password hash stored in the databaase. Note that the hash and salt are removed from the user record for security before the user record is returned to the client. LacesIDE uses tokens, so the X-Token is set with the value of a generated token, and the response is encoded and returned.
login: function (request, response) {
let authorization = request.header("Authorization") || "";
let username = "";
let password = "";
if (authorization.length) {
let authArray = authorization.split(" ");
authorization = Base64.decode(authArray[1]);
username = authorization.split(":")[0];
password = authorization.split(":")[1];
}
userservice.getByUsername(username)
.then(function (results) {
let valid = false;
if (results) {
let hash = crypto.pbkdf2Sync(password, results.salt, 1000, 64, `sha512`).toString(`hex`);
valid = ( hash === results.hash );
}
if (valid) {
let user = results;
// remove the hash and salt from the token so we don't send it outside the system
delete user.hash;
delete user.salt;
response.setHeader("X-Token", utils.generateToken(user));
response.send({ user: user, success: true });
} else {
throw ({ success: false, error: "Invalid username/password combination." });
}
})
.catch(function (error) {
console.log(error);
response.send(JSON.stringify(error));
});
},
logout: function (request, response) {
response.send({ success: true });
},
refresh: function (request, response) {
//let token = request.header( "X-Token" );
response.send({ success: true });
}
Session Tokens
Alt-seven uses the session tokens to authenticate a user during the logged in session. Note that the timeout of the server side session should match the timeout of the timeout value in the alt-seven application so the session stays active as long as the user is logged in and the browser window stays open. This is the default behavior of the application.
While the user is authenticated, alt-seven will automatically include the X-Token header in every remote request, and will pass the current token for the user in that header. The token should be in string format in order for the system to pass it back and forth in the X-Token header. Beyond that, the format and contents of the token are left to the programmer. Below are the generateToken and checkAuthToken methods from the LacesIDE application. In this case, you can see that the token is the user object with a key expires
added to it. The expires
key is used to check the expiration of the session.
generateToken: function( user ){
const hash = new SHA3(512);
let authtoken = Object.assign( {}, user );
let now = new Date();
authtoken.expires = new Date( new Date( now ).getTime() + securityConfig.ttl );
console.log( "current date " + new Date( now ) );
console.log( "expires " + authtoken.expires );
let base64Token = Base64.encode( JSON.stringify( authtoken ) );
//let base64Token = Base64.encode( authtoken );
hash.update( base64Token );
let myhash = hash.digest( 'hex' );
console.log( myhash );
return JSON.stringify( { token: base64Token, hash: myhash } );
},
checkAuthToken: function( authtoken, timeout ){
const hash = new SHA3(512);
let auth;
authtoken = JSON.parse( authtoken );
let token = authtoken.token;
let hashCode = authtoken.hash;
hash.update( token );
let myhash = hash.digest( 'hex' );
if( hashCode == myhash ){
// token is valid
auth = JSON.parse( Base64.decode( token ) );
}
return auth;
},
LacesIDE uses the Express framework, which enables the user of interceptors to check HTTP inbound requests before they are routed to the rest of the application. The application combines an httpinterceptor with route-based security to determine wheter a user is allowed to access a given URL path in the application. Below is the checkHTTPAuth method from the interceptor, used to authenticate the user with each request. If you use the alt-seven authentication system with tokens, how you implement session management on the server is up to you. This code from LacesIDE provides one method of implementing session management in NodeJS.
checkHTTPAuth: function (request, response, next) {
// is it an open route?
let openRoute = securityConfig.routes.open.find(function (route) {
//console.log( "route: " + route );
return request.url.match(route);
});
// is it a secured route?
let securedRoute = securityConfig.routes.secured.find(function (route) {
//console.log( "route: " + route );
return request.url.match(route);
});
console.log("Checking auth");
// check token
let token = request.headers["x-token"];
// anonymous access
if (token === undefined || token.length === 0) {
// not a logged in user
if (openRoute !== undefined) {
// if this is an open route, pass them along
next();
} else if (securedRoute !== undefined) {
notAuthorized(request, response);
} else {
// route not found
console.log("WARN: Route not found for URL:" + request.url);
console.log("Pass through allowed.");
next();
}
} else {
let auth = utils.checkAuthToken(token, securityConfig.ttl);
userservice.read(auth.userID)
.then(function (results) {
let valid = false;
let user = results;
let now = new Date();
console.log("userID: " + user.userID);
let expires = new Date(auth.expires).getTime() ;
let nowtime = now.getTime();
console.log( "sessiontime: " + expires - nowtime );
// if there is a valid logged in user, pass them
if (user.userID.length > 0 && (new Date(auth.expires).getTime() - now.getTime() > 0)) {
valid = true;
// remove the password hash from the token so we don't send it outside the system
delete user.hash;
delete user.salt;
// this call sets a user into the request
request.user = user;
// set a new header token
response.setHeader("X-Token", utils.generateToken(user));
console.log("X-Token set");
// move on to the route handlers
//next();
} else if (user.userID.length > 0) {
console.log("Expires: " + new Date(auth.expires).getTime());
console.log("Now: " + now.getTime());
console.log("Authorization expired.");
response.messages = "Authorization expired.";
}
if (securedRoute !== undefined) {
console.log(request.url + " matches " + securedRoute);
console.log("User validated? " + valid);
if (valid === true) {
// securedRoute and logged in user, forward them along
next();
} else {
notAuthorized(request, response);
}
} else {
if (openRoute === undefined) {
console.log("WARN: Route not found for URL:" + request.url);
console.log("Pass through allowed.");
}
// open routes pass through
next();
}
})
.catch(function (error) {
console.log(error);
response.status(500);
response.send("Error");
});
}
}
Logout
Logout, like login, is handled automatically by the alt-seven framework. Simply call the auth.login event and pass the success and failure arguments, which, like the login arguments, can be functions, events, or router paths.
a7.events.publish('auth.logout', {
success: "/auth/logoutsuccess",
failure: "/auth/logoutfailure"
});
Note that the auth.logout method sets the Authorization header with the username and password from the event arguments, like the login method, but these are optional arguments. You can include those arguments if you have the username and password cached on the client and you want to use it on the server side.
a7.events.publish('auth.logout', {
username: username,
password: password,
success: "/auth/logoutsuccess",
failure: "/auth/logoutfailure"
});
3 - The Console
The alt-seven console shows all of the logging information sent to a7.log in a scrolling window. Normally, this option is enabled and placed in a floating window to give the programmer visibility into what the framework is doing without having to open the browser console. The console also has the option of receiving remote logging messages from a websocket server. This feature is meant to enable the programmer to send log events from the server so the programmer can see them in real time without having to review log events on the server. This feature can be very valuable in situations where it is unclear what is happening on the server otherwise.
Configuration
There is only minimal configuration required for the console. You can specify size and location on the screen, or you can leave the defaults. For more detail, look at the Configuration tutorial.
In the below example, floatingpane is the FloatingPane component of the gadget-ui open source library. For the moment, only gadget-ui components have been tested with the console.
import {floatingpane} from '/node_modules/gadget-ui/dist/gadget-ui.es.js';
console: {
enabled: true,
container: floatingpane
},
This feature is currently under revision and documentation will be updated when revisions are complete.
4 - Implementing Events
Events are a core concept in writing alt-seven-based applications. While it is not strictly necessary to use events at all, alt-seven is meant to be an event-driven framework,and using the publish/subcribe event system is part of that intention.
Configuration
In order to use events, you need to include them in the application startup so they are available. In LacesIDE, you can see the includes of the event modules in the app.js
entry point of the application.
import {events} from '/js/app.events.js';
import {appLibraryEvents} from '/js/event/applibraries.js';
import {appEvents} from '/js/event/apps.js';
import {authEvents} from '/js/event/auth.js';
import {libraryEvents} from '/js/event/libraries.js';
import {mainEvents} from '/js/event/main.js';
import {menuEvents} from '/js/event/menu.js';
import {profileEvents} from '/js/event/profile.js';
import {sandboxEvents} from '/js/event/sandbox.js';
import {userEvents} from '/js/event/user.js';
//initialize pub/sub events
events();
appLibraryEvents();
appEvents();
authEvents();
libraryEvents();
mainEvents();
menuEvents();
profileEvents();
sandboxEvents();
userEvents();
Note that the events are not passed into the a7.init() function, they are simply included here for availability when the application starts.
Modules
Events are typically organized into modules of related events for convenience. Below is a section of the profile.js
event module from LacesIDE.
import { a7 } from '/lib/altseven/dist/a7.js';
import { ui } from '/js/app.ui.js';
//import {main} from '/js/app.main.js';
import * as utils from '/js/app.utils.js';
export var profileEvents = function init() {
a7.events.subscribe("profile.update", function (obj) {
a7.remote.invoke("profile.update", obj)
.then(function (response) {
// get json response and pass to handler to resolve
return response.json();
})
.then(function (json) {
if (json.success) {
utils.showNotice("Profile saved.", "#pTab1Notice");
a7.events.publish("profile.refreshProfile");
}else{
utils.showNotice("Profile not saved.", "#pTab1Notice");
}
});
});
};
In this case, the profile.update event calls the remote method of the same name and updates the picture for the user’s profile. You can see in the profile.js view how that call is implemented. Below is a fragment of that code that shows how to publish an event. You call a7.events.publish( eventname, args )
where args is an object that contains the arguments you want to pass to the event subscription.
updatePic: function (file) {
let user = a7.model.get("user");
user.profilePic = file.path + file.filename;
a7.events.publish("profile.update", user);
},
While many of the event subscriptions in the LacesIDE application work with remote methods, it is not necessary to call remote methods in the event. You can look at the sandbox.execute
subscription in the sandbox.js
module for an example of why you might want to use events aside from interacting with remote methods.
The event system contains a set of pre-defined events for the built-in authentication system. You can see more details about these events in the Authentication tutorial.
a7.events.subscribe("auth.login", function(params) {
a7.remote.invoke("auth.login", params);
});
a7.events.subscribe("auth.logout", function(params) {
a7.remote.invoke("auth.logout", params);
});
a7.events.subscribe("auth.refresh", function(params) {
a7.remote.invoke("auth.refresh", params);
});
a7.events.subscribe("auth.sessionTimeout", function() {
a7.security.invalidateSession();
});
a7.events.subscribe("auth.invalidateSession", function() {
a7.security.invalidateSession();
});
5 - The Model
What is the Model?
In alt-seven, the model refers to two separate things. First there is the a7.model component that interacts directly wit alt-seven based applications. Second, there is a model implementation, which could be any model object that exposes the required methods (listed below from a7.model) necessary to meet the contract for what alt-seven expects in a model. When the framework was built, it used its own internal model, a7.Components.Model, as the implementation of the model. It also allowed the use of the model from the open source gadget-ui library, but any model that meets the contract for the methods below should work. Programmers can choose to implement their own model if it serves their needs.
The alt-seven model includes a variety of experimental features that are not currently exposed but may be included in future releases of the framework.
destroy : function(){
return _methods[ "destroy" ].apply( _model, arguments );
},
get : function(){
return _methods[ "get" ].apply( _model, arguments );
},
set : function(){
return _methods[ "set" ].apply( _model, arguments );
},
exists : function(){
return _methods[ "exists" ].apply( _model, arguments );
},
Working with the Model
As it stands today, the four methods listed above are the only methods exposed in a7.model, so implementation is very simple.
// get the current user from the model
let user = a7.model.get("user")
// set the current user into the model
a7.model.set( "user", user )
// check if a key exists in the model
let userExists = a7.model.exists("user")
// delete the key from the model
a7.model.delete("user")
6 - Remote Resources
Alt-seven has a remote module that facilitates working with remote resources on your server. First, you need to specify where your remote methods live, in the application config. Below is a section of the application confif in app.js
in the LacesIDE application. app.remote
is specified as a object containing a key for each remote module.
var app = {
main: main,
auth: auth,
remote: {
apps: apps,
applibraries: applibraries,
libraries: libraries,
profile: profile,
user: user
},
ui: ui,
utils: utils
};
The object is then used in the options for the application init, like so, also from LacesIDE:
remote: {
modules: app.remote,
loginURL: '/api/auth/login',
logoutURL: '/api/auth/logout',
refreshURL: '/api/auth/refresh',
useTokens: true, // defaults to true for the auth system
},
See the Configuration tutorial for details on the options here. Note that useTokens defaults to true, as it enables session management through the built-in alt-seven authentication system. See the Authentication tutorial for more information there.
Remote methods are typically structured as modules for related methods. Again, drawing from LacesIDE, below you can see the profile module:
import { a7 } from '/lib/altseven/dist/a7.js';
export { update };
var update = function (obj) {
var request;
var params = {
method: 'PUT',
headers: {
'Accept': 'application/json, application/xml, text/play, text/html, *.*',
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
profilePic: obj.profilePic
})
};
return a7.remote.fetch("/api/user/" + obj.userID + "/profile", params, true);
};
The remote methods in LacesIDE generally work in conjunction with the events system. In this case, the remote methods themsleves tend to be very small, containing only the information needed to make the remote call, and a fetch call to the remote resource that is returned to the method caller. As you can see here, using the built-in authentication system simplifies the remote calls by hiding everything associated with authentication and session management. It is all handled seamelessly in the background. If you want to learn more about the details of the authentication system, check the Authentication tutoral.
When remote resources are used in conjuncion with the events system, usually the heavy lifting is done in the calling event.
a7.events.subscribe("profile.update", function (obj) {
a7.remote.invoke("profile.update", obj)
.then(function (response) {
// get json response and pass to handler to resolve
return response.json();
})
.then(function (json) {
if (json.success) {
utils.showNotice("Profile saved.", "#pTab1Notice");
a7.events.publish("profile.refreshProfile");
}else{
utils.showNotice("Profile not saved.", "#pTab1Notice");
}
});
});
In this example, the profile.update event calls the remote resource, then handles the response. While it is not necessary to use events like this to call remote resources, this is the standard program flow in alt-seven.
7 - The URL Router
The purpose of routes is to give the user a URL in the browser’s location bar.
Routes mapped in an application using events look like this:
export var routes = [
[ '/auth/loginSuccess', 'auth.loginSuccess' ],
['/u/:username/profile', 'profile.show'],
[ '/', 'main.home' ]
];
Routes mapped in an application using functions without events look like this:
export var routes = [
['/auth/loginSuccess', loginSuccess],
['/u/:username/profile', showUserProfile],
['/', home]
];
Using router.open()
// Define a handler function for the /u/:username/profile route
function showUserProfile(params) {
console.log('Showing user profile for:', params.username);
}
// Navigate to the /u/johndoe/profile route
a7Router.open('/u/johndoe/profile', { username: 'johndoe' });
In this example, we first define a handler function for the /u/:username/profile
route. Then, we add this route and parameters to router.open()
, which updates the browser’s URL and calls the appropriate handler.
Using router.match()
The router.match()
method allows you to handle route changes dynamically without updating the browser’s URL. This is useful for handling hashchange events or external navigation:
// Function to handle route changes
function handleRouteChange() {
let path = window.location.pathname + window.location.search;
a7Router.match(path);
}
// Bind the handleRouteChange function to hashchange events
window.addEventListener('hashchange', handleRouteChange);
// Simulate navigation to /u/johndoe/profile using hashchange event
window.location.hash = '/u/johndoe/profile';
In this example, we first define a handler function for the /u/:username/profile
route and add it. Then, we create a handleRouteChange()
function that uses router.match()
to handle changes in the URL path.
By using these methods, you can dynamically navigate within your application and handle route changes programmatically or through external events.