Skip to content

Commit

Permalink
Google auth v1 (#53)
Browse files Browse the repository at this point in the history
* Impl web local storage

* WIP: Session

* Support more LocalStorage operations

* WIP: Google auth

* WIP: Google auth redirect fix

* WIP: Fix Google OAuth2

* WIP: Fix idToken parsing

* WIP: Fix fetching public profile

* Working Google login

* Improve logs

* Fix Google login

* Handle prod URL

* Implement logout
  • Loading branch information
ILIYANGERMANOV authored Nov 30, 2024
1 parent 13f722f commit 6348daf
Show file tree
Hide file tree
Showing 31 changed files with 363 additions and 66 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ Learn programming by thinking.

You need to have a PostgreSQL database.

```
```zshrc
brew install postgresql
brew services start postgresql@14
psql -U postgres -c "CREATE DATABASE ivy_learn;"
psql -d ivy_learn -c "CREATE USER postgres WITH PASSWORD 'password';"
psql -d ivy_learn -c "ALTER USER postgres WITH SUPERUSER;"
```

**(optional) Drop local database:**

```zshrc
psql -U postgres -c "DROP DATABASE ivy_learn;"
psql -U postgres -c "CREATE DATABASE ivy_learn;"
```

**Environment Variables**

```zshrc
Expand Down
5 changes: 5 additions & 0 deletions composeApp/src/commonMain/kotlin/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import domain.di.DomainModule
import ivy.di.Di
import ivy.di.Di.register
import ivy.di.SharedModule
import ivy.di.autowire.autoWire
import navigation.Navigation
import org.jetbrains.compose.ui.tooling.preview.Preview
import ui.theme.LearnTheme
Expand All @@ -30,6 +31,7 @@ fun App() {
)
Di.appScope {
register { uriHandler }
autoWire(::AppViewModel)
}
initialized = true
}
Expand All @@ -43,6 +45,9 @@ fun App() {

@Composable
private fun NavGraph() {
val appViewModel = remember { Di.get<AppViewModel>() }
appViewModel.Init()

val navigation = remember { Di.get<Navigation>() }
Box(modifier = Modifier.fillMaxSize()) {
navigation.NavHost()
Expand Down
3 changes: 2 additions & 1 deletion composeApp/src/commonMain/kotlin/AppConfiguration.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
class AppConfiguration {
val fakesEnabled = true
val fakesEnabled = false
val useLocalServer = true
}
34 changes: 34 additions & 0 deletions composeApp/src/commonMain/kotlin/AppViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import domain.SessionManager
import ivy.model.auth.SessionToken
import navigation.Navigation
import ui.screen.home.HomeScreen

class AppViewModel(
private val sessionManager: SessionManager,
private val platform: Platform,
private val navigation: Navigation,
) {

@Composable
fun Init() {
LaunchedEffect(Unit) {
redirectLoggedUsers()
}
}

private suspend fun redirectLoggedUsers() {
if (sessionManager.getSession() != null) {
// already logged
navigation.replaceWith(HomeScreen())
return
}

val sessionTokenParam = platform.getUrlParam(IvyConstants.SessionTokenParam)
if (sessionTokenParam != null) {
sessionManager.authenticate(SessionToken(sessionTokenParam))
navigation.replaceWith(HomeScreen())
}
}
}
3 changes: 3 additions & 0 deletions composeApp/src/commonMain/kotlin/data/di/DataModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import data.LessonRepository
import data.LessonRepositoryImpl
import data.TopicsRepository
import data.fake.FakeLessonRepository
import data.storage.LocalStorage
import data.storage.localStorage
import di.bindWithFake
import ivy.di.Di
import ivy.di.Di.register
Expand All @@ -25,5 +27,6 @@ object DataModule : Di.Module {
}
}
bindWithFake<LessonRepository, LessonRepositoryImpl, FakeLessonRepository>()
register<LocalStorage> { localStorage() }
}
}
18 changes: 18 additions & 0 deletions composeApp/src/commonMain/kotlin/data/storage/LocalStorage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package data.storage

interface LocalStorage {
suspend fun putString(key: String, value: String)
suspend fun getString(key: String): String?
suspend fun putInt(key: String, value: Int)
suspend fun getInt(key: String): Int?
suspend fun putDouble(key: String, value: Double)
suspend fun getDouble(key: String): Double?
suspend fun putBoolean(key: String, value: Boolean)
suspend fun getBoolean(key: String): Boolean?

suspend fun remove(key: String)
suspend fun removeAll()
suspend fun keys(): List<String>
}

