@@ -1,88 +0,0 @@ | |||
import axios from 'axios'; | |||
export default class Auth { | |||
constructor(storage) { | |||
this.user = { | |||
authenticated: false | |||
}; | |||
this.storage = storage; | |||
} | |||
isAuthenticated() { | |||
const token = this.storage.getToken(); | |||
if (token.stillValid()) { | |||
this.user.authenticated = true; | |||
return true; | |||
} | |||
this.user.authenticated = false; | |||
return false; | |||
} | |||
isGuest() { | |||
return !this.isAuthenticated() | |||
} | |||
logout() { | |||
return new Promise(resolve => { | |||
this.storage.clear(); | |||
this.user.authenticated = false; | |||
resolve(); | |||
}); | |||
} | |||
login(user, baseURL) { | |||
const config = this.storage.json(); | |||
if (baseURL) { | |||
config.baseURL = baseURL; | |||
} | |||
return Auth._requestToken(user, config).then(token => { | |||
this.storage.saveToken(token) | |||
}) | |||
} | |||
static _requestToken(user, config = {}) { | |||
return axios.post('/api/tokens/auth/', user, config).then(response => { | |||
return response.data.token; | |||
}); | |||
} | |||
refreshToken() { | |||
const config = this.storage.json(); | |||
const token = this.storage.getToken(); | |||
return Auth._requestNewToken({token: token.name}, config).then(token => { | |||
this.storage.saveToken(token) | |||
}); | |||
} | |||
static _requestNewToken(token, config = {}) { | |||
return axios.post('/api/tokens/refresh/', token, config).then(response => { | |||
return response.data.token; | |||
}); | |||
} | |||
register(user, baseURL) { | |||
const config = this.storage.json(); | |||
if (baseURL) { | |||
config.baseURL = baseURL; | |||
} | |||
return axios.post('/api/auth/register/', user, config).then(response => { | |||
return response.data; | |||
}); | |||
} | |||
resetPassword(email, baseURL) { | |||
const config = this.storage.json(); | |||
if (baseURL) { | |||
config.baseURL = baseURL; | |||
} | |||
return axios.post('/api/auth/password/reset/', email, config); | |||
} | |||
confirmResetPassword(password, baseURL) { | |||
const config = this.storage.json(); | |||
if (baseURL) { | |||
config.baseURL = baseURL; | |||
} | |||
return axios.post('/api/auth/password/reset/confirm/', password, config); | |||
} | |||
} |
@@ -1,39 +0,0 @@ | |||
import axios from 'axios'; | |||
import {TOKEN_KEY} from './storage'; | |||
export default class HTTP { | |||
constructor(resource, storage) { | |||
this.storage = storage; | |||
this.resource = resource; | |||
} | |||
getRequestConfig(params = {}) { | |||
const config = this.storage.json(); | |||
return { | |||
...params, | |||
baseURL: config.baseURL, | |||
headers: {Authorization: `JWT ${config[TOKEN_KEY]}`} | |||
}; | |||
} | |||
create(resource, params = {}) { | |||
return axios.post('/api/' + this.resource + '/', resource, this.getRequestConfig(params)); | |||
} | |||
all(params = {}) { | |||
return axios.get('/api/' + this.resource + '/', this.getRequestConfig(params)); | |||
} | |||
get(resource, params = {}) { | |||
return axios.get('/api/' + this.resource + '/' + resource.id + '/', this.getRequestConfig(params)); | |||
} | |||
update(resource, params = {}) { | |||
return axios.put('/api/' + this.resource + '/' + resource.id + '/', resource, this.getRequestConfig(params)); | |||
} | |||
remove(resource, params = {}) { | |||
return axios.delete('/api/' + this.resource + '/' + resource.id + '/', this.getRequestConfig(params)); | |||
} | |||
} |
@@ -1,44 +0,0 @@ | |||
import Token from './token'; | |||
export const LOCAL_STORAGE_KEY = 'lesspass'; | |||
export const TOKEN_KEY = 'jwt'; | |||
export default class Storage { | |||
constructor(storage = window.localStorage) { | |||
this.storage = storage; | |||
} | |||
_getLocalStorage() { | |||
return JSON.parse(this.storage.getItem(LOCAL_STORAGE_KEY) || '{}') | |||
} | |||
json() { | |||
const defaultStorage = { | |||
baseURL: 'https://lesspass.com', | |||
timeout: 5000 | |||
}; | |||
const localStorage = this._getLocalStorage(); | |||
return Object.assign(defaultStorage, localStorage); | |||
} | |||
save(data) { | |||
const newData = Object.assign(this._getLocalStorage(), data); | |||
this.storage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newData)); | |||
} | |||
clear() { | |||
this.storage.clear(); | |||
} | |||
getToken() { | |||
const storage = this.json(); | |||
if (TOKEN_KEY in storage) { | |||
return new Token(storage[TOKEN_KEY]); | |||
} | |||
return new Token(); | |||
} | |||
saveToken(token) { | |||
this.save({[TOKEN_KEY]: token}) | |||
} | |||
} |
@@ -1,42 +0,0 @@ | |||
import jwtDecode from 'jwt-decode'; | |||
export const TOKEN_KEY = 'jwt'; | |||
export default class Token { | |||
constructor(tokenName) { | |||
this.name = tokenName | |||
} | |||
stillValid(now = new Date()) { | |||
try { | |||
return this._expirationDateSuperiorTo(now); | |||
} | |||
catch (err) { | |||
return false; | |||
} | |||
} | |||
expiresInMinutes(minutes, now = new Date()) { | |||
try { | |||
const nowPlusDuration = new Date(now.getTime() + minutes*60000); | |||
return this._expirationDateInferiorTo(nowPlusDuration); | |||
} | |||
catch (err) { | |||
return false; | |||
} | |||
} | |||
_expirationDateInferiorTo(date) { | |||
const expireDate = this._getTokenExpirationDate(); | |||
return expireDate < date; | |||
} | |||
_expirationDateSuperiorTo(date) { | |||
return !this._expirationDateInferiorTo(date) | |||
} | |||
_getTokenExpirationDate() { | |||
const decodedToken = jwtDecode(this.name); | |||
return new Date(decodedToken.exp * 1000); | |||
} | |||
} |
@@ -1,14 +1,7 @@ | |||
import User from '../api/user'; | |||
import Storage from '../api/storage'; | |||
import Auth from '../api/auth'; | |||
import HTTP from '../api/http'; | |||
import Password from '../api/password'; | |||
import * as types from './mutation-types' | |||
const storage = new Storage(); | |||
const auth = new Auth(storage); | |||
const Passwords = new HTTP('passwords', storage); | |||
export const loadPasswordFirstTime = ({commit}) => { | |||
commit(types.LOAD_PASSWORD_FIRST_TIME); | |||
}; | |||
@@ -44,19 +37,18 @@ export const login = ({commit}, payload) => { | |||
}; | |||
export const logout = ({commit}) => { | |||
auth.logout(); | |||
commit(types.LOGOUT); | |||
}; | |||
export const getPasswords = ({commit}) => { | |||
if (auth.isAuthenticated()) { | |||
Passwords.all().then(response => commit(types.SET_PASSWORDS, {passwords: response.data.results})); | |||
export const getPasswords = ({commit, state}) => { | |||
if (state.authenticated) { | |||
Password.all(state).then(response => commit(types.SET_PASSWORDS, {passwords: response.data.results})); | |||
} | |||
}; | |||
export const getPassword = ({commit}, payload) => { | |||
if (auth.isAuthenticated()) { | |||
Passwords.get(payload).then(response => commit(types.SET_PASSWORD, {password: response.data})); | |||
export const getPassword = ({commit, state}, payload) => { | |||
if (state.authenticated) { | |||
Password.read(payload, state).then(response => commit(types.SET_PASSWORD, {password: response.data})); | |||
} | |||
}; | |||
@@ -65,19 +57,22 @@ export const saveOrUpdatePassword = ({commit, state}) => { | |||
const site = state.password.site; | |||
const login = state.password.login; | |||
if (site || login) { | |||
Passwords.create(state.password).then(() => { | |||
getPasswords({commit}); | |||
}) | |||
Password.create(state.password, state) | |||
.then(() => { | |||
getPasswords({commit}); | |||
}) | |||
} | |||
} else { | |||
Passwords.update(state.password).then(() => { | |||
getPasswords({commit}); | |||
}) | |||
Password.update(state.password, state) | |||
.then(() => { | |||
getPasswords({commit}); | |||
}) | |||
} | |||
}; | |||
export const deletePassword = ({commit}, payload) => { | |||
Passwords.remove(payload).then(() => { | |||
commit(types.DELETE_PASSWORD, payload); | |||
}); | |||
Password.delete(payload, state) | |||
.then(() => { | |||
commit(types.DELETE_PASSWORD, payload); | |||
}); | |||
}; |
@@ -34,5 +34,5 @@ export default new Vuex.Store({ | |||
getters, | |||
actions, | |||
mutations, | |||
plugins: [createPersistedState({key: 'lesspass-store'})] | |||
plugins: [createPersistedState({key: 'lesspass'})] | |||
}); |
@@ -39,17 +39,12 @@ | |||
</form> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import Auth from '../api/auth'; | |||
import Storage from '../api/storage'; | |||
import User from '../api/user'; | |||
import {mapActions, mapGetters} from 'vuex'; | |||
export default { | |||
data() { | |||
const storage = new Storage(); | |||
const auth = new Auth(storage); | |||
return { | |||
auth, | |||
storage, | |||
email: '', | |||
emailRequired: false, | |||
showError: false, | |||
@@ -77,13 +72,15 @@ | |||
return; | |||
} | |||
this.loading = true; | |||
this.auth.resetPassword({email: this.email}).then(()=> { | |||
this.cleanErrors(); | |||
this.successMessage = true; | |||
}).catch(() => { | |||
this.cleanErrors(); | |||
this.showError = true; | |||
}); | |||
User.resetPassword({email: this.email}) | |||
.then(() => { | |||
this.cleanErrors(); | |||
this.successMessage = true; | |||
}) | |||
.catch(() => { | |||
this.cleanErrors(); | |||
this.showError = true; | |||
}); | |||
} | |||
} | |||
} | |||
@@ -39,17 +39,12 @@ | |||
</form> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import Auth from '../api/auth'; | |||
import Storage from '../api/storage'; | |||
import User from '../api/user'; | |||
import {mapActions, mapGetters} from 'vuex'; | |||
export default { | |||
data() { | |||
const storage = new Storage(); | |||
const auth = new Auth(storage); | |||
return { | |||
auth, | |||
storage, | |||
new_password: '', | |||
passwordRequired: false, | |||
showError: false, | |||
@@ -72,18 +67,19 @@ | |||
this.passwordRequired = true; | |||
return; | |||
} | |||
this.auth.confirmResetPassword({ | |||
uid: this.$route.params.uid, | |||
token: this.$route.params.token, | |||
new_password: this.new_password | |||
}).then(()=> { | |||
this.successMessage = true | |||
}).catch(err => { | |||
if(err.response.status === 400){ | |||
this.errorMessage = 'This password reset link become invalid.' | |||
} | |||
this.showError = true; | |||
}); | |||
const resetPassword = { | |||
uid: this.$route.params.uid, token: this.$route.params.token, new_password: this.new_password | |||
}; | |||
User.confirmResetPassword(resetPassword) | |||
.then(() => { | |||
this.successMessage = true | |||
}) | |||
.catch(err => { | |||
if (err.response.status === 400) { | |||
this.errorMessage = 'This password reset link become invalid.' | |||
} | |||
this.showError = true; | |||
}); | |||
} | |||
}, | |||
computed: { | |||
@@ -1,26 +0,0 @@ | |||
export class LocalStorageMock { | |||
constructor(storage = {}) { | |||
this.storage = storage; | |||
} | |||
setItem(key, value) { | |||
this.storage[key] = value || ''; | |||
} | |||
getItem(key) { | |||
return this.storage[key] || null; | |||
} | |||
removeItem(key) { | |||
delete this.storage[key]; | |||
} | |||
key(i) { | |||
const keys = Object.keys(this.storage); | |||
return keys[i] || null; | |||
} | |||
clear() { | |||
this.storage = {}; | |||
} | |||
} |
@@ -1,133 +0,0 @@ | |||
import test from 'ava'; | |||
import {LocalStorageMock} from './_helpers'; | |||
import Auth from '../src/api/auth'; | |||
import Storage, {LOCAL_STORAGE_KEY} from '../src/api/storage'; | |||
import nock from 'nock'; | |||
function AuthFactory(token, localStorage = new LocalStorageMock()) { | |||
const storage = new Storage(localStorage); | |||
storage.saveToken(token); | |||
return new Auth(storage); | |||
} | |||
test('request token', t => { | |||
const token = '5e0651'; | |||
const user = {email: 'test@example.org', password: 'password'}; | |||
nock('https://lesspass.com').post('/api/tokens/auth/', user).reply(201, {token}); | |||
return Auth._requestToken(user, {baseURL: 'https://lesspass.com'}).then(requestedToken => { | |||
t.is(requestedToken, token); | |||
}); | |||
}); | |||
test('request new token', t => { | |||
const token = '3e3231'; | |||
const newToken = 'wibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIi'; | |||
nock('https://lesspass.com').post('/api/tokens/refresh/', {token}).reply(200, {token: newToken}); | |||
return Auth._requestNewToken({token}, {baseURL: 'https://lesspass.com'}).then(refreshedToken => { | |||
t.is(refreshedToken, newToken); | |||
}); | |||
}); | |||
test('user first connection is guest', t => { | |||
const storage = new Storage(new LocalStorageMock()); | |||
const auth = new Auth(storage); | |||
t.true(auth.isGuest()); | |||
}); | |||
test('user return on site before token expire', t => { | |||
const auth = AuthFactory('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTc1NzkyODQzNH0.KzEBhVgm3xa51jsBklB0Ib9DDwAkvynOnkwLLJoD5AU'); | |||
t.true(auth.isAuthenticated()); | |||
t.false(auth.isGuest()); | |||
}); | |||
test('user return on site after token expiration', t => { | |||
const auth = AuthFactory('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||
t.false(auth.isAuthenticated()); | |||
t.true(auth.isGuest()); | |||
t.false(auth.user.authenticated); | |||
}); | |||
test('login save token', t => { | |||
const token = '3e3231'; | |||
const storage = new LocalStorageMock(); | |||
const auth = AuthFactory(token, storage); | |||
const user = { | |||
email: 'test@lesspass.com', | |||
password: 'password' | |||
}; | |||
nock('https://lesspass.com').post('/api/tokens/auth/', user).reply(201, {token}); | |||
return auth.login(user).then(() => { | |||
t.is(JSON.parse(storage.getItem(LOCAL_STORAGE_KEY)).jwt, token); | |||
}); | |||
}); | |||
test('logout user remove token and unauthenticate user', t => { | |||
const token = '3e3231'; | |||
const storage = new LocalStorageMock(); | |||
const auth = AuthFactory(token, storage); | |||
return auth.logout().then(() => { | |||
t.falsy(storage.getItem(LOCAL_STORAGE_KEY)); | |||
}); | |||
}); | |||
test('login custom endpoint', t => { | |||
const token = '3e3231'; | |||
const storage = new LocalStorageMock(); | |||
const auth = AuthFactory(token, storage); | |||
const user = { | |||
email: 'test@lesspass.com', | |||
password: 'password' | |||
}; | |||
nock('https://test.example.org').post('/api/tokens/auth/', user).reply(201, {token}); | |||
return auth.login(user, 'https://test.example.org').then(() => { | |||
t.is(JSON.parse(storage.getItem(LOCAL_STORAGE_KEY)).jwt, token); | |||
}); | |||
}); | |||
test('refresh token', t => { | |||
const token = '3e3231'; | |||
const storage = new LocalStorageMock(); | |||
const auth = AuthFactory(token, storage); | |||
const newToken = 'wibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIi'; | |||
nock('https://lesspass.com').post('/api/tokens/refresh/', {token}).reply(200, {token: newToken}); | |||
return auth.refreshToken().then(() => { | |||
t.is(JSON.parse(storage.getItem(LOCAL_STORAGE_KEY)).jwt, newToken); | |||
}); | |||
}); | |||
test('should register a user', t => { | |||
const user = { | |||
email: 'test@lesspass.com', | |||
password: 'password' | |||
}; | |||
const localStorage = new LocalStorageMock(); | |||
const storage = new Storage(localStorage); | |||
const auth = new Auth(storage); | |||
nock('https://lesspass.com').post('/api/auth/register/', user).reply(201, {email: user.email, pk: 1}); | |||
return auth.register(user).then(newUser => { | |||
t.is(newUser.email, user.email); | |||
}); | |||
}); | |||
test('should reset a password', t => { | |||
var email = 'test@lesspass.com'; | |||
const localStorage = new LocalStorageMock(); | |||
const storage = new Storage(localStorage); | |||
const auth = new Auth(storage); | |||
nock('https://lesspass.com').post('/api/auth/password/reset/', {email}).reply(204); | |||
t.notThrows(auth.resetPassword({email})); | |||
}); | |||
test('should confirm reset password', t => { | |||
var newPassword ={ | |||
uid: 'MQ', | |||
token: '5g1-2bd69bd6f6dcd73f8124', | |||
new_password: 'password1' | |||
}; | |||
const localStorage = new LocalStorageMock(); | |||
const storage = new Storage(localStorage); | |||
const auth = new Auth(storage); | |||
nock('https://lesspass.com').post('/api/auth/password/reset/confirm/', newPassword).reply(204); | |||
t.notThrows(auth.confirmResetPassword(newPassword)); | |||
}); |
@@ -1,39 +0,0 @@ | |||
import test from 'ava'; | |||
import {LocalStorageMock} from './_helpers'; | |||
import Storage, {LOCAL_STORAGE_KEY} from '../src/api/storage'; | |||
const localStorage = new LocalStorageMock(); | |||
const storage = new Storage(localStorage); | |||
test('get default storage', t => { | |||
t.is(storage.json().baseURL, 'https://lesspass.com'); | |||
}); | |||
test('get storage saved in local storage', t => { | |||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify({baseURL: 'https://example.org'})); | |||
t.is(storage.json().baseURL, 'https://example.org'); | |||
}); | |||
test('save storage in local storage', t => { | |||
storage.save({baseURL: 'https://example.org'}); | |||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), '{"baseURL":"https://example.org"}'); | |||
}); | |||
test('save storage in local storage', t => { | |||
storage.save({baseURL: 'https://example.org'}); | |||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), '{"baseURL":"https://example.org"}'); | |||
}); | |||
test('save storage in local storage merge', t => { | |||
localStorage.clear(); | |||
storage.save({a: 'a'}); | |||
storage.save({b: 'b'}); | |||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), '{"a":"a","b":"b"}'); | |||
}); | |||
test('storage clear local storage', t => { | |||
storage.save({a: 'a'}); | |||
storage.clear(); | |||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), null); | |||
}); | |||
@@ -1,28 +0,0 @@ | |||
import test from 'ava'; | |||
import Token from '../src/api/token'; | |||
test('token is near the end', t => { | |||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||
t.true(token.expiresInMinutes(15, new Date(1437018283 * 1000))); | |||
t.false(token.expiresInMinutes(5, new Date(1437018283 * 1000))); | |||
}); | |||
test('token still valid', t => { | |||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTc1NzkyODQzNH0.KzEBhVgm3xa51jsBklB0Ib9DDwAkvynOnkwLLJoD5AU'); | |||
t.true(token.stillValid()); | |||
}); | |||
test('token still valid check payload date', t => { | |||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||
t.true(token.stillValid(new Date(1437018283 * 1000))); | |||
}); | |||
test('token expired', t => { | |||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||
t.false(token.stillValid()); | |||
}); | |||
test('token invalid does not raise an error', t => { | |||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'); | |||
t.false(token.stillValid()); | |||
}); |