Implement updated authentication

This commit is contained in:
Billy Brawner 2019-10-31 09:40:54 -06:00
parent 6f24feed7b
commit 74601bc18d
21 changed files with 64 additions and 73 deletions

View file

@ -20,8 +20,9 @@ android {
useSupportLibrary = true useSupportLibrary = true
} }
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "API_URL", "\"http://192.168.86.92:8080/\"" // buildConfigField "String", "API_URL", "\"http://192.168.86.92:8080/\""
// buildConfigField "String", "API_URL", "\"http://10.0.2.2:8080/\"" // buildConfigField "String", "API_URL", "\"http://10.0.2.2:8080/\""
buildConfigField "String", "API_URL", "\"https://budget-api.intra.wbrawner.com/\""
} }
buildTypes { buildTypes {
release { release {
@ -38,7 +39,7 @@ android {
dependencies { dependencies {
implementation project(':common') implementation project(':common')
implementation project(':budgetlib') implementation project(':budgetlib')
implementation project(':auth') implementation project(':storage')
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'

View file

@ -1,8 +1,8 @@
package com.wbrawner.budget.di package com.wbrawner.budget.di
import android.content.Context import android.content.Context
import com.wbrawner.budget.auth.AuthModule
import com.wbrawner.budget.lib.network.NetworkModule import com.wbrawner.budget.lib.network.NetworkModule
import com.wbrawner.budget.storage.StorageModule
import com.wbrawner.budget.ui.MainActivity import com.wbrawner.budget.ui.MainActivity
import com.wbrawner.budget.ui.SplashActivity import com.wbrawner.budget.ui.SplashActivity
import com.wbrawner.budget.ui.SplashViewModelMapper import com.wbrawner.budget.ui.SplashViewModelMapper
@ -29,7 +29,7 @@ import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@Component(modules = [AuthModule::class, AppModule::class, NetworkModule::class]) @Component(modules = [StorageModule::class, AppModule::class, NetworkModule::class])
interface AppComponent { interface AppComponent {
fun inject(fragment: OverviewFragment) fun inject(fragment: OverviewFragment)
fun inject(fragment: LoginFragment) fun inject(fragment: LoginFragment)

View file

@ -2,9 +2,8 @@ package com.wbrawner.budget.ui
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.wbrawner.budget.auth.CredentialsProvider
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.User import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.common.user.UserRepository
import com.wbrawner.budget.di.ViewModelKey import com.wbrawner.budget.di.ViewModelKey
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -12,14 +11,13 @@ import dagger.multibindings.IntoMap
import javax.inject.Inject import javax.inject.Inject
class SplashViewModel @Inject constructor( class SplashViewModel @Inject constructor(
private val credentialsProvider: CredentialsProvider, private val userRepository: UserRepository
private val budgetRepository: BudgetRepository
) : ViewModel() { ) : ViewModel() {
val isLoading: MutableLiveData<Boolean> = MutableLiveData(false) val isLoading: MutableLiveData<Boolean> = MutableLiveData(false)
suspend fun checkForExistingCredentials(): User? { suspend fun checkForExistingCredentials(): User? {
return try { return try {
login(credentialsProvider.username, credentialsProvider.password) userRepository.getProfile()
} catch (ignored: Exception) { } catch (ignored: Exception) {
null null
} }
@ -27,9 +25,8 @@ class SplashViewModel @Inject constructor(
suspend fun login(username: String, password: String): User { suspend fun login(username: String, password: String): User {
isLoading.value = true isLoading.value = true
credentialsProvider.saveCredentials(username, password)
return try { return try {
budgetRepository.login(username, password) userRepository.login(username, password)
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
isLoading.value = false isLoading.value = false
throw e throw e

View file

@ -1,25 +0,0 @@
package com.wbrawner.budget.auth
import android.content.SharedPreferences
interface CredentialsProvider {
val username: String
val password: String
fun saveCredentials(username: String, password: String)
}
private const val PREF_KEY_USERNAME = "PREF_KEY_USERNAME"
private const val PREF_KEY_PASSWORD = "PREF_KEY_PASSWORD"
class SharedPreferencesCredentialsProvider(private val sharedPreferences: SharedPreferences) : CredentialsProvider {
override val username: String = sharedPreferences.getString(PREF_KEY_USERNAME, null) ?: ""
override val password: String = sharedPreferences.getString(PREF_KEY_PASSWORD, null) ?: ""
override fun saveCredentials(username: String, password: String) {
sharedPreferences.edit()
.putString(PREF_KEY_USERNAME, username)
.putString(PREF_KEY_PASSWORD, password)
.apply()
}
}

View file

@ -25,7 +25,7 @@ android {
dependencies { dependencies {
implementation project(':common') implementation project(':common')
implementation project(':auth') implementation project(':storage')
// Retrofit // Retrofit
api "com.squareup.retrofit2:retrofit:$rootProject.ext.retrofit" api "com.squareup.retrofit2:retrofit:$rootProject.ext.retrofit"
implementation "com.squareup.retrofit2:converter-moshi:$rootProject.ext.retrofit" implementation "com.squareup.retrofit2:converter-moshi:$rootProject.ext.retrofit"

View file

@ -95,6 +95,9 @@ interface BudgetApiService {
@POST("users/login") @POST("users/login")
suspend fun login(@Body request: LoginRequest): User suspend fun login(@Body request: LoginRequest): User
@GET("users/me")
suspend fun getProfile(): User
@GET("users/search") @GET("users/search")
suspend fun searchUsers(@Query("query") query: String): List<User> suspend fun searchUsers(@Query("query") query: String): List<User>

View file

@ -1,8 +1,9 @@
package com.wbrawner.budget.lib.network package com.wbrawner.budget.lib.network
import android.content.SharedPreferences
import android.util.Base64
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
import com.wbrawner.budget.auth.CredentialsProvider
import com.wbrawner.budget.common.budget.BudgetRepository import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.category.CategoryRepository import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.common.transaction.TransactionRepository import com.wbrawner.budget.common.transaction.TransactionRepository
@ -13,10 +14,13 @@ import com.wbrawner.budget.lib.repository.NetworkTransactionRepository
import com.wbrawner.budget.lib.repository.NetworkUserRepository import com.wbrawner.budget.lib.repository.NetworkUserRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okhttp3.Credentials import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import java.nio.charset.Charset
import java.util.* import java.util.*
import javax.inject.Named import javax.inject.Named
@ -28,25 +32,39 @@ class NetworkModule {
.build() .build()
@Provides @Provides
fun provideOkHttpClient(credentialsProvider: CredentialsProvider): OkHttpClient = OkHttpClient.Builder() fun provideCookieJar(sharedPreferences: SharedPreferences): CookieJar {
.addInterceptor { return object : CookieJar {
if (it.request().headers().get("Authorization") != null) override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
return@addInterceptor it.proceed(it.request()) sharedPreferences.edit()
val credentials = Credentials.basic( .putString(
credentialsProvider.username, url.host(),
credentialsProvider.password cookies.joinToString(separator = ",") {
) Base64.encode(it.toString().toByteArray(), 0)
val newHeaders = it.request() .toString(charset = Charset.forName("UTF-8"))
.headers() }
.newBuilder() )
.add("Authorization: $credentials") .apply()
.build()
val newRequest = it.request()
.newBuilder()
.headers(newHeaders)
.build()
it.proceed(newRequest)
} }
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return sharedPreferences.getString(url.host(), "")
?.split(",")
?.mapNotNull {
Cookie.parse(
url,
Base64.decode(it, 0).toString(Charset.forName("UTF-8"))
)
}
?.toMutableList()
?: mutableListOf()
}
}
}
@Provides
fun provideOkHttpClient(cookieJar: CookieJar): OkHttpClient = OkHttpClient.Builder()
.cookieJar(cookieJar)
// TODO: Add Gander interceptor
.build() .build()
@Provides @Provides

View file

@ -2,8 +2,6 @@ package com.wbrawner.budget.lib.repository
import com.wbrawner.budget.common.budget.Budget import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.LoginRequest
import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.lib.network.BudgetApiService import com.wbrawner.budget.lib.network.BudgetApiService
import javax.inject.Inject import javax.inject.Inject
@ -21,9 +19,6 @@ class NetworkBudgetRepository @Inject constructor(private val apiService: Budget
override suspend fun delete(id: Long) = apiService.deleteBudget(id) override suspend fun delete(id: Long) = apiService.deleteBudget(id)
override suspend fun login(username: String, password: String): User =
apiService.login(LoginRequest(username, password))
override suspend fun getBalance(id: Long): Long = apiService.getBudgetBalance(id).balance override suspend fun getBalance(id: Long): Long = apiService.getBudgetBalance(id).balance
} }

View file

@ -1,11 +1,18 @@
package com.wbrawner.budget.lib.repository package com.wbrawner.budget.lib.repository
import com.wbrawner.budget.common.user.LoginRequest
import com.wbrawner.budget.common.user.User import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.common.user.UserRepository import com.wbrawner.budget.common.user.UserRepository
import com.wbrawner.budget.lib.network.BudgetApiService import com.wbrawner.budget.lib.network.BudgetApiService
import javax.inject.Inject import javax.inject.Inject
class NetworkUserRepository @Inject constructor(private val apiService: BudgetApiService) : UserRepository { class NetworkUserRepository @Inject constructor(private val apiService: BudgetApiService) : UserRepository {
override suspend fun login(username: String, password: String): User =
apiService.login(LoginRequest(username, password))
override suspend fun getProfile(): User = apiService.getProfile()
override suspend fun create(newItem: User): User = apiService.newUser(newItem) override suspend fun create(newItem: User): User = apiService.newUser(newItem)
override suspend fun findAll(accountId: Long?): Collection<User> = apiService.getUsers(accountId) override suspend fun findAll(accountId: Long?): Collection<User> = apiService.getUsers(accountId)

View file

@ -10,7 +10,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.android.tools.build:gradle:3.5.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
classpath 'io.fabric.tools:gradle:1.31.0' classpath 'io.fabric.tools:gradle:1.31.0'

View file

@ -1,9 +1,7 @@
package com.wbrawner.budget.common.budget package com.wbrawner.budget.common.budget
import com.wbrawner.budget.common.Repository import com.wbrawner.budget.common.Repository
import com.wbrawner.budget.common.user.User
interface BudgetRepository : Repository<Budget, Long> { interface BudgetRepository : Repository<Budget, Long> {
suspend fun login(username: String, password: String): User
suspend fun getBalance(id: Long): Long suspend fun getBalance(id: Long): Long
} }

View file

@ -3,6 +3,8 @@ package com.wbrawner.budget.common.user
import com.wbrawner.budget.common.Repository import com.wbrawner.budget.common.Repository
interface UserRepository : Repository<User, Long> { interface UserRepository : Repository<User, Long> {
suspend fun login(username: String, password: String): User
suspend fun getProfile(): User
suspend fun findAll(accountId: Long? = null): Collection<User> suspend fun findAll(accountId: Long? = null): Collection<User>
suspend fun findAllByNameLike(query: String): Collection<User> suspend fun findAllByNameLike(query: String): Collection<User>
} }

View file

@ -1 +1 @@
include ':app', ':budgetlib', ':common', ':auth' include ':app', ':budgetlib', ':common', ':storage'

View file

@ -1,4 +1,4 @@
package com.wbrawner.budget.auth package com.wbrawner.budget.storage
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
@ -10,12 +10,7 @@ import javax.inject.Singleton
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "shared-prefs.encrypted" const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "shared-prefs.encrypted"
@Module @Module
class AuthModule { class StorageModule {
@Singleton
@Provides
fun provideCredentialsProvider(sharedPreferences: SharedPreferences): CredentialsProvider =
SharedPreferencesCredentialsProvider(sharedPreferences)
@Provides @Provides
@Singleton @Singleton
fun provideSharedPreferences(context: Context): SharedPreferences = fun provideSharedPreferences(context: Context): SharedPreferences =