Skip to content

Commit

Permalink
Initial implementation of a (very) basic auth screen (#1)
Browse files Browse the repository at this point in the history
* More version catalogging

* Add koin-android lib

* Create new auth feature module

* Temporarily wire up auth screen

* Allow cleartext traffic to truenas.local

* Add missing contentTypes

* Add missing gitignores

* Wire up auth screen to check credentials

* String-ify

* Create & use ApiStateProvider for storing auth token and base URL

* Add Ktor logging

* Fix for server address

* Create token if username/password are valid

* More fixes for base URL

* No autocorrect for Uri

* Wire up navigation
  • Loading branch information
boswelja authored Apr 18, 2023
1 parent ede365d commit 79ff25d
Show file tree
Hide file tree
Showing 26 changed files with 501 additions and 66 deletions.
1 change: 1 addition & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .idea/.name

This file was deleted.

3 changes: 3 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 14 additions & 10 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,23 @@ android {
}

dependencies {
implementation(projects.core.api)
implementation(projects.features.auth)

implementation(libs.androidx.core)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.activity.compose)
implementation(libs.androidx.lifecycle.runtime)

implementation(libs.koin.android)

// Compose
implementation(libs.accompanist.navigation)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3)
implementation(libs.bundles.compose)
debugImplementation(libs.bundles.compose.tooling)
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.compose.ui.test.junit4)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.compose.ui.test.junit4)
debugImplementation(libs.compose.ui.tooling)
debugImplementation(libs.compose.ui.test.manifest)
}
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".MainApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -11,6 +12,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TrueManager"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31">
<activity
android:name=".MainActivity"
Expand Down
42 changes: 19 additions & 23 deletions app/src/main/java/com/boswelja/truemanager/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,40 @@ package com.boswelja.truemanager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.boswelja.truemanager.auth.authNavigation
import com.boswelja.truemanager.ui.theme.TrueManagerTheme
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TrueManagerTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
MainScreen()
}
}
}
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
TrueManagerTheme {
Greeting("Android")
fun MainScreen() {
val navController = rememberAnimatedNavController()
Scaffold {
AnimatedNavHost(
navController = navController,
startDestination = "auth",
modifier = Modifier.fillMaxSize().padding(it)
) {
authNavigation("auth")
}
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/com/boswelja/truemanager/MainApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.boswelja.truemanager

import android.app.Application
import com.boswelja.truemanager.auth.authModule
import com.boswelja.truemanager.core.api.v2.apiV2Module
import org.koin.core.context.startKoin

class MainApplication : Application() {

override fun onCreate() {
super.onCreate()

startKoin {
modules(apiV2Module)

modules(
authModule,
)
}
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">truenas.local</domain>
</domain-config>
</network-security-config>
1 change: 1 addition & 0 deletions core/api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
14 changes: 8 additions & 6 deletions core/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
kotlin("plugin.serialization") version "1.8.20"
alias(libs.plugins.kotlin.serialization)
}

android {
Expand Down Expand Up @@ -37,10 +37,12 @@ android {
}

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
implementation("io.ktor:ktor-client-android:2.2.4")
implementation("io.ktor:ktor-client-content-negotiation:2.2.4")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
// Ktor
implementation(libs.bundles.ktor.client.android)
debugImplementation(libs.ktor.logging)

implementation("io.insert-koin:koin-core:3.4.0")
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)

implementation(libs.koin.core)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.boswelja.truemanager.core.api.v2

interface ApiStateProvider {
var serverAddress: String?

var sessionToken: String?
}

internal class InMemoryApiStateProvider : ApiStateProvider {
override var serverAddress: String? = null
set(value) {
if (field != value) {
if (value == null) {
field = null
return
}

field = "${value.removeSuffix("/")}/api/v2.0/"
}
}

override var sessionToken: String? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,32 @@ import com.boswelja.truemanager.core.api.v2.reporting.ReportingV2ApiImpl
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.bearerAuth
import io.ktor.serialization.kotlinx.json.json
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module

val apiV2Module = module {
// API state
singleOf(::InMemoryApiStateProvider) bind ApiStateProvider::class

// Ktor client
single {
val apiStateProvider: ApiStateProvider = get()
HttpClient(Android) {
install(ContentNegotiation)
// TODO if debug, BuildConfig appears to be missing
install(Logging)

install(ContentNegotiation) {
json()
}
defaultRequest {
apiStateProvider.sessionToken?.let { bearerAuth(it) }
apiStateProvider.serverAddress?.let { url(it) }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import io.ktor.client.request.basicAuth
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand All @@ -16,15 +18,17 @@ internal class AuthV2ApiImpl(
private val client: HttpClient
) : AuthV2Api {
override suspend fun checkPassword(username: String, password: String): Boolean {
val response = client.post("/auth/check_password") {
val response = client.post("auth/check_password") {
contentType(ContentType.Application.Json)
setBody(UserCredentialsDto(username, password))
basicAuth(username, password)
}
return response.status == HttpStatusCode.OK
}

override suspend fun checkUser(username: String, password: String): Boolean {
val response = client.post("/auth/check_user") {
val response = client.post("auth/check_user") {
contentType(ContentType.Application.Json)
setBody(UserCredentialsDto(username, password))
basicAuth(username, password)
}
Expand All @@ -38,22 +42,24 @@ internal class AuthV2ApiImpl(
attrs: Map<String, Any>,
matchOrigin: Boolean
): String {
val response = client.post("/auth/generate_token") {
val response = client.post("auth/generate_token") {
contentType(ContentType.Application.Json)
setBody(SessionTokenRequestDto(timeToLive.inWholeSeconds, attrs, matchOrigin))
basicAuth(username, password)
}
return response.body()
}

override suspend fun terminateSession(sessionId: String) {
val response = client.post("/auth/generate_token") {
val response = client.post("auth/generate_token") {
contentType(ContentType.Text.Plain)
setBody(sessionId)
}
return response.body()
}

override suspend fun twoFactorAuth(): Boolean {
val response = client.get("/auth/two_factor_auth")
val response = client.get("auth/two_factor_auth")
return response.body()
}
}
Expand Down
Loading

0 comments on commit 79ff25d

Please sign in to comment.