From 5a362de6caaeb8be28bc68313f7256a49a6d2cf3 Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Tue, 24 May 2022 00:24:03 +0700 Subject: [PATCH 1/5] new way --- README.md | 3 +- .../refreshtokensample/data/AuthRepoImpl.kt | 9 +- .../refreshtokensample/data/DataModule.kt | 3 +- .../data/local/UserLocalSerializer.kt | 3 +- .../data/local/UserLocalSource.kt | 5 +- .../data/local/UserLocalSourceImpl.kt | 19 +- .../remote/interceptor/AuthInterceptor.kt | 103 +- .../refreshtokensample/di/AppModule.kt | 9 +- server/package-lock.json | 1056 ++++++++++++++++- 9 files changed, 1139 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 0de8f93..49eea35 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,5 @@ npm i npm run start ``` -Change `baseUrl` at `app/src/main/java/com/hoc081098/refreshtokensample/data/DataModule.kt` +- Change `baseUrl` (e.g. `http://YOUR_ID_ADDRESS:3000/`) at `app/src/main/java/com/hoc081098/refreshtokensample/data/DataModule.kt` +- Change `expiresIn` (default value is 1 minute) at `server/routes/index.js`. diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/AuthRepoImpl.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/AuthRepoImpl.kt index e93a0fd..4c312f2 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/AuthRepoImpl.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/AuthRepoImpl.kt @@ -51,17 +51,20 @@ class AuthRepoImpl @Inject constructor( ), ) - userLocalSource.save( + userLocalSource.update { UserLocal.newBuilder() .setId(response.id) .setUsername(response.username) .setToken(response.token) .setRefreshToken(response.refreshToken) .build() - ) + } + + Unit } override suspend fun logout() = withContext(appDispatchers.io) { - userLocalSource.save(null) + userLocalSource.update { null } + Unit } } diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/DataModule.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/DataModule.kt index a0f1301..efc895a 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/DataModule.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/DataModule.kt @@ -38,6 +38,7 @@ import javax.inject.Singleton @Retention(AnnotationRetention.BINARY) @Qualifier +@MustBeDocumented annotation class BaseUrl @Module @@ -107,7 +108,7 @@ abstract class DataModule { @Provides @BaseUrl - fun baseUrl(): String = "http://192.168.0.5:3000/" + fun baseUrl(): String = "http://10.0.2.2:3000/" @Provides @Singleton diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSerializer.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSerializer.kt index 4ebfc4c..fac322e 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSerializer.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSerializer.kt @@ -12,8 +12,7 @@ import javax.inject.Inject class UserLocalSerializer @Inject constructor( private val crypto: Crypto ) : Serializer { - override val defaultValue: UserLocal - get() = UserLocal.getDefaultInstance() + override val defaultValue: UserLocal get() = USER_LOCAL_NULL override suspend fun readFrom(input: InputStream): UserLocal { return try { diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt index c0cd3e9..fd87edc 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt @@ -5,5 +5,8 @@ import kotlinx.coroutines.flow.Flow interface UserLocalSource { fun user(): Flow - suspend fun save(userLocal: UserLocal?) + suspend fun update(transform: suspend (current: UserLocal?) -> UserLocal?): UserLocal? } + +@JvmField +internal val USER_LOCAL_NULL: UserLocal = UserLocal.getDefaultInstance() diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSourceImpl.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSourceImpl.kt index d45cbe8..167684e 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSourceImpl.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSourceImpl.kt @@ -1,7 +1,6 @@ package com.hoc081098.refreshtokensample.data.local import androidx.datastore.core.DataStore -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -11,9 +10,9 @@ import javax.inject.Inject class UserLocalSourceImpl @Inject constructor(private val dataStore: DataStore) : UserLocalSource { - override fun user(): Flow = dataStore.data - .onEach { Timber.d("User=$it") } - .map { if (it != USER_LOCAL_NULL) it else null } + override fun user() = dataStore.data + .onEach { Timber.d("userLocal=$it") } + .map { v -> v.takeIf { it != USER_LOCAL_NULL } } .catch { cause: Throwable -> if (cause is IOException) { emit(null) @@ -22,14 +21,8 @@ class UserLocalSourceImpl @Inject constructor(private val dataStore: DataStore UserLocal?) = + dataStore.updateData { current -> + transform(current.takeIf { it != USER_LOCAL_NULL }) ?: USER_LOCAL_NULL } - } - - companion object { - private val USER_LOCAL_NULL: UserLocal = UserLocal.getDefaultInstance() - } } diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt index bea3d4e..cd6bd85 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt @@ -7,82 +7,93 @@ import com.hoc081098.refreshtokensample.data.remote.ApiService.Factory.NO_AUTH import com.hoc081098.refreshtokensample.data.remote.body.RefreshTokenBody import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import timber.log.Timber import java.net.HttpURLConnection.HTTP_OK import java.net.HttpURLConnection.HTTP_UNAUTHORIZED import javax.inject.Inject import javax.inject.Provider +import timber.log.Timber.Forest.d as debug class AuthInterceptor @Inject constructor( private val userLocalSource: UserLocalSource, private val apiService: Provider, ) : Interceptor { - private val mutex = Mutex() - override fun intercept(chain: Interceptor.Chain): Response { - val req = chain.request().also { Timber.d("[1] $it") } + val request = chain.request().also { debug("[1] START url=${it.url}") } + + if (NO_AUTH in request.headers.values(CUSTOM_HEADER)) { + return chain.proceedWithToken(request, null) + } - if (NO_AUTH in req.headers.values(CUSTOM_HEADER)) { - return chain.proceedWithToken(req, null) + val token = runBlocking { userLocalSource.user().first() } + ?.token + .also { debug("[2] READ TOKEN token=$it, url=${request.url}") } + val response = chain.proceedWithToken(request, token) + + if (response.code != HTTP_UNAUTHORIZED || token == null) { + return response } - val token = - runBlocking { userLocalSource.user().first() }?.token.also { Timber.d("[2] $req $it") } - val res = chain.proceedWithToken(req, token) + val newToken = executeTokenRefreshing(request, token) - if (res.code != HTTP_UNAUTHORIZED || token == null) { - return res + return if (newToken !== null && newToken != token) { + chain.proceedWithToken(request, newToken) + } else { + response } + } - Timber.d("[3] $req") + private fun executeTokenRefreshing(request: Request, token: String?): String? { + val requestUrl = request.url + debug("[3] BEFORE REFRESHING token=$token, url=$requestUrl") - val newToken: String? = runBlocking { - mutex.withLock { - val user = - userLocalSource.user().first().also { Timber.d("[4] $req $it") } - val maybeUpdatedToken = user?.token + return runBlocking { + userLocalSource.update { currentUserLocal -> + val maybeUpdatedToken = currentUserLocal?.token + debug("[4] INSIDE REFRESHING maybeUpdatedToken=$maybeUpdatedToken, url=$requestUrl") when { - user == null || maybeUpdatedToken == null -> null.also { Timber.d("[5-1] $req") } // already logged out! - maybeUpdatedToken != token -> maybeUpdatedToken.also { Timber.d("[5-2] $req") } // refreshed by another request + maybeUpdatedToken == null -> + null + .also { debug("[5-1] LOGGED OUT url=$requestUrl") } // already logged out! + maybeUpdatedToken != token -> + currentUserLocal + .also { debug("[5-2] REFRESHED BY ANOTHER url=$requestUrl") } // refreshed by another request else -> { - Timber.d("[5-3] $req") - - val refreshTokenRes = - apiService.get().refreshToken(RefreshTokenBody(user.refreshToken, user.username)) - .also { - Timber.d("[6] $req $it") - } + debug("[5-3] START REFRESHING REQUEST url=$requestUrl") - val code = refreshTokenRes.code() - if (code == HTTP_OK) { - refreshTokenRes.body()?.token?.also { - Timber.d("[7-1] $req") - userLocalSource.save( - user.toBuilder() - .setToken(it) - .build() + val refreshTokenRes = apiService.get() + .refreshToken( + RefreshTokenBody( + refreshToken = currentUserLocal.refreshToken, + username = currentUserLocal.username ) + ) + .also { debug("[6] DONE REFRESHING REQUEST status=${it.code()}, url=$requestUrl") } + + when (val code = refreshTokenRes.code()) { + HTTP_OK -> { + currentUserLocal + .toBuilder() + .setToken(refreshTokenRes.body()!!.token) + .build() + .also { debug("[7-1] REFRESH SUCCESSFULLY newToken=${it.token}, url=$requestUrl") } + } + HTTP_UNAUTHORIZED -> { + debug("[7-2] REFRESH FAILED HTTP_UNAUTHORIZED url=$requestUrl") + null + } + else -> { + debug("[7-3] REFRESH FAILED code=$code, url=$requestUrl") + currentUserLocal } - } else if (code == HTTP_UNAUTHORIZED) { - Timber.d("[7-2] $req") - userLocalSource.save(null) - null - } else { - Timber.d("[7-3] $req") - null } } } - } + }?.token } - - return if (newToken !== null) chain.proceedWithToken(req, newToken) else res } private fun Interceptor.Chain.proceedWithToken(req: Request, token: String?): Response = diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/di/AppModule.kt b/app/src/main/java/com/hoc081098/refreshtokensample/di/AppModule.kt index 2815498..27b36cf 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/di/AppModule.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/di/AppModule.kt @@ -5,6 +5,7 @@ import com.google.crypto.tink.Aead import com.google.crypto.tink.KeyTemplates import com.google.crypto.tink.aead.AeadConfig import com.google.crypto.tink.integration.android.AndroidKeysetManager +import com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient import com.hoc081098.refreshtokensample.AppDispatchers import com.hoc081098.refreshtokensample.AppDispatchersImpl import dagger.Binds @@ -20,6 +21,7 @@ import javax.inject.Singleton @Retention(AnnotationRetention.BINARY) @Qualifier +@MustBeDocumented annotation class AppCoroutineScope @Module @@ -30,9 +32,10 @@ abstract class AppModule { abstract fun appDispatchers(impl: AppDispatchersImpl): AppDispatchers internal companion object { - private const val KEYSET_NAME = "keyset" - private const val PREF_FILE_NAME = "keyset_prefs" - private const val MASTER_KEY_URI = "android-keystore://master_key" + private const val KEYSET_NAME = "__refresh_token_sample_encrypted_prefs_keyset__" + private const val PREF_FILE_NAME = "refresh_token_sample_secret_prefs" + private const val MASTER_KEY_URI = + "${AndroidKeystoreKmsClient.PREFIX}refresh_token_sample_master_key" @Provides @Singleton diff --git a/server/package-lock.json b/server/package-lock.json index 8f54969..6d5b7db 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,8 +1,1062 @@ { "name": "server", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "server", + "version": "0.0.0", + "dependencies": { + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "^4.16.4", + "http-errors": "~1.6.3", + "jade": "~1.11.0", + "jsonwebtoken": "^8.5.1", + "morgan": "~1.9.1" + } + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", + "dependencies": { + "acorn": "^2.1.0" + } + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/asap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", + "integrity": "sha1-sqRdpf36ILBJb8N2jMJ8EvqRan0=" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/character-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", + "integrity": "sha1-wN3kqxgnE7kZuXCVmhI+zBow/NY=" + }, + "node_modules/clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "dependencies": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "bin": { + "cleancss": "bin/cleancss" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css/node_modules/commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/cliui/node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/constantinople": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", + "integrity": "sha1-S5RdmTeQe82Y7ldRIsOBdRZUQUE=", + "deprecated": "Please update to at least constantinople 3.1.1", + "dependencies": { + "acorn": "^2.1.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", + "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/css": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", + "integrity": "sha1-k4aBHKgrzMnuf7WnMrHioxfIo+c=", + "dependencies": { + "css-parse": "1.0.4", + "css-stringify": "1.0.5" + } + }, + "node_modules/css-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", + "integrity": "sha1-OLBQP7+dqfVOnB29pg4UXHcRe90=" + }, + "node_modules/css-stringify": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", + "integrity": "sha1-sNBClG2ylTu50pKQCmy19tASIDE=" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "dependencies": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/jade": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", + "integrity": "sha1-nIDlOMEtP7lcjZu5VZ+gzAQEBf0=", + "deprecated": "Jade has been renamed to pug, please install the latest version of pug instead of jade", + "dependencies": { + "character-parser": "1.2.1", + "clean-css": "^3.1.9", + "commander": "~2.6.0", + "constantinople": "~3.0.1", + "jstransformer": "0.0.2", + "mkdirp": "~0.5.0", + "transformers": "2.1.0", + "uglify-js": "^2.4.19", + "void-elements": "~2.0.1", + "with": "~4.0.0" + }, + "bin": { + "jade": "bin/jade.js" + } + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jstransformer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", + "integrity": "sha1-eq4pqQPRls+glz2IXT5HlH7Ndqs=", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^6.0.1" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "bin": { + "mime": "cli.js" + } + }, + "node_modules/mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "dependencies": { + "mime-db": "1.48.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "dependencies": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "dependencies": { + "wordwrap": "~0.0.2" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/promise": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", + "integrity": "sha1-LOcp9rlLRcJoka0GAsXJDgTG7vY=", + "dependencies": { + "asap": "~1.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/transformers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", + "integrity": "sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac=", + "deprecated": "Deprecated, use jstransformer", + "dependencies": { + "css": "~1.0.8", + "promise": "~2.0", + "uglify-js": "~2.2.5" + } + }, + "node_modules/transformers/node_modules/is-promise": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", + "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" + }, + "node_modules/transformers/node_modules/promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", + "integrity": "sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4=", + "dependencies": { + "is-promise": "~1" + } + }, + "node_modules/transformers/node_modules/source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/transformers/node_modules/uglify-js": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", + "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", + "dependencies": { + "optimist": "~0.3.5", + "source-map": "~0.1.7" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/uglify-js/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/with": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", + "integrity": "sha1-7v0VTp550sjTQXtkeo8U2f7M4U4=", + "dependencies": { + "acorn": "^1.0.1", + "acorn-globals": "^1.0.3" + } + }, + "node_modules/with/node_modules/acorn": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", + "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + }, "dependencies": { "accepts": { "version": "1.3.7", From 21764eebec2426b3536e1e06d737e66b2f910128 Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Tue, 24 May 2022 01:22:39 +0700 Subject: [PATCH 2/5] update --- .../remote/interceptor/AuthInterceptor.kt | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt index cd6bd85..650987e 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt @@ -10,11 +10,11 @@ import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response +import timber.log.Timber import java.net.HttpURLConnection.HTTP_OK import java.net.HttpURLConnection.HTTP_UNAUTHORIZED import javax.inject.Inject import javax.inject.Provider -import timber.log.Timber.Forest.d as debug class AuthInterceptor @Inject constructor( private val userLocalSource: UserLocalSource, @@ -29,7 +29,7 @@ class AuthInterceptor @Inject constructor( val token = runBlocking { userLocalSource.user().first() } ?.token - .also { debug("[2] READ TOKEN token=$it, url=${request.url}") } + .also { debug("[2] READ TOKEN token=${it.firstTwoCharactersAndLastTwoCharacters()}, url=${request.url}") } val response = chain.proceedWithToken(request, token) if (response.code != HTTP_UNAUTHORIZED || token == null) { @@ -47,12 +47,12 @@ class AuthInterceptor @Inject constructor( private fun executeTokenRefreshing(request: Request, token: String?): String? { val requestUrl = request.url - debug("[3] BEFORE REFRESHING token=$token, url=$requestUrl") + debug("[3] BEFORE REFRESHING token=${token.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") return runBlocking { userLocalSource.update { currentUserLocal -> val maybeUpdatedToken = currentUserLocal?.token - debug("[4] INSIDE REFRESHING maybeUpdatedToken=$maybeUpdatedToken, url=$requestUrl") + debug("[4] INSIDE REFRESHING maybeUpdatedToken=${maybeUpdatedToken.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") when { maybeUpdatedToken == null -> @@ -71,7 +71,6 @@ class AuthInterceptor @Inject constructor( username = currentUserLocal.username ) ) - .also { debug("[6] DONE REFRESHING REQUEST status=${it.code()}, url=$requestUrl") } when (val code = refreshTokenRes.code()) { HTTP_OK -> { @@ -79,15 +78,13 @@ class AuthInterceptor @Inject constructor( .toBuilder() .setToken(refreshTokenRes.body()!!.token) .build() - .also { debug("[7-1] REFRESH SUCCESSFULLY newToken=${it.token}, url=$requestUrl") } + .also { debug("[6-1] REFRESH SUCCESSFULLY newToken=${it.token.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") } } - HTTP_UNAUTHORIZED -> { - debug("[7-2] REFRESH FAILED HTTP_UNAUTHORIZED url=$requestUrl") - null + HTTP_UNAUTHORIZED -> null.also { + debug("[6-2] REFRESH FAILED HTTP_UNAUTHORIZED url=$requestUrl") } - else -> { - debug("[7-3] REFRESH FAILED code=$code, url=$requestUrl") - currentUserLocal + else -> currentUserLocal.also { + debug("[6-3] REFRESH FAILED code=$code, url=$requestUrl") } } } @@ -107,3 +104,13 @@ class AuthInterceptor @Inject constructor( .build() .let(::proceed) } + +@Suppress("NOTHING_TO_INLINE") +private inline fun debug(message: String) = + Timber.tag(AuthInterceptor::class.java.simpleName).d(message) + +@Suppress("NOTHING_TO_INLINE") +private inline fun String?.firstTwoCharactersAndLastTwoCharacters(): String? { + this ?: return null + return if (length <= 4) this else "${take(2)}...${takeLast(2)}" +} From dae98bf784012238b7c7181da02c5bfe6a2a179b Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Wed, 25 May 2022 16:43:54 +0700 Subject: [PATCH 3/5] update --- .../data/local/UserLocalSource.kt | 14 ++++++++++++++ .../data/remote/interceptor/AuthInterceptor.kt | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt index fd87edc..03d1163 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt @@ -1,10 +1,24 @@ package com.hoc081098.refreshtokensample.data.local import kotlinx.coroutines.flow.Flow +import java.io.IOException interface UserLocalSource { fun user(): Flow + /** + * Updates the data transactionally in an atomic read-modify-write operation. All operations + * are serialized, and the [transform] itself is a coroutine so it can perform heavy work + * such as RPCs. + * + * The coroutine completes when the data has been persisted durably to disk (after which + * [user] will reflect the update). If the transform or write to disk fails, the + * transaction is aborted and an exception is thrown. + * + * @return the snapshot returned by the transform + * @throws IOException when an exception is encountered when writing data to disk + * @throws Exception when thrown by the transform function + */ suspend fun update(transform: suspend (current: UserLocal?) -> UserLocal?): UserLocal? } diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt index 650987e..1a3082b 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt @@ -1,5 +1,6 @@ package com.hoc081098.refreshtokensample.data.remote.interceptor +import com.hoc081098.refreshtokensample.AppDispatchers import com.hoc081098.refreshtokensample.data.local.UserLocalSource import com.hoc081098.refreshtokensample.data.remote.ApiService import com.hoc081098.refreshtokensample.data.remote.ApiService.Factory.CUSTOM_HEADER @@ -19,6 +20,7 @@ import javax.inject.Provider class AuthInterceptor @Inject constructor( private val userLocalSource: UserLocalSource, private val apiService: Provider, + private val appDispatchers: AppDispatchers, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().also { debug("[1] START url=${it.url}") } @@ -49,7 +51,7 @@ class AuthInterceptor @Inject constructor( val requestUrl = request.url debug("[3] BEFORE REFRESHING token=${token.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") - return runBlocking { + return runBlocking(appDispatchers.io) { userLocalSource.update { currentUserLocal -> val maybeUpdatedToken = currentUserLocal?.token debug("[4] INSIDE REFRESHING maybeUpdatedToken=${maybeUpdatedToken.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") From 24ec3e7ed0f35c1bf78df1759e7507a51354e98d Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Fri, 27 May 2022 01:23:44 +0700 Subject: [PATCH 4/5] merge master --- .../remote/interceptor/AuthInterceptor.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt index 00f50a0..c926792 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt @@ -1,6 +1,7 @@ package com.hoc081098.refreshtokensample.data.remote.interceptor import com.hoc081098.refreshtokensample.AppDispatchers +import com.hoc081098.refreshtokensample.data.local.UserLocal import com.hoc081098.refreshtokensample.data.local.UserLocalSource import com.hoc081098.refreshtokensample.data.remote.ApiService import com.hoc081098.refreshtokensample.data.remote.ApiService.Factory.CUSTOM_HEADER @@ -66,13 +67,9 @@ class AuthInterceptor @Inject constructor( else -> { debug("[5-3] START REFRESHING REQUEST url=$requestUrl") - val refreshTokenRes = apiService.get() - .refreshToken( - RefreshTokenBody( - refreshToken = currentUserLocal.refreshToken, - username = currentUserLocal.username - ) - ) + val refreshTokenRes = apiService + .get() + .refreshToken(currentUserLocal.toRefreshTokenBody()) when (val code = refreshTokenRes.code()) { HTTP_OK -> { @@ -105,11 +102,16 @@ class AuthInterceptor @Inject constructor( .removeHeader(CUSTOM_HEADER) .build() .let(::proceed) + + @Suppress("NOTHING_TO_INLINE") + private inline fun debug(s: String) = Timber.tag(LOG_TAG).d(s) + + private companion object { + private val LOG_TAG = AuthInterceptor::class.java.simpleName + } } -@Suppress("NOTHING_TO_INLINE") -private inline fun debug(message: String) = - Timber.tag(AuthInterceptor::class.java.simpleName).d(message) +private fun UserLocal.toRefreshTokenBody() = RefreshTokenBody(refreshToken, username) @Suppress("NOTHING_TO_INLINE") private inline fun String?.firstTwoCharactersAndLastTwoCharacters(): String? { From 7b38839f099a90b6d7b2210be1d52757eae043be Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Mon, 20 Jun 2022 14:37:26 +0700 Subject: [PATCH 5/5] spotless --- .../hoc081098/refreshtokensample/data/local/UserLocalSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt index 03d1163..5cca6c7 100644 --- a/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt +++ b/app/src/main/java/com/hoc081098/refreshtokensample/data/local/UserLocalSource.kt @@ -1,7 +1,7 @@ package com.hoc081098.refreshtokensample.data.local -import kotlinx.coroutines.flow.Flow import java.io.IOException +import kotlinx.coroutines.flow.Flow interface UserLocalSource { fun user(): Flow