expect fun localStorage(): LocalStorage
13 changes: 12 additions & 1 deletion composeApp/src/commonMain/kotlin/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import ivy.data.LocalServerUrlProvider
import ivy.data.ServerUrlProvider
import ivy.di.Di
import ivy.di.Di.register
import ivy.di.Di.singleton
import ivy.di.autowire.autoWireSingleton
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import navigation.Navigation
import navigation.systemNavigation
import util.DispatchersProvider
Expand All @@ -20,7 +24,14 @@ object AppModule : Di.Module {
autoWireSingleton(::Navigation)
autoWireSingleton(::AppConfiguration)
register<DispatchersProvider> { DispatchersProviderImpl() }
bindWithFake<ServerUrlProvider, HerokuServerUrlProvider, LocalServerUrlProvider>()
register<ServerUrlProvider> {
if (Di.get<AppConfiguration>().useLocalServer) {
Di.get<LocalServerUrlProvider>()
} else {
Di.get<HerokuServerUrlProvider>()
}
}
singleton { CoroutineScope(Dispatchers.Main + CoroutineName("App")) }
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions composeApp/src/commonMain/kotlin/domain/SessionManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package domain

import data.storage.LocalStorage
import ivy.model.auth.SessionToken

class SessionManager(
private val localStorage: LocalStorage,
) {
private var sessionToken: SessionToken? = null

suspend fun authenticate(token: SessionToken) {
localStorage.putString(SESSION_TOKEN_KEY, token.value)
sessionToken = token
}

suspend fun getSession(): SessionToken? {
return sessionToken ?: localStorage.getString(SESSION_TOKEN_KEY)
?.let(::SessionToken)
?.also {
sessionToken = it
}
}

suspend fun logout() {
sessionToken = null
localStorage.remove(SESSION_TOKEN_KEY)
}

companion object {
const val SESSION_TOKEN_KEY = "sessionToken"
}
}
3 changes: 3 additions & 0 deletions composeApp/src/commonMain/kotlin/domain/di/DomainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ package domain.di
import di.bindWithFake
import domain.GoogleAuthenticationUseCase
import domain.GoogleAuthenticationUseCaseImpl
import domain.SessionManager
import domain.fake.FakeGoogleAuthenticationUseCase
import ivy.di.Di
import ivy.di.autowire.autoWire
import ivy.di.autowire.autoWireSingleton

object DomainModule : Di.Module {
override fun init() = Di.appScope {
autoWire(::GoogleAuthenticationUseCaseImpl)
autoWire(::FakeGoogleAuthenticationUseCase)
bindWithFake<GoogleAuthenticationUseCase, GoogleAuthenticationUseCaseImpl, FakeGoogleAuthenticationUseCase>()
autoWireSingleton(::SessionManager)
}
}
2 changes: 0 additions & 2 deletions composeApp/src/commonMain/kotlin/navigation/Navigation.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package navigation

import Platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -9,7 +8,6 @@ import ui.screen.NotFoundPage

class Navigation(
private val systemNavigation: SystemNavigation,
private val platform: Platform,
) {
@Composable
fun NavHost() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ fun HomeContent(
onEvent: (HomeViewEvent) -> Unit
) {
LearnScaffold(
backButton = BackButton(
onBackClick = {
onEvent(HomeViewEvent.OnBackClick)
}
),
backButton = null,
title = "Learn",
actions = {
SettingsButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import navigation.Screen
import ui.screen.intro.composable.IntroContent

object IntroRouter : Router<IntroScreen> {
const val PATH = ""
const val PATH = "intro"

override fun fromRoute(route: Route): Option<IntroScreen> = option {
ensure(route.path == PATH)
ensure(route.path == PATH || route.path == "")
IntroScreen()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package ui.screen.settings

import androidx.compose.runtime.*
import androidx.compose.ui.platform.UriHandler
import domain.SessionManager
import ivy.IvyUrls
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import navigation.Navigation
import ui.ComposeViewModel
import ui.screen.intro.IntroScreen

class SettingsViewModel(
private val navigation: Navigation,
private val uriHandler: UriHandler
private val uriHandler: UriHandler,
private val sessionManager: SessionManager,
private val scope: CoroutineScope,
) : ComposeViewModel<SettingsViewState, SettingsViewEvent> {
private var soundEnabled by mutableStateOf(true)
private var deleteDialog by mutableStateOf<DeleteDialogViewState?>(null)
Expand Down Expand Up @@ -40,7 +46,7 @@ class SettingsViewModel(
SettingsViewEvent.OnPremiumClick -> handlePremiumClick()
is SettingsViewEvent.OnSoundEnabledChange -> handleSoundEnabledChange(event)
SettingsViewEvent.OnPrivacyClick -> handlePrivacyClick()
SettingsViewEvent.OnLogOutClick -> handleLogOutClick()
SettingsViewEvent.OnLogoutClick -> handleLogoutClick()
SettingsViewEvent.OnDeleteAccountClick -> handleDeleteAccountClick()
SettingsViewEvent.OnTermsOfUseClick -> handleTermsOfUseClick()
SettingsViewEvent.OnPrivacyPolicyClick -> handlePrivacyPolicyClick()
Expand All @@ -65,8 +71,12 @@ class SettingsViewModel(
// TODO - handle event
}

private fun handleLogOutClick() {
// TODO - handle event
private fun handleLogoutClick() {
println("On logout click")
scope.launch {
sessionManager.logout()
navigation.replaceWith(IntroScreen())
}
}

private fun handleTermsOfUseClick() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ sealed interface SettingsViewEvent {
data object OnPremiumClick : SettingsViewEvent
data class OnSoundEnabledChange(val enabled: Boolean) : SettingsViewEvent
data object OnPrivacyClick : SettingsViewEvent
data object OnLogOutClick : SettingsViewEvent
data object OnLogoutClick : SettingsViewEvent
data object OnDeleteAccountClick : SettingsViewEvent
data object OnTermsOfUseClick : SettingsViewEvent
data object OnPrivacyPolicyClick : SettingsViewEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ fun SettingsContent(
key = "spacer 1",
height = 8.dp
)
logOutButton(
logoutButton(
onLogOutClick = {
onEvent(SettingsViewEvent.OnLogOutClick)
onEvent(SettingsViewEvent.OnLogoutClick)
}
)
spacerItem(
Expand Down Expand Up @@ -192,7 +192,7 @@ private fun LazyListScope.privacyButton(
}
}

private fun LazyListScope.logOutButton(
private fun LazyListScope.logoutButton(
onLogOutClick: () -> Unit
) {
item(key = "log-out") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package data.storage

actual fun localStorage(): LocalStorage {
TODO("Not yet implemented")
}
Loading

0 comments on commit 6348daf

Please sign in to comment.