Compare commits

..

2 Commits

Author SHA1 Message Date
github-actions[bot]
f33e07b191 chore: update versions (#2538)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/hasura-auth-js@2.3.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

## @nhost/react@3.2.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost/vue@2.2.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost/apollo@6.0.5

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost/react-apollo@9.0.0

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/apollo@6.0.5

## @nhost/react-urql@6.0.0

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost/nextjs@2.1.2

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost/nhost-js@3.0.5

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/hasura-auth-js@2.3.0

## @nhost-examples/react-apollo@0.3.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-apollo@9.0.0

## @nhost-examples/vue-apollo@0.2.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/vue@2.2.0
    -   @nhost/nhost-js@3.0.5
    -   @nhost/apollo@6.0.5

## @nhost/dashboard@1.6.9

### Patch Changes

-   @nhost/react-apollo@9.0.0
-   @nhost/nextjs@2.1.2

## @nhost-examples/cli@0.1.6

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/codegen-react-apollo@0.1.14

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-apollo@9.0.0

## @nhost-examples/codegen-react-query@0.1.15

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost-examples/codegen-react-urql@0.0.11

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-urql@6.0.0

## @nhost-examples/multi-tenant-one-to-many@2.0.4

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/nextjs@0.1.16

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-apollo@9.0.0
    -   @nhost/nextjs@2.1.2

## @nhost-examples/node-storage@0.0.8

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/nextjs-server-components@0.2.2

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/react-gqty@1.0.4

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost-examples/vue-quickstart@0.0.13

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/vue@2.2.0
    -   @nhost/apollo@6.0.5

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-14 15:12:34 +01:00
Hassan Ben Jobrane
017f1a6c7b feat: add elevate workflow to react-apollo and vue-apollo example projects (#2521)
part-2 of https://github.com/nhost/nhost/issues/2394

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2024-02-14 14:52:43 +01:00
68 changed files with 1042 additions and 160 deletions

View File

@@ -1,5 +1,12 @@
# @nhost/dashboard
## 1.6.9
### Patch Changes
- @nhost/react-apollo@9.0.0
- @nhost/nextjs@2.1.2
## 1.6.8
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "1.6.8",
"version": "1.6.9",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/cli
## 0.1.6
### Patch Changes
- @nhost/nhost-js@3.0.5
## 0.1.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/cli",
"version": "0.1.5",
"version": "0.1.6",
"main": "src/index.mjs",
"private": true,
"scripts": {

View File

@@ -1,5 +1,13 @@
# @nhost-examples/codegen-react-apollo
## 0.1.14
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
- @nhost/react-apollo@9.0.0
## 0.1.13
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/codegen-react-apollo",
"version": "0.1.13",
"version": "0.1.14",
"private": true,
"scripts": {
"codegen": "graphql-codegen",

View File

@@ -1,5 +1,12 @@
# @nhost-examples/codegen-react-query
## 0.1.15
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
## 0.1.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/codegen-react-query",
"version": "0.1.14",
"version": "0.1.15",
"private": true,
"scripts": {
"codegen": "graphql-codegen",

View File

@@ -1,5 +1,13 @@
# @nhost-examples/react-urql
## 0.0.11
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
- @nhost/react-urql@6.0.0
## 0.0.10
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/codegen-react-urql",
"private": true,
"version": "0.0.10",
"version": "0.0.11",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/multi-tenant-one-to-many
## 2.0.4
### Patch Changes
- @nhost/nhost-js@3.0.5
## 2.0.3
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/multi-tenant-one-to-many",
"private": true,
"version": "2.0.3",
"version": "2.0.4",
"description": "",
"main": "index.js",
"scripts": {},

View File

@@ -1,5 +1,14 @@
# @nhost-examples/nextjs
## 0.1.16
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
- @nhost/react-apollo@9.0.0
- @nhost/nextjs@2.1.2
## 0.1.15
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/nextjs",
"version": "0.1.15",
"version": "0.1.16",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/node-storage
## 0.0.8
### Patch Changes
- @nhost/nhost-js@3.0.5
## 0.0.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/node-storage",
"version": "0.0.7",
"version": "0.0.8",
"private": true,
"description": "This is an example of how to use the Storage with Node.js",
"main": "src/index.mjs",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/nextjs-server-components
## 0.2.2
### Patch Changes
- @nhost/nhost-js@3.0.5
## 0.2.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/nextjs-server-components",
"version": "0.2.1",
"version": "0.2.2",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -1,5 +1,17 @@
# @nhost-examples/react-apollo
## 0.3.0
### Minor Changes
- 017f1a6: feat: add elevated permission examples
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
- @nhost/react-apollo@9.0.0
## 0.2.1
### Patch Changes

View File

@@ -46,4 +46,4 @@ delete_permissions:
permission:
filter:
user_id:
_eq: X-Hasura-User-Id
_eq: x-hasura-auth-elevated

View File

@@ -0,0 +1,42 @@
table:
name: virus
schema: storage
configuration:
column_config:
created_at:
custom_name: createdAt
file_id:
custom_name: fileId
filename:
custom_name: filename
id:
custom_name: id
updated_at:
custom_name: updatedAt
user_session:
custom_name: userSession
virus:
custom_name: virus
custom_column_names:
created_at: createdAt
file_id: fileId
filename: filename
id: id
updated_at: updatedAt
user_session: userSession
virus: virus
custom_name: virus
custom_root_fields:
delete: deleteViruses
delete_by_pk: deleteVirus
insert: insertViruses
insert_one: insertVirus
select: viruses
select_aggregate: virusesAggregate
select_by_pk: virus
update: updateViruses
update_by_pk: updateVirus
object_relationships:
- name: file
using:
foreign_key_constraint_on: file_id

View File

@@ -11,3 +11,4 @@
- "!include public_todos.yaml"
- "!include storage_buckets.yaml"
- "!include storage_files.yaml"
- "!include storage_virus.yaml"

View File

@@ -1,7 +1,7 @@
[global]
[hasura]
version = 'v2.25.1-ce'
version = 'v2.33.4-ce'
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
@@ -28,7 +28,10 @@ httpPoolSize = 100
version = 18
[auth]
version = '0.25.0'
version = '0.26.0'
[auth.elevatedPrivileges]
mode = 'required'
[auth.redirections]
clientUrl = 'https://react-apollo.example.nhost.io/'
@@ -149,12 +152,12 @@ enabled = true
issuer = 'nhost'
[postgres]
version = '14.6-20230406-2'
version = '14.6-20240129-1'
[provider]
[storage]
version = '0.3.4'
version = '0.6.0'
[observability]
[observability.grafana]

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/react-apollo",
"version": "0.2.1",
"version": "0.3.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.9.4",

View File

@@ -2,9 +2,19 @@ import { FaFile, FaHouseUser, FaQuestion, FaSignOutAlt, FaLock } from 'react-ico
import { SiApollographql } from 'react-icons/si'
import { useLocation, useNavigate } from 'react-router'
import { Link } from 'react-router-dom'
import { showNotification } from '@mantine/notifications'
import { Group, MantineColor, Navbar, Text, ThemeIcon, UnstyledButton } from '@mantine/core'
import { useAuthenticated, useSignOut } from '@nhost/react'
import {
Button,
Card,
Group,
MantineColor,
Navbar,
Text,
ThemeIcon,
UnstyledButton
} from '@mantine/core'
import { useAuthenticated, useElevateSecurityKeyEmail, useSignOut, useUserData } from '@nhost/react'
interface MenuItemProps {
icon: React.ReactNode
color?: MantineColor
@@ -59,10 +69,45 @@ const data: MenuItemProps[] = [
]
export default function NavBar() {
const authenticated = useAuthenticated()
const { signOut } = useSignOut()
const userData = useUserData()
const navigate = useNavigate()
const { signOut } = useSignOut()
const authenticated = useAuthenticated()
const { elevateEmailSecurityKey, elevated } = useElevateSecurityKeyEmail()
const handleElevate = async () => {
if (!authenticated) {
showNotification({
color: 'red',
title: 'Logged out',
message: 'Please login first'
})
return
}
if (userData?.email) {
const { elevated, isError } = await elevateEmailSecurityKey(userData.email)
if (elevated) {
showNotification({
title: 'Success',
message: 'You now have an elevated permission'
})
}
if (isError) {
showNotification({
color: 'red',
title: 'Failed',
message: 'Could not elevate permission'
})
}
}
}
const links = data.map((link) => <MenuItem {...link} key={link.label} />)
return (
<Navbar width={{ sm: 300, lg: 400, base: 100 }} aria-label="main navigation">
<Navbar.Section grow mt="md">
@@ -78,6 +123,13 @@ export default function NavBar() {
/>
)}
</Navbar.Section>
<Card p="lg" m="sm">
<Group position="apart">
<span>Elevated permissions: {String(elevated)}</span>
<Button onClick={handleElevate}>Elevate</Button>
</Group>
</Card>
</Navbar>
)
}

View File

@@ -1,4 +1,9 @@
import { DeleteNoteMutation, InsertNoteMutation, NotesListQuery } from 'src/generated'
import {
DeleteNoteMutation,
InsertNoteMutation,
NotesListQuery,
SecurityKeysQuery
} from 'src/generated'
import { gql, useMutation } from '@apollo/client'
import {
@@ -17,6 +22,8 @@ import { showNotification } from '@mantine/notifications'
import { useAuthQuery } from '@nhost/react-apollo'
import { useElevateSecurityKeyEmail, useUserData } from '@nhost/react'
import { FaTrash } from 'react-icons/fa'
import { SECURITY_KEYS_LIST } from 'src/utils'
import { useState } from 'react'
const NOTES_LIST = gql`
query notesList {
@@ -54,15 +61,44 @@ export const NotesPage: React.FC = () => {
})
const [content, setContent] = useInputState('')
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const { elevateEmailSecurityKey, elevated } = useElevateSecurityKeyEmail()
const [userHasSecurityKey, setUserHasSecurityKey] = useState(false)
useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, {
variables: { userId: userData?.id },
onCompleted: ({ authUserSecurityKeys }) => {
setUserHasSecurityKey(authUserSecurityKeys?.length > 0)
}
})
const [addNoteMutation] = useMutation<InsertNoteMutation>(INSERT_NOTE)
const [deleteNoteMutation] = useMutation<DeleteNoteMutation>(DELETE_NOTE)
const add = () => {
const checkElevatedPermission = async () => {
if (!elevated && userHasSecurityKey) {
const { elevated } = await elevateEmailSecurityKey(userData?.email as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
}
}
const add = async () => {
if (!content) return
try {
await checkElevatedPermission()
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
return
}
addNoteMutation({
variables: { content },
onCompleted: () => setContent(''),
@@ -94,9 +130,20 @@ export const NotesPage: React.FC = () => {
})
}
const deleteNote = (noteId: string) => {
const deleteNote = async (noteId: string) => {
if (!noteId) return
try {
await checkElevatedPermission()
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
return
}
deleteNoteMutation({
variables: { noteId },
onCompleted: () => setContent(''),
@@ -126,20 +173,6 @@ export const NotesPage: React.FC = () => {
return (
<Container>
{loading && <Loader />}
<Card shadow="sm" p="lg" m="sm">
<Group position="apart">
<span>Elevated permissions: {String(elevated)}</span>
<Button
onClick={async (e: React.MouseEvent) => {
if (userData?.email) {
await elevateEmailSecurityKey(userData.email)
}
}}
>
Elevate
</Button>
</Group>
</Card>
<Card shadow="sm" p="lg" m="sm">
<Title>Secret Notes</Title>
<Grid>

View File

@@ -2,11 +2,25 @@ import { useState } from 'react'
import { Button, Card, Grid, TextInput, Title } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useChangeEmail, useUserEmail } from '@nhost/react'
import { useChangeEmail, useElevateSecurityKeyEmail, useUserEmail, useUserId } from '@nhost/react'
import { useAuthQuery } from '@nhost/react-apollo'
import { SecurityKeysQuery } from 'src/generated'
import { SECURITY_KEYS_LIST } from 'src/utils'
export const ChangeEmail: React.FC = () => {
const [newEmail, setNewEmail] = useState('')
const userId = useUserId()
const email = useUserEmail()
const [newEmail, setNewEmail] = useState('')
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const [userHasSecurityKey, setUserHasSecurityKey] = useState(false)
useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, {
variables: { userId },
onCompleted: ({ authUserSecurityKeys }) => {
setUserHasSecurityKey(authUserSecurityKeys?.length > 0)
}
})
const { changeEmail } = useChangeEmail({
redirectTo: '/profile'
})
@@ -19,7 +33,26 @@ export const ChangeEmail: React.FC = () => {
})
return
}
if (!elevated && userHasSecurityKey) {
try {
const { elevated } = await elevateEmailSecurityKey(email as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
return
}
}
const result = await changeEmail(newEmail)
if (result.needsEmailVerification) {
showNotification({
message: `An email has been sent to ${newEmail}. Please check your inbox and follow the link to confirm the email change.`
@@ -33,6 +66,7 @@ export const ChangeEmail: React.FC = () => {
})
}
}
return (
<Card shadow="sm" p="lg" m="sm">
<Title>Change email</Title>

View File

@@ -1,14 +1,53 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Button, Card, Grid, PasswordInput, Title } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useChangePassword } from '@nhost/react'
import {
useChangePassword,
useElevateSecurityKeyEmail,
useUserEmail,
useUserId
} from '@nhost/react'
import { SecurityKeysQuery } from 'src/generated'
import { SECURITY_KEYS_LIST } from 'src/utils'
import { useAuthQuery } from '@nhost/react-apollo'
export const ChangePassword: React.FC = () => {
const userEmail = useUserEmail()
const userId = useUserId()
const [password, setPassword] = useState('')
const { changePassword } = useChangePassword()
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const [userHasSecurityKey, setUserHasSecurityKey] = useState(false)
const { data } = useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, { variables: { userId } })
useEffect(() => {
const authUserSecurityKeys = data?.authUserSecurityKeys
if (authUserSecurityKeys) {
setUserHasSecurityKey(authUserSecurityKeys.length > 0)
}
}, [data])
const change = async () => {
if (!elevated && userHasSecurityKey) {
try {
const { elevated } = await elevateEmailSecurityKey(userEmail as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
return
}
}
const result = await changePassword(password)
if (result.isSuccess) {
showNotification({

View File

@@ -2,11 +2,56 @@ import { useState } from 'react'
import { Button, Card, TextInput, Title } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useConfigMfa } from '@nhost/react'
import { useConfigMfa, useElevateSecurityKeyEmail, useUserEmail, useUserId } from '@nhost/react'
import { SECURITY_KEYS_LIST } from 'src/utils'
import { SecurityKeysQuery } from 'src/generated'
import { useAuthQuery } from '@nhost/react-apollo'
export const Mfa: React.FC = () => {
const userId = useUserId()
const userEmail = useUserEmail()
const [code, setCode] = useState('')
const { generateQrCode, activateMfa, isActivated, isGenerated, qrCodeDataUrl } = useConfigMfa()
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const [userHasSecurityKey, setUserHasSecurityKey] = useState(false)
useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, {
variables: { userId },
onCompleted: ({ authUserSecurityKeys }) => {
setUserHasSecurityKey(authUserSecurityKeys?.length > 0)
}
})
const activate = async () => {
if (!elevated && userHasSecurityKey) {
try {
const { elevated } = await elevateEmailSecurityKey(userEmail as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
} catch (error) {
showNotification({
title: 'Error',
message: 'Could not elevate permissions'
})
return
}
}
const { error, isError } = await activateMfa(code)
if (isError) {
showNotification({
color: 'red',
title: 'Error',
message: error?.message
})
}
}
const generate = async () => {
const result = await generateQrCode()
if (result.error) {
@@ -33,7 +78,7 @@ export const Mfa: React.FC = () => {
onChange={(e) => setCode(e.target.value)}
placeholder="Enter activation code"
/>
<Button fullWidth onClick={() => activateMfa(code)}>
<Button fullWidth onClick={activate}>
Activate
</Button>
</div>

View File

@@ -2,36 +2,22 @@ import { useState } from 'react'
import { FaMinus } from 'react-icons/fa'
import { RemoveSecurityKeyMutation, SecurityKeysQuery } from 'src/generated'
import { gql, useMutation } from '@apollo/client'
import { ApolloError, useApolloClient, useMutation } from '@apollo/client'
import { ActionIcon, Button, Card, SimpleGrid, Table, TextInput, Title } from '@mantine/core'
import { useInputState } from '@mantine/hooks'
import { showNotification } from '@mantine/notifications'
import { useAddSecurityKey, useUserId } from '@nhost/react'
import { useAuthQuery } from '@nhost/react-apollo'
const SECURITY_KEYS_LIST = gql`
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}
`
const REMOVE_SECURITY_KEY = gql`
mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id
}
}
`
import { REMOVE_SECURITY_KEY, SECURITY_KEYS_LIST } from 'src/utils'
export const SecurityKeys: React.FC = () => {
const { add } = useAddSecurityKey()
const userId = useUserId()
const client = useApolloClient()
const { add } = useAddSecurityKey()
// Nickname of the security key
const [nickname, setNickname] = useInputState('')
const [list, setList] = useState<{ id: string; nickname?: string | null }[]>([])
useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, {
variables: { userId },
onCompleted: ({ authUserSecurityKeys }) => {
@@ -43,9 +29,10 @@ export const SecurityKeys: React.FC = () => {
const addKey = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const { key, isError, error } = await add(nickname)
if (isError) {
console.log(error)
showNotification({
color: 'red',
title: 'Error',
@@ -53,11 +40,17 @@ export const SecurityKeys: React.FC = () => {
})
} else {
setNickname('')
// refetch securityKeys so that we know if need to elevate in other components
await client.refetchQueries({
include: [SECURITY_KEYS_LIST]
})
}
if (key) {
setList([...list, key])
}
}
const [removeKey] = useMutation<RemoveSecurityKeyMutation>(REMOVE_SECURITY_KEY, {
onCompleted: ({ deleteAuthUserSecurityKey }) => {
if (deleteAuthUserSecurityKey?.id) {
@@ -66,6 +59,25 @@ export const SecurityKeys: React.FC = () => {
}
})
const handleRemoveKey = async (id: string) => {
try {
await removeKey({ variables: { id } })
// refetch securityKeys so that we know if need to elevate in other components
await client.refetchQueries({
include: [SECURITY_KEYS_LIST]
})
} catch (error) {
const e = error as ApolloError
showNotification({
color: 'red',
title: 'Error',
message: e?.message
})
}
}
return (
<Card shadow="sm" p="lg" m="sm">
<Title>Security keys</Title>
@@ -79,7 +91,7 @@ export const SecurityKeys: React.FC = () => {
<tr key={id}>
<td>{nickname || id}</td>
<td>
<ActionIcon onClick={() => removeKey({ variables: { id } })} color="red">
<ActionIcon onClick={() => handleRemoveKey(id)} color="red">
<FaMinus />
</ActionIcon>
</td>

View File

@@ -0,0 +1 @@
export * from './security-keys'

View File

@@ -0,0 +1,18 @@
import { gql } from '@apollo/client'
export const SECURITY_KEYS_LIST = gql`
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}
`
export const REMOVE_SECURITY_KEY = gql`
mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id
}
}
`

View File

@@ -1,5 +1,5 @@
import { defineConfig } from 'vite'
import path from 'path'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
@@ -8,5 +8,10 @@ export default defineConfig({
include: ['react/jsx-runtime'],
exclude: ['@nhost/react']
},
resolve: {
alias: {
src: path.resolve(__dirname, './src')
}
},
plugins: [react()]
})

View File

@@ -1,5 +1,12 @@
# @nhost-examples/react-gqty
## 1.0.4
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
## 1.0.3
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/react-gqty",
"private": true,
"version": "1.0.3",
"version": "1.0.4",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,18 @@
# @nhost-examples/vue-apollo
## 0.2.0
### Minor Changes
- 017f1a6: feat: add elevated permission examples
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/vue@2.2.0
- @nhost/nhost-js@3.0.5
- @nhost/apollo@6.0.5
## 0.1.1
### Patch Changes

View File

@@ -31,3 +31,19 @@ object_relationships:
- name: user
using:
foreign_key_constraint_on: user_id
select_permissions:
- role: user
permission:
columns:
- id
- nickname
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
delete_permissions:
- role: user
permission:
filter:
user_id:
_eq: x-hasura-auth-elevated

View File

@@ -0,0 +1,42 @@
table:
name: virus
schema: storage
configuration:
column_config:
created_at:
custom_name: createdAt
file_id:
custom_name: fileId
filename:
custom_name: filename
id:
custom_name: id
updated_at:
custom_name: updatedAt
user_session:
custom_name: userSession
virus:
custom_name: virus
custom_column_names:
created_at: createdAt
file_id: fileId
filename: filename
id: id
updated_at: updatedAt
user_session: userSession
virus: virus
custom_name: virus
custom_root_fields:
delete: deleteViruses
delete_by_pk: deleteVirus
insert: insertViruses
insert_one: insertVirus
select: viruses
select_aggregate: virusesAggregate
select_by_pk: virus
update: updateViruses
update_by_pk: updateVirus
object_relationships:
- name: file
using:
foreign_key_constraint_on: file_id

View File

@@ -11,3 +11,4 @@
- "!include public_notes.yaml"
- "!include storage_buckets.yaml"
- "!include storage_files.yaml"
- "!include storage_virus.yaml"

View File

@@ -1,7 +1,7 @@
[global]
[hasura]
version = 'v2.25.1-ce'
version = 'v2.33.4-ce'
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
@@ -28,7 +28,10 @@ httpPoolSize = 100
version = 18
[auth]
version = '0.25.0'
version = '0.26.0'
[auth.elevatedPrivileges]
mode = 'required'
[auth.redirections]
clientUrl = 'http://localhost:5173'
@@ -66,11 +69,11 @@ expiresIn = 43200
enabled = false
[auth.method.emailPasswordless]
enabled = false
enabled = true
[auth.method.emailPassword]
hibpEnabled = false
emailVerificationRequired = false
emailVerificationRequired = true
passwordMinLength = 9
[auth.method.smsPasswordless]
@@ -137,12 +140,12 @@ timeout = 60000
enabled = false
[postgres]
version = '14.6-20230406-2'
version = '14.6-20240129-1'
[provider]
[storage]
version = '0.3.4'
version = '0.6.0'
[observability]
[observability.grafana]

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/vue-apollo",
"private": true,
"version": "0.1.1",
"version": "0.2.0",
"scripts": {
"dev": "vite",
"build": "vite build",

View File

@@ -17,19 +17,68 @@
prepend-icon="mdi-exit-to-app"
@click="signOutHandler"
/>
<v-card-text class="d-flex flex-column align-center justify-space-between align-self-end">
<span>Elevated permissions: {{ elevated }}</span>
<v-btn variant="text" color="primary" @click="handleElevate(user?.email)"> Elevate </v-btn>
</v-card-text>
</v-list>
<v-snackbar :modelValue="showElevateSuccess">
You now have an elevated permission
<template v-slot:actions>
<v-btn color="indigo" variant="text" @click="showElevateError = false"> Close </v-btn>
</template>
</v-snackbar>
<v-snackbar :modelValue="showElevateError">
Could not elevate permission
<template v-slot:actions>
<v-btn color="indigo" variant="text" @click="showElevateError = false"> Close </v-btn>
</template>
</v-snackbar>
<v-snackbar :modelValue="loggedOutWarning">
You are logged out. Please login first!
<template v-slot:actions>
<v-btn color="indigo" variant="text" @click="loggedOutWarning = false"> Close </v-btn>
</template>
</v-snackbar>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { ref } from 'vue'
import { useAuthenticated, useSignOut, useElevateSecurityKeyEmail, useUserData } from '@nhost/vue'
import { useAuthenticated, useSignOut } from '@nhost/vue'
const user = useUserData()
const router = useRouter()
const { signOut } = useSignOut()
const showElevateError = ref(false)
const showElevateSuccess = ref(false)
const loggedOutWarning = ref(false)
const authenticated = useAuthenticated()
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const signOutHandler = async () => {
await signOut()
router.push('/')
}
const handleElevate = async (email: string | undefined) => {
if (!authenticated.value) {
loggedOutWarning.value = true
return
}
if (email) {
const { elevated, isError } = await elevateEmailSecurityKey(email)
if (elevated) {
showElevateSuccess.value = true
}
if (isError) {
showElevateError.value = true
}
}
}
</script>

View File

@@ -2,7 +2,228 @@
<div className="d-flex align-center flex-column">
<v-card width="400">
<v-card-title>Profile page</v-card-title>
<v-card-text> Here is the profile page </v-card-text>
<v-card-text> {{ userEmail }} </v-card-text>
</v-card>
<v-card width="400" class="mt-2 pa-4">
<v-card-title>Add Security Key</v-card-title>
<form @submit="handleAddSecurityKey">
<v-text-field v-model="nickname" label="NickName" />
<v-btn
block
color="primary"
class="my-1"
type="submit"
:disabled="isChangeEmailLoading"
:loading="isChangeEmailLoading"
>
Add
</v-btn>
</form>
<v-list density="compact">
<v-list-subheader>Security Keys</v-list-subheader>
<v-list-item v-for="(key, i) in securityKeysList" :key="i" :value="key.id">
<div className="d-flex align-center justify-space-between">
<v-list-item-title>{{ key.id }}</v-list-item-title>
<v-btn
variant="flat"
prepend-icon="mdi-delete"
@click="handleRemoveSecurityKey(key.id)"
/>
</div>
</v-list-item>
</v-list>
</v-card>
<v-card width="400" class="mt-2 pa-4">
<v-card-title>Change Email</v-card-title>
<form @submit="handleChangeEmail">
<v-text-field v-model="email" label="Email" />
<v-btn
block
color="primary"
class="my-1"
type="submit"
:disabled="isChangeEmailLoading"
:loading="isChangeEmailLoading"
>
Change email
</v-btn>
</form>
</v-card>
<v-card width="400" class="mt-2 pa-4">
<v-card-title>Change Password</v-card-title>
<form @submit="handleChangePassword">
<v-text-field v-model="password" label="Password" type="password" />
<v-btn
block
color="primary"
class="my-1"
type="submit"
:disabled="isChangePasswordLoading"
:loading="isChangePasswordLoading"
>
Change password
</v-btn>
</form>
</v-card>
</div>
<error-snack-bar :error="elevateError" />
<error-snack-bar :error="changeEmailError" />
<v-snackbar :modelValue="successSnackBar">OK</v-snackbar>
<error-snack-bar v-model="showElevatePermissionError"
>Could not elevate permission</error-snack-bar
>
<error-snack-bar v-model="showRemoveKeyError"></error-snack-bar>
<v-snackbar v-model="showRemoveKeyError">
Could not remove key
<template #actions>
<v-btn color="blue" variant="text" @click="showRemoveKeyError = false"> Close </v-btn>
</template>
</v-snackbar>
<verification-email-dialog v-model="emailVerificationDialog" :email="email" />
</template>
<script lang="ts" setup>
import { gql } from '@apollo/client/core'
import {
useChangeEmail,
useChangePassword,
useElevateSecurityKeyEmail,
useAddSecurityKey,
useUserEmail,
useUserId
} from '@nhost/vue'
import { useMutation, useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import { ref, unref } from 'vue'
const email = ref('')
const password = ref('')
const nickname = ref('')
const successSnackBar = ref(false)
const userId = useUserId()
const userEmail = useUserEmail()
const emailVerificationDialog = ref(false)
const showElevatePermissionError = ref(false)
const showRemoveKeyError = ref(false)
const addSecurityKeyError = ref(false)
const elevateError = ref(null)
const changeEmailError = ref(null)
const { changeEmail, isLoading: isChangeEmailLoading } = useChangeEmail()
const { changePassword, isLoading: isChangePasswordLoading } = useChangePassword()
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const { add } = useAddSecurityKey()
const SECURITY_KEYS_LIST = gql`
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}
`
const REMOVE_SECURITY_KEY = gql`
mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id
}
}
`
const { result: securityKeys, refetch } = useQuery(SECURITY_KEYS_LIST, { userId }, {})
const { mutate: removeKey } = useMutation(REMOVE_SECURITY_KEY)
const securityKeysList = computed(() => securityKeys.value?.authUserSecurityKeys || [])
const checkElevatedPermission = async () => {
let elevatedValue = unref(elevated)
if (!elevatedValue && securityKeys.value.authUserSecurityKeys.length > 0) {
const { elevated } = await elevateEmailSecurityKey(userEmail.value as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
}
}
const handleChangeEmail = async (e: Event) => {
e.preventDefault()
try {
await checkElevatedPermission()
} catch (error) {
showElevatePermissionError.value = true
}
const { needsEmailVerification } = await changeEmail(email)
if (needsEmailVerification) {
emailVerificationDialog.value = true
} else {
successSnackBar.value = true
}
}
const handleChangePassword = async (e: Event) => {
e.preventDefault()
try {
await checkElevatedPermission()
} catch (error) {
showElevatePermissionError.value = true
}
const { error: changePasswordError } = await changePassword(password)
if (!changePasswordError) {
successSnackBar.value = true
}
}
const handleAddSecurityKey = async (e: Event) => {
e.preventDefault()
try {
await checkElevatedPermission()
} catch (error) {
showElevatePermissionError.value = true
}
const { isError } = await add(nickname.value)
if (isError) {
addSecurityKeyError.value = true
} else {
nickname.value = ''
refetch()
}
}
const handleRemoveSecurityKey = async (id: string) => {
try {
await checkElevatedPermission()
} catch (error) {
showElevatePermissionError.value = true
}
try {
await removeKey({ id })
await refetch()
} catch (error) {
showRemoveKeyError.value = true
}
}
</script>

View File

@@ -2,29 +2,17 @@
<div className="d-flex align-center flex-column">
<v-card width="400" tile>
<v-card-title>Secret Notes</v-card-title>
<v-card-text class="d-flex align-center justify-space-between">
<span>Elevated permissions: {{ elevated }}</span>
<v-btn variant="text" color="primary" @click="elevatePermission(user?.email)">
Elevate
</v-btn>
</v-card-text>
<v-col>
<v-row class="px-4 mb-2 align-center">
<v-text-field v-model="content" label="Note" class="mt-4 mr-2" />
<v-btn size="large" @click="insertNote({ content }, { refetchQueries: ['notesList'] })"
>Add</v-btn
>
<v-btn size="large" @click="addNote">Add</v-btn>
</v-row>
<v-list density="compact" v-if="result">
<v-list-subheader>Notes</v-list-subheader>
<v-list-item v-for="(note, i) in result.notes" :key="i" :value="note.id">
<div className="d-flex align-center justify-space-between">
<v-list-item-title v-text="note.content"></v-list-item-title>
<v-btn
variant="flat"
prepend-icon="mdi-delete"
@click="deleteNote({ noteId: note.id }, { refetchQueries: ['notesList'] })"
/>
<v-btn variant="flat" prepend-icon="mdi-delete" @click="deleteNote(note.id)" />
</div>
</v-list-item>
</v-list>
@@ -33,26 +21,24 @@
</div>
<error-snack-bar :error="insertNoteError" />
<error-snack-bar :error="deleteNoteError" />
<error-snack-bar v-model="showElevatePermissionError"
>Could not elevate permission</error-snack-bar
>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { computed, ref, unref } from 'vue'
import { gql } from '@apollo/client/core'
import { useAuthenticated, useElevateSecurityKeyEmail, useUserData } from '@nhost/vue'
import { useAuthenticated, useElevateSecurityKeyEmail, useUserEmail, useUserId } from '@nhost/vue'
import { useQuery, useMutation } from '@vue/apollo-composable'
const content = ref('')
const user = useUserData()
const userId = useUserId()
const userEmail = useUserEmail()
const isAuthenticated = useAuthenticated()
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
const elevatePermission = async (email: string | undefined) => {
if (email) {
await elevateEmailSecurityKey(email)
}
}
const showElevatePermissionError = ref(false)
const GET_NOTES = gql`
query notesList {
@@ -81,8 +67,15 @@ const DELETE_NOTE = gql`
}
`
const isAuthenticated = useAuthenticated()
// TODO check if the query always runs with the headers
const SECURITY_KEYS_LIST = gql`
query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}
`
const { result } = useQuery(
GET_NOTES,
null,
@@ -91,6 +84,44 @@ const { result } = useQuery(
}))
)
const { mutate: insertNote, error: insertNoteError } = useMutation(INSERT_NOTE)
const { mutate: deleteNote, error: deleteNoteError } = useMutation(DELETE_NOTE)
const { result: securityKeys } = useQuery(SECURITY_KEYS_LIST, { userId })
const { mutate: insertNoteMutation, error: insertNoteError } = useMutation(INSERT_NOTE)
const { mutate: deleteNoteMutation, error: deleteNoteError } = useMutation(DELETE_NOTE)
const checkElevatedPermission = async () => {
let elevatedValue = unref(elevated)
if (!elevatedValue && securityKeys.value.authUserSecurityKeys.length > 0) {
const { elevated } = await elevateEmailSecurityKey(userEmail.value as string)
if (!elevated) {
throw new Error('Permissions were not elevated')
}
}
}
const addNote = async () => {
try {
await checkElevatedPermission()
} catch (error) {
showElevatePermissionError.value = true
}
await insertNoteMutation({ content: content.value }, { refetchQueries: ['notesList'] })
content.value = ''
}
const deleteNote = async (noteId: string) => {
try {
await checkElevatedPermission()
} catch (error) {
showElevatePermissionError.value = true
}
await deleteNoteMutation({ noteId: noteId }, { refetchQueries: ['notesList'] })
content.value = ''
}
</script>

