Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve config flow validation #15

Merged
merged 3 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 93 additions & 48 deletions custom_components/google_keep_sync/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Config flow for Google Keep Sync integration."""

import logging
import re
from collections.abc import Mapping
from typing import Any

Expand Down Expand Up @@ -28,7 +29,8 @@

SCHEMA_REAUTH = vol.Schema(
{
vol.Required("password"): str,
vol.Optional("password"): str,
vol.Optional("token"): str,
}
)

Expand Down Expand Up @@ -109,34 +111,20 @@ async def async_step_reauth_confirm(
_LOGGER.error("Configuration entry not found")
return self.async_abort(reason="config_entry_not_found")

password = user_input["password"]
data = {
"username": self.entry.data["username"],
"password": password,
}
# Add the username to user_input, since the reauth step doesn't include it
user_input["username"] = self.entry.data["username"]

try:
await self.validate_input(self.hass, data)

except InvalidAuthError:
errors["base"] = "invalid_auth"
except CannotConnectError:
errors["base"] = "cannot_connect"
else:
self.hass.config_entries.async_update_entry(
self.entry,
data={
"username": self.entry.data["username"],
"password": password,
},
)
# Process validation and handle errors
errors = await self.handle_user_input(user_input)

# No errors, so update the entry and reload the integration
if not errors:
self.hass.config_entries.async_update_entry(self.entry, data=user_input)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")

return self.async_show_form(
step_id="reauth_confirm",
data_schema=SCHEMA_REAUTH,
errors=errors,
step_id="reauth_confirm", data_schema=SCHEMA_REAUTH, errors=errors
)

@staticmethod
Expand All @@ -153,20 +141,32 @@ def __init__(self):
self.api = None
self.user_data = {}

async def validate_input(
self, hass: HomeAssistant, data: dict[str, Any]
) -> dict[str, Any]:
async def validate_input(self, hass: HomeAssistant, data: dict[str, Any]) -> None:
"""Validate the user input allows us to connect."""
username = data.get("username", "").strip()
password = data.get("password", "").strip()
token = data.get("token", "").strip()

if not (username and (password or token)):
_LOGGER.error(
"Credentials are missing; a username and password or "
"token must be provided."
)
raise InvalidAuthError
# Check for blank username
if not username:
raise BlankUsernameError

# Validate email address
if not re.match(r"[^@]+@[^@]+\.[^@]+", username):
raise InvalidEmailError

# Check password and token conditions
if password and token:
raise BothPasswordAndTokenError
if not (password or token):
raise NeitherPasswordNorTokenError

# Validate token format
valid_token_length = 223
if token and (
not token.startswith("aas_et/") or len(token) != valid_token_length
):
raise InvalidTokenFormatError

self.api = GoogleKeepAPI(hass, username, password, token)
success = await self.api.authenticate()
Expand All @@ -175,34 +175,59 @@ async def validate_input(
raise InvalidAuthError

self.user_data = {"username": username, "password": password, "token": token}
return {"title": "Google Keep", "entry_id": username.lower()}

async def handle_user_input(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Handle user input, checking for any errors."""
errors = {}
try:
await self.validate_input(self.hass, user_input)
except InvalidAuthError:
errors["base"] = "invalid_auth"
except CannotConnectError:
errors["base"] = "cannot_connect"
except BlankUsernameError:
errors["base"] = "blank_username"
except InvalidEmailError:
errors["base"] = "invalid_email"
except BothPasswordAndTokenError:
errors["base"] = "both_password_and_token"
except NeitherPasswordNorTokenError:
errors["base"] = "neither_password_nor_token"
except InvalidTokenFormatError:
errors["base"] = "invalid_token_format"
except Exception as exc:
_LOGGER.exception("Unexpected exception: %s", exc)
errors["base"] = "unknown"
return errors

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step for the user to enter their credentials."""
errors = {}
if user_input:
# Check to see if the same username has already been configured
try:
info = await self.validate_input(self.hass, user_input)

unique_id = info["entry_id"]
unique_id = user_input["username"].lower()
await self.async_set_unique_id(unique_id)

# Check if an entry with the same unique_id already exists
self._abort_if_unique_id_configured()

return await self.async_step_options()

except InvalidAuthError:
errors["base"] = "invalid_auth"
except CannotConnectError:
errors["base"] = "cannot_connect"
# Show an error if the same username has already been configured
except AbortFlow:
errors["base"] = "already_configured"
except Exception as exc:
_LOGGER.exception("Unexpected exception: %s", exc)
errors["base"] = "unknown"
return self.async_show_form(
step_id="user",
data_schema=SCHEMA_USER_DATA_STEP,
errors=errors,
description_placeholders={"invalid_auth_url": INVALID_AUTH_URL},
)

# Validate the user input for any issues
errors = await self.handle_user_input(user_input)

# No errors, so proceed to the next step
if not errors:
return await self.async_step_options()

return self.async_show_form(
step_id="user",
Expand Down Expand Up @@ -251,3 +276,23 @@ class CannotConnectError(HomeAssistantError):

class InvalidAuthError(HomeAssistantError):
"""Error to indicate there is invalid auth."""


class BlankUsernameError(HomeAssistantError):
"""Exception raised when the username is blank."""


class InvalidEmailError(HomeAssistantError):
"""Exception raised when the username is not a valid email."""


class BothPasswordAndTokenError(HomeAssistantError):
"""Exception raised when both password and token are provided."""


class NeitherPasswordNorTokenError(HomeAssistantError):
"""Exception raised when neither password nor token are provided."""


class InvalidTokenFormatError(HomeAssistantError):
"""Exception raised when the token format is invalid."""
7 changes: 6 additions & 1 deletion custom_components/google_keep_sync/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"already_configured": "[%key:common::config_flow::error::already_configured%]"
"already_configured": "[%key:common::config_flow::error::already_configured%]",
"blank_username": "Username cannot be blank.",
"invalid_email": "Invalid email address.",
"both_password_and_token": "Provide either password or token, not both.",
"neither_password_nor_token": "Either password or token must be provided.",
"invalid_token_format": "Invalid token provided. The token must start with aas_et/ and be 223 characters long."
},
"step": {
"user": {
Expand Down
17 changes: 11 additions & 6 deletions custom_components/google_keep_sync/translations/en.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Integration is already configured",
"already_configured": "Integration is already configured for this user.",
"reauth_successful": "Reauthentication successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error",
"already_configured": "Integration is already configured"
"cannot_connect": "Failed to connect.",
"invalid_auth": "Invalid authentication.",
"unknown": "An unexpected error occurred.",
"already_configured": "Integration is already configured for this user.",
"blank_username": "Username cannot be blank.",
"invalid_email": "Invalid email address.",
"both_password_and_token": "Provide either password or token, not both.",
"neither_password_nor_token": "Either password or token must be provided.",
"invalid_token_format": "Invalid token provided. The token must start with aas_et/ and be 223 characters long."
},
"step": {
"user": {
Expand Down Expand Up @@ -52,7 +57,7 @@
}
},
"error": {
"list_fetch_error": "An error occurred while fetching lists"
"list_fetch_error": "An error occurred while fetching lists."
},
"abort": {
"reauth_required": "Reauthentication required",
Expand Down
15 changes: 10 additions & 5 deletions custom_components/google_keep_sync/translations/es.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
{
"config": {
"abort": {
"already_configured": "La integración ya está configurada",
"already_configured": "Integración ya configurada para este usuario.",
"reauth_successful": "Reautenticación exitosa"
},
"error": {
"cannot_connect": "Error al conectar",
"invalid_auth": "Autenticación inválida",
"unknown": "Error desconocido",
"already_configured": "La integración ya está configurada"
"cannot_connect": "Fallo al conectar.",
"invalid_auth": "Autenticación inválida.",
"unknown": "Ocurrió un error inesperado.",
"already_configured": "Integración ya configurada para este usuario.",
"blank_username": "El nombre de usuario no puede estar en blanco.",
"invalid_email": "Dirección de correo electrónico inválida.",
"both_password_and_token": "Proporcione contraseña o token, no ambos.",
"neither_password_nor_token": "Se debe proporcionar contraseña o token.",
"invalid_token_format": "Token inválido proporcionado. El token debe comenzar con aas_et/ y tener 223 caracteres."
},
"step": {
"user": {
Expand Down
17 changes: 11 additions & 6 deletions custom_components/google_keep_sync/translations/sv.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Integrationen är redan konfigurerad",
"reauth_successful": "Omautentisering lyckades"
"already_configured": "Integrationen är redan konfigurerad för denna användare.",
"reauth_successful": "Om-autentisering lyckades"
},
"error": {
"cannot_connect": "Misslyckades med att ansluta",
"invalid_auth": "Ogiltig autentisering",
"unknown": "Oväntat fel",
"already_configured": "Integrationen är redan konfigurerad"
"cannot_connect": "Misslyckades med att ansluta.",
"invalid_auth": "Ogiltig autentisering.",
"unknown": "Ett oväntat fel inträffade.",
"already_configured": "Integrationen är redan konfigurerad för denna användare.",
"blank_username": "Användarnamnet får inte vara tomt.",
"invalid_email": "Ogiltig e-postadress.",
"both_password_and_token": "Ange antingen lösenord eller token, inte båda.",
"neither_password_nor_token": "Antingen lösenord eller token måste anges.",
"invalid_token_format": "Ogiltig token angiven. Token måste börja med aas_et/ och vara 223 tecken lång."
},
"step": {
"user": {
Expand Down
Loading
Loading