Skip to content

Commit 7d7803d

Browse files
authored
Merge pull request #19 from richmolj/master
Optionally add JWT to localStorage
2 parents 07ad792 + e4a484b commit 7d7803d

File tree

6 files changed

+152
-9
lines changed

6 files changed

+152
-9
lines changed

src/configuration.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,23 @@ export default class Config {
1515
static models: Array<typeof Model> = [];
1616
static typeMapping: Object = {};
1717
static logger: Logger = new Logger();
18+
static jwtLocalStorage: string | false = 'jwt';
19+
static localStorage;
1820

1921
static setup(options? : Object) : void {
2022
if (!options) options = {};
2123

24+
this.jwtLocalStorage = options['jwtLocalStorage'];
25+
2226
for (let model of this.models) {
2327
this.typeMapping[model.jsonapiType] = model;
2428

2529
if (options['jwtOwners'] && options['jwtOwners'].indexOf(model) !== -1) {
2630
model.isJWTOwner = true;
31+
32+
if (this.jwtLocalStorage) {
33+
model.jwt = this.localStorage.getItem(this.jwtLocalStorage);
34+
}
2735
}
2836
}
2937

@@ -52,3 +60,10 @@ export default class Config {
5260
}
5361
}
5462
}
63+
64+
// In node, no localStorage available
65+
// We do this so we can mock it
66+
try {
67+
Config.localStorage = localStorage
68+
} catch(e) {
69+
}

src/model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import WritePayload from './util/write-payload';
1111
import IncludeDirective from './util/include-directive';
1212
import DirtyChecker from './util/dirty-check';
1313
import ValidationErrors from './util/validation-errors';
14+
import refreshJWT from './util/refresh-jwt';
1415
import relationshipIdentifiersFor from './util/relationship-identifiers';
1516
import Request from './request';
1617
import * as _cloneDeep from './util/clonedeep';
@@ -321,6 +322,8 @@ export default class Model {
321322
}
322323

