@@ -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' | 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}) => { | export const loadPasswordFirstTime = ({commit}) => { | ||||
commit(types.LOAD_PASSWORD_FIRST_TIME); | commit(types.LOAD_PASSWORD_FIRST_TIME); | ||||
}; | }; | ||||
@@ -44,19 +37,18 @@ export const login = ({commit}, payload) => { | |||||
}; | }; | ||||
export const logout = ({commit}) => { | export const logout = ({commit}) => { | ||||
auth.logout(); | |||||
commit(types.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 site = state.password.site; | ||||
const login = state.password.login; | const login = state.password.login; | ||||
if (site || login) { | if (site || login) { | ||||
Passwords.create(state.password).then(() => { | |||||
getPasswords({commit}); | |||||
}) | |||||
Password.create(state.password, state) | |||||
.then(() => { | |||||
getPasswords({commit}); | |||||
}) | |||||
} | } | ||||
} else { | } else { | ||||
Passwords.update(state.password).then(() => { | |||||
getPasswords({commit}); | |||||
}) | |||||
Password.update(state.password, state) | |||||
.then(() => { | |||||
getPasswords({commit}); | |||||
}) | |||||
} | } | ||||
}; | }; | ||||
export const deletePassword = ({commit}, payload) => { | 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, | getters, | ||||
actions, | actions, | ||||
mutations, | mutations, | ||||
plugins: [createPersistedState({key: 'lesspass-store'})] | |||||
plugins: [createPersistedState({key: 'lesspass'})] | |||||
}); | }); |
@@ -39,17 +39,12 @@ | |||||
</form> | </form> | ||||
</template> | </template> | ||||
<script type="text/ecmascript-6"> | <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'; | import {mapActions, mapGetters} from 'vuex'; | ||||
export default { | export default { | ||||
data() { | data() { | ||||
const storage = new Storage(); | |||||
const auth = new Auth(storage); | |||||
return { | return { | ||||
auth, | |||||
storage, | |||||
email: '', | email: '', | ||||
emailRequired: false, | emailRequired: false, | ||||
showError: false, | showError: false, | ||||
@@ -77,13 +72,15 @@ | |||||
return; | return; | ||||
} | } | ||||
this.loading = true; | 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> | </form> | ||||
</template> | </template> | ||||
<script type="text/ecmascript-6"> | <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'; | import {mapActions, mapGetters} from 'vuex'; | ||||
export default { | export default { | ||||
data() { | data() { | ||||
const storage = new Storage(); | |||||
const auth = new Auth(storage); | |||||
return { | return { | ||||
auth, | |||||
storage, | |||||
new_password: '', | new_password: '', | ||||
passwordRequired: false, | passwordRequired: false, | ||||
showError: false, | showError: false, | ||||
@@ -72,18 +67,19 @@ | |||||
this.passwordRequired = true; | this.passwordRequired = true; | ||||
return; | 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: { | 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()); | |||||
}); |