Rework credential manager flow

This commit is contained in:
Z. Charles Dziura 2025-03-27 18:09:32 -04:00
parent a3b53d6fa6
commit 4333ad764b
24 changed files with 566 additions and 148 deletions

View file

@ -32,11 +32,13 @@ import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginS
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreen
import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreenViewModel
import ing.bikeshedengineer.debtpirate.app.screen.home.presentation.overview.OverviewScreen
import ing.bikeshedengineer.debtpirate.domain.credential.GetCredentialsUseCase
import ing.bikeshedengineer.debtpirate.domain.credential.StoreCredentialsUseCase
import ing.bikeshedengineer.debtpirate.domain.credential.UserCredentialManager
import ing.bikeshedengineer.debtpirate.domain.navigation.Destination
import ing.bikeshedengineer.debtpirate.domain.navigation.NavigationAction
import ing.bikeshedengineer.debtpirate.domain.navigation.Navigator
import ing.bikeshedengineer.debtpirate.domain.usecase.GetStoredTokensUseCase
import ing.bikeshedengineer.debtpirate.domain.usecase.StoreCredentialsUseCase
import ing.bikeshedengineer.debtpirate.theme.DebtPirateTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -52,6 +54,12 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var getStoredTokens: GetStoredTokensUseCase
@Inject
lateinit var userCredentialManager: UserCredentialManager
@Inject
lateinit var getCredentials: GetCredentialsUseCase
@Inject
lateinit var storeCredentials: StoreCredentialsUseCase
@ -64,8 +72,21 @@ class MainActivity : ComponentActivity() {
setContent {
val navController = rememberNavController()
ObserveAsEvents(userCredentialManager.requestUserCredentials) {
lifecycleScope.launch {
val result = getCredentials()
userCredentialManager.returnUserCredentialsResponse(result)
}
}
ObserveAsEvents(navigator.navigationActions) { action ->
ObserveAsEvents(userCredentialManager.storeUserCredentials) { credentials ->
val (emailAddress, password) = credentials
lifecycleScope.launch {
storeCredentials(emailAddress, password)
}
}
ObserveAsEvents(navigator.actions) { action ->
when (action) {
is NavigationAction.Navigate -> {
navController.navigate(action.destination) {
@ -108,15 +129,17 @@ class MainActivity : ComponentActivity() {
composable<Destination.AuthLogin> {
val viewModel = hiltViewModel<LoginScreenViewModel>()
val toastMessages = viewModel.toastMessages.collectAsState("")
val storeCredentialMessages = viewModel.storeCredentialsMessages.collectAsState(null);
val storeCredentialMessages =
viewModel.storeCredentialsMessages.collectAsState(null)
LoginScreen(
emailAddress = viewModel.emailAddress.collectAsState(""),
isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true),
isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(
true
),
password = viewModel.password.collectAsState(""),
isPasswordValid = viewModel.isPasswordValid.collectAsState(true),
toastMessages = toastMessages,
handleCredentialManagerSignIn = viewModel::handleCredentialManagerSignIn,
onRegisterButtonClick = viewModel::onRegisterButtonClick,
onAction = viewModel::onAction,
)
@ -127,7 +150,9 @@ class MainActivity : ComponentActivity() {
RegistrationScreen(
emailAddress = viewModel.emailAddress.collectAsState(""),
emailAddressError = viewModel.emailAddressError.collectAsState(""),
isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(true),
isEmailAddressValid = viewModel.isEmailAddressValid.collectAsState(
true
),
name = viewModel.name.collectAsState(""),
nameError = viewModel.nameError.collectAsState(""),
isNameValid = viewModel.isNameValid.collectAsState(true),
@ -135,10 +160,16 @@ class MainActivity : ComponentActivity() {
passwordError = viewModel.passwordError.collectAsState(""),
isPasswordValid = viewModel.isPasswordValid.collectAsState(true),
confirmPassword = viewModel.confirmPassword.collectAsState(""),
confirmPasswordError = viewModel.confirmPasswordError.collectAsState(""),
isConfirmPasswordValid = viewModel.isConfirmPasswordValid.collectAsState(true),
confirmPasswordError = viewModel.confirmPasswordError.collectAsState(
""
),
isConfirmPasswordValid = viewModel.isConfirmPasswordValid.collectAsState(
true
),
onAction = viewModel::onAction,
onRegistrationMessage = viewModel.onRegistrationComplete.collectAsState(null),
onRegistrationMessage = viewModel.onRegistrationComplete.collectAsState(
null
),
navigateToConfirmationScreen = viewModel::navigateToConfirmationScreen,
onNavigateUp = viewModel::navigateUp
)
@ -151,7 +182,12 @@ class MainActivity : ComponentActivity() {
}
composable<Destination.AuthUserConfirmation> {
val (userId, verificationToken) = it.toRoute<Destination.AuthUserConfirmation>()
ConfirmationScreen(ConfirmationData.UserConfirmationData(userId, verificationToken))
ConfirmationScreen(
ConfirmationData.UserConfirmationData(
userId,
verificationToken
)
)
}
}

View file

@ -1,7 +1,6 @@
package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.annotation.SuppressLint
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background
@ -40,12 +39,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.NoCredentialException
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
@ -55,35 +48,10 @@ fun LoginScreen(
password: State<String>,
isPasswordValid: State<Boolean>,
toastMessages: State<String>,
handleCredentialManagerSignIn: (GetCredentialResponse) -> Unit,
onRegisterButtonClick: () -> Unit,
onAction: (LoginScreenStateAction) -> Unit,
) {
val context = LocalActivity.current!!
val credentialManager = CredentialManager.create(context)
LaunchedEffect(Unit) {
try {
val result = credentialManager.getCredential(
context, GetCredentialRequest(
listOf(GetPasswordOption())
)
)
handleCredentialManagerSignIn(result)
} catch (err: GetCredentialException) {
when (err) {
is NoCredentialException -> {
Log.i("LoginScreen", "No credentials stored")
}
else -> {
Log.e("LoginScreen", "Exception thrown when getting credentials: $err")
}
}
}
}
LaunchedEffect(toastMessages.value) {
val message = toastMessages.value

View file

@ -5,4 +5,8 @@ sealed interface LoginScreenStateAction {
data class UpdatePassword(val password: String) : LoginScreenStateAction
data object ValidateCredentials : LoginScreenStateAction
data object SubmitLoginRequest : LoginScreenStateAction
data class SubmitLoginRequestWithStoredCredentials(
val emailAddress: String,
val password: String
) : LoginScreenStateAction
}

View file

@ -2,8 +2,6 @@ package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login
import android.util.Log
import android.util.Patterns
import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@ -12,11 +10,14 @@ import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.LoginCredentialsV
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.UserNotFoundException
import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateLoginCredentialsUseCase
import ing.bikeshedengineer.debtpirate.domain.credential.UserCredentialManager
import ing.bikeshedengineer.debtpirate.domain.credential.UserCredentialResponse
import ing.bikeshedengineer.debtpirate.domain.navigation.Destination
import ing.bikeshedengineer.debtpirate.domain.navigation.Navigator
import ing.bikeshedengineer.debtpirate.domain.usecase.UpdateStoreDataUseCase
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
@ -29,6 +30,7 @@ import javax.inject.Inject
@HiltViewModel
class LoginScreenViewModel @Inject constructor(
private val navigator: Navigator,
private val userCredentialManager: UserCredentialManager,
private val submitLoginCredentials: SubmitLoginCredentialsUseCase,
private val validateLoginCredentials: ValidateLoginCredentialsUseCase,
private val updateStoreData: UpdateStoreDataUseCase
@ -68,8 +70,32 @@ class LoginScreenViewModel @Inject constructor(
}
init {
viewModelScope.launch {
userCredentialManager.makeUserCredentialsRequest()
}
userCredentialManager.userCredentialsResponse.distinctUntilChanged()
.onEach { response ->
when (response) {
is UserCredentialResponse.HasCredentials -> {
val (emailAddress, password) = response
onAction(
LoginScreenStateAction.SubmitLoginRequestWithStoredCredentials(
emailAddress,
password
)
)
}
is UserCredentialResponse.NoCredentials -> {}
}
}
_state.distinctUntilChangedBy { it.emailAddress }
.map { it.emailAddress.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(it.emailAddress).matches() }
.map {
it.emailAddress.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(it.emailAddress)
.matches()
}
.onEach { isEmailAddressValid ->
_state.update {
it.copy(
@ -112,14 +138,17 @@ class LoginScreenViewModel @Inject constructor(
onSubmitLoginRequest()
}
}
is LoginScreenStateAction.SubmitLoginRequestWithStoredCredentials -> {
onValidateLoginCredentials(action.emailAddress, action.password)
}
}
}
private fun onValidateLoginCredentials(emailAddress: String, password: String) {
_state.update { it.copy(isEmailAddressPristine = true, isPasswordPristine = true) }
val validationResult = validateLoginCredentials(emailAddress, password)
when (validationResult) {
when (val validationResult = validateLoginCredentials(emailAddress, password)) {
is LoginCredentialsValidationResult.ValidCredentials -> {
onAction(LoginScreenStateAction.SubmitLoginRequest)
}
@ -133,7 +162,11 @@ class LoginScreenViewModel @Inject constructor(
private suspend fun onSubmitLoginRequest() {
try {
val (userId, auth, session) = submitLoginCredentials(_state.value.emailAddress, _state.value.password)
val (userId, auth, session) = submitLoginCredentials(
_state.value.emailAddress,
_state.value.password
)
updateStoreData(
userId = userId,
authToken = auth.token,
@ -141,6 +174,11 @@ class LoginScreenViewModel @Inject constructor(
sessionToken = session.token,
sessionTokenExpiresAt = session.expiresAt
)
userCredentialManager.storeUserCredentials(
_state.value.emailAddress,
_state.value.password
)
} catch (err: Exception) {
when (err) {
is InvalidCredentialsException -> {
@ -163,20 +201,4 @@ class LoginScreenViewModel @Inject constructor(
navigator.navigate(destination = Destination.AuthRegistration)
}
}
fun handleCredentialManagerSignIn(result: GetCredentialResponse) {
val credentials = result.credential
when (credentials) {
is PasswordCredential -> {
val emailAddress = credentials.id
val password = credentials.password
onAction(LoginScreenStateAction.SubmitLoginRequest)
}
else -> {
// TODO: Handle this...
}
}
}
}

View file

@ -1,42 +0,0 @@
package ing.bikeshedengineer.debtpirate.domain.credential
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object CredentialManagerModule {
@Provides
@Singleton
fun provideCredentialManager(): CredentialManager = CredentialManagerImpl()
}
sealed interface CredentialManagerRequest {
data object GetCredentials : CredentialManagerRequest
}
sealed interface CredentialManagerResponse {
data object NoCredentials : CredentialManagerResponse
}
interface CredentialManager {
val credentialRequests: Flow<CredentialManagerRequest>
suspend fun getCredentials()
}
class CredentialManagerImpl() : CredentialManager {
private val _credentialRequests = Channel<CredentialManagerRequest>()
override val credentialRequests = _credentialRequests.receiveAsFlow()
override suspend fun getCredentials() {
this._credentialRequests.send(CredentialManagerRequest.GetCredentials)
}
}

View file

@ -0,0 +1,23 @@
package ing.bikeshedengineer.debtpirate.domain.credential
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
object GetCredentialsUseCaseModule {
@Provides
@ActivityScoped
fun provideGetCredentialsUseCase(repository: UserCredentialRepository) =
GetCredentialsUseCase(repository)
}
class GetCredentialsUseCase(private val repository: UserCredentialRepository) {
suspend operator fun invoke(): UserCredentialResponse {
return repository.getUserCredentials()
}
}

View file

@ -0,0 +1,23 @@
package ing.bikeshedengineer.debtpirate.domain.credential
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
object StoreCredentialsUseCaseModule {
@Provides
@ActivityScoped
fun provideStoreCredentialsUseCase(repository: UserCredentialRepository) =
StoreCredentialsUseCase(repository)
}
class StoreCredentialsUseCase(private val repository: UserCredentialRepository) {
suspend operator fun invoke(emailAddress: String, password: String) {
this.repository.storeUserCredentials(emailAddress, password)
}
}

View file

@ -0,0 +1,53 @@
package ing.bikeshedengineer.debtpirate.domain.credential
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object UserCredentialManagerModule {
@Provides
@Singleton
fun provideUserCredentialManager(): UserCredentialManager = UserCredentialManagerImpl()
}
interface UserCredentialManager {
val requestUserCredentials: Flow<Unit>
suspend fun makeUserCredentialsRequest()
val userCredentialsResponse: Flow<UserCredentialResponse>
suspend fun returnUserCredentialsResponse(response: UserCredentialResponse)
val storeUserCredentials: Flow<Pair<String, String>>
suspend fun storeUserCredentials(emailAddress: String, password: String)
}
class UserCredentialManagerImpl : UserCredentialManager {
private val _requestUserCredentials = Channel<Unit>()
override val requestUserCredentials = _requestUserCredentials.receiveAsFlow()
private val _userCredentialsResponse = Channel<UserCredentialResponse>()
override val userCredentialsResponse = _userCredentialsResponse.receiveAsFlow()
private val _storeUserCredentials = Channel<Pair<String, String>>()
override val storeUserCredentials = _storeUserCredentials.receiveAsFlow()
override suspend fun makeUserCredentialsRequest() {
_requestUserCredentials.send(Unit)
}
override suspend fun returnUserCredentialsResponse(response: UserCredentialResponse) {
_userCredentialsResponse.send(response)
}
override suspend fun storeUserCredentials(emailAddress: String, password: String) {
_storeUserCredentials.send(Pair(emailAddress, password))
}
}

View file

@ -0,0 +1,77 @@
package ing.bikeshedengineer.debtpirate.domain.credential
import android.content.Context
import android.util.Log
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetPasswordOption
import androidx.credentials.PasswordCredential
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Module
@InstallIn(ActivityComponent::class)
object UserCredentialModule {
@Provides
@ActivityScoped
fun provideUserCredentialRepository(@ActivityContext context: Context): UserCredentialRepository =
UserCredentialRepositoryImpl(context = context)
}
interface UserCredentialRepository {
suspend fun getUserCredentials(): UserCredentialResponse
suspend fun storeUserCredentials(emailAddress: String, password: String)
}
class UserCredentialRepositoryImpl(
private val context: Context,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserCredentialRepository {
private val credentialManager = CredentialManager.create(this.context)
override suspend fun getUserCredentials(): UserCredentialResponse {
val getCredentialRequest = GetCredentialRequest(listOf(GetPasswordOption()))
return try {
val result = withContext(this.defaultDispatcher) {
credentialManager.getCredential(
context,
getCredentialRequest
)
}
val credential = result.credential as PasswordCredential
UserCredentialResponse.HasCredentials(
emailAddress = credential.id,
password = credential.password
)
} catch (err: GetCredentialException) {
UserCredentialResponse.NoCredentials
}
}
override suspend fun storeUserCredentials(emailAddress: String, password: String) {
val storeCredentialsRequest = CreatePasswordRequest(id = emailAddress, password = password)
try {
withContext(this.defaultDispatcher) {
credentialManager.createCredential(context, storeCredentialsRequest)
}
} catch (err: CreateCredentialException) {
Log.e(
"UserCredentialRepository",
"Unable to store user credentials in the credential manager: $err"
)
// TODO: Do something with this error...
}
}
}

View file

@ -0,0 +1,7 @@
package ing.bikeshedengineer.debtpirate.domain.credential
sealed interface UserCredentialResponse {
data object NoCredentials : UserCredentialResponse
data class HasCredentials(val emailAddress: String, val password: String) :
UserCredentialResponse
}

View file

@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
interface Navigator {
val startDestination: Destination
val navigationActions: Flow<NavigationAction>
val actions: Flow<NavigationAction>
suspend fun navigate(destination: Destination, navOptions: NavOptionsBuilder.() -> Unit = {})
@ -17,17 +17,17 @@ interface Navigator {
class NavigatorImpl(
override val startDestination: Destination = Destination.AuthGraph
) : Navigator {
private val _navigationActions = Channel<NavigationAction>()
override val navigationActions = _navigationActions.receiveAsFlow()
private val _actions = Channel<NavigationAction>()
override val actions = _actions.receiveAsFlow()
override suspend fun navigate(
destination: Destination,
navOptions: NavOptionsBuilder.() -> Unit
) {
_navigationActions.send(NavigationAction.Navigate(destination, navOptions))
_actions.send(NavigationAction.Navigate(destination, navOptions))
}
override suspend fun navigateUp() {
_navigationActions.send(NavigationAction.NavigateUp)
_actions.send(NavigationAction.NavigateUp)
}
}

View file

@ -1,36 +0,0 @@
package ing.bikeshedengineer.debtpirate.domain.usecase
import android.content.Context
import android.util.Log
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.CreateCredentialException
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
object StoreCredentialsUseCaseModule {
@Provides
@ActivityScoped
fun provideStoreCredentialsUseCase(@ActivityContext context: Context) = StoreCredentialsUseCase(context)
}
class StoreCredentialsUseCase(val context: Context) {
suspend operator fun invoke(username: String, password: String) {
val credentialManager = CredentialManager.create(this.context)
val createPasswordRequest = CreatePasswordRequest(id = username, password = password)
try {
credentialManager.createCredential(this.context, createPasswordRequest)
Log.d("StoreCredentialsUseCase", "Successfully stored login credentials")
} catch (err: CreateCredentialException) {
// TODO: Throw an error to be displayed as a Toast
}
}
}

View file

@ -1,6 +1,6 @@
[versions]
activityCompose = "1.10.1"
agp = "8.9.0"
agp = "8.9.1"
appcompat = "1.7.0"
biometric = "1.4.0-alpha02"
composeBom = "2025.03.00"

View file

@ -0,0 +1,29 @@
meta {
name: create
type: http
seq: 1
}
post {
url: {{protocol}}//{{domain}}/account
body: json
auth: bearer
}
headers {
Accept: application/json;charset=utf-8
Content-Type: application/json;charset=utf-8
}
auth:bearer {
token: {{sessionToken}}
}
body:json {
{
"type": "asset",
"name": "Checking",
"description": "Bank 1",
"currencyCode": "USD"
}
}

View file

@ -0,0 +1,28 @@
meta {
name: read
type: http
seq: 2
}
get {
url: {{protocol}}//{{domain}}/account
body: none
auth: bearer
}
headers {
Accept: application/json;charset=utf-8
}
auth:bearer {
token: {{sessionToken}}
}
body:json {
{
"type": "asset",
"name": "Quick Cash Account",
"description": "Franklin Savings Bank",
"currencyCode": "USD"
}
}

View file

@ -0,0 +1,41 @@
meta {
name: login
type: http
seq: 1
}
post {
url: {{protocol}}//{{domain}}/auth/login
body: json
auth: none
}
headers {
Accept: application/json; charset=utf-8
Content-Type: application/json; charset=utf-8
}
body:json {
{
"email": "{{email}}",
"password": "{{password}}"
}
}
script:post-response {
const body = res.getBody();
const data = body.data;
if (res.status < 300) {
bru.setEnvVar("userId", data.id);
if (data.auth?.token) {
bru.setEnvVar("authToken", data.auth.token);
}
if (data.session?.token) {
bru.setEnvVar("sessionToken", data.session.token);
}
}
}

View file

@ -0,0 +1,33 @@
meta {
name: session
type: http
seq: 2
}
get {
url: {{protocol}}//{{domain}}/auth/session
body: none
auth: bearer
}
headers {
Accept: application/json; charset=utf-8
}
auth:bearer {
token: {{authToken}}
}
script:post-response {
const body = res.getBody();
const data = body.data;
if (res.status < 300) {
bru.setEnvVar("userId", data.id);
if (data.session?.token) {
bru.setEnvVar("sessionToken", data.token);
}
}
}

View file

@ -0,0 +1,9 @@
{
"version": "1",
"name": "Debt Pirate",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View file

@ -0,0 +1,28 @@
meta {
name: create
type: http
seq: 1
}
post {
url: {{protocol}}//{{domain}}/budget
body: json
auth: bearer
}
headers {
Accept: application/json;charset=utf-8
Content-Type: application/json;charset=utf-8
}
auth:bearer {
token: {{sessionToken}}
}
body:json {
{
"name": "Daily Living",
"description": "Daily living expenses",
"icon": "person"
}
}

View file

@ -0,0 +1,28 @@
meta {
name: read
type: http
seq: 2
}
get {
url: {{protocol}}//{{domain}}/budget
body: none
auth: bearer
}
headers {
Accept: application/json;charset=utf-8
}
auth:bearer {
token: {{sessionToken}}
}
body:json {
{
"type": "asset",
"name": "Quick Cash Account",
"description": "Franklin Savings Bank",
"currencyCode": "USD"
}
}

View file

View file

@ -0,0 +1,11 @@
vars {
domain: localhost:42069
protocol: http:
email: zachary@dziura.email
}
vars:secret [
password,
verificationToken,
sessionToken,
authToken
]

View file

@ -0,0 +1,34 @@
meta {
name: create
type: http
seq: 1
}
post {
url: {{protocol}}//{{domain}}/user
body: json
auth: none
}
headers {
Content-Type: application/json; charset=utf-8
Accept: application/json; charset=utf-8
}
body:json {
{
"email": "{{email}}",
"password": "{{password}}",
"name": "Z. Charles Dziura"
}
}
script:post-response {
const body = res.getBody();
const data = body.data;
if (res.status < 300 && data.sessionToken) {
bru.setEnvVar("verificationToken", data.sessionToken);
}
}

View file

@ -0,0 +1,42 @@
meta {
name: verify
type: http
seq: 2
}
get {
url: {{protocol}}//{{domain}}/user/verify?t={{verificationToken}}
body: json
auth: none
}
params:query {
t: {{verificationToken}}
}
body:json {
{
"username": "{{username}}",
"password": "{{password}}",
"email": "zachary@dziura.email",
"name": "Z. Charles Dziura"
}
}
script:post-response {
const body = res.getBody();
const data = body.data;
if (res.status < 300) {
bru.setEnvVar("userId", data.userId);
if (data.auth?.token) {
bru.setEnvVar("authToken", data.auth.token);
}
if (data.session?.token) {
bru.setEnvVar("sessionToken", data.session.token);
}
}
}