View File

@@ -1,5 +1,13 @@
# @nhost-examples/vue-quickstart
## 0.0.13
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/vue@2.2.0
- @nhost/apollo@6.0.5
## 0.0.12
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/vue-quickstart",
"version": "0.0.12",
"version": "0.0.13",
"private": true,
"scripts": {
"build": "vite build",

View File

@@ -1,5 +1,11 @@
# @nhost/apollo
## 6.0.5
### Patch Changes
- @nhost/nhost-js@3.0.5
## 6.0.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/apollo",
"version": "6.0.4",
"version": "6.0.5",
"description": "Nhost Apollo Client library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,13 @@
# @nhost/react-apollo
## 9.0.0
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
- @nhost/apollo@6.0.5
## 8.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-apollo",
"version": "8.0.1",
"version": "9.0.0",
"description": "Nhost React Apollo client",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,12 @@
# @nhost/react-urql
## 6.0.0
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
## 5.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-urql",
"version": "5.0.1",
"version": "6.0.0",
"description": "Nhost React URQL client",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/hasura-auth-js
## 2.3.0
### Minor Changes
- 017f1a6: feat: add elevated permission examples
## 2.2.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/hasura-auth-js",
"version": "2.2.0",
"version": "2.3.0",
"description": "Hasura-auth client",
"license": "MIT",
"keywords": [

View File

@@ -445,16 +445,14 @@ export class HasuraAuthClient {
*
* @docs https://docs.nhost.io/reference/javascript/auth/elevate-security-key
*/
async elevateWebAuthn(
email: string
): Promise<SignInResponse & { providerUrl?: string; provider?: string }> {
async elevateEmailSecurityKey(email: string) {
if (!email) {
throw Error('A user email is required')
}
const res = await elevateEmailSecurityKeyPromise(this._client, email)
return { ...getAuthenticationResult(res), mfa: null }
return { ...res, mfa: null }
}
/**

View File

@@ -3,15 +3,19 @@ import {
PublicKeyCredentialRequestOptionsJSON
} from '@simplewebauthn/typescript-types'
import {
AuthActionErrorState,
AuthActionSuccessState,
AuthClient,
AuthErrorPayload,
CodifiedError,
postFetch,
SessionActionHandlerResult,
SignInResponse
} from '..'
import { startAuthentication } from '@simplewebauthn/browser'
export interface ElevateWithSecurityKeyHandlerResult extends SessionActionHandlerResult {
export interface ElevateWithSecurityKeyHandlerResult
extends AuthActionSuccessState,
AuthActionErrorState {
elevated: boolean
}
@@ -36,48 +40,42 @@ export const elevateEmailSecurityKeyPromise = (authClient: AuthClient, email: st
throw new CodifiedError(e as Error)
}
const {
data: { session },
error: signInError
} = await postFetch<SignInResponse>(
`${authClient.backendUrl}/elevate/webauthn/verify`,
{
email,
credential
},
accessToken
)
try {
const {
data: { session },
error: signInError
} = await postFetch<SignInResponse>(
`${authClient.backendUrl}/elevate/webauthn/verify`,
{
email,
credential
},
accessToken
)
if (session && !signInError) {
authClient.interpreter?.send({
type: 'SESSION_UPDATE',
data: {
session
}
})
}
if (session && !signInError) {
authClient.interpreter?.send({
type: 'SESSION_UPDATE',
data: {
session
}
})
authClient.interpreter?.onTransition((state) => {
if (state.matches({ authentication: 'signedIn' })) {
resolve({
accessToken: state.context.accessToken.value,
refreshToken: state.context.refreshToken.value,
error: null,
isError: false,
isSuccess: true,
user: state.context.user,
elevated: true
})
} else {
resolve({
accessToken: state.context.accessToken.value,
refreshToken: state.context.refreshToken.value,
error: null, // TODO pass error
isError: true,
isSuccess: false,
user: state.context.user,
elevated: false
})
}
})
} catch (e) {
const { error } = e as { error: AuthErrorPayload }
resolve({
error,
isError: true,
isSuccess: false,
elevated: false
})
}
})

View File

@@ -1,5 +1,12 @@
# @nhost/nextjs
## 2.1.2
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/react@3.2.0
## 2.1.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nextjs",
"version": "2.1.1",
"version": "2.1.2",
"description": "Nhost NextJS library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,12 @@
# @nhost/nhost-js
## 3.0.5
### Patch Changes
- Updated dependencies [017f1a6]
- @nhost/hasura-auth-js@2.3.0
## 3.0.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nhost-js",
"version": "3.0.4",
"version": "3.0.5",
"description": "Nhost JavaScript SDK",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,15 @@
# @nhost/react
## 3.2.0
### Minor Changes
- 017f1a6: feat: add elevated permission examples
### Patch Changes
- @nhost/nhost-js@3.0.5
## 3.1.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react",
"version": "3.1.1",
"version": "3.2.0",
"description": "Nhost React library",
"license": "MIT",
"keywords": [

View File

@@ -35,14 +35,16 @@ export const useElevateSecurityKeyEmail = (): ElevateWithSecurityKeyHook => {
const nhost = useNhostClient()
const claims = useHasuraClaims()
const [elevated, setElevated] = useState(claims?.['x-hasura-auth-elevated'] === user?.id)
const hasElevatedClaim = user ? claims?.['x-hasura-auth-elevated'] === user?.id : false
const [elevated, setElevated] = useState(!!hasElevatedClaim)
const elevateEmailSecurityKey: ElevateWithSecurityKeyHandler = (email: string) =>
elevateEmailSecurityKeyPromise(nhost.auth.client, email)
useEffect(() => {
setElevated(claims?.['x-hasura-auth-elevated'] === user?.id)
}, [claims, user])
setElevated(!!hasElevatedClaim)
}, [hasElevatedClaim])
return {
elevated,

View File

@@ -1,5 +1,15 @@
# @nhost/vue
## 2.2.0
### Minor Changes
- 017f1a6: feat: add elevated permission examples
### Patch Changes
- @nhost/nhost-js@3.0.5
## 2.1.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/vue",
"version": "2.1.1",
"version": "2.2.0",
"description": "Nhost Vue library",
"license": "MIT",
"keywords": [

View File

@@ -34,3 +34,4 @@ export * from './useUserRoles'
export * from './useElevateSecurityKeyEmail'
export * from './useSignUpEmailSecurityKey'
export * from './useSignInEmailSecurityKey'
export * from './useAddSecurityKey'

View File

@@ -0,0 +1,67 @@
import {
ActionErrorState,
ActionLoadingState,
ActionSuccessState,
AddSecurityKeyHandlerResult,
addSecurityKeyPromise,
ErrorPayload
} from '@nhost/nhost-js'
import { ToRefs, ref, computed } from 'vue'
import { useNhostClient } from './useNhostClient'
interface AddSecurityKeyHandler {
(
/** Optional human-readable name of the security key */
nickname?: string
): Promise<AddSecurityKeyHandlerResult>
}
export interface AddSecuritKeyComposableResult
extends ToRefs<ActionErrorState>,
ToRefs<ActionSuccessState>,
ToRefs<ActionLoadingState> {
/** Add a security key to the current user with the WebAuthn API */
add: AddSecurityKeyHandler
}
/**
* Use the composable `useAddSecurityKey` to add a WebAuthn security key.
*
* @example
* ```tsx
* const { add, isLoading, isSuccess, isError, error } = useAddSecurityKey()
*
* const handleFormSubmit = async (e) => {
* e.preventDefault();
*
* await add('key nickname')
* }
* ```
*
* @docs https://docs.nhost.io/reference/vue/use-add-security-key
*/
export const useAddSecurityKey = (): AddSecuritKeyComposableResult => {
const { nhost } = useNhostClient()
const error = ref<ErrorPayload | null>(null)
const isSuccess = computed(() => !error)
const isError = computed(() => !!error)
const isLoading = ref<boolean>(false)
const add: AddSecurityKeyHandler = async (nickname) => {
isLoading.value = true
const result = await addSecurityKeyPromise(nhost.auth.client, nickname)
const { error: addSecurityKeyError } = result
if (error) {
error.value = addSecurityKeyError
}
isLoading.value = false
return result
}
return { add, isLoading, isSuccess, isError, error }
}

View File

@@ -2,7 +2,7 @@ import {
elevateEmailSecurityKeyPromise,
ElevateWithSecurityKeyHandlerResult
} from '@nhost/nhost-js'
import { computed, ref, unref } from 'vue'
import { computed, unref } from 'vue'
import { RefOrValue } from './helpers'
import { useHasuraClaims } from './useHasuraClaims'
import { useNhostClient } from './useNhostClient'
@@ -39,7 +39,11 @@ export const useElevateSecurityKeyEmail = (): ElevateWithSecurityKeyResult => {
const claims = useHasuraClaims()
const { nhost } = useNhostClient()
const elevated = computed(() => claims.value?.['x-hasura-auth-elevated'] === user.value?.id)
const hasElevatedClaim = computed(() =>
user.value ? claims.value?.['x-hasura-auth-elevated'] === user.value?.id : false
)
const elevated = computed(() => !!hasElevatedClaim.value)
const elevateEmailSecurityKey: ElevateWithSecurityKeyHandler = (email: RefOrValue<string>) =>
elevateEmailSecurityKeyPromise(nhost.auth.client, unref(email))