diff --git a/auth/build.gradle b/auth/build.gradle index e8bf564..14b1d1e 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -6,14 +6,14 @@ apply plugin: 'org.jetbrains.dokka' android { namespace 'com.alfresco.auth' defaultConfig { - versionName "0.8.1" + versionName "0.8.2" } } dependencies { implementation libs.appauth implementation libs.jwtdecode - + implementation libs.auth0 implementation libs.kotlin.serialization.json implementation libs.androidx.appcompat diff --git a/auth/src/main/java/com/alfresco/auth/AuthConfig.kt b/auth/src/main/java/com/alfresco/auth/AuthConfig.kt index 463c687..4eec86f 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthConfig.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthConfig.kt @@ -38,7 +38,16 @@ data class AuthConfig( /** * Path to content service */ - var contentServicePath: String + var contentServicePath: String, + + /** + * scheme for Auth0 + */ + var scheme: String = "", + /** + * selected AuthType + */ + var authType: String = "" ) { /** * Convenience method for JSON serialization. @@ -51,7 +60,8 @@ data class AuthConfig( /** * Convenience method for deserializing a JSON string representation. */ - @JvmStatic fun jsonDeserialize(str: String): AuthConfig? { + @JvmStatic + fun jsonDeserialize(str: String): AuthConfig? { return try { Json.decodeFromString(serializer(), str) } catch (ex: SerializationException) { diff --git a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt index ca3631b..f46e150 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt @@ -40,6 +40,7 @@ class AuthInterceptor( provider = when (authType) { AuthType.BASIC -> BasicProvider(stateString) AuthType.PKCE -> PkceProvider(stateString, config) + AuthType.OIDC -> OIDCProvider(stateString) AuthType.UNKNOWN -> PlainProvider() } } @@ -224,10 +225,27 @@ class AuthInterceptor( } } + private inner class OIDCProvider (val accessToken : String) : Provider{ + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(AuthType.OIDC, accessToken) + + // If still error notify listener of failure + if (response.code == HTTP_RESPONSE_401_UNAUTHORIZED) { + listener?.onAuthFailure(accountId, response.request.url.toString()) + } + return response + } + + override fun finish() { + localScope.coroutineContext.cancelChildren() + } + } + private fun Interceptor.Chain.proceed(type: AuthType, token: String?): Response { val headerValue = when (type) { AuthType.BASIC -> "Basic $token" AuthType.PKCE -> "Bearer $token" + AuthType.OIDC -> "bearer $token" AuthType.UNKNOWN -> null } return proceedWithAuthorization(headerValue) diff --git a/auth/src/main/java/com/alfresco/auth/AuthType.kt b/auth/src/main/java/com/alfresco/auth/AuthType.kt index b033db7..4e5d75c 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthType.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthType.kt @@ -15,6 +15,11 @@ enum class AuthType(val value: String) { */ PKCE("pkce"), + /** + * Used to specify the need of SSO auth + */ + OIDC("oidc"), + /** * Used to specify that the auth type is unknown */ diff --git a/auth/src/main/java/com/alfresco/auth/Credentials.kt b/auth/src/main/java/com/alfresco/auth/Credentials.kt index 14ee237..1e5255a 100644 --- a/auth/src/main/java/com/alfresco/auth/Credentials.kt +++ b/auth/src/main/java/com/alfresco/auth/Credentials.kt @@ -17,5 +17,9 @@ data class Credentials( /** * String representation of authentication type. */ - val authType: String + val authType: String, + + val hostName: String = "", + + val clientId: String = "" ) diff --git a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt index 7912713..708985d 100644 --- a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt +++ b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt @@ -2,15 +2,16 @@ package com.alfresco.auth import android.content.Context import android.net.Uri +import com.alfresco.auth.data.AppConfigDetails import com.alfresco.auth.data.ContentServerDetails import com.alfresco.auth.data.ContentServerDetailsData import com.alfresco.auth.pkce.PkceAuthService -import java.net.URL -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import java.net.URL +import java.util.concurrent.TimeUnit /** * Class that facilitates service discovery process. @@ -24,10 +25,27 @@ class DiscoveryService( * Determine which [AuthType] is supported by the [endpoint]. */ suspend fun getAuthType(endpoint: String): AuthType { + + when (authConfig.authType.lowercase()) { + AuthType.OIDC.value -> { + if (isOIDC(endpoint)) { + return AuthType.OIDC + } + } + + AuthType.PKCE.value -> { + if (isPkceType(endpoint)) { + return AuthType.PKCE + } + } + } + return when { isPkceType(endpoint) -> AuthType.PKCE + isOIDC(endpoint) -> AuthType.OIDC + isBasicType(endpoint) -> AuthType.BASIC else -> AuthType.UNKNOWN @@ -62,6 +80,31 @@ class DiscoveryService( } } + suspend fun isOIDCInstalled(appConfigURL: String): Boolean { + val uri = PkceAuthService.discoveryUriWithAuth0(appConfigURL).toString() + return withContext(Dispatchers.IO) { + try { + val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .build() + val request = Request.Builder() + .url(URL(uri)) + .get() + .build() + val response = client.newCall(request).execute() + + if (response.code != 200) return@withContext false + + val body = response.body?.string() ?: "" + val data = AppConfigDetails.jsonDeserialize(body) + return@withContext data?.oauth2?.audience?.isNotBlank() == true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + } + /** * returns content server details based on [endpoint]. */ @@ -97,10 +140,15 @@ class DiscoveryService( val result = try { val authService = PkceAuthService(context, null, authConfig) authService.fetchDiscoveryFromUrl(uri) - } catch (exception: Exception) { null } + } catch (exception: Exception) { + null + } return result != null } + private suspend fun isOIDC(endpoint: String): Boolean = + isOIDCInstalled(endpoint) && authConfig.realm.isBlank() + /** * Return content service url based on [endpoint]. */ @@ -110,6 +158,12 @@ class DiscoveryService( .appendPath(authConfig.contentServicePath) .build() + fun oidcUrl(endpoint: String): Uri = + PkceAuthService.endpointWith(endpoint, authConfig) + .buildUpon() + .appendPath("alfresco") + .build() + private fun contentServiceDiscoveryUrl(endpoint: String): Uri = contentServiceUrl(endpoint) .buildUpon() diff --git a/auth/src/main/java/com/alfresco/auth/data/AppConfigDetails.kt b/auth/src/main/java/com/alfresco/auth/data/AppConfigDetails.kt new file mode 100644 index 0000000..cc6225f --- /dev/null +++ b/auth/src/main/java/com/alfresco/auth/data/AppConfigDetails.kt @@ -0,0 +1,42 @@ +package com.alfresco.auth.data + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +@Serializable +data class OAuth2Data( + val host: String, + val clientId: String, + val secret: String, + val scope: String, + val implicitFlow: Boolean, + val codeFlow: Boolean, + val silentLogin: Boolean, + val publicUrls: List, + val redirectSilentIframeUri: String, + val redirectUri: String, + val logoutUrl: String, + val logoutParameters: List, + val redirectUriLogout: String, + val audience: String, + val skipIssuerCheck: Boolean, + val strictDiscoveryDocumentValidation: Boolean +) + +@Serializable +internal data class AppConfigDetails( + val oauth2: OAuth2Data +) { + companion object { + private val json = Json { ignoreUnknownKeys = true } + + fun jsonDeserialize(str: String): AppConfigDetails? { + return try { + json.decodeFromString(serializer(), str) + } catch (ex: SerializationException) { + null + } + } + } +} diff --git a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt index 9a43067..f1fadd0 100644 --- a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt +++ b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt @@ -4,11 +4,18 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import android.util.Log import com.alfresco.auth.AuthConfig +import com.alfresco.auth.R +import com.alfresco.auth.data.AppConfigDetails +import com.alfresco.auth.data.OAuth2Data +import com.alfresco.auth.ui.AuthenticationActivity +import com.alfresco.auth.ui.EndSessionActivity +import com.auth0.android.Auth0 +import com.auth0.android.authentication.AuthenticationException import com.auth0.android.jwt.JWT -import java.util.Locale -import java.util.concurrent.atomic.AtomicReference -import kotlin.coroutines.resumeWithException +import com.auth0.android.provider.WebAuthProvider +import com.auth0.android.result.Credentials import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -24,6 +31,13 @@ import net.openid.appauth.ResponseTypeValues import net.openid.appauth.TokenResponse import net.openid.appauth.browser.AnyBrowserMatcher import net.openid.appauth.connectivity.ConnectionBuilder +import okhttp3.OkHttpClient +import okhttp3.Request +import java.net.URL +import java.util.Locale +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.resumeWithException internal class PkceAuthService(context: Context, authState: AuthState?, authConfig: AuthConfig) { @@ -64,9 +78,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf serviceConfiguration != null -> { it.resumeWith(Result.success(serviceConfiguration)) } + ex != null -> { it.resumeWithException(ex) } + else -> it.resumeWithException(Exception()) } }, @@ -74,6 +90,31 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf ) } + suspend fun getAppConfigOAuth2Details(appConfigURL: String): OAuth2Data? { + + return withContext(Dispatchers.IO) { + try { + val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .build() + val request = Request.Builder() + .url(URL(appConfigURL)) + .get() + .build() + val response = client.newCall(request).execute() + + if (response.code != 200) return@withContext null + + val body = response.body?.string() ?: "" + val data = AppConfigDetails.jsonDeserialize(body) + data?.oauth2 + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + /** * Initiates the login in [activity] with activity result [requestCode] */ @@ -82,27 +123,93 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf checkConfig(authConfig) // build discovery url using auth configuration - val discoveryUri = discoveryUriWith(endpoint, authConfig) - withContext(Dispatchers.IO) { - val config = fetchDiscoveryFromUrl(discoveryUri) + if (authConfig.realm.isBlank()) { + val uri = discoveryUriWithAuth0(endpoint).toString() + withContext(Dispatchers.IO) { + + val authDetails = getAppConfigOAuth2Details(uri) + + authDetails?.let { oauth2 -> - // save the authorization configuration - authState.set(AuthState(config)) + val credentials = webAuthAsync( + oauth2, + activity + ) - val authRequest = generateAuthorizationRequest(config) - val authIntent = generateAuthIntent(authRequest) + withContext(Dispatchers.Main) { + (activity as AuthenticationActivity<*>).handleResult( + credentials, + authDetails + ) + } + } + + } + } else { + val discoveryUri = discoveryUriWith(endpoint, authConfig) + + withContext(Dispatchers.IO) { + val config = fetchDiscoveryFromUrl(discoveryUri) + + // save the authorization configuration + authState.set(AuthState(config)) + + val authRequest = generateAuthorizationRequest(config) + val authIntent = generateAuthIntent(authRequest) + + withContext(Dispatchers.Main) { + activity.startActivityForResult(authIntent, requestCode) + } + } + } + } + + private suspend fun webAuthAsync( + oauth2: OAuth2Data, + activity: Activity + ): Credentials? { + return try { + val host = URL(oauth2.host).host + + + val account = Auth0(oauth2.clientId, host) + val credentials = WebAuthProvider.login(account) + .withTrustedWebActivity() + .withScheme(activity.getString(R.string.com_auth0_scheme)) + .withScope(oauth2.scope) + .withAudience(oauth2.audience) + .await(activity) + credentials + } catch (error: AuthenticationException) { + val message = + if (error.isCanceled) "Browser was closed" else error.getDescription() + null + } + } + + + suspend fun logoutAuth0(hostName: String, clientId: String, activity: Activity, requestCode: Int) { + withContext(Dispatchers.IO) { + + val account = Auth0(clientId, hostName) + WebAuthProvider.logout(account) + .withScheme(activity.getString(R.string.com_auth0_scheme)) + .await(activity) withContext(Dispatchers.Main) { - activity.startActivityForResult(authIntent, requestCode) + (activity as EndSessionActivity<*>).handleResult(requestCode) } + + } } fun initiateReLogin(activity: Activity, requestCode: Int) { requireNotNull(authState.get()) - val authRequest = generateAuthorizationRequest(authState.get().authorizationServiceConfiguration!!) + val authRequest = + generateAuthorizationRequest(authState.get().authorizationServiceConfiguration!!) val authIntent = generateAuthIntent(authRequest) activity.startActivityForResult(authIntent, requestCode) @@ -154,9 +261,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf response != null -> { it.resumeWith(Result.success(authState.get().jsonSerializeString())) } + ex != null -> { it.resumeWithException(ex) } + else -> it.resumeWithException(Exception()) } } @@ -177,9 +286,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf response != null -> { it.resumeWith(Result.success(response)) } + ex != null -> { it.resumeWithException(ex) } + else -> it.resumeWithException(Exception()) } } @@ -250,10 +361,12 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf * @throws [IllegalArgumentException] */ private fun checkConfig(authConfig: AuthConfig) { - require(authConfig.contentServicePath.isNotBlank()) { "Content service path is blank or empty" } - require(authConfig.realm.isNotBlank()) { "Realm is blank or empty" } - require(authConfig.clientId.isNotBlank()) { "Client id is blank or empty" } - require(authConfig.redirectUrl.isNotBlank()) { "Redirect url is blank or empty" } + if (authConfig.scheme.isBlank()) { + require(authConfig.contentServicePath.isNotBlank()) { "Content service path is blank or empty" } + require(authConfig.realm.isNotBlank()) { "Realm is blank or empty" } + require(authConfig.clientId.isNotBlank()) { "Client id is blank or empty" } + require(authConfig.redirectUrl.isNotBlank()) { "Redirect url is blank or empty" } + } } companion object { @@ -288,7 +401,7 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf * If the [endpoint] contains either schema or port that will override [config] information. */ fun endpointWith(endpoint: String, config: AuthConfig): Uri { - val src = endpoint.trim().toLowerCase(Locale.ROOT) + val src = endpoint.trim().lowercase(Locale.ROOT) var uri = Uri.parse(src) var uriBuilder = uri.buildUpon() @@ -321,5 +434,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf .appendPath(OPENID_CONFIGURATION_RESOURCE) .build() } + + fun discoveryUriWithAuth0(endpoint: String): Uri { + return Uri.parse("https://${endpoint}/app.config.json") + .buildUpon() + .build() + } } } diff --git a/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt b/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt index 78b177b..4640b6b 100644 --- a/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt +++ b/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt @@ -14,6 +14,7 @@ import com.alfresco.auth.Credentials import com.alfresco.auth.DiscoveryService import com.alfresco.auth.data.LiveEvent import com.alfresco.auth.data.MutableLiveEvent +import com.alfresco.auth.data.OAuth2Data import com.alfresco.auth.pkce.PkceAuthService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -45,9 +46,7 @@ abstract class AuthenticationViewModel : ViewModel() { * Check which [AuthType] is supported by the [endpoint] based on the provided [authConfig]. */ fun checkAuthType( - endpoint: String, - authConfig: AuthConfig, - onResult: (authType: AuthType) -> Unit + endpoint: String, authConfig: AuthConfig, onResult: (authType: AuthType) -> Unit ) = viewModelScope.launch { discoveryService = DiscoveryService(context, authConfig) val authType = withContext(Dispatchers.IO) { discoveryService.getAuthType(endpoint) } @@ -86,6 +85,7 @@ abstract class AuthenticationViewModel : ViewModel() { open fun onPkceAuthCancelled() {} internal val pkceAuth = PkceAuth() + internal inner class PkceAuth { private lateinit var authService: PkceAuthService @@ -127,6 +127,18 @@ abstract class AuthenticationViewModel : ViewModel() { } } } + + fun handleActivityResult(credentials: com.auth0.android.result.Credentials, oauth2: OAuth2Data) { + viewModelScope.launch { + try { + val result = credentials.accessToken + val userEmail = credentials.user.email ?: "" + _onCredentials.value = Credentials(userEmail, result, AuthType.OIDC.value, oauth2.host, oauth2.clientId) + } catch (ex: Exception) { + _onError.value = ex.message ?: "" + } + } + } } } @@ -166,6 +178,10 @@ abstract class AuthenticationActivity : AppCompatAc super.onActivityResult(requestCode, resultCode, data) } + fun handleResult(data: com.auth0.android.result.Credentials?, oauth2: OAuth2Data) { + data?.let { viewModel.pkceAuth.handleActivityResult(it, oauth2) } + } + /** * Called when [credentials] become available. */ diff --git a/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt b/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt index 69b6405..8acc843 100644 --- a/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt +++ b/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt @@ -12,6 +12,7 @@ import com.alfresco.auth.pkce.PkceAuthService import kotlinx.coroutines.launch import net.openid.appauth.AuthState import org.json.JSONException +import java.net.URL /** * Companion [ViewModel] to [EndSessionActivity] for invoking the logout procedure. @@ -20,7 +21,9 @@ open class EndSessionViewModel( context: Context, authType: AuthType?, authState: String, - authConfig: AuthConfig + authConfig: AuthConfig, + val hostName: String, + val clientId: String ) : ViewModel() { private val authType = authType private val authService: PkceAuthService? @@ -32,7 +35,7 @@ open class EndSessionViewModel( null } - authService = if (authType == AuthType.PKCE) { + authService = if (authType == AuthType.PKCE || authType == AuthType.OIDC) { PkceAuthService(context, state, authConfig) } else { null @@ -44,11 +47,17 @@ open class EndSessionViewModel( */ fun logout(activity: Activity, requestCode: Int) { viewModelScope.launch { - if (authType == AuthType.PKCE) { - authService?.endSession(activity, requestCode) - } else { - activity.setResult(Activity.RESULT_OK) - activity.finish() + when (authType) { + AuthType.PKCE -> { + authService?.endSession(activity, requestCode) + } + AuthType.OIDC -> { + authService?.logoutAuth0(URL(hostName).host,clientId, activity, requestCode) + } + else -> { + activity.setResult(Activity.RESULT_OK) + activity.finish() + } } } } @@ -82,6 +91,13 @@ abstract class EndSessionActivity : AppCompatActivi } } + fun handleResult(requestCode: Int) { + if (requestCode == REQUEST_CODE_END_SESSION) { + setResult(Activity.RESULT_OK) + finish() + } + } + private companion object { const val REQUEST_CODE_END_SESSION = 1 } diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml new file mode 100644 index 0000000..a6b6314 --- /dev/null +++ b/auth/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + zTSfC3VEcrguvP5CKIgNIBCOTliawIKg + *.auth0.com + demo + diff --git a/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt b/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt index f079e02..fba9ddf 100644 --- a/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt +++ b/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt @@ -16,6 +16,6 @@ interface AppConfigApi { "Content-Type: application/json" ) @GET("app.config.json") - suspend fun getAppConfig() :AppConfigModel + suspend fun getAppConfig(): AppConfigModel } diff --git a/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt b/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt index e953ca6..dd60502 100644 --- a/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt +++ b/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt @@ -14,230 +14,3 @@ import kotlinx.parcelize.Parcelize data class AppConfigModel( @Json(name = "search") @field:Json(name = "search") val search: List? ) : Parcelable - -/** - * Categories model - * @property expanded - * @property component - * @property name - * @property id - * @property enabled - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class CategoriesItem( - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "component") @field:Json(name = "component") val component: Component?, - @Json(name = "name") @field:Json(name = "name") val name: String?, - @Json(name = "id") @field:Json(name = "id") val id: String?, - @Json(name = "enabled") @field:Json(name = "enabled") val enabled: Boolean? -) : Parcelable - -/** - * Component model - * @property settings - * @property selector - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class Component( - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings?, - @Json(name = "selector") @field:Json(name = "selector") val selector: String? -) : Parcelable - -/** - * SearchItem model - * @property default - * @property name - * @property filterWithContains - * @property categories - * @property resetButton - * @property facetFields - * @property facetQueries - * @property facetIntervals - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class SearchItem( - @Json(name = "default") @field:Json(name = "default") val default: Boolean?, - @Json(name = "name") @field:Json(name = "name") val name: String?, - @Json(name = "filterWithContains") @field:Json(name = "filterWithContains") val filterWithContains: Boolean?, - @Json(name = "categories") @field:Json(name = "categories") val categories: List?, - @Json(name = "resetButton") @field:Json(name = "resetButton") val resetButton: Boolean?, - @Json(name = "filterQueries") @field:Json(name = "filterQueries") val filterQueries: List?, - @Json(name = "facetFields") @field:Json(name = "facetFields") val facetFields: FacetFieldsItem?, - @Json(name = "facetQueries") @field:Json(name = "facetQueries") val facetQueries: FacetQueriesItem?, - @Json(name = "facetIntervals") @field:Json(name = "facetIntervals") val facetIntervals: FacetIntervalsItem? -) : Parcelable - -/** - * Filter Queries Model - * @property query - * @property fields - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FilterQueriesItem( - @Json(name = "query") @field:Json(name = "query") val query: String? -) : Parcelable - -/** - * Facet Fields Model - * @property expanded - * @property fields - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FacetFieldsItem( - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "fields") @field:Json(name = "fields") val fields: List? -) : Parcelable - -/** - * Facet Queries Model - * @property label - * @property pageSize - * @property expanded - * @property mincount - * @property queries - * @property settings - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FacetQueriesItem( - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, - @Json(name = "queries") @field:Json(name = "queries") val queries: List?, - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? -) : Parcelable - -/** - * Facet Fields Model - * @property expanded - * @property intervals - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FacetIntervalsItem( - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "intervals") @field:Json(name = "intervals") val intervals: List? -) : Parcelable - -/** - * Fields Model - * @property field - * @property mincount - * @property label - * @property settings - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FieldsItem( - @Json(name = "field") @field:Json(name = "field") val field: String?, - @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? -) : Parcelable - -/** - * Queries Model - * @property query - * @property label - * @property group - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class QueriesItem( - @Json(name = "query") @field:Json(name = "query") val query: String?, - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "group") @field:Json(name = "group") val group: String? -) : Parcelable - -/** - * Queries Model - * @property label - * @property field - * @property sets - * @property settings - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class IntervalsItem( - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "field") @field:Json(name = "field") val field: String?, - @Json(name = "sets") @field:Json(name = "sets") val sets: List?, - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? -) : Parcelable - -/** - * Queries Model - * @property label - * @property start - * @property end - * @property endInclusive - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class SetsItem( - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "start") @field:Json(name = "start") val start: String?, - @Json(name = "end") @field:Json(name = "end") val end: String?, - @Json(name = "startInclusive") @field:Json(name = "startInclusive") val startInclusive: Boolean?, - @Json(name = "endInclusive") @field:Json(name = "endInclusive") val endInclusive: Boolean? -) : Parcelable - -/** - * Settings model - * @property field - * @property pattern - * @property placeholder - * @property pageSize - * @property operator - * @property options - * @property min - * @property max - * @property step - * @property thumbLabel - * @property format - * @property dateFormat - * @property maxDate - * @property allowUpdateOnChange - * @property hideDefaultAction - * @property unit - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class Settings( - @Json(name = "field") @field:Json(name = "field") val field: String?, - @Json(name = "pattern") @field:Json(name = "pattern") val pattern: String?, - @Json(name = "placeholder") @field:Json(name = "placeholder") val placeholder: String?, - @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, - @Json(name = "operator") @field:Json(name = "operator") val operator: String?, - @Json(name = "options") @field:Json(name = "options") val options: List?, - @Json(name = "min") @field:Json(name = "min") val min: Int?, - @Json(name = "max") @field:Json(name = "max") val max: Int?, - @Json(name = "step") @field:Json(name = "step") val step: Int?, - @Json(name = "thumbLabel") @field:Json(name = "thumbLabel") val thumbLabel: Boolean?, - @Json(name = "format") @field:Json(name = "format") val format: String?, - @Json(name = "dateFormat") @field:Json(name = "dateFormat") val dateFormat: String?, - @Json(name = "maxDate") @field:Json(name = "maxDate") val maxDate: String?, - @Json(name = "allowUpdateOnChange") @field:Json(name = "allowUpdateOnChange") val allowUpdateOnChange: Boolean?, - @Json(name = "hideDefaultAction") @field:Json(name = "hideDefaultAction") val hideDefaultAction: Boolean?, - @Json(name = "unit") @field:Json(name = "unit") val unit: String? -) : Parcelable - -/** - * Options model - * @property name - * @property value - * @property default - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class Options( - @Json(name = "name") @field:Json(name = "name") val name: String?, - @Json(name = "value") @field:Json(name = "value") val value: String?, - @Json(name = "default") @field:Json(name = "default") val default: Boolean? -) : Parcelable diff --git a/content/src/main/kotlin/com/alfresco/content/models/SearchItem.kt b/content/src/main/kotlin/com/alfresco/content/models/SearchItem.kt new file mode 100644 index 0000000..0bbaa20 --- /dev/null +++ b/content/src/main/kotlin/com/alfresco/content/models/SearchItem.kt @@ -0,0 +1,232 @@ +package com.alfresco.content.models + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize +/** + * Categories model + * @property expanded + * @property component + * @property name + * @property id + * @property enabled + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class CategoriesItem( + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "component") @field:Json(name = "component") val component: Component?, + @Json(name = "name") @field:Json(name = "name") val name: String?, + @Json(name = "id") @field:Json(name = "id") val id: String?, + @Json(name = "enabled") @field:Json(name = "enabled") val enabled: Boolean? +) : Parcelable + +/** + * Component model + * @property settings + * @property selector + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class Component( + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings?, + @Json(name = "selector") @field:Json(name = "selector") val selector: String? +) : Parcelable + +/** + * SearchItem model + * @property default + * @property name + * @property filterWithContains + * @property categories + * @property resetButton + * @property facetFields + * @property facetQueries + * @property facetIntervals + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class SearchItem( + @Json(name = "default") @field:Json(name = "default") val default: Boolean?, + @Json(name = "name") @field:Json(name = "name") val name: String?, + @Json(name = "filterWithContains") @field:Json(name = "filterWithContains") val filterWithContains: Boolean?, + @Json(name = "categories") @field:Json(name = "categories") val categories: List?, + @Json(name = "resetButton") @field:Json(name = "resetButton") val resetButton: Boolean?, + @Json(name = "filterQueries") @field:Json(name = "filterQueries") val filterQueries: List?, + @Json(name = "facetFields") @field:Json(name = "facetFields") val facetFields: FacetFieldsItem?, + @Json(name = "facetQueries") @field:Json(name = "facetQueries") val facetQueries: FacetQueriesItem?, + @Json(name = "facetIntervals") @field:Json(name = "facetIntervals") val facetIntervals: FacetIntervalsItem? +) : Parcelable + +/** + * Filter Queries Model + * @property query + * @property fields + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FilterQueriesItem( + @Json(name = "query") @field:Json(name = "query") val query: String? +) : Parcelable + +/** + * Facet Fields Model + * @property expanded + * @property fields + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FacetFieldsItem( + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "fields") @field:Json(name = "fields") val fields: List? +) : Parcelable + +/** + * Facet Queries Model + * @property label + * @property pageSize + * @property expanded + * @property mincount + * @property queries + * @property settings + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FacetQueriesItem( + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, + @Json(name = "queries") @field:Json(name = "queries") val queries: List?, + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? +) : Parcelable + +/** + * Facet Fields Model + * @property expanded + * @property intervals + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FacetIntervalsItem( + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "intervals") @field:Json(name = "intervals") val intervals: List? +) : Parcelable + +/** + * Fields Model + * @property field + * @property mincount + * @property label + * @property settings + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FieldsItem( + @Json(name = "field") @field:Json(name = "field") val field: String?, + @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? +) : Parcelable + +/** + * Queries Model + * @property query + * @property label + * @property group + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class QueriesItem( + @Json(name = "query") @field:Json(name = "query") val query: String?, + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "group") @field:Json(name = "group") val group: String? +) : Parcelable + +/** + * Queries Model + * @property label + * @property field + * @property sets + * @property settings + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class IntervalsItem( + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "field") @field:Json(name = "field") val field: String?, + @Json(name = "sets") @field:Json(name = "sets") val sets: List?, + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? +) : Parcelable + +/** + * Queries Model + * @property label + * @property start + * @property end + * @property endInclusive + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class SetsItem( + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "start") @field:Json(name = "start") val start: String?, + @Json(name = "end") @field:Json(name = "end") val end: String?, + @Json(name = "startInclusive") @field:Json(name = "startInclusive") val startInclusive: Boolean?, + @Json(name = "endInclusive") @field:Json(name = "endInclusive") val endInclusive: Boolean? +) : Parcelable + +/** + * Settings model + * @property field + * @property pattern + * @property placeholder + * @property pageSize + * @property operator + * @property options + * @property min + * @property max + * @property step + * @property thumbLabel + * @property format + * @property dateFormat + * @property maxDate + * @property allowUpdateOnChange + * @property hideDefaultAction + * @property unit + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class Settings( + @Json(name = "field") @field:Json(name = "field") val field: String?, + @Json(name = "pattern") @field:Json(name = "pattern") val pattern: String?, + @Json(name = "placeholder") @field:Json(name = "placeholder") val placeholder: String?, + @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, + @Json(name = "operator") @field:Json(name = "operator") val operator: String?, + @Json(name = "options") @field:Json(name = "options") val options: List?, + @Json(name = "min") @field:Json(name = "min") val min: Int?, + @Json(name = "max") @field:Json(name = "max") val max: Int?, + @Json(name = "step") @field:Json(name = "step") val step: Int?, + @Json(name = "thumbLabel") @field:Json(name = "thumbLabel") val thumbLabel: Boolean?, + @Json(name = "format") @field:Json(name = "format") val format: String?, + @Json(name = "dateFormat") @field:Json(name = "dateFormat") val dateFormat: String?, + @Json(name = "maxDate") @field:Json(name = "maxDate") val maxDate: String?, + @Json(name = "allowUpdateOnChange") @field:Json(name = "allowUpdateOnChange") val allowUpdateOnChange: Boolean?, + @Json(name = "hideDefaultAction") @field:Json(name = "hideDefaultAction") val hideDefaultAction: Boolean?, + @Json(name = "unit") @field:Json(name = "unit") val unit: String? +) : Parcelable + +/** + * Options model + * @property name + * @property value + * @property default + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class Options( + @Json(name = "name") @field:Json(name = "name") val name: String?, + @Json(name = "value") @field:Json(name = "value") val value: String?, + @Json(name = "default") @field:Json(name = "default") val default: Boolean? +) : Parcelable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b35a73f..22fd57c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,12 @@ [versions] +auth0 = "2.10.2" kotlin = "1.8.0" retrofit = "2.9.0" moshi = "1.15.0" +coreKtx = "1.13.1" +junit = "1.1.5" +espressoCore = "3.5.1" +material = "1.12.0" [libraries] android-desugar = "com.android.tools:desugar_jdk_libs:2.0.3" @@ -13,6 +18,7 @@ androidx-lifecycle-viewmodelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:2. appauth = "net.openid:appauth:0.11.1" +auth0 = { module = "com.auth0.android:auth0", version.ref = "auth0" } dokka = "org.jetbrains.dokka:dokka-gradle-plugin:1.8.10" gradleVersionsPlugin = "com.github.ben-manes:gradle-versions-plugin:0.46.0" @@ -40,3 +46,7 @@ retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "ret spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.18.0" swaggerCodegen = "com.agologan:swagger-gradle-codegen:1.4.1-67-gfbda3ab" +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } diff --git a/sample/build.gradle b/sample/build.gradle index 205b076..92ea767 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -22,6 +22,7 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "@string/com_auth0_scheme"] } buildFeatures { diff --git a/sample/src/main/java/com/alfresco/sample/AuthConfig.kt b/sample/src/main/java/com/alfresco/sample/AuthConfig.kt index 629e103..6517222 100644 --- a/sample/src/main/java/com/alfresco/sample/AuthConfig.kt +++ b/sample/src/main/java/com/alfresco/sample/AuthConfig.kt @@ -3,6 +3,16 @@ package com.alfresco.sample import com.alfresco.auth.AuthConfig val AuthConfig.Companion.defaultConfig: AuthConfig + get() = AuthConfig( + https = true, + port = "443", + clientId = "zTSfC3VEcrguvP5CKIgNIBCOTliawIKg", + realm = "", + redirectUrl = "demo://dev-ps-alfresco.auth0.com/android/com.alfresco.sample/callback", + contentServicePath = "alfresco", + scheme = "demo" + ) +/*val AuthConfig.Companion.defaultConfig: AuthConfig get() = AuthConfig( https = true, port = "443", @@ -10,4 +20,4 @@ val AuthConfig.Companion.defaultConfig: AuthConfig realm = "alfresco", redirectUrl = "androidacsapp://aims/auth", contentServicePath = "alfresco" - ) + )*/ diff --git a/sample/src/main/java/com/alfresco/sample/LoginActivity.kt b/sample/src/main/java/com/alfresco/sample/LoginActivity.kt index 8d2858e..a61db4f 100644 --- a/sample/src/main/java/com/alfresco/sample/LoginActivity.kt +++ b/sample/src/main/java/com/alfresco/sample/LoginActivity.kt @@ -33,7 +33,7 @@ class LoginViewModel(override var context: Context) : AuthenticationViewModel() } private fun onAuthType(authType: AuthType) { - if (authType == AuthType.PKCE) { + if (authType == AuthType.PKCE || authType == AuthType.OIDC) { pkceLogin(server, AuthConfig.defaultConfig) } else { _onError.value = context.getString(R.string.auth_error_check_connect_url) @@ -76,7 +76,7 @@ class LoginActivity : AuthenticationActivity() { this, credentials.username, credentials.authState, - credentials.authType, + AuthType.OIDC.value, viewModel.applicationUrl ) navigateToMain() diff --git a/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt b/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt index 136446b..e0fe5da 100644 --- a/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt +++ b/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt @@ -9,12 +9,26 @@ import com.alfresco.auth.AuthType import com.alfresco.auth.ui.EndSessionActivity import com.alfresco.auth.ui.EndSessionViewModel -class LogoutViewModel(context: Context, authType: AuthType?, authState: String, authConfig: AuthConfig) : EndSessionViewModel(context, authType, authState, authConfig) { +class LogoutViewModel( + context: Context, + authType: AuthType?, + authState: String, + authConfig: AuthConfig, + hostName: String, + clientId: String +) : EndSessionViewModel(context, authType, authState, authConfig, hostName, clientId) { companion object { fun build(context: Context): LogoutViewModel { val acc = requireNotNull(Account.getAccount(context)) - return LogoutViewModel(context, AuthType.PKCE, acc.authState, AuthConfig.defaultConfig) + return LogoutViewModel( + context, + AuthType.PKCE, + acc.authState, + AuthConfig.defaultConfig, + "", + "" + ) } } @@ -32,5 +46,9 @@ class LogoutViewModel(context: Context, authType: AuthType?, authState: String, class LogoutActivity : EndSessionActivity() { - override val viewModel: LogoutViewModel by viewModels { LogoutViewModel.Factory(applicationContext) } + override val viewModel: LogoutViewModel by viewModels { + LogoutViewModel.Factory( + applicationContext + ) + } } diff --git a/settings.gradle b/settings.gradle index 7154fa7..257a8af 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,4 +3,4 @@ include ':sample',':auth', 'content', 'content-ktx', ':process' // Enable Gradle's version catalog support // Ref: https://docs.gradle.org/current/userguide/platforms.html -//enableFeaturePreview("VERSION_CATALOGS") +//enableFeaturePreview("VERSION_CATALOGS") \ No newline at end of file