diff --git a/.sqlx/query-e770f574ecb1c8fc700e610334914e72dbffca5448b0aef3e0b0e3f60af6b5f7.json b/.sqlx/query-e770f574ecb1c8fc700e610334914e72dbffca5448b0aef3e0b0e3f60af6b5f7.json new file mode 100644 index 0000000000..e08b8d9d28 --- /dev/null +++ b/.sqlx/query-e770f574ecb1c8fc700e610334914e72dbffca5448b0aef3e0b0e3f60af6b5f7.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT b.id, b.pub_key, b.device_id FROM biometric_auth as b JOIN device d ON b.device_id = d.id WHERE d.user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "pub_key", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "device_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "e770f574ecb1c8fc700e610334914e72dbffca5448b0aef3e0b0e3f60af6b5f7" +} diff --git a/crates/defguard_core/src/db/models/biometric_auth.rs b/crates/defguard_core/src/db/models/biometric_auth.rs index 45e3e4d5ee..74d06fb944 100644 --- a/crates/defguard_core/src/db/models/biometric_auth.rs +++ b/crates/defguard_core/src/db/models/biometric_auth.rs @@ -54,6 +54,21 @@ impl BiometricAuth { .fetch_optional(executor) .await } + + pub(crate) async fn find_by_user_id<'e, E>( + executor: E, + user_id: Id, + ) -> Result, sqlx::Error> + where + E: PgExecutor<'e>, + { + query_as!( + Self, + "SELECT b.id, b.pub_key, b.device_id FROM biometric_auth as b JOIN device d ON b.device_id = d.id WHERE d.user_id = $1", &user_id + ) + .fetch_all(executor) + .await + } } #[derive(Clone, Debug)] diff --git a/crates/defguard_core/src/db/models/device.rs b/crates/defguard_core/src/db/models/device.rs index 84785f64f5..c45c371dae 100644 --- a/crates/defguard_core/src/db/models/device.rs +++ b/crates/defguard_core/src/db/models/device.rs @@ -46,7 +46,7 @@ pub struct DeviceConfig { // The type of a device: // User: A device of a user, which may be in multiple networks, e.g. a laptop -// Network: A standalone device added by a user permamently bound to one network, e.g. a printer +// Network: A stand-alone device added by a user permanently bound to one network, e.g. a printer #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Type)] #[sqlx(type_name = "device_type", rename_all = "snake_case")] pub enum DeviceType { diff --git a/crates/defguard_core/src/db/models/mod.rs b/crates/defguard_core/src/db/models/mod.rs index 369b114b7e..b2b2f00732 100644 --- a/crates/defguard_core/src/db/models/mod.rs +++ b/crates/defguard_core/src/db/models/mod.rs @@ -29,6 +29,8 @@ use std::collections::HashSet; use sqlx::{Error as SqlxError, PgConnection, PgPool, query_as}; use utoipa::ToSchema; +use crate::db::models::biometric_auth::BiometricAuth; + use self::{ device::UserDevice, user::{MFAMethod, User}, @@ -204,6 +206,7 @@ pub struct UserDetails { pub user: UserInfo, #[serde(default)] pub devices: Vec, + pub biometric_enabled_devices: Vec, #[serde(default)] pub security_keys: Vec, } @@ -212,11 +215,16 @@ impl UserDetails { pub async fn from_user(pool: &PgPool, user: &User) -> Result { let devices = user.user_devices(pool).await?; let security_keys = user.security_keys(pool).await?; - + let biometric_enabled_devices = BiometricAuth::find_by_user_id(pool, user.id) + .await? + .iter() + .map(|a| a.device_id) + .collect::>(); Ok(Self { user: UserInfo::from_user(pool, user).await?, devices, security_keys, + biometric_enabled_devices, }) } } diff --git a/web/biome.json b/web/biome.json index 47dc2cac51..e9cea92c71 100644 --- a/web/biome.json +++ b/web/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json", + "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { "ignoreUnknown": false, diff --git a/web/package.json b/web/package.json index f76351ddea..21e02132bb 100644 --- a/web/package.json +++ b/web/package.json @@ -113,7 +113,7 @@ }, "devDependencies": { "@babel/core": "^7.28.0", - "@biomejs/biome": "2.1.1", + "@biomejs/biome": "2.1.3", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@hookform/devtools": "^4.4.0", @@ -139,7 +139,7 @@ "sass": "~1.70.0", "standard-version": "^9.5.0", "type-fest": "^4.41.0", - "typescript": "~5.8.3", + "typescript": "~5.9.2", "vite": "^7.0.6", "vite-plugin-package-version": "^1.1.0" } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 80ee19bb71..fc7698f978 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -202,7 +202,7 @@ importers: version: 1.2.4 typesafe-i18n: specifier: ^5.26.2 - version: 5.26.2(typescript@5.8.3) + version: 5.26.2(typescript@5.9.2) use-breakpoint: specifier: ^4.0.6 version: 4.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -217,8 +217,8 @@ importers: specifier: ^7.28.0 version: 7.28.0 '@biomejs/biome': - specifier: 2.1.1 - version: 2.1.1 + specifier: 2.1.3 + version: 2.1.3 '@csstools/css-parser-algorithms': specifier: ^3.0.5 version: 3.0.5(@csstools/css-tokenizer@3.0.4) @@ -295,8 +295,8 @@ importers: specifier: ^4.41.0 version: 4.41.0 typescript: - specifier: ~5.8.3 - version: 5.8.3 + specifier: ~5.9.2 + version: 5.9.2 vite: specifier: ^7.0.6 version: 7.0.6(@types/node@24.2.0)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) @@ -381,55 +381,55 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.1.1': - resolution: {integrity: sha512-HFGYkxG714KzG+8tvtXCJ1t1qXQMzgWzfvQaUjxN6UeKv+KvMEuliInnbZLJm6DXFXwqVi6446EGI0sGBLIYng==} + '@biomejs/biome@2.1.3': + resolution: {integrity: sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.1.1': - resolution: {integrity: sha512-2Muinu5ok4tWxq4nu5l19el48cwCY/vzvI7Vjbkf3CYIQkjxZLyj0Ad37Jv2OtlXYaLvv+Sfu1hFeXt/JwRRXQ==} + '@biomejs/cli-darwin-arm64@2.1.3': + resolution: {integrity: sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.1.1': - resolution: {integrity: sha512-cC8HM5lrgKQXLAK+6Iz2FrYW5A62pAAX6KAnRlEyLb+Q3+Kr6ur/sSuoIacqlp1yvmjHJqjYfZjPvHWnqxoEIA==} + '@biomejs/cli-darwin-x64@2.1.3': + resolution: {integrity: sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.1.1': - resolution: {integrity: sha512-/7FBLnTswu4jgV9ttI3AMIdDGqVEPIZd8I5u2D4tfCoj8rl9dnjrEQbAIDlWhUXdyWlFSz8JypH3swU9h9P+2A==} + '@biomejs/cli-linux-arm64-musl@2.1.3': + resolution: {integrity: sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.1.1': - resolution: {integrity: sha512-tw4BEbhAUkWPe4WBr6IX04DJo+2jz5qpPzpW/SWvqMjb9QuHY8+J0M23V8EPY/zWU4IG8Ui0XESapR1CB49Q7g==} + '@biomejs/cli-linux-arm64@2.1.3': + resolution: {integrity: sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.1.1': - resolution: {integrity: sha512-kUu+loNI3OCD2c12cUt7M5yaaSjDnGIksZwKnueubX6c/HWUyi/0mPbTBHR49Me3F0KKjWiKM+ZOjsmC+lUt9g==} + '@biomejs/cli-linux-x64-musl@2.1.3': + resolution: {integrity: sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.1.1': - resolution: {integrity: sha512-3WJ1GKjU7NzZb6RTbwLB59v9cTIlzjbiFLDB0z4376TkDqoNYilJaC37IomCr/aXwuU8QKkrYoHrgpSq5ffJ4Q==} + '@biomejs/cli-linux-x64@2.1.3': + resolution: {integrity: sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.1.1': - resolution: {integrity: sha512-vEHK0v0oW+E6RUWLoxb2isI3rZo57OX9ZNyyGH701fZPj6Il0Rn1f5DMNyCmyflMwTnIQstEbs7n2BxYSqQx4Q==} + '@biomejs/cli-win32-arm64@2.1.3': + resolution: {integrity: sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.1.1': - resolution: {integrity: sha512-i2PKdn70kY++KEF/zkQFvQfX1e8SkA8hq4BgC+yE9dZqyLzB/XStY2MvwI3qswlRgnGpgncgqe0QYKVS1blksg==} + '@biomejs/cli-win32-x64@2.1.3': + resolution: {integrity: sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -956,8 +956,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/types@0.1.23': - resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + '@swc/types@0.1.24': + resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} '@tanstack/query-core@5.83.1': resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==} @@ -2852,8 +2852,8 @@ packages: peerDependencies: typescript: '>=3.5.1' - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true @@ -3183,39 +3183,39 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@biomejs/biome@2.1.1': + '@biomejs/biome@2.1.3': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.1.1 - '@biomejs/cli-darwin-x64': 2.1.1 - '@biomejs/cli-linux-arm64': 2.1.1 - '@biomejs/cli-linux-arm64-musl': 2.1.1 - '@biomejs/cli-linux-x64': 2.1.1 - '@biomejs/cli-linux-x64-musl': 2.1.1 - '@biomejs/cli-win32-arm64': 2.1.1 - '@biomejs/cli-win32-x64': 2.1.1 - - '@biomejs/cli-darwin-arm64@2.1.1': + '@biomejs/cli-darwin-arm64': 2.1.3 + '@biomejs/cli-darwin-x64': 2.1.3 + '@biomejs/cli-linux-arm64': 2.1.3 + '@biomejs/cli-linux-arm64-musl': 2.1.3 + '@biomejs/cli-linux-x64': 2.1.3 + '@biomejs/cli-linux-x64-musl': 2.1.3 + '@biomejs/cli-win32-arm64': 2.1.3 + '@biomejs/cli-win32-x64': 2.1.3 + + '@biomejs/cli-darwin-arm64@2.1.3': optional: true - '@biomejs/cli-darwin-x64@2.1.1': + '@biomejs/cli-darwin-x64@2.1.3': optional: true - '@biomejs/cli-linux-arm64-musl@2.1.1': + '@biomejs/cli-linux-arm64-musl@2.1.3': optional: true - '@biomejs/cli-linux-arm64@2.1.1': + '@biomejs/cli-linux-arm64@2.1.3': optional: true - '@biomejs/cli-linux-x64-musl@2.1.1': + '@biomejs/cli-linux-x64-musl@2.1.3': optional: true - '@biomejs/cli-linux-x64@2.1.1': + '@biomejs/cli-linux-x64@2.1.3': optional: true - '@biomejs/cli-win32-arm64@2.1.1': + '@biomejs/cli-win32-arm64@2.1.3': optional: true - '@biomejs/cli-win32-x64@2.1.1': + '@biomejs/cli-win32-x64@2.1.3': optional: true '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': @@ -3620,7 +3620,7 @@ snapshots: '@swc/core@1.13.3': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.23 + '@swc/types': 0.1.24 optionalDependencies: '@swc/core-darwin-arm64': 1.13.3 '@swc/core-darwin-x64': 1.13.3 @@ -3635,7 +3635,7 @@ snapshots: '@swc/counter@0.1.3': {} - '@swc/types@0.1.23': + '@swc/types@0.1.24': dependencies: '@swc/counter': 0.1.3 @@ -5749,11 +5749,11 @@ snapshots: typedarray@0.0.6: {} - typesafe-i18n@5.26.2(typescript@5.8.3): + typesafe-i18n@5.26.2(typescript@5.9.2): dependencies: - typescript: 5.8.3 + typescript: 5.9.2 - typescript@5.8.3: {} + typescript@5.9.2: {} uglify-js@3.19.3: optional: true diff --git a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx index cf73aba967..1cfcafd433 100644 --- a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx +++ b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx @@ -1,11 +1,11 @@ import './style.scss'; import classNames from 'classnames'; +import clsx from 'clsx'; import dayjs from 'dayjs'; import type { TargetAndTransition } from 'framer-motion'; import { isUndefined, orderBy } from 'lodash-es'; import { useMemo, useState } from 'react'; - import { useI18nContext } from '../../../../../i18n/i18n-react'; import { ListCellTags } from '../../../../../shared/components/Layout/ListCellTags/ListCellTags'; import IconClip from '../../../../../shared/components/svg/IconClip'; @@ -21,6 +21,7 @@ import { EditButtonOption } from '../../../../../shared/defguard-ui/components/L import { EditButtonOptionStyleVariant } from '../../../../../shared/defguard-ui/components/Layout/EditButton/types'; import { Label } from '../../../../../shared/defguard-ui/components/Layout/Label/Label'; import { LimitedText } from '../../../../../shared/defguard-ui/components/Layout/LimitedText/LimitedText'; +import { ListCellText } from '../../../../../shared/defguard-ui/components/Layout/ListCellText/ListCellText'; import { NoData } from '../../../../../shared/defguard-ui/components/Layout/NoData/NoData'; import { useAppStore } from '../../../../../shared/hooks/store/useAppStore'; import { useUserProfileStore } from '../../../../../shared/hooks/store/useUserProfileStore'; @@ -40,10 +41,11 @@ const formatDate = (date: string): string => { interface Props { device: Device; + biometricEnabled: boolean; modifiable: boolean; } -export const DeviceCard = ({ device, modifiable }: Props) => { +export const DeviceCard = ({ device, modifiable, biometricEnabled }: Props) => { const [hovered, setHovered] = useState(false); const [expanded, setExpanded] = useState(false); const { LL } = useI18nContext(); @@ -105,9 +107,14 @@ export const DeviceCard = ({ device, modifiable }: Props) => { onMouseOut={() => setHovered(false)} >
-
+
-

{device.name}

+ {biometricEnabled && } +
@@ -255,7 +262,7 @@ const DeviceLocation = ({
-

{network_name}

+ {!isUndefined(network_gateway_ip) && }
@@ -312,3 +319,21 @@ const ExpandButton = ({ expanded, onClick }: ExpandButtonProps) => { ); }; + +const IconBiometry = () => { + return ( + + + + ); +}; diff --git a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/style.scss b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/style.scss index 630c435a38..b30a20f760 100644 --- a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/style.scss +++ b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/style.scss @@ -15,7 +15,6 @@ h3 { @include small-header; - @include text-overflow-dots; user-select: none; } @@ -32,10 +31,32 @@ max-width: 120px; } + .list-cell-text { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + + h3 { + display: inline-block; + } + } + .main-info { & > header { grid-template-rows: 40px; grid-template-columns: 40px 1fr 55px; + max-width: 100%; + overflow: hidden; + + &.biometry { + grid-template-columns: 40px 12px 1fr 55px; + } + + .biometry-icon { + width: 12px; + height: 12px; + } & > .avatar-icon { grid-row: 1; @@ -59,6 +80,7 @@ .location { max-width: 100%; overflow: hidden; + & > header { grid-template-rows: 40px; grid-template-columns: 22px 1fr; diff --git a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx index 0e172be584..e01ae311ce 100644 --- a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx +++ b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx @@ -48,6 +48,9 @@ export const UserDevices = () => { key={device.id} device={device} modifiable={canManageDevices} + biometricEnabled={userProfile.biometric_enabled_devices.includes( + device.id, + )} /> ))}
diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 99042918cf..e8958406e5 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 99042918cf99655c2598c1d7e18d8e19b4abdf2c +Subproject commit e8958406e528679302751bdf10c0fa68d73e5897 diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 5035b7823f..b8d846af63 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -61,6 +61,7 @@ export type UserProfile = { user: User; devices: Device[]; security_keys: SecurityKey[]; + biometric_enabled_devices: number[]; }; export interface OAuth2AuthorizedApps {