diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index 8096ea8b2..f0d4db21f 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -46,16 +46,13 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa sqlUpdateTOTPConfigRecordSignIn: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignIn, tableTOTPConfigurations), sqlUpdateTOTPConfigRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignInByUsername, tableTOTPConfigurations), - sqlUpsertWebauthnDevice: fmt.Sprintf(queryFmtUpsertWebauthnDevice, tableWebauthnDevices), + sqlInsertWebauthnDevice: fmt.Sprintf(queryFmtUpsertInsertDevice, tableWebauthnDevices), sqlSelectWebauthnDevices: fmt.Sprintf(queryFmtSelectWebauthnDevices, tableWebauthnDevices), sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices), sqlSelectWebauthnDevicesByRPIDByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByRPIDByUsername, tableWebauthnDevices), sqlSelectWebauthnDeviceByID: fmt.Sprintf(queryFmtSelectWebauthnDeviceByID, tableWebauthnDevices), sqlUpdateWebauthnDeviceDescriptionByUsernameAndID: fmt.Sprintf(queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID, tableWebauthnDevices), - sqlUpdateWebauthnDevicePublicKey: fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKey, tableWebauthnDevices), - sqlUpdateWebauthnDevicePublicKeyByUsername: fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKeyByUsername, tableWebauthnDevices), sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices), - sqlUpdateWebauthnDeviceRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignInByUsername, tableWebauthnDevices), sqlDeleteWebauthnDevice: fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices), sqlDeleteWebauthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices), sqlDeleteWebauthnDeviceByUsernameAndDescription: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDescription, tableWebauthnDevices), @@ -164,17 +161,14 @@ type SQLProvider struct { sqlUpdateTOTPConfigRecordSignInByUsername string // Table: webauthn_devices. - sqlUpsertWebauthnDevice string + sqlInsertWebauthnDevice string sqlSelectWebauthnDevices string sqlSelectWebauthnDevicesByUsername string sqlSelectWebauthnDevicesByRPIDByUsername string sqlSelectWebauthnDeviceByID string sqlUpdateWebauthnDeviceDescriptionByUsernameAndID string - sqlUpdateWebauthnDevicePublicKey string - sqlUpdateWebauthnDevicePublicKeyByUsername string sqlUpdateWebauthnDeviceRecordSignIn string - sqlUpdateWebauthnDeviceRecordSignInByUsername string sqlDeleteWebauthnDevice string sqlDeleteWebauthnDeviceByUsername string @@ -847,7 +841,7 @@ func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.Webau return fmt.Errorf("error encrypting the Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err) } - if _, err = p.db.ExecContext(ctx, p.sqlUpsertWebauthnDevice, + if _, err = p.db.ExecContext(ctx, p.sqlInsertWebauthnDevice, device.CreatedAt, device.LastUsedAt, device.RPID, device.Username, device.Description, device.KID, device.PublicKey, diff --git a/internal/storage/sql_provider_backend_postgres.go b/internal/storage/sql_provider_backend_postgres.go index 44062fc09..53cc46a6d 100644 --- a/internal/storage/sql_provider_backend_postgres.go +++ b/internal/storage/sql_provider_backend_postgres.go @@ -31,7 +31,6 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo // Specific alterations to this provider. // PostgreSQL doesn't have a UPSERT statement but has an ON CONFLICT operation instead. - provider.sqlUpsertWebauthnDevice = fmt.Sprintf(queryFmtUpsertWebauthnDevicePostgreSQL, tableWebauthnDevices) provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtUpsertDuoDevicePostgreSQL, tableDuoDevices) provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtUpsertTOTPConfigurationPostgreSQL, tableTOTPConfigurations) provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtUpsertPreferred2FAMethodPostgreSQL, tableUserPreferences) @@ -59,14 +58,13 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig) provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs) + provider.sqlInsertWebauthnDevice = provider.db.Rebind(provider.sqlInsertWebauthnDevice) provider.sqlSelectWebauthnDevices = provider.db.Rebind(provider.sqlSelectWebauthnDevices) provider.sqlSelectWebauthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByUsername) + provider.sqlSelectWebauthnDevicesByRPIDByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByRPIDByUsername) provider.sqlSelectWebauthnDeviceByID = provider.db.Rebind(provider.sqlSelectWebauthnDeviceByID) provider.sqlUpdateWebauthnDeviceDescriptionByUsernameAndID = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceDescriptionByUsernameAndID) - provider.sqlUpdateWebauthnDevicePublicKey = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKey) - provider.sqlUpdateWebauthnDevicePublicKeyByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKeyByUsername) provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn) - provider.sqlUpdateWebauthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignInByUsername) provider.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice) provider.sqlDeleteWebauthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsername) provider.sqlDeleteWebauthnDeviceByUsernameAndDescription = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndDescription) diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go index 3bb5cd18c..165c08b3f 100644 --- a/internal/storage/sql_provider_queries.go +++ b/internal/storage/sql_provider_queries.go @@ -149,11 +149,6 @@ const ( SET public_key = ? WHERE id = ?;` - queryFmtUpdateWebauthnDevicePublicKeyByUsername = ` - UPDATE %s - SET public_key = ? - WHERE username = ? AND kid = ?;` - queryFmtUpdateUpdateWebauthnDeviceDescriptionByUsernameAndID = ` UPDATE %s SET description = ? @@ -166,22 +161,9 @@ const ( clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END WHERE id = ?;` - queryFmtUpdateWebauthnDeviceRecordSignInByUsername = ` - UPDATE %s - SET - rpid = ?, last_used_at = ?, sign_count = ?, - clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END - WHERE username = ? AND kid = ?;` - - queryFmtUpsertWebauthnDevice = ` - REPLACE INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);` - - queryFmtUpsertWebauthnDevicePostgreSQL = ` + queryFmtUpsertInsertDevice = ` INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) - ON CONFLICT (rpid, username, description) - DO UPDATE SET created_at = $1, last_used_at = $2, kid = $6, public_key = $7, attestation_type = $8, transport = $9, aaguid = $10, sign_count = $11, clone_warning = $12;` + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);` queryFmtDeleteWebauthnDevice = ` DELETE FROM %s diff --git a/web/src/services/Client.ts b/web/src/services/Client.ts index d63459dd7..cffc08d2a 100644 --- a/web/src/services/Client.ts +++ b/web/src/services/Client.ts @@ -8,6 +8,7 @@ export async function PostWithOptionalResponse(path: string, body if (res.status !== 200 || hasServiceError(res).errored) { throw new Error(`Failed POST to ${path}. Code: ${res.status}. Message: ${hasServiceError(res).message}`); } + return toData(res); } @@ -32,3 +33,21 @@ export async function Get(path: string): Promise { } return d; } + +export async function GetWithOptionalData(path: string): Promise { + const res = await axios.get>(path); + + if (res.status !== 200 || hasServiceError(res).errored) { + throw new Error(`Failed GET from ${path}. Code: ${res.status}.`); + } + + const d = toData(res); + if (d === null) { + return null; + } + + if (!d) { + throw new Error("unexpected type of response"); + } + return d; +} diff --git a/web/src/services/UserWebauthnDevices.ts b/web/src/services/UserWebauthnDevices.ts index 0a82180b3..2075ee995 100644 --- a/web/src/services/UserWebauthnDevices.ts +++ b/web/src/services/UserWebauthnDevices.ts @@ -1,8 +1,8 @@ import { WebauthnDevice } from "@models/Webauthn"; import { WebauthnDevicesPath } from "@services/Api"; -import { Get } from "@services/Client"; +import { GetWithOptionalData } from "@services/Client"; // getWebauthnDevices returns the list of webauthn devices for the authenticated user. -export async function getWebauthnDevices(): Promise { - return Get(WebauthnDevicesPath); +export async function getWebauthnDevices(): Promise { + return GetWithOptionalData(WebauthnDevicesPath); } diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx index d903df3c4..4a11d26e3 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx @@ -4,10 +4,9 @@ import { Fingerprint } from "@mui/icons-material"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import { Box, Button, Paper, Stack, Tooltip, Typography } from "@mui/material"; +import { Box, Button, CircularProgress, Paper, Stack, Tooltip, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; -import LoadingButton from "@components/LoadingButton"; import { useNotifications } from "@hooks/NotificationsContext"; import { WebauthnDevice } from "@models/Webauthn"; import { deleteDevice, updateDevice } from "@services/Webauthn"; @@ -169,26 +168,26 @@ export default function WebauthnDeviceItem(props: Props) { - } - onClick={() => setShowDialogEdit(true)} + startIcon={loadingEdit ? : } + onClick={loadingEdit ? undefined : () => setShowDialogEdit(true)} > {translate("Edit")} - + - } - onClick={() => setShowDialogDelete(true)} + color="primary" + startIcon={ + loadingDelete ? : + } + onClick={loadingDelete ? undefined : () => setShowDialogDelete(true)} > {translate("Remove")} - + diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog.tsx index 02fb6916d..80cc211fe 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog.tsx @@ -64,11 +64,11 @@ const WebauthnDeviceRegisterDialog = function (props: Props) { setName(""); }; - const handleClose = () => { + const handleClose = useCallback(() => { resetStates(); props.setCancelled(); - }; + }, [props]); const finishAttestation = async () => { if (!result || !result.response) { diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx index cab1ad05f..7d361a5c2 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx @@ -15,11 +15,11 @@ interface Props { export default function WebauthnDevicesStack(props: Props) { const { t: translate } = useTranslation("settings"); - const [devices, setDevices] = useState([]); + const [devices, setDevices] = useState(null); useEffect(() => { (async function () { - setDevices([]); + setDevices(null); const devices = await getWebauthnDevices(); setDevices(devices); })(); @@ -27,7 +27,7 @@ export default function WebauthnDevicesStack(props: Props) { return ( - {devices.length !== 0 ? ( + {devices !== null && devices.length !== 0 ? ( {devices.map((x, idx) => (