Skip to content

Commit

Permalink
Merge pull request #21 from ryan-conway/feature/auth_interceptor
Browse files Browse the repository at this point in the history
Feature/auth interceptor
  • Loading branch information
ryan-conway authored Nov 25, 2021
2 parents 2b006c6 + 21b069d commit 19ad974
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 88 deletions.
50 changes: 42 additions & 8 deletions app/src/main/java/com/example/nimblesurveys/di/ApiModule.kt
Original file line number Diff line number Diff line change
@@ -1,36 +1,70 @@
package com.example.nimblesurveys.di

import com.example.nimblesurveys.BuildConfig
import com.example.nimblesurveys.data.api.auth.AuthInterceptor
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

@Provides
fun provideRetrofit(): Retrofit {
fun provideLoggingInterceptor(): Interceptor {
return HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
}

@Provides
@Singleton
fun provideAuthInterceptor(): AuthInterceptor {
return AuthInterceptor()
}

@Provides
fun provideOkHttpClient(
loggingInterceptor: Interceptor,
authInterceptor: AuthInterceptor,
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor)
.build()
}

@Provides
fun provideConverterFactory(): Converter.Factory {
val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
val interceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
return MoshiConverterFactory.create(moshi)
}

@Provides
fun provideRetrofit(
okHttpClient: OkHttpClient,
converterFactory: Converter.Factory
): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addConverterFactory(converterFactory)
.build()
}
}
6 changes: 3 additions & 3 deletions app/src/main/java/com/example/nimblesurveys/di/AuthModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.example.nimblesurveys.di
import com.example.nimblesurveys.BuildConfig
import com.example.nimblesurveys.data.api.ApiCredential
import com.example.nimblesurveys.data.api.auth.AuthApiService
import com.example.nimblesurveys.data.api.auth.AuthInterceptor
import com.example.nimblesurveys.data.cache.SurveyDatabase
import com.example.nimblesurveys.data.repository.AuthRepositoryImpl
import com.example.nimblesurveys.domain.provider.TimeProvider
import com.example.nimblesurveys.domain.repository.AuthRepository
import com.example.nimblesurveys.domain.usecase.GetUserUseCase
import com.example.nimblesurveys.domain.usecase.IsLoggedInUseCase
Expand Down Expand Up @@ -33,13 +33,13 @@ object AuthModule {
database: SurveyDatabase,
retrofit: Retrofit,
apiCredential: ApiCredential,
timeProvider: TimeProvider
authInterceptor: AuthInterceptor
): AuthRepository {
return AuthRepositoryImpl(
database.authDao(),
retrofit.create(AuthApiService::class.java),
apiCredential,
timeProvider,
authInterceptor,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.example.nimblesurveys.data.api.auth
import com.squareup.moshi.Json

data class AccessTokenRequest(
@Json(name = "grant_type") val grantType: String = "password",
@Json(name = "grant_type") val grantType: String = "refresh_token",
@Json(name = "refresh_token") val refreshToken: String,
@Json(name = "client_id") val clientId: String,
@Json(name = "client_secret") val clientSecret: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ package com.example.nimblesurveys.data.api.auth
import com.example.nimblesurveys.data.api.response.ApiResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.POST

interface AuthApiService {

@POST("api/v1/oauth/token")
@Headers("No-Authentication: true")
suspend fun signIn(
@Body signInRequest: SignInRequest
): ApiResponse<SignInAttributes>

@POST("api/v1/oauth/token")
@Headers("No-Authentication: true")
suspend fun getAccessToken(
@Body accessTokenRequest: AccessTokenRequest
): ApiResponse<AccessTokenAttributes>

@GET("api/v1/me")
suspend fun getUser(
@Header("Authorization") authorization: String
): ApiResponse<UserAttributes>
suspend fun getUser(): ApiResponse<UserAttributes>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.nimblesurveys.data.api.auth

import com.example.nimblesurveys.domain.exception.UnauthorizedException
import com.example.nimblesurveys.domain.model.Token
import okhttp3.Interceptor
import okhttp3.Response

class AuthInterceptor: Interceptor {

private var token: Token? = null

override fun intercept(chain: Interceptor.Chain): Response {
val hasAuth = chain.request().header("No-Authentication") == null
val requestBuilder = chain.request().newBuilder()
if (hasAuth) {
val token = token
if (token == null) {
throw UnauthorizedException()
} else {
val authorization = "${token.tokenType} ${token.accessToken}"
requestBuilder.addHeader("Authorization", authorization)
}
}

return chain.proceed(requestBuilder.build())
}

fun setAccessToken(token: Token) {
this.token = token
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.example.nimblesurveys.data.api.survey

import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query

interface SurveyApiService {

@GET("api/v1/surveys")
suspend fun getSurveys(
@Header("Authorization") authorization: String,
@Query("page[number]") page: Int = 1,
@Query("page[size]") pageSize: Int
): SurveyListResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ package com.example.nimblesurveys.data.repository
import com.example.nimblesurveys.data.adapter.toEntity
import com.example.nimblesurveys.data.adapter.toToken
import com.example.nimblesurveys.data.api.ApiCredential
import com.example.nimblesurveys.data.api.auth.AccessTokenAttributes
import com.example.nimblesurveys.data.api.auth.AccessTokenRequest
import com.example.nimblesurveys.data.api.auth.AuthApiService
import com.example.nimblesurveys.data.api.auth.AuthInterceptor
import com.example.nimblesurveys.data.api.auth.SignInRequest
import com.example.nimblesurveys.data.cache.AuthDao
import com.example.nimblesurveys.domain.exception.UnauthorizedException
import com.example.nimblesurveys.domain.model.Token
import com.example.nimblesurveys.domain.model.User
import com.example.nimblesurveys.domain.provider.TimeProvider
import com.example.nimblesurveys.domain.repository.AuthRepository
import retrofit2.HttpException

class AuthRepositoryImpl(
private val authDao: AuthDao,
private val api: AuthApiService,
private val apiCredential: ApiCredential,
private val timeProvider: TimeProvider,
private val authInterceptor: AuthInterceptor
) : AuthRepository {

@Throws(Throwable::class)
Expand All @@ -31,43 +32,38 @@ class AuthRepositoryImpl(
val apiResponse = api.signIn(signInRequest)
val signInResult = apiResponse.data.attributes
authDao.deleteToken()
authDao.insertToken(signInResult.toEntity())
return Token(
tokenType = signInResult.tokenType,
accessToken = signInResult.accessToken,
refreshToken = signInResult.refreshToken,
expiry = signInResult.createdAt + signInResult.expiresIn
)
val tokenEntity = signInResult.toEntity()
authDao.insertToken(tokenEntity)
val token = tokenEntity.toToken()
setInterceptorAuthToken(token)
return token
}

override suspend fun getAccessToken(): Token? {
val cachedToken = authDao.getToken() ?: return null
val tokenEntity =
if (cachedToken.expiry <= timeProvider.getCurrentTime()) {
val newToken = fetchNewToken(cachedToken.refreshToken)
val tokenEntity = newToken.toEntity()
authDao.deleteToken()
authDao.insertToken(tokenEntity)
tokenEntity
} else {
cachedToken
}
return tokenEntity.toToken()
return authDao.getToken()?.toToken()
}

private suspend fun fetchNewToken(refreshToken: String): AccessTokenAttributes {
override suspend fun refreshAccessToken() {
val cachedToken = getAccessToken() ?: throw UnauthorizedException()
val accessTokenRequest = AccessTokenRequest(
refreshToken = refreshToken,
refreshToken = cachedToken.refreshToken,
clientId = apiCredential.key,
clientSecret = apiCredential.secret
)
val apiResponse = api.getAccessToken(accessTokenRequest)
return apiResponse.data.attributes
val apiResponse = try {
api.getAccessToken(accessTokenRequest)
} catch (e: HttpException) {
throw e
}
val tokenEntity = apiResponse.data.attributes.toEntity()
authDao.deleteToken()
authDao.insertToken(tokenEntity)
val token = tokenEntity.toToken()
setInterceptorAuthToken(token)
}

override suspend fun getUser(token: Token): User {
val authorization = "${token.tokenType} ${token.accessToken}"
val apiResponse = api.getUser(authorization)
override suspend fun getUser(): User {
val apiResponse = api.getUser()
val getUserResult = apiResponse.data.attributes
return User(
id = apiResponse.data.id,
Expand All @@ -77,6 +73,16 @@ class AuthRepositoryImpl(
}

override suspend fun isLoggedIn(): Boolean {
return authDao.getToken() != null
val cachedToken = authDao.getToken()?.toToken()
return if (cachedToken == null) {
false
} else {
setInterceptorAuthToken(cachedToken)
true
}
}

private fun setInterceptorAuthToken(token: Token) {
authInterceptor.setAccessToken(token)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@ import com.example.nimblesurveys.data.cache.SurveyDao
import com.example.nimblesurveys.domain.model.Answer
import com.example.nimblesurveys.domain.model.Question
import com.example.nimblesurveys.domain.model.Survey
import com.example.nimblesurveys.domain.model.Token
import com.example.nimblesurveys.domain.repository.SurveyRepository

class SurveyRepositoryImpl(
private val api: SurveyApiService,
private val surveyDao: SurveyDao,
) : SurveyRepository {

override suspend fun getSurveys(token: Token): List<Survey> {
override suspend fun getSurveys(): List<Survey> {
var surveys = getCachedSurveys()
if (surveys.isEmpty()) {
surveys = getNewSurveys(token)
surveys = getNewSurveys()
surveyDao.insertSurveys(surveys.map { it.toEntity() })
surveys = getCachedSurveys()
}
Expand All @@ -29,15 +28,13 @@ class SurveyRepositoryImpl(
return surveyDao.getSurveys().map { it.toSurvey() }
}

private suspend fun getNewSurveys(token: Token): List<Survey> {
private suspend fun getNewSurveys(): List<Survey> {
val surveys = mutableListOf<Survey>()
val authorization = "${token.tokenType} ${token.accessToken}"
var currentPage = 1
var pageCount = 1

while (currentPage <= pageCount) {
val response = api.getSurveys(
authorization = authorization,
page = currentPage,
pageSize = PAGE_SIZE
)
Expand Down
Loading

0 comments on commit 19ad974

Please sign in to comment.