diff --git a/app/adapters/application.js b/app/adapters/application.js
new file mode 100644
index 0000000..3850d21
--- /dev/null
+++ b/app/adapters/application.js
@@ -0,0 +1,18 @@
+import JSONAPIAdapter from '@ember-data/adapter/json-api';
+import { computed } from '@ember/object';
+import { inject as service } from '@ember/service';
+
+export default class ApplicationAdapter extends JSONAPIAdapter {
+ @service session;
+
+ @computed('session.{data.authenticated.access_token,isAuthenticated}')
+ get headers() {
+ let headers = {};
+ if (this.session.isAuthenticated) {
+ // OAuth 2
+ headers['Authorization'] = `Bearer ${this.session.data.authenticated.access_token}`;
+ }
+
+ return headers;
+ }
+}
diff --git a/app/authenticators/jwt.js b/app/authenticators/jwt.js
new file mode 100644
index 0000000..c0bd0c2
--- /dev/null
+++ b/app/authenticators/jwt.js
@@ -0,0 +1,282 @@
+import RSVP from 'rsvp';
+import fetch from 'fetch';
+import {
+ run, later, cancel
+} from '@ember/runloop';
+import {
+ isEmpty
+} from '@ember/utils';
+import {
+ warn
+} from '@ember/debug';
+import BaseAuthenticator from 'ember-simple-auth/authenticators/base';
+
+export default BaseAuthenticator.extend({
+ /**
+ The endpoint on the server that the authentication request is sent to.
+ @property serverTokenEndpoint
+ @type String
+ @default '/login'
+ @public
+ */
+ serverTokenEndpoint: '/login',
+
+ /**
+ The endpoint on the server that the refresh request is sent to.
+ @property serverRefreshTokenEndpoint
+ @type String
+ @default '/api/token-refresh'
+ @public
+ */
+ serverRefreshTokenEndpoint: '/jwt-refresh',
+
+ /**
+ The identification attribute name. __This will be used in the request.__
+ @property identificationAttributeName
+ @type String
+ @default 'username'
+ @public
+ */
+ identificationAttributeName: 'email',
+
+ /**
+ The password attribute name. __This will be used in the request.__
+ @property passwordAttributeName
+ @type String
+ @default 'password'
+ @public
+ */
+ passwordAttributeName: 'password',
+
+ /**
+ Time (ms) before the JWT expires to call the serverRefreshTokenEndpoint
+ @property refreshTokenOffset
+ @type Integer
+ @default '1000'
+ @public
+ */
+ refreshTokenOffset: 1000,
+
+ /**
+ Time (ms) after a call to serverRefreshTokenEndpoint during which no
+ further refresh token calls will be made.
+
+ Used to reduce the number of refresh token calls made when the same
+ app is simultaneously open in multiple tabs/windows.
+
+ For example: if the JWT is set to expire 30s after being issued, and the
+ 'refreshTokenAfter' is set at 25s, requests may only be sent out in the
+ last 5 seconds.
+
+ @property refreshTokenAfter
+ @type Integer
+ @default '25000'
+ @public
+ */
+ refreshTokenAfter: 25000,
+
+ _refreshTokenTimeout: null,
+
+ /**
+ Restores the session from a session data object; __will return a resolving
+ promise when there is a non-empty `access_token` in the session data__ and
+ a rejecting promise otherwise.
+ @method restore
+ @param {Object} data The data to restore the session from
+ @return {Ember.RSVP.Promise} A promise that when it resolves results in the session becoming or remaining authenticated
+ @public
+ */
+ restore(data) {
+ if (this._refreshTokenTimeout) {
+ run.cancel(this._refreshTokenTimeout);
+ delete this._refreshTokenTimeout;
+ }
+ return this._refreshAccessToken(data);
+ },
+
+ /**
+ Authenticates the session with the specified `identification` & `password`.
+ Issues a `POST` request to the serverTokenEndpoint and receives the JWT token in response.
+
+ If the credentials are valid and thus authentication succeeds, a promise that resolves with the
+ server's response is returned, otherwise a promise that rejects with the error as returned by
+ the server is returned.
+
+ This method also schedules refresh requests for the access token before it expires.
+ TODO: make the refresh token support optional
+ @method authenticate
+ @param {String} identification The resource owner username
+ @param {String} password The resource owner password
+ @return {Ember.RSVP.Promise} A promise that when it resolves results in the session becoming authenticated
+ @public
+ */
+ authenticate(identification, password) {
+ return new RSVP.Promise((resolve, reject) => {
+ const {
+ identificationAttributeName,
+ passwordAttributeName
+ } = this.getProperties('identificationAttributeName', 'passwordAttributeName');
+ const data = {
+ [identificationAttributeName]: identification,
+ [passwordAttributeName]: password
+ };
+ const serverTokenEndpoint = this.get('serverTokenEndpoint');
+
+ this.makeRequest(serverTokenEndpoint, data)
+ .then((response) => {
+ return this._validateTokenAndScheduleRefresh(response);
+ })
+ .then((response) => {
+ run(() => {
+ resolve(response);
+ });
+ })
+ .catch((reason) => {
+ if (reason.responseJSON) {
+ reason = reason.responseJSON;
+ }
+ run(null, reject, reason);
+ });
+ });
+ },
+
+ /**
+ Deletes the JWT token
+ @method invalidate
+ @return {Ember.RSVP.Promise} A promise that when it resolves results in the session being invalidated
+ @public
+ */
+ invalidate() {
+ run.cancel(this._refreshTokenTimeout);
+ delete this._refreshTokenTimeout;
+ return RSVP.Promise.resolve();
+ },
+
+ /**
+ Makes a request to the JWT server.
+ @method makeRequest
+ @param {String} url The request URL
+ @param {Object} data The request data
+ @param {Object} headers Additional headers to send in request
+ @return {Promise} A promise that resolves with the response object
+ @protected
+ */
+ makeRequest(url, data, headers = {}) {
+ headers['Content-Type'] = 'application/json';
+
+ const options = {
+ body: JSON.stringify(data),
+ headers,
+ method: 'POST'
+ };
+
+ return new RSVP.Promise((resolve, reject) => {
+ fetch(url, options).then((response) => {
+ response.text().then((text) => {
+ let json = text ? JSON.parse(text) : {};
+ if (!response.ok) {
+ response.responseJSON = json;
+ reject(response);
+ } else {
+ window.localStorage.setItem('jwtLastRefreshAt', Date.now());
+ resolve(json);
+ }
+ });
+ }).catch(reject);
+ });
+ },
+
+ /**
+ Validate that the response contains a valid JWT token
+ */
+ _validate(data) {
+ // Validate that a token is present
+ if (isEmpty(data['access_token'])) {
+ return false;
+ }
+
+ let jwtToken = data['access_token'].split('.');
+
+ // Validate the three elements of a JWT are present
+ if (jwtToken.length !== 3) {
+ return false;
+ }
+
+ // Validate the JWT header
+ let jwtHeader = JSON.parse(atob(jwtToken[0]));
+ if (!jwtHeader.alg) {
+ return false;
+ }
+
+ // Validate the JWT payload:
+ // iat: issued at time
+ // exp: expiration time
+ let jwtPayload = JSON.parse(atob(jwtToken[1]));
+ if (isNaN(jwtPayload['iat']) || isNaN(jwtPayload['exp'])) {
+ return false;
+ }
+
+ return true;
+ },
+
+ _scheduleTokenRefresh(data) {
+ const jwtPayload = JSON.parse(atob(data['access_token'].split('.')[1]));
+ const jwtPayloadExpiresAt = jwtPayload.exp;
+
+ const offset = 1000; // Refresh 1 sec before JWT expires
+ const now = Date.now();
+ const waitMs = (jwtPayloadExpiresAt * 1000) - now - offset; //expiresAt is in sec
+
+ if (this._refreshTokenTimeout) {
+ cancel(this._refreshTokenTimeout);
+ delete this._refreshTokenTimeout;
+ }
+
+ // Reschedule if the JWT is still valid
+ if (waitMs > 0) {
+ this._refreshTokenTimeout = later(this, this._refreshAccessToken, data, waitMs);
+ }
+ },
+
+ _refreshAccessToken(data) {
+ var timeElapsedSinceLastRefresh = Date.now() - window.localStorage.getItem('jwtLastRefreshAt')
+ if (timeElapsedSinceLastRefresh <= this.get('refreshTokenAfter')) {
+ // Request attempted too soon! Reschedule
+ return this._validateTokenAndScheduleRefresh(data);
+ }
+
+ const serverRefreshTokenEndpoint = this.get('serverRefreshTokenEndpoint');
+
+ return new RSVP.Promise((resolve, reject) => {
+ this.makeRequest(serverRefreshTokenEndpoint, data)
+ .then((response) => {
+ return this._validateTokenAndScheduleRefresh(response);
+ })
+ .then((response) => {
+ run(() => {
+ this.trigger('sessionDataUpdated', response);
+ resolve(response);
+ });
+ })
+ .catch((reason) => {
+ if (reason.responseJSON) {
+ reason = JSON.stringify(reason.responseJSON);
+ }
+ warn(`JWT token could not be refreshed: ${reason}.`, false, {
+ id: 'ember-simple-auth-jwt.failedJWTTokenRefresh'
+ });
+
+ reject();
+ });
+ });
+ },
+
+ _validateTokenAndScheduleRefresh(response) {
+ if (!this._validate(response)) {
+ return RSVP.Promise.reject('token is missing or invalid in server response');
+ }
+
+ this._scheduleTokenRefresh(response);
+ return RSVP.Promise.resolve(response);
+ }
+});
diff --git a/app/controllers/application.js b/app/controllers/application.js
new file mode 100644
index 0000000..6be8ccd
--- /dev/null
+++ b/app/controllers/application.js
@@ -0,0 +1,12 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+import { action } from "@ember/object";
+
+export default class ApplicationController extends Controller {
+ @service session;
+
+ @action
+ invalidateSession() {
+ this.session.invalidate();
+ }
+}
diff --git a/app/controllers/login.js b/app/controllers/login.js
new file mode 100644
index 0000000..137ad4b
--- /dev/null
+++ b/app/controllers/login.js
@@ -0,0 +1,34 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+import { action } from "@ember/object";
+import { tracked } from "@glimmer/tracking";
+
+export default class LoginController extends Controller {
+ @tracked errorMessage;
+ @service session;
+
+ @action
+ async authenticate(e) {
+ e.preventDefault();
+ let { identification, password } = this;
+ try {
+ await this.session.authenticate('authenticator:jwt', identification, password);
+ } catch(error) {
+ this.errorMessage = error.error || error;
+ }
+
+ if (this.session.isAuthenticated) {
+ // What to do with all this success?
+ }
+ }
+
+ @action
+ updateIdentification(e) {
+ this.identification = e.target.value;
+ }
+
+ @action
+ updatePassword(e) {
+ this.password = e.target.value;
+ }
+}
diff --git a/app/router.js b/app/router.js
index 752b634..95d5333 100644
--- a/app/router.js
+++ b/app/router.js
@@ -6,4 +6,6 @@ export default class Router extends EmberRouter {
rootURL = config.rootURL;
}
-Router.map(function () {});
+Router.map(function() {
+ this.route('login');
+});
diff --git a/app/routes/authenticated.js b/app/routes/authenticated.js
new file mode 100644
index 0000000..5dcc2e0
--- /dev/null
+++ b/app/routes/authenticated.js
@@ -0,0 +1,10 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+export default class AuthenticatedRoute extends Route {
+ @service session;
+
+ beforeModel(transition) {
+ this.session.requireAuthentication(transition, 'login');
+ }
+}
diff --git a/app/services/session.js b/app/services/session.js
new file mode 100644
index 0000000..3da3895
--- /dev/null
+++ b/app/services/session.js
@@ -0,0 +1,3 @@
+import Service from 'ember-simple-auth/services/session';
+
+export default class SessionService extends Service {}
diff --git a/app/session-stores/application.js b/app/session-stores/application.js
new file mode 100644
index 0000000..5778059
--- /dev/null
+++ b/app/session-stores/application.js
@@ -0,0 +1,3 @@
+import AdaptiveStore from 'ember-simple-auth/session-stores/adaptive';
+
+export default class SessionStore extends AdaptiveStore {}
diff --git a/app/templates/application.hbs b/app/templates/application.hbs
index 3d850a6..210ca43 100644
--- a/app/templates/application.hbs
+++ b/app/templates/application.hbs
@@ -1,7 +1,12 @@
{{page-title "Holygamesapp"}}
-{{outlet}}
-
-{{! The following component displays Ember's default welcome message. }}
-