Repository: directus/directus
File: app/src/routes/login/components/login-form/login-form.vue (Vue)

Code before:

		const redirectQuery = router.currentRoute.value.query.redirect as string;

		let lastPage: string | null = null;

		if (userStore.currentUser && 'last_page' in userStore.currentUser) {
			lastPage = userStore.currentUser.last_page;
		}

		router.push(redirectQuery || lastPage || '/content');
	} catch (err: any) {
		if (err.errors?.[0]?.extensions?.code === 'INVALID_OTP' && requiresTFA.value === false) {
			requiresTFA.value = true;
		} else {
			error.value = err.errors?.[0]?.extensions?.code || err;
		}
	} finally {
		loggingIn.value = false;
	}
}
</script>

<template>
	<form novalidate @submit.prevent="onSubmit">
		<v-input v-model="email" autofocus autocomplete="username" type="email" :placeholder="t('email')" />
		<interface-system-input-password v-model="password" autocomplete="current-password" />

		<transition-expand>
			<v-input
				v-if="requiresTFA"
				v-model="otp"
				type="text"
				autocomplete="one-time-code"
				:placeholder="t('otp')"
				autofocus
			/>
		</transition-expand>

		<v-notice v-if="error" type="warning">
			{{ errorFormatted }}
		</v-notice>
		<div class="buttons">
			<v-button type="submit" :loading="loggingIn" large>{{ t('sign_in') }}</v-button>
			<router-link to="/reset-password" class="forgot-password">
				{{ t('forgot_password') }}
			</router-link>
		</div>
	</form>
</template>

<style lang="scss" scoped>

Review comment (suggestion):
The two way `v-model` binding works by pure coincidence here. The interface expects the `value` prop. But since the `modelValue` prop is not given it is instead passed through as an attribute to the first root level component, the `v-input`, which happens to work with the `modelValue` prop. 

![Screenshot 2024-08-26 at 13 39 05](https://github.com/user-attachments/assets/6d2fb613-3566-4968-b3a5-95bce379c5f3)
![Screenshot 2024-08-26 at 13 38 56](https://github.com/user-attachments/assets/396425a8-ba5b-4596-b170-9349e23bcce4)

So the value binding here should be `:value="password" @input="password = $event"`

Code after:

		const redirectQuery = router.currentRoute.value.query.redirect as string;

		let lastPage: string | null = null;

		if (userStore.currentUser && 'last_page' in userStore.currentUser) {
			lastPage = userStore.currentUser.last_page;
		}

		router.push(redirectQuery || lastPage || '/content');
	} catch (err: any) {
		if (err.errors?.[0]?.extensions?.code === 'INVALID_OTP' && requiresTFA.value === false) {
			requiresTFA.value = true;
		} else {
			error.value = err.errors?.[0]?.extensions?.code || err;
		}
	} finally {
		loggingIn.value = false;
	}
}
</script>

<template>
	<form novalidate @submit.prevent="onSubmit">
		<v-input v-model="email" autofocus autocomplete="username" type="email" :placeholder="t('email')" />
		<interface-system-input-password :value="password" autocomplete="current-password" @input="password = $event" />

		<transition-expand>
			<v-input
				v-if="requiresTFA"
				v-model="otp"
				type="text"
				autocomplete="one-time-code"
				:placeholder="t('otp')"
				autofocus
			/>
		</transition-expand>

		<v-notice v-if="error" type="warning">
			{{ errorFormatted }}
		</v-notice>
		<div class="buttons">
			<v-button type="submit" :loading="loggingIn" large>{{ t('sign_in') }}</v-button>
			<router-link to="/reset-password" class="forgot-password">
				{{ t('forgot_password') }}
			</router-link>
		</div>
	</form>
</template>

<style lang="scss" scoped>