323324
private _handleResponse(response: any, resolve: Function, reject: Function, callback: Function) : void {
325+
refreshJWT(this.klass, response);
326+
324327
if (response.status == 422) {
325328
ValidationErrors.apply(this, response['jsonPayload']);
326329
resolve(false);

src/scope.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import IncludeDirective from './util/include-directive';
55
import { CollectionProxy, RecordProxy } from './proxies';
66
import Request from './request';
77
import colorize from './util/colorize';
8+
import refreshJWT from './util/refresh-jwt';
89
import * as _cloneDeep from './util/clonedeep';
910
let cloneDeep: any = (<any>_cloneDeep).default || _cloneDeep;
1011
cloneDeep = cloneDeep.default || cloneDeep;
@@ -222,10 +223,7 @@ export default class Scope {
222223
let fetchOpts = this.model.fetchOptions()
223224

224225
return request.get(url, fetchOpts).then((response) => {
225-
let jwtHeader = response.headers.get('X-JWT');
226-
if (jwtHeader) {
227-
this.model.setJWT(jwtHeader);
228-
}
226+
refreshJWT(this.model, response);
229227
return response['jsonPayload'];
230228
});
231229
}

src/util/refresh-jwt.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Model from '../model';
2+
import Config from '../configuration';
3+
4+
export default function refreshJWT(klass: typeof Model, serverResponse: Response) : void {
5+
let jwt = serverResponse.headers.get('X-JWT');
6+
let localStorage = Config.localStorage;
7+
8+
if (localStorage) {
9+
let localStorageKey = Config.jwtLocalStorage;
10+
if (localStorageKey) {
11+
localStorage['setItem'](localStorageKey, jwt);
12+
}
13+
}
14+
15+
if (jwt) {
16+
klass.setJWT(jwt);
17+
}
18+
}

test/fixtures.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,17 @@ const TestJWTSubclass = ApplicationRecord.extend({
7878
const NonJWTOwner = Model.extend({
7979
});
8080

81-
Config.setup({
82-
jwtOwners: [
81+
const configSetup = function(opts = {}) {
82+
opts['jwtOwners'] = [
8383
ApplicationRecord,
8484
TestJWTSubclass
8585
]
86-
});
86+
Config.setup(opts);
87+
}
88+
configSetup();
8789

8890
export {
91+
configSetup,
8992
ApplicationRecord,
9093
TestJWTSubclass,
9194
NonJWTOwner,

test/integration/authorization-test.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { expect, fetchMock } from '../test-helper';
2-
import { ApplicationRecord, Author } from '../fixtures';
1+
import { sinon, expect, fetchMock } from '../test-helper';
2+
import { Config } from '../../src/index';
3+
import { configSetup, ApplicationRecord, Author } from '../fixtures';
34

45
after(function () {
56
fetchMock.restore();
@@ -89,5 +90,110 @@ describe('authorization headers', function() {
8990
Author.all();
9091
});
9192
});
93+
94+
describe('local storage', function() {
95+
beforeEach(function() {
96+
Config.localStorage = { setItem: sinon.spy() }
97+
Config.jwtLocalStorage = 'jwt';
98+
});
99+
100+
afterEach(function() {
101+
Config.localStorage = undefined;
102+
Config.jwtLocalStorage = undefined;
103+
});
104+
105+
describe('when configured to store jwt', function() {
106+
beforeEach(function() {
107+
Config.jwtLocalStorage = 'jwt';
108+
});
109+
110+
it('updates localStorage on server response', function(done) {
111+
Author.all().then((response) => {
112+
let called = Config.localStorage.setItem
113+
.calledWith('jwt', 'somet0k3n');
114+
expect(called).to.eq(true);
115+
done();
116+
});
117+
});
118+
119+
it('uses the new jwt in subsequent requests', function(done) {
120+
Author.all().then((response) => {
121+
fetchMock.restore();
122+
123+
fetchMock.mock((url, opts) => {
124+
expect(opts.headers.Authorization).to.eq('Token token="somet0k3n"')
125+
done();
126+
return true;
127+
}, 200);
128+
expect(Author.getJWT()).to.eq('somet0k3n');
129+
expect(ApplicationRecord.jwt).to.eq('somet0k3n');
130+
Author.all();
131+
});
132+
});
133+
134+
describe('when JWT is already in localStorage', function() {
135+
beforeEach(function() {
136+
fetchMock.restore();
137+
Config.localStorage['getItem'] = sinon.stub().returns('myt0k3n');
138+
configSetup({ jwtLocalStorage: 'jwt' });
139+
});
140+
141+
afterEach(function() {
142+
configSetup();
143+
});
144+
145+
it('sends it in initial request', function(done) {
146+
fetchMock.mock((url, opts) => {
147+
expect(opts.headers.Authorization).to.eq('Token token="myt0k3n"')
148+
done();
149+
return true;
150+
}, 200);
151+
Author.find(1);
152+
});
153+
});
154+
});
155+
156+
describe('when configured to NOT store jwt', function() {
157+
beforeEach(function() {
158+
Config.jwtLocalStorage = false;
159+
});
160+
161+
it('is does NOT update localStorage on server response', function(done) {
162+
Author.all().then((response) => {
163+
let called = Config.localStorage.setItem.called;
164+
expect(called).to.eq(false);
165+
done();
166+
});
167+
});
168+
});
169+
});
170+
});
171+
172+
describe('a write request', function() {
173+
beforeEach(function() {
174+
fetchMock.mock({
175+
matcher: '*',
176+
response: {
177+
status: 200,
178+
body: { data: [] },
179+
headers: {
180+
'X-JWT': 'somet0k3n'
181+
}
182+
}
183+
});
184+
});
185+
186+
afterEach(function() {
187+
fetchMock.restore();
188+
ApplicationRecord.jwt = null;
189+
});
190+
191+
it('also refreshes the jwt', function(done) {
192+
let author = new Author({ firstName: 'foo' });
193+
author.save().then(() => {
194+
expect(ApplicationRecord.jwt).to.eq('somet0k3n');
195+
done();
196+
});
197+
});
92198
});
93199
});

0 commit comments

Comments
 